1
0
mirror of https://github.com/elisspace/autopsy.git synced 2026-08-31 08:32:01 +00:00

Merge branch 'release-4.13.0' of https://github.com/sleuthkit/autopsy into 5419-browsers-maps-apps

# Conflicts:
#	InternalPythonModules/android/module.py
This commit is contained in:
Raman
2019-09-20 11:57:05 -04:00
16 changed files with 2412 additions and 31 deletions

View File

@@ -0,0 +1,35 @@
"""
Autopsy Forensic Browser
Copyright 2019 Basis Technology Corp.
Contact: carrier <at> sleuthkit <dot> org
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
class ResultSetIterator(object):
"""
Generic base class for iterating through database recordms
"""
def __init__(self, result_set):
self.result_set = result_set
def next(self):
if self.result_set is None:
return False
return self.result_set.next()
def close(self):
if self.result_set is not None:
self.result_set.close()

View File

@@ -0,0 +1,65 @@
"""
Autopsy Forensic Browser
Copyright 2019 Basis Technology Corp.
Contact: carrier <at> sleuthkit <dot> org
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from ResultSetIterator import ResultSetIterator
from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import CallMediaType
from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import CommunicationDirection
from org.sleuthkit.datamodel import Account
class TskCallLogsParser(ResultSetIterator):
"""
Generic TSK_CALLLOG artifact template. Each of these methods
will contain the extraction and transformation logic for
converting raw database records to the expected TSK_CALLLOG
format.
A simple example of data transformation would be computing
the end time of a call when the database only supplies the start
time and duration.
"""
def __init__(self, result_set):
super(TskCallLogsParser, self).__init__(result_set)
self._DEFAULT_STRING = ""
self._DEFAULT_DIRECTION = CommunicationDirection.UNKNOWN
self._DEFAULT_ADDRESS = None
self._DEFAULT_CALL_TYPE = CallMediaType.UNKNOWN
self._DEFAULT_LONG = -1L
self.INCOMING_CALL = CommunicationDirection.INCOMING
self.OUTGOING_CALL = CommunicationDirection.OUTGOING
self.AUDIO_CALL = CallMediaType.AUDIO
self.VIDEO_CALL = CallMediaType.VIDEO
def get_call_direction(self):
return self._DEFAULT_DIRECTION
def get_phone_number_from(self):
return self._DEFAULT_ADDRESS
def get_phone_number_to(self):
return self._DEFAULT_ADDRESS
def get_call_start_date_time(self):
return self._DEFAULT_LONG
def get_call_end_date_time(self):
return self._DEFAULT_LONG
def get_call_type(self):
return self._DEFAULT_CALL_TYPE

View File

@@ -0,0 +1,49 @@
"""
Autopsy Forensic Browser
Copyright 2019 Basis Technology Corp.
Contact: carrier <at> sleuthkit <dot> org
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from ResultSetIterator import ResultSetIterator
class TskContactsParser(ResultSetIterator):
"""
Generic TSK_CONTACT artifact template. Each of these methods
will contain the extraction and transformation logic for
converting raw database records to the expected TSK_CONTACT
format.
"""
def __init__(self, result_set):
super(TskContactsParser, self).__init__(result_set)
self._DEFAULT_VALUE = ""
def get_account_name(self):
return self._DEFAULT_VALUE
def get_contact_name(self):
return self._DEFAULT_VALUE
def get_phone(self):
return self._DEFAULT_VALUE
def get_home_phone(self):
return self._DEFAULT_VALUE
def get_mobile_phone(self):
return self._DEFAULT_VALUE
def get_email(self):
return self._DEFAULT_VALUE

View File

@@ -0,0 +1,73 @@
"""
Autopsy Forensic Browser
Copyright 2019 Basis Technology Corp.
Contact: carrier <at> sleuthkit <dot> org
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from ResultSetIterator import ResultSetIterator
from org.sleuthkit.datamodel import Account
from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import MessageReadStatus
from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import CommunicationDirection
class TskMessagesParser(ResultSetIterator):
"""
Generic TSK_MESSAGE artifact template. Each of these methods
will contain the extraction and transformation logic for
converting raw database records to the expected TSK_MESSAGE
format.
An easy example of such a transformation would be converting
message date time from milliseconds to seconds.
"""
def __init__(self, result_set):
super(TskMessagesParser, self).__init__(result_set)
self._DEFAULT_TEXT = ""
self._DEFAULT_LONG = -1L
self._DEFAULT_MSG_READ_STATUS = MessageReadStatus.UNKNOWN
self._DEFAULT_ACCOUNT_ADDRESS = None
self._DEFAULT_COMMUNICATION_DIRECTION = CommunicationDirection.UNKNOWN
self.INCOMING = CommunicationDirection.INCOMING
self.OUTGOING = CommunicationDirection.OUTGOING
self.READ = MessageReadStatus.READ
self.UNREAD = MessageReadStatus.UNREAD
def get_message_type(self):
return self._DEFAULT_TEXT
def get_message_direction(self):
return self._DEFAULT_COMMUNICATION_DIRECTION
def get_phone_number_from(self):
return self._DEFAULT_ACCOUNT_ADDRESS
def get_phone_number_to(self):
return self._DEFAULT_ACCOUNT_ADDRESS
def get_message_date_time(self):
return self._DEFAULT_LONG
def get_message_read_status(self):
return self._DEFAULT_MSG_READ_STATUS
def get_message_subject(self):
return self._DEFAULT_TEXT
def get_message_text(self):
return self._DEFAULT_TEXT
def get_thread_id(self):
return self._DEFAULT_TEXT

View File

@@ -27,8 +27,6 @@ class AndroidComponentAnalyzer:
def analyze(self, dataSource, fileManager, context):
raise NotImplementedError
"""
A utility method to append list of attachments to msg body
"""
@@ -39,5 +37,3 @@ def appendAttachmentList(msgBody, attachmentsList):
body = body + "\n".join(attachmentsList)
return body

View File

@@ -29,6 +29,7 @@ from java.util.logging import Level
from java.util import ArrayList
from org.apache.commons.codec.binary import Base64
from org.sleuthkit.autopsy.casemodule import Case
from org.sleuthkit.autopsy.casemodule import NoCurrentCaseException
from org.sleuthkit.autopsy.coreutils import Logger
from org.sleuthkit.autopsy.coreutils import MessageNotifyUtil
from org.sleuthkit.autopsy.coreutils import AppSQLiteDB
@@ -56,10 +57,14 @@ and adds artifacts to the case.
class IMOAnalyzer(general.AndroidComponentAnalyzer):
def __init__(self):
self._logger = Logger.getLogger(self.__class__.__name__)
self._PACKAGE_NAME = "com.imo.android.imous"
self._PARSER_NAME = "IMO Parser"
self._MESSAGE_TYPE = "IMO Message"
self._VERSION = "9.8.0"
def analyze(self, dataSource, fileManager, context):
selfAccountAddress = None
accountDbs = AppSQLiteDB.findAppDatabases(dataSource, "accountdb.db", True, "com.imo.android.imous")
accountDbs = AppSQLiteDB.findAppDatabases(dataSource, "accountdb.db", True, self._PACKAGE_NAME)
for accountDb in accountDbs:
try:
accountResultSet = accountDb.runQuery("SELECT uid, name FROM account")
@@ -71,16 +76,26 @@ class IMOAnalyzer(general.AndroidComponentAnalyzer):
selfAccountAddress = Account.Address(accountResultSet.getString("uid"), accountResultSet.getString("name"))
except SQLException as ex:
self._logger.log(Level.SEVERE, "Error processing query result for account", ex)
self._logger.log(Level.WARNING, "Error processing query result for account", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
finally:
accountDb.close()
friendsDbs = AppSQLiteDB.findAppDatabases(dataSource, "imofriends.db", True, "com.imo.android.imous")
friendsDbs = AppSQLiteDB.findAppDatabases(dataSource, "imofriends.db", True, self._PACKAGE_NAME)
for friendsDb in friendsDbs:
try:
friendsDBHelper = CommunicationArtifactsHelper(Case.getCurrentCase().getSleuthkitCase(),
"IMO Parser", friendsDb.getDBFile(),
current_case = Case.getCurrentCaseThrows()
if selfAccountAddress is not None:
friendsDBHelper = CommunicationArtifactsHelper(current_case.getSleuthkitCase(),
self._PARSER_NAME,
friendsDb.getDBFile(),
Account.Type.IMO, Account.Type.IMO, selfAccountAddress )
else:
friendsDBHelper = CommunicationArtifactsHelper(current_case.getSleuthkitCase(),
self._PARSER_NAME,
friendsDb.getDBFile(),
Account.Type.IMO
)
contactsResultSet = friendsDb.runQuery("SELECT buid, name FROM friends")
if contactsResultSet is not None:
while contactsResultSet.next():
@@ -121,7 +136,7 @@ class IMOAnalyzer(general.AndroidComponentAnalyzer):
messageArtifact = friendsDBHelper.addMessage(
"IMO Message",
self._MESSAGE_TYPE,
direction,
fromAddress,
toAddress,
@@ -137,8 +152,16 @@ class IMOAnalyzer(general.AndroidComponentAnalyzer):
except SQLException as ex:
self._logger.log(Level.WARNING, "Error processing query result for IMO friends", ex)
except (TskCoreException, BlackboardException) as ex:
self._logger.log(Level.WARNING, "Failed to create IMO message artifacts.", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
except TskCoreException as ex:
self._logger.log(Level.SEVERE, "Failed to add IMO 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())
except NoCurrentCaseException as ex:
self._logger.log(Level.WARNING, "No case currently open.", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
finally:
friendsDb.close()

View File

@@ -0,0 +1,386 @@
"""
Autopsy Forensic Browser
Copyright 2019 Basis Technology Corp.
Contact: carrier <at> sleuthkit <dot> org
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from java.io import File
from java.lang import Class
from java.lang import ClassNotFoundException
from java.lang import Long
from java.lang import String
from java.sql import ResultSet
from java.sql import SQLException
from java.sql import Statement
from java.util.logging import Level
from java.util import ArrayList
from org.apache.commons.codec.binary import Base64
from org.sleuthkit.autopsy.casemodule import Case
from org.sleuthkit.autopsy.coreutils import Logger
from org.sleuthkit.autopsy.coreutils import MessageNotifyUtil
from org.sleuthkit.autopsy.coreutils import AppSQLiteDB
from org.sleuthkit.autopsy.datamodel import ContentUtils
from org.sleuthkit.autopsy.ingest import IngestJobContext
from org.sleuthkit.datamodel import AbstractFile
from org.sleuthkit.datamodel import BlackboardArtifact
from org.sleuthkit.datamodel import BlackboardAttribute
from org.sleuthkit.datamodel import Content
from org.sleuthkit.datamodel import TskCoreException
from org.sleuthkit.datamodel.Blackboard import BlackboardException
from org.sleuthkit.autopsy.casemodule import NoCurrentCaseException
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 TskContactsParser import TskContactsParser
from TskMessagesParser import TskMessagesParser
from TskCallLogsParser import TskCallLogsParser
import traceback
import general
class LineAnalyzer(general.AndroidComponentAnalyzer):
"""
Parses the Line App databases for TSK contacts & message artifacts.
"""
def __init__(self):
self._logger = Logger.getLogger(self.__class__.__name__)
self._LINE_PACKAGE_NAME = "jp.naver.line.android"
self._PARSER_NAME = "Line Parser"
self._VERSION = "9.15.1"
def analyze(self, dataSource, fileManager, context):
try:
contact_and_message_dbs = AppSQLiteDB.findAppDatabases(dataSource,
"naver_line", True, self._LINE_PACKAGE_NAME)
calllog_dbs = AppSQLiteDB.findAppDatabases(dataSource,
"call_history", True, self._LINE_PACKAGE_NAME)
for contact_and_message_db in contact_and_message_dbs:
current_case = Case.getCurrentCaseThrows()
helper = CommunicationArtifactsHelper(
current_case.getSleuthkitCase(), self._PARSER_NAME,
contact_and_message_db.getDBFile(), Account.Type.LINE)
self.parse_contacts(contact_and_message_db, helper)
self.parse_messages(contact_and_message_db, helper)
for calllog_db in calllog_dbs:
current_case = Case.getCurrentCaseThrows()
helper = CommunicationArtifactsHelper(
current_case.getSleuthkitCase(), self._PARSER_NAME,
calllog_db.getDBFile(), Account.Type.LINE)
self.parse_calllogs(dataSource, calllog_db, helper)
except NoCurrentCaseException as ex:
# Error parsing Line databases.
self._logger.log(Level.WARNING, "Error parsing the Line App Databases", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
for contact_and_message_db in contact_and_message_dbs:
contact_and_message_db.close()
for calllog_db in calllog_dbs:
calllog_db.close()
def parse_contacts(self, contacts_db, helper):
try:
contacts_parser = LineContactsParser(contacts_db)
while contacts_parser.next():
helper.addContact(
contacts_parser.get_account_name(),
contacts_parser.get_contact_name(),
contacts_parser.get_phone(),
contacts_parser.get_home_phone(),
contacts_parser.get_mobile_phone(),
contacts_parser.get_email()
)
contacts_parser.close()
except SQLException as ex:
self._logger.log(Level.WARNING, "Error parsing the Line App Database for contacts", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
except TskCoreException as ex:
#Error adding artifact to case database... case is not complete.
self._logger.log(Level.SEVERE,
"Error adding Line contact artifacts to the case database.", ex)
self._logger.log(Level.SEVERE, traceback.format_exc())
except BlackboardException as ex:
#Error posting notification to blackboard
self._logger.log(Level.WARNING,
"Error posting Line contact artifacts to blackboard.", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
def parse_calllogs(self, dataSource, calllogs_db, helper):
try:
calllogs_db.attachDatabase(
dataSource, "naver_line",
calllogs_db.getDBFile().getParentPath(), "naver")
calllog_parser = LineCallLogsParser(calllogs_db)
while calllog_parser.next():
helper.addCalllog(
calllog_parser.get_call_direction(),
calllog_parser.get_phone_number_from(),
calllog_parser.get_phone_number_to(),
calllog_parser.get_call_start_date_time(),
calllog_parser.get_call_end_date_time(),
calllog_parser.get_call_type()
)
calllog_parser.close()
except SQLException as ex:
self._logger.log(Level.WARNING, "Error parsing the Line App Database for calllogs", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
except TskCoreException as ex:
#Error adding artifact to case database... case is not complete.
self._logger.log(Level.SEVERE,
"Error adding Line calllog artifacts to the case database.", ex)
self._logger.log(Level.SEVERE, traceback.format_exc())
except BlackboardException as ex:
#Error posting notification to blackboard
self._logger.log(Level.WARNING,
"Error posting Line calllog artifacts to blackboard.", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
def parse_messages(self, messages_db, helper):
try:
messages_parser = LineMessagesParser(messages_db)
while messages_parser.next():
helper.addMessage(
messages_parser.get_message_type(),
messages_parser.get_message_direction(),
messages_parser.get_phone_number_from(),
messages_parser.get_phone_number_to(),
messages_parser.get_message_date_time(),
messages_parser.get_message_read_status(),
messages_parser.get_message_subject(),
messages_parser.get_message_text(),
messages_parser.get_thread_id()
)
messages_parser.close()
except SQLException as ex:
self._logger.log(Level.WARNING, "Error parsing the Line App Database for messages.", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
except TskCoreException as ex:
#Error adding artifact to case database... case is not complete.
self._logger.log(Level.SEVERE,
"Error adding Line message artifacts to the case database.", ex)
self._logger.log(Level.SEVERE, traceback.format_exc())
except BlackboardException as ex:
#Error posting notification to blackboard
self._logger.log(Level.WARNING,
"Error posting Line message artifacts to blackboard.", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
class LineCallLogsParser(TskCallLogsParser):
"""
Parses out TSK_CALLLOG information from the Line database.
TSK_CALLLOG fields that are not in the line database are given
a default value inherited from the super class.
"""
def __init__(self, calllog_db):
super(LineCallLogsParser, self).__init__(calllog_db.runQuery(
"""
SELECT substr(CallH.call_type, -1) AS direction,
CallH.start_time AS start_time,
CallH.end_time AS end_time,
ConT.server_name AS name,
CallH.voip_type AS call_type,
ConT.m_id
FROM call_history AS CallH
JOIN naver.contacts AS ConT
ON CallH.caller_mid = ConT.m_id
"""
)
)
self._OUTGOING_CALL_TYPE = "O"
self._INCOMING_CALL_TYPE = "I"
self._VIDEO_CALL_TYPE = "V"
self._AUDIO_CALL_TYPE = "A"
def get_call_direction(self):
direction = self.result_set.getString("direction")
if direction == self._OUTGOING_CALL_TYPE:
return self.OUTGOING_CALL
return self.INCOMING_CALL
def get_call_start_date_time(self):
try:
return long(self.result_set.getString("start_time")) / 1000
except ValueError as ve:
return super(LineCallLogsParser, self).get_call_start_date_time()
def get_call_end_date_time(self):
try:
return long(self.result_set.getString("end_time")) / 1000
except ValueError as ve:
return super(LineCallLogsParser, self).get_call_end_date_time()
def get_phone_number_to(self):
if self.get_call_direction() == self.OUTGOING_CALL:
return Account.Address(self.result_set.getString("m_id"),
self.result_set.getString("name"))
return super(LineCallLogsParser, self).get_phone_number_to()
def get_phone_number_from(self):
if self.get_call_direction() == self.INCOMING_CALL:
return Account.Address(self.result_set.getString("m_id"),
self.result_set.getString("name"))
return super(LineCallLogsParser, self).get_phone_number_from()
def get_call_type(self):
if self.result_set.getString("call_type") == self._VIDEO_CALL_TYPE:
return self.VIDEO_CALL
if self.result_set.getString("call_type") == self._AUDIO_CALL_TYPE:
return self.AUDIO_CALL
return super(LineCallLogsParser, self).get_call_type()
class LineContactsParser(TskContactsParser):
"""
Parses out TSK_CONTACT information from the Line database.
TSK_CONTACT fields that are not in the line database are given
a default value inherited from the super class.
"""
def __init__(self, contact_db):
super(LineContactsParser, self).__init__(contact_db.runQuery(
"""
SELECT m_id,
server_name
FROM contacts
"""
)
)
def get_account_name(self):
return self.result_set.getString("m_id")
def get_contact_name(self):
return self.result_set.getString("server_name")
class LineMessagesParser(TskMessagesParser):
"""
Parse out TSK_MESSAGE information from the Line database.
TSK_MESSAGE fields that are not in the line database are given
a default value inherited from the super class.
"""
def __init__(self, message_db):
super(LineMessagesParser, self).__init__(message_db.runQuery(
"""
SELECT contact_list_with_groups.name,
contact_list_with_groups.id,
contact_list_with_groups.members,
contact_list_with_groups.member_names,
CH.from_mid,
C.server_name AS from_name,
CH.content,
CH.created_time,
CH.attachement_type,
CH.attachement_local_uri,
CH.status
FROM (SELECT G.name,
group_members.id,
group_members.members,
group_members.member_names
FROM (SELECT id,
group_concat(M.m_id) AS members,
group_concat(replace(C.server_name,
",",
"")) as member_names
FROM membership AS M
JOIN contacts as C
ON M.m_id = C.m_id
GROUP BY id) AS group_members
JOIN groups AS G
ON G.id = group_members.id
UNION
SELECT server_name,
m_id,
NULL,
NULL
FROM contacts) AS contact_list_with_groups
JOIN chat_history AS CH
ON CH.chat_id = contact_list_with_groups.id
LEFT JOIN contacts as C
ON C.m_id = CH.from_mid
WHERE attachement_type != 6
"""
)
)
self._LINE_MESSAGE_TYPE = "Line Message"
#From the limited test data, it appeared that incoming
#was only associated with a 1 status. Status # 3 and 7
#was only associated with outgoing.
self._INCOMING_MESSAGE_TYPE = 1
def get_message_type(self):
return self._LINE_MESSAGE_TYPE
def get_message_date_time(self):
created_time = self.result_set.getString("created_time")
try:
#Get time in seconds (created_time is stored in ms from epoch)
return long(created_time) / 1000
except ValueError as ve:
return super(LineMessagesParser, self).get_message_date_time()
def get_message_text(self):
content = self.result_set.getString("content")
attachment_uri = self.result_set.getString("attachement_local_uri")
if attachment_uri is not None and content is not None:
return general.appendAttachmentList(content, [attachment_uri])
elif attachment_uri is not None and content is None:
return general.appendAttachmentList("", [attachment_uri])
return content
def get_message_direction(self):
if self.result_set.getInt("status") == self._INCOMING_MESSAGE_TYPE:
return self.INCOMING
return self.OUTGOING
def get_phone_number_from(self):
if self.get_message_direction() == self.INCOMING:
from_mid = self.result_set.getString("from_mid")
if from_mid is not None:
return Account.Address(from_mid,
self.result_set.getString("from_name"))
return super(LineMessagesParser, self).get_phone_number_from()
def get_phone_number_to(self):
if self.get_message_direction() == self.OUTGOING:
group = self.result_set.getString("members")
if group is not None:
group = group.split(",")
names = self.result_set.getString("member_names").split(",")
recipients = []
for recipient_id, recipient_name in zip(group, names):
recipients.append(Account.Address(recipient_id, recipient_name))
return recipients
return Account.Address(self.result_set.getString("id"),
self.result_set.getString("name"))
return super(LineMessagesParser, self).get_phone_number_to()
def get_thread_id(self):
members = self.result_set.getString("members")
if members is not None:
return self.result_set.getString("id")
return super(LineMessagesParser, self).get_thread_id()

View File

@@ -50,6 +50,11 @@ import imo
import xender
import zapya
import shareit
import viber
import skype
import line
import whatsapp
import textnow
import sbrowser
import operabrowser
import oruxmaps
@@ -101,6 +106,8 @@ class AndroidIngestModule(DataSourceIngestModule):
googlemaplocation.GoogleMapLocationAnalyzer(), browserlocation.BrowserLocationAnalyzer(),
cachelocation.CacheLocationAnalyzer(), imo.IMOAnalyzer(),
xender.XenderAnalyzer(), zapya.ZapyaAnalyzer(), shareit.ShareItAnalyzer(),
line.LineAnalyzer(), whatsapp.WhatsAppAnalyzer(),
textnow.TextNowAnalyzer(), skype.SkypeAnalyzer(), viber.ViberAnalyzer(),
sbrowser.SBrowserAnalyzer(), operabrowser.OperaAnalyzer(),
oruxmaps.OruxMapsAnalyzer(),
installedapps.InstalledApplicationsAnalyzer()]

View File

@@ -29,6 +29,7 @@ from java.util.logging import Level
from java.util import ArrayList
from org.apache.commons.codec.binary import Base64
from org.sleuthkit.autopsy.casemodule import Case
from org.sleuthkit.autopsy.casemodule import NoCurrentCaseException
from org.sleuthkit.autopsy.coreutils import Logger
from org.sleuthkit.autopsy.coreutils import MessageNotifyUtil
from org.sleuthkit.autopsy.coreutils import AppSQLiteDB

View File

@@ -0,0 +1,505 @@
"""
Autopsy Forensic Browser
Copyright 2019 Basis Technology Corp.
Contact: carrier <at> sleuthkit <dot> org
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from java.io import File
from java.lang import Class
from java.lang import ClassNotFoundException
from java.lang import Long
from java.lang import String
from java.sql import ResultSet
from java.sql import SQLException
from java.sql import Statement
from java.util.logging import Level
from java.util import ArrayList
from org.apache.commons.codec.binary import Base64
from org.sleuthkit.autopsy.casemodule import Case
from org.sleuthkit.autopsy.coreutils import Logger
from org.sleuthkit.autopsy.coreutils import MessageNotifyUtil
from org.sleuthkit.autopsy.coreutils import AppSQLiteDB
from org.sleuthkit.autopsy.datamodel import ContentUtils
from org.sleuthkit.autopsy.ingest import IngestJobContext
from org.sleuthkit.datamodel import AbstractFile
from org.sleuthkit.datamodel import BlackboardArtifact
from org.sleuthkit.datamodel import BlackboardAttribute
from org.sleuthkit.datamodel import Content
from org.sleuthkit.datamodel import TskCoreException
from org.sleuthkit.datamodel.Blackboard import BlackboardException
from org.sleuthkit.autopsy.casemodule import NoCurrentCaseException
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 TskMessagesParser import TskMessagesParser
from TskContactsParser import TskContactsParser
from TskCallLogsParser import TskCallLogsParser
import traceback
import general
class SkypeAnalyzer(general.AndroidComponentAnalyzer):
"""
Parses the Skype App databases for TSK contacts, message
and calllog artifacts.
About version 8.15.0.428 (9/17/2019) Skype database:
- There are 4 tables this parser uses:
1) person - this table appears to hold all contacts known to the user.
2) user - this table holds information pertaining to the user.
3) particiapnt - Yes, that is not a typo. This table maps group chat
ids to skype ids (1 to many).
4) chatItem - This table contains all messages. It maps the group id or
skype id (for 1 to 1 communication) to the message content
and metadata. Either the group id or skype id is stored in
a column named 'conversation_link'.
More info and implementation details:
- The person table does not include groups. To get
all 1 to 1 communications, we could simply join the person and chatItem tables.
This would mean we'd need to do a second pass to get all the group information
as they would be excluded in the join. Since the chatItem table stores both the
group id or skype_id in one column, an implementation decision was made to union
the person and particiapnt table together so that all rows are matched in one join
with chatItem. This result is consistently labeled contact_list_with_groups in the
following queries.
- In order to keep the formatting of the name consistent throughout each query,
a _format_user_name() function was created to encapsulate the CASE statement
that was being shared across them. Refer to the method for more details.
"""
def __init__(self):
self._logger = Logger.getLogger(self.__class__.__name__)
self._SKYPE_PACKAGE_NAME = "com.skype.raider"
self._PARSER_NAME = "Skype Parser"
self._VERSION = "8.15.0.428"
def get_user_account(self, skype_db):
account_query_result = skype_db.runQuery(
"""
SELECT entry_id,
"""+_format_user_name()+""" AS name
FROM user
"""
)
if account_query_result is not None and account_query_result.next():
return Account.Address(account_query_result.getString("entry_id"),
account_query_result.getString("name"))
return None
def analyze(self, dataSource, fileManager, context):
#Skype databases are of the form: live:XYZ.db, where
#XYZ is the skype id of the user. The following search
#does a generic substring match for 'live' in the skype
#package.
skype_dbs = AppSQLiteDB.findAppDatabases(dataSource,
"live:", False, self._SKYPE_PACKAGE_NAME)
try:
for skype_db in skype_dbs:
#Attempt to get the user account id from the database
user_account_instance = None
try:
user_account_instance = self.get_user_account(skype_db)
except SQLException as ex:
self._logger.log(Level.WARNING,
"Error querying for the user account in the Skype db.", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
current_case = Case.getCurrentCaseThrows()
if user_account_instance is None:
helper = CommunicationArtifactsHelper(
current_case.getSleuthkitCase(), self._PARSER_NAME,
skype_db.getDBFile(), Account.Type.SKYPE
)
else:
helper = CommunicationArtifactsHelper(
current_case.getSleuthkitCase(), self._PARSER_NAME,
skype_db.getDBFile(), Account.Type.SKYPE,
Account.Type.SKYPE, user_account_instance
)
self.parse_contacts(skype_db, helper)
self.parse_calllogs(skype_db, helper)
self.parse_messages(skype_db, helper)
except NoCurrentCaseException as ex:
self._logger.log(Level.WARNING, "No case currently open.", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
for skype_db in skype_dbs:
skype_db.close()
def parse_contacts(self, skype_db, helper):
#Query for contacts and iterate row by row adding
#each contact artifact
try:
contacts_parser = SkypeContactsParser(skype_db)
while contacts_parser.next():
helper.addContact(
contacts_parser.get_account_name(),
contacts_parser.get_contact_name(),
contacts_parser.get_phone(),
contacts_parser.get_home_phone(),
contacts_parser.get_mobile_phone(),
contacts_parser.get_email()
)
contacts_parser.close()
except SQLException as ex:
#Error parsing Skype db
self._logger.log(Level.WARNING,
"Error parsing contact database for call logs artifacts.", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
except TskCoreException as ex:
#Severe error trying to add to case database.. case is not complete.
#These exceptions are thrown by the CommunicationArtifactsHelper.
self._logger.log(Level.SEVERE,
"Failed to add contact artifacts to the case database.", ex)
self._logger.log(Level.SEVERE, traceback.format_exc())
except BlackboardException as ex:
#Failed to post notification to blackboard
self._logger.log(Level.WARNING,
"Failed to post contact artifact to the blackboard", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
def parse_calllogs(self, skype_db, helper):
#Query for call logs and iterate row by row adding
#each call log artifact
try:
calllog_parser = SkypeCallLogsParser(skype_db)
while calllog_parser.next():
helper.addCalllog(
calllog_parser.get_call_direction(),
calllog_parser.get_phone_number_from(),
calllog_parser.get_phone_number_to(),
calllog_parser.get_call_start_date_time(),
calllog_parser.get_call_end_date_time(),
calllog_parser.get_call_type()
)
calllog_parser.close()
except SQLException as ex:
#Error parsing Skype db
self._logger.log(Level.WARNING,
"Error parsing Skype database for call logs artifacts.", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
except TskCoreException as ex:
#Severe error trying to add to case database.. case is not complete.
#These exceptions are thrown by the CommunicationArtifactsHelper.
self._logger.log(Level.SEVERE,
"Failed to add call log artifacts to the case database.", ex)
self._logger.log(Level.SEVERE, traceback.format_exc())
except BlackboardException as ex:
#Failed to post notification to blackboard
self._logger.log(Level.WARNING,
"Failed to post call log artifact to the blackboard", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
def parse_messages(self, skype_db, helper):
#Query for messages and iterate row by row adding
#each message artifact
try:
messages_parser = SkypeMessagesParser(skype_db)
while messages_parser.next():
helper.addMessage(
messages_parser.get_message_type(),
messages_parser.get_message_direction(),
messages_parser.get_phone_number_from(),
messages_parser.get_phone_number_to(),
messages_parser.get_message_date_time(),
messages_parser.get_message_read_status(),
messages_parser.get_message_subject(),
messages_parser.get_message_text(),
messages_parser.get_thread_id()
)
messages_parser.close()
except SQLException as ex:
#Error parsing Skype db
self._logger.log(Level.WARNING,
"Error parsing Skype database for message artifacts.", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
except TskCoreException as ex:
#Severe error trying to add to case database.. case is not complete.
#These exceptions are thrown by the CommunicationArtifactsHelper.
self._logger.log(Level.SEVERE,
"Failed to add message artifacts to the case database.", ex)
self._logger.log(Level.SEVERE, traceback.format_exc())
except BlackboardException as ex:
#Failed to post notification to blackboard
self._logger.log(Level.WARNING,
"Failed to post message artifact to the blackboard", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
class SkypeCallLogsParser(TskCallLogsParser):
"""
Extracts TSK_CALLLOG information from the Skype database.
TSK_CALLLOG fields that are not in the Skype database are given
a default value inherited from the super class.
"""
def __init__(self, calllog_db):
"""
Big picture:
The query below creates a contacts_list_with_groups table, which
represents the recipient info. A chatItem record holds ids for
both the recipient and sender. The first join onto chatItem fills
in the blanks for the recipients. The second join back onto person
handles the sender info. The result is a table with all of the
communication details.
Implementation details:
- message_type w/ value 3 appeared to be the call type, regardless
of if it was audio or video.
"""
super(SkypeCallLogsParser, self).__init__(calllog_db.runQuery(
"""
SELECT contacts_list_with_groups.conversation_id,
contacts_list_with_groups.participant_ids,
contacts_list_with_groups.participants,
time,
duration,
is_sender_me,
person_id as sender_id,
sender_name.name as sender_name
FROM (SELECT conversation_id,
Group_concat(person_id) AS participant_ids,
Group_concat("""+_format_user_name()+""") AS participants
FROM particiapnt AS PART
JOIN person AS P
ON PART.person_id = P.entry_id
GROUP BY conversation_id
UNION
SELECT entry_id,
NULL,
"""+_format_user_name()+""" AS participant
FROM person) AS contacts_list_with_groups
JOIN chatitem AS C
ON C.conversation_link = contacts_list_with_groups.conversation_id
JOIN (SELECT entry_id as id,
"""+_format_user_name()+""" AS name
FROM person
UNION
SELECT entry_id as id,
"""+_format_user_name()+""" AS name
FROM user) AS sender_name
ON sender_name.id = C.person_id
WHERE message_type == 3
"""
)
)
self._INCOMING_CALL_TYPE = 0
self._OUTGOING_CALL_TYPE = 1
def get_phone_number_from(self):
if self.get_call_direction() == self.INCOMING_CALL:
return Account.Address(self.result_set.getString("sender_id"),
self.result_set.getString("sender_name"))
def get_phone_number_to(self):
if self.get_call_direction() == self.OUTGOING_CALL:
group_ids = self.result_set.getString("participant_ids")
name = self.result_set.getString("participants")
if group_ids is not None:
group_ids = group_ids.split(",")
name = name.split(",")
recipients = []
for person_id, person_name in zip(group_ids, name):
recipients.append(Account.Address(person_id, person_name))
return recipients
return Account.Address(self.result_set.getString("conversation_id"), name)
return super(SkypeCallLogsParser, self).get_phone_number_to()
def get_call_direction(self):
direction = self.result_set.getInt("is_sender_me")
if direction == self._INCOMING_CALL_TYPE:
return self.INCOMING_CALL
if direction == self._OUTGOING_CALL_TYPE:
return self.OUTGOING_CALL
return super(SkypeCallLogsParser, self).get_call_direction()
def get_call_start_date_time(self):
return self.result_set.getLong("time") / 1000
def get_call_end_date_time(self):
start = self.get_call_start_date_time()
duration = self.result_set.getInt("duration") / 1000
return start + duration
class SkypeContactsParser(TskContactsParser):
"""
Extracts TSK_CONTACT information from the Skype database.
TSK_CONTACT fields that are not in the Skype database are given
a default value inherited from the super class.
"""
def __init__(self, contact_db):
super(SkypeContactsParser, self).__init__(contact_db.runQuery(
"""
SELECT entry_id,
"""+_format_user_name()+""" AS name
FROM person
"""
)
)
def get_account_name(self):
return self.result_set.getString("entry_id")
def get_contact_name(self):
return self.result_set.getString("name")
class SkypeMessagesParser(TskMessagesParser):
"""
Extract TSK_MESSAGE information from the Skype database.
TSK_CONTACT fields that are not in the Skype database are given
a default value inherited from the super class.
"""
def __init__(self, message_db):
"""
This query is very similar to the call logs query, the only difference is
it grabs more columns in the SELECT and excludes message_types which have
the call type value (3).
"""
super(SkypeMessagesParser, self).__init__(message_db.runQuery(
"""
SELECT contacts_list_with_groups.conversation_id,
contacts_list_with_groups.participant_ids,
contacts_list_with_groups.participants,
time,
content,
device_gallery_path,
is_sender_me,
person_id as sender_id,
sender_name.name AS sender_name
FROM (SELECT conversation_id,
Group_concat(person_id) AS participant_ids,
Group_concat("""+_format_user_name()+""") AS participants
FROM particiapnt AS PART
JOIN person AS P
ON PART.person_id = P.entry_id
GROUP BY conversation_id
UNION
SELECT entry_id as conversation_id,
NULL,
"""+_format_user_name()+""" AS participant
FROM person) AS contacts_list_with_groups
JOIN chatitem AS C
ON C.conversation_link = contacts_list_with_groups.conversation_id
JOIN (SELECT entry_id as id,
"""+_format_user_name()+""" AS name
FROM person
UNION
SELECT entry_id as id,
"""+_format_user_name()+""" AS name
FROM user) AS sender_name
ON sender_name.id = C.person_id
WHERE message_type != 3
"""
)
)
self._SKYPE_MESSAGE_TYPE = "Skype Message"
self._OUTGOING_MESSAGE_TYPE = 1
self._INCOMING_MESSAGE_TYPE = 0
def get_message_type(self):
return self._SKYPE_MESSAGE_TYPE
def get_phone_number_from(self):
if self.get_message_direction() == self.INCOMING:
return Account.Address(self.result_set.getString("sender_id"),
self.result_set.getString("sender_name"))
return super(SkypeMessagesParser, self).get_phone_number_from()
def get_message_direction(self):
direction = self.result_set.getInt("is_sender_me")
if direction == self._OUTGOING_MESSAGE_TYPE:
return self.OUTGOING
if direction == self._INCOMING_MESSAGE_TYPE:
return self.INCOMING
return super(SkypeMessagesParser, self).get_message_direction()
def get_phone_number_to(self):
if self.get_message_direction() == self.OUTGOING:
group_ids = self.result_set.getString("participant_ids")
names = self.result_set.getString("participants")
if group_ids is not None:
group_ids = group_ids.split(",")
names = names.split(",")
recipients = []
for participant_id, participant_name in zip(group_ids, names):
recipients.append(Account.Address(participant_id, participant_name))
return recipients
return Account.Address(self.result_set.getString("conversation_id"), names)
return super(SkypeMessagesParser, self).get_phone_number_to()
def get_message_date_time(self):
date = self.result_set.getLong("time")
return date / 1000
def get_message_text(self):
content = self.result_set.getString("content")
if content is not None:
file_path = self.result_set.getString("device_gallery_path")
#if a file name and file path are associated with a message, append it
if file_path is not None:
return general.appendAttachmentList(content, [file_path])
return content
return super(SkypeMessagesParser, self).get_message_text()
def get_thread_id(self):
group_ids = self.result_set.getString("participant_ids")
if group_ids is not None:
return self.result_set.getString("conversation_id")
return super(SkypeMessagesParser, self).get_thread_id()
def _format_user_name():
"""
This CASE SQL statement is used in many queries to
format the names of users. For a user, there is a first_name
column and a last_name column. Some of these columns can be null
and our goal is to produce the cleanest data possible. In the event
that both the first and last name columns are null, we return the skype_id
which is stored in the database as 'entry_id'. Commas are removed from the name
so that we can concatenate names into a comma seperate list for group chats.
"""
return """
CASE
WHEN Ifnull(first_name, "") == "" AND Ifnull(last_name, "") == "" THEN entry_id
WHEN first_name is NULL THEN replace(last_name, ",", "")
WHEN last_name is NULL THEN replace(first_name, ",", "")
ELSE replace(first_name, ",", "") || " " || replace(last_name, ",", "")
END
"""

View File

@@ -0,0 +1,392 @@
"""
Autopsy Forensic Browser
Copyright 2019 Basis Technology Corp.
Contact: carrier <at> sleuthkit <dot> org
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from java.io import File
from java.lang import Class
from java.lang import ClassNotFoundException
from java.lang import Long
from java.lang import String
from java.sql import ResultSet
from java.sql import SQLException
from java.sql import Statement
from java.util.logging import Level
from java.util import ArrayList
from org.apache.commons.codec.binary import Base64
from org.sleuthkit.autopsy.casemodule import Case
from org.sleuthkit.autopsy.coreutils import Logger
from org.sleuthkit.autopsy.coreutils import MessageNotifyUtil
from org.sleuthkit.autopsy.coreutils import AppSQLiteDB
from org.sleuthkit.autopsy.datamodel import ContentUtils
from org.sleuthkit.autopsy.ingest import IngestJobContext
from org.sleuthkit.datamodel import AbstractFile
from org.sleuthkit.datamodel import BlackboardArtifact
from org.sleuthkit.datamodel import BlackboardAttribute
from org.sleuthkit.datamodel import Content
from org.sleuthkit.datamodel import TskCoreException
from org.sleuthkit.datamodel.Blackboard import BlackboardException
from org.sleuthkit.autopsy.casemodule import NoCurrentCaseException
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 TskMessagesParser import TskMessagesParser
from TskContactsParser import TskContactsParser
from TskCallLogsParser import TskCallLogsParser
import traceback
import general
class TextNowAnalyzer(general.AndroidComponentAnalyzer):
"""
Parses the TextNow App databases for TSK contacts, message
and calllog artifacts.
The TextNow database in v6.41.0.2 is structured as follows:
- A messages table, which stores messages from/to a number
- A contacts table, which stores phone numbers
- A groups table, which stores each group the device owner is a part of
- A group_members table, which stores who is in each group
The messages table contains both call logs and messages, with a type
column differentiating the two.
"""
def __init__(self):
self._logger = Logger.getLogger(self.__class__.__name__)
self._TEXTNOW_PACKAGE_NAME = "com.enflick.android.TextNow"
self._PARSER_NAME = "TextNow Parser"
self._VERSION = "6.41.0.2"
def analyze(self, dataSource, fileManager, context):
"""
Extract, Transform and Load all messages, contacts and
calllogs from the TextNow databases.
"""
textnow_dbs = AppSQLiteDB.findAppDatabases(dataSource,
"textnow_data.db", True, self._TEXTNOW_PACKAGE_NAME)
try:
for textnow_db in textnow_dbs:
current_case = Case.getCurrentCaseThrows()
helper = CommunicationArtifactsHelper(
current_case.getSleuthkitCase(), self._PARSER_NAME,
textnow_db.getDBFile(), Account.Type.TEXTNOW
)
self.parse_contacts(textnow_db, helper)
self.parse_calllogs(textnow_db, helper)
self.parse_messages(textnow_db, helper)
except NoCurrentCaseException as ex:
self._logger.log(Level.WARNING, "No case currently open.", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
for textnow_db in textnow_dbs:
textnow_db.close()
def parse_contacts(self, textnow_db, helper):
#Query for contacts and iterate row by row adding
#each contact artifact
try:
contacts_parser = TextNowContactsParser(textnow_db)
while contacts_parser.next():
helper.addContact(
contacts_parser.get_account_name(),
contacts_parser.get_contact_name(),
contacts_parser.get_phone(),
contacts_parser.get_home_phone(),
contacts_parser.get_mobile_phone(),
contacts_parser.get_email()
)
contacts_parser.close()
except SQLException as ex:
#Error parsing TextNow db
self._logger.log(Level.WARNING, "Error parsing TextNow databases for contacts", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
except TskCoreException as ex:
#Error adding artifacts to the case database.. case database is not complete.
self._logger.log(Level.SEVERE,
"Error adding TextNow contacts artifacts to the case database", ex)
self._logger.log(Level.SEVERE, traceback.format_exc())
except BlackboardException as ex:
#Error posting notification to blackboard...
self._logger.log(Level.WARNING,
"Error posting TextNow contacts artifact to the blackboard", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
def parse_calllogs(self, textnow_db, helper):
#Query for call logs and iterate row by row adding
#each call log artifact
try:
calllog_parser = TextNowCallLogsParser(textnow_db)
while calllog_parser.next():
helper.addCalllog(
calllog_parser.get_call_direction(),
calllog_parser.get_phone_number_from(),
calllog_parser.get_phone_number_to(),
calllog_parser.get_call_start_date_time(),
calllog_parser.get_call_end_date_time(),
calllog_parser.get_call_type()
)
calllog_parser.close()
except SQLException as ex:
self._logger.log(Level.WARNING, "Error parsing TextNow databases for calllogs", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
except TskCoreException as ex:
#Error adding artifacts to the case database.. case database is not complete.
self._logger.log(Level.SEVERE,
"Error adding TextNow call log artifacts to the case database", ex)
self._logger.log(Level.SEVERE, traceback.format_exc())
except BlackboardException as ex:
#Error posting notification to blackboard...
self._logger.log(Level.WARNING,
"Error posting TextNow call log artifact to the blackboard", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
def parse_messages(self, textnow_db, helper):
#Query for messages and iterate row by row adding
#each message artifact
try:
messages_parser = TextNowMessagesParser(textnow_db)
while messages_parser.next():
helper.addMessage(
messages_parser.get_message_type(),
messages_parser.get_message_direction(),
messages_parser.get_phone_number_from(),
messages_parser.get_phone_number_to(),
messages_parser.get_message_date_time(),
messages_parser.get_message_read_status(),
messages_parser.get_message_subject(),
messages_parser.get_message_text(),
messages_parser.get_thread_id()
)
messages_parser.close()
except SQLException as ex:
#Error parsing TextNow db
self._logger.log(Level.WARNING, "Error parsing TextNow databases for messages.", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
except TskCoreException as ex:
#Error adding artifacts to the case database.. case database is not complete.
self._logger.log(Level.SEVERE,
"Error adding TextNow messages artifacts to the case database", ex)
self._logger.log(Level.SEVERE, traceback.format_exc())
except BlackboardException as ex:
#Error posting notification to blackboard...
self._logger.log(Level.WARNING,
"Error posting TextNow messages artifact to the blackboard", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
class TextNowCallLogsParser(TskCallLogsParser):
"""
Extracts TSK_CALLLOG information from the TextNow database.
TSK_CALLLOG fields that are not in the TextNow database are given
a default value inherited from the super class.
"""
def __init__(self, calllog_db):
"""
message_type of 100 or 102 are for calls (audio, video)
"""
super(TextNowCallLogsParser, self).__init__(calllog_db.runQuery(
"""
SELECT contact_value AS num,
message_direction AS direction,
message_text AS duration,
date AS datetime
FROM messages AS M
WHERE message_type IN ( 100, 102 )
"""
)
)
self._INCOMING_CALL_TYPE = 1
self._OUTGOING_CALL_TYPE = 2
def get_phone_number_from(self):
if self.get_call_direction() == self.OUTGOING_CALL:
return super(TextNowCallLogsParser, self).get_phone_number_from()
return Account.Address(self.result_set.getString("num"),
self.result_set.getString("num"))
def get_phone_number_to(self):
if self.get_call_direction() == self.INCOMING_CALL:
return super(TextNowCallLogsParser, self).get_phone_number_to()
return Account.Address(self.result_set.getString("num"),
self.result_set.getString("num"))
def get_call_direction(self):
if self.result_set.getInt("direction") == self._INCOMING_CALL_TYPE:
return self.INCOMING_CALL
return self.OUTGOING_CALL
def get_call_start_date_time(self):
return self.result_set.getLong("datetime") / 1000
def get_call_end_date_time(self):
start = self.get_call_start_date_time()
duration = self.result_set.getString("duration")
try:
return start + long(duration)
except ValueError as ve:
return super(TextNowCallLogsParser, self).get_call_end_date_time()
class TextNowContactsParser(TskContactsParser):
"""
Extracts TSK_CONTACT information from the TextNow database.
TSK_CONTACT fields that are not in the TextNow database are given
a default value inherited from the super class.
"""
def __init__(self, contact_db):
super(TextNowContactsParser, self).__init__(contact_db.runQuery(
"""
SELECT C.contact_value AS number,
CASE
WHEN contact_name IS NULL THEN contact_value
WHEN contact_name == "" THEN contact_value
ELSE contact_name
END name
FROM contacts AS C
"""
)
)
def get_account_name(self):
return self.result_set.getString("number")
def get_contact_name(self):
return self.result_set.getString("name")
def get_phone(self):
return self.result_set.getString("number")
class TextNowMessagesParser(TskMessagesParser):
"""
Extract TSK_MESSAGE information from the TextNow database.
TSK_CONTACT fields that are not in the TextNow database are given
a default value inherited from the super class.
"""
def __init__(self, message_db):
"""
The query below does the following:
- The group_info inner query creates a comma seperated list of group recipients
for each group. This result is then joined on the groups table to get the thread id.
- The contacts table is unioned with this result so we have a complete map
of "from" phone_numbers -> recipients (group or single). This is the
'to_from_map' inner query.
- Finally, the to_from_map results are joined with the messages table to get all
of the communication details.
"""
super(TextNowMessagesParser, self).__init__(message_db.runQuery(
"""
SELECT CASE
WHEN message_direction == 2 THEN ""
WHEN to_addresses IS NULL THEN M.contact_value
ELSE contact_name
end from_address,
CASE
WHEN message_direction == 1 THEN ""
WHEN to_addresses IS NULL THEN M.contact_value
ELSE to_addresses
end to_address,
message_direction,
message_text,
M.READ,
M.date,
M.attach,
thread_id
FROM (SELECT group_info.contact_value,
group_info.to_addresses,
G.contact_value AS thread_id
FROM (SELECT GM.contact_value,
Group_concat(GM.member_contact_value) AS to_addresses
FROM group_members AS GM
GROUP BY GM.contact_value) AS group_info
JOIN groups AS G
ON G.contact_value = group_info.contact_value
UNION
SELECT c.contact_value,
NULL,
"-1"
FROM contacts AS c) AS to_from_map
JOIN messages AS M
ON M.contact_value = to_from_map.contact_value
WHERE message_type NOT IN ( 102, 100 )
"""
)
)
self._TEXTNOW_MESSAGE_TYPE = "TextNow Message"
self._INCOMING_MESSAGE_TYPE = 1
self._OUTGOING_MESSAGE_TYPE = 2
self._UNKNOWN_THREAD_ID = "-1"
def get_message_type(self):
return self._TEXTNOW_MESSAGE_TYPE
def get_phone_number_from(self):
if self.result_set.getString("from_address") == "":
return super(TextNowMessagesParser, self).get_phone_number_from()
return Account.Address(self.result_set.getString("from_address"),
self.result_set.getString("from_address"))
def get_message_direction(self):
direction = self.result_set.getInt("message_direction")
if direction == self._INCOMING_MESSAGE_TYPE:
return self.INCOMING
return self.OUTGOING
def get_phone_number_to(self):
if self.result_set.getString("to_address") == "":
return super(TextNowMessagesParser, self).get_phone_number_to()
recipients = self.result_set.getString("to_address").split(",")
recipient_accounts = []
for recipient in recipients:
recipient_accounts.append(Account.Address(recipient, recipient))
return recipient_accounts
def get_message_date_time(self):
#convert ms to s
return self.result_set.getLong("date") / 1000;
def get_message_read_status(self):
read = self.result_set.getBoolean("read")
if self.get_message_direction() == self.INCOMING:
if read == True:
return self.READ
return self.UNREAD
#read status for outgoing messages cannot be determined, give default
return super(TextNowMessagesParser, self).get_message_read_status()
def get_message_text(self):
text = self.result_set.getString("message_text")
attachment = self.result_set.getString("attach")
if attachment != "":
text = general.appendAttachmentList(text, [attachment])
return text
def get_thread_id(self):
thread_id = self.result_set.getString("thread_id")
if thread_id == self._UNKNOWN_THREAD_ID:
return super(TextNowMessagesParser, self).get_thread_id()
return thread_id

View File

@@ -0,0 +1,376 @@
"""
Autopsy Forensic Browser
Copyright 2019 Basis Technology Corp.
Contact: carrier <at> sleuthkit <dot> org
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from java.io import File
from java.lang import Class
from java.lang import ClassNotFoundException
from java.lang import Long
from java.lang import String
from java.sql import ResultSet
from java.sql import SQLException
from java.sql import Statement
from java.util.logging import Level
from java.util import ArrayList
from org.apache.commons.codec.binary import Base64
from org.sleuthkit.autopsy.casemodule import Case
from org.sleuthkit.autopsy.coreutils import Logger
from org.sleuthkit.autopsy.coreutils import MessageNotifyUtil
from org.sleuthkit.autopsy.coreutils import AppSQLiteDB
from org.sleuthkit.autopsy.datamodel import ContentUtils
from org.sleuthkit.autopsy.ingest import IngestJobContext
from org.sleuthkit.datamodel import AbstractFile
from org.sleuthkit.datamodel import BlackboardArtifact
from org.sleuthkit.datamodel import BlackboardAttribute
from org.sleuthkit.datamodel import Content
from org.sleuthkit.datamodel import TskCoreException
from org.sleuthkit.datamodel.Blackboard import BlackboardException
from org.sleuthkit.autopsy.casemodule import NoCurrentCaseException
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 TskMessagesParser import TskMessagesParser
from TskContactsParser import TskContactsParser
from TskCallLogsParser import TskCallLogsParser
import traceback
import general
class ViberAnalyzer(general.AndroidComponentAnalyzer):
"""
Parses the Viber App databases for TSK contacts, message
and calllog artifacts.
The Viber v11.5.0 database structure is as follows:
- People can take part in N conversation(s). A conversation can have M
members and messages are exchanged in a conversation.
- Viber has a conversation table, a participant table (the people/members in the above
analogy) and a messages table.
- Each row of the participants table maps a person to a conversation_id
- Each row in the messages table has a from participant id and a conversation id.
"""
def __init__(self):
self._logger = Logger.getLogger(self.__class__.__name__)
self._VIBER_PACKAGE_NAME = "com.viber.voip"
self._PARSER_NAME = "Viber Parser"
self._VERSION = "11.5.0"
def analyze(self, dataSource, fileManager, context):
"""
Extract, Transform and Load all messages, contacts and
calllogs from the Viber databases.
"""
try:
contact_and_calllog_dbs = AppSQLiteDB.findAppDatabases(dataSource,
"viber_data", True, self._VIBER_PACKAGE_NAME)
message_dbs = AppSQLiteDB.findAppDatabases(dataSource,
"viber_messages", True, self._VIBER_PACKAGE_NAME)
#Extract TSK_CONTACT and TSK_CALLLOG information
for contact_and_calllog_db in contact_and_calllog_dbs:
current_case = Case.getCurrentCaseThrows()
helper = CommunicationArtifactsHelper(
current_case.getSleuthkitCase(), self._PARSER_NAME,
contact_and_calllog_db.getDBFile(), Account.Type.VIBER)
self.parse_contacts(contact_and_calllog_db, helper)
self.parse_calllogs(contact_and_calllog_db, helper)
#Extract TSK_MESSAGE information
for message_db in message_dbs:
current_case = Case.getCurrentCaseThrows()
helper = CommunicationArtifactsHelper(
current_case.getSleuthkitCase(), self._PARSER_NAME,
message_db.getDBFile(), Account.Type.VIBER)
self.parse_messages(message_db, helper)
except NoCurrentCaseException as ex:
self._logger.log(Level.WARNING, "No case currently open.", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
for message_db in messages_db:
message_db.close()
for contact_and_calllog_db in contact_and_calllog_dbs:
contact_and_calllog_db.close()
def parse_contacts(self, contacts_db, helper):
try:
contacts_parser = ViberContactsParser(contacts_db)
while contacts_parser.next():
helper.addContact(
contacts_parser.get_account_name(),
contacts_parser.get_contact_name(),
contacts_parser.get_phone(),
contacts_parser.get_home_phone(),
contacts_parser.get_mobile_phone(),
contacts_parser.get_email()
)
contacts_parser.close()
except SQLException as ex:
self._logger.log(Level.WARNING, "Error querying the viber database for contacts.", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
except TskCoreException as ex:
self._logger.log(Level.SEVERE,
"Error adding viber contacts artifact to case database.", ex)
self._logger.log(Level.SEVERE, traceback.format_exc())
except BlackboardException as ex:
self._logger.log(Level.WARNING,
"Error posting viber contacts artifact to the blackboard.", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
def parse_calllogs(self, calllogs_db, helper):
try:
calllog_parser = ViberCallLogsParser(calllogs_db)
while calllog_parser.next():
helper.addCalllog(
calllog_parser.get_call_direction(),
calllog_parser.get_phone_number_from(),
calllog_parser.get_phone_number_to(),
calllog_parser.get_call_start_date_time(),
calllog_parser.get_call_end_date_time(),
calllog_parser.get_call_type()
)
calllog_parser.close()
except SQLException as ex:
self._logger.log(Level.WARNING, "Error querying the viber database for calllogs.", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
except TskCoreException as ex:
self._logger.log(Level.SEVERE,
"Error adding viber calllogs artifact to case database.", ex)
self._logger.log(Level.SEVERE, traceback.format_exc())
except BlackboardException as ex:
self._logger.log(Level.WARNING,
"Error posting viber calllogs artifact to the blackboard.", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
def parse_messages(self, messages_db, helper):
try:
messages_parser = ViberMessagesParser(messages_db)
while messages_parser.next():
helper.addMessage(
messages_parser.get_message_type(),
messages_parser.get_message_direction(),
messages_parser.get_phone_number_from(),
messages_parser.get_phone_number_to(),
messages_parser.get_message_date_time(),
messages_parser.get_message_read_status(),
messages_parser.get_message_subject(),
messages_parser.get_message_text(),
messages_parser.get_thread_id()
)
messages_parser.close()
except SQLException as ex:
self._logger.log(Level.WARNING, "Error querying the viber database for messages.", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
except TskCoreException as ex:
self._logger.log(Level.SEVERE,
"Error adding viber messages artifact to case database.", ex)
self._logger.log(Level.SEVERE, traceback.format_exc())
except BlackboardException as ex:
self._logger.log(Level.WARNING,
"Error posting viber messages artifact to the blackboard.", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
class ViberCallLogsParser(TskCallLogsParser):
"""
Extracts TSK_CALLLOG information from the Viber database.
TSK_CALLLOG fields that are not in the Viber database are given
a default value inherited from the super class.
"""
def __init__(self, calllog_db):
super(ViberCallLogsParser, self).__init__(calllog_db.runQuery(
"""
SELECT C.canonized_number AS number,
C.type AS direction,
C.duration AS seconds,
C.date AS start_time,
C.viber_call_type AS call_type
FROM calls AS C
"""
)
)
self._OUTGOING_CALL_TYPE = 2
self._INCOMING_CALL_TYPE = 1
self._MISSED_CALL_TYPE = 3
self._AUDIO_CALL_TYPE = 1
self._VIDEO_CALL_TYPE = 4
def get_phone_number_from(self):
if self.get_call_direction() == self.INCOMING_CALL:
return Account.Address(self.result_set.getString("number"),
self.result_set.getString("number"))
#Give default value if the call is outgoing,
#the device's # is not stored in the database.
return super(ViberCallLogsParser, self).get_phone_number_from()
def get_phone_number_to(self):
if self.get_call_direction() == self.OUTGOING_CALL:
return Account.Address(self.result_set.getString("number"),
self.result_set.getString("number"))
#Give default value if the call is incoming,
#the device's # is not stored in the database.
return super(ViberCallLogsParser, self).get_phone_number_to()
def get_call_direction(self):
direction = self.result_set.getInt("direction")
if direction == self._INCOMING_CALL_TYPE or direction == self._MISSED_CALL_TYPE:
return self.INCOMING_CALL
return self.OUTGOING_CALL
def get_call_start_date_time(self):
return self.result_set.getLong("start_time") / 1000
def get_call_end_date_time(self):
start_time = self.get_call_start_date_time()
duration = self.result_set.getLong("seconds")
return start_time + duration
def get_call_type(self):
call_type = self.result_set.getInt("call_type")
if call_type == self._AUDIO_CALL_TYPE:
return self.AUDIO_CALL
if call_type == self._VIDEO_CALL_TYPE:
return self.VIDEO_CALL
return super(ViberCallLogsParser, self).get_call_type()
class ViberContactsParser(TskContactsParser):
"""
Extracts TSK_CONTACT information from the Viber database.
TSK_CONTACT fields that are not in the Viber database are given
a default value inherited from the super class.
"""
def __init__(self, contact_db):
super(ViberContactsParser, self).__init__(contact_db.runQuery(
"""
SELECT C.display_name AS name,
D.data2 AS number
FROM phonebookcontact AS C
JOIN phonebookdata AS D
ON C._id = D.contact_id
"""
)
)
def get_account_name(self):
return self.result_set.getString("number")
def get_contact_name(self):
return self.result_set.getString("name")
def get_phone(self):
return self.result_set.getString("number")
class ViberMessagesParser(TskMessagesParser):
"""
Extract TSK_MESSAGE information from the Viber database.
TSK_CONTACT fields that are not in the Viber database are given
a default value inherited from the super class.
"""
def __init__(self, message_db):
"""
The query below does the following:
- The first two inner joins on participants and participants_info build
the 1 to many (M) mappings between the sender and the recipients for each
conversation_id. If a and b do private messaging, then 2 rows in the result
will be a -> b and b -> a.
If a, b, c, d are in a group, then 4 rows containing a -> b,c,d. b -> a,c,d. etc.
Participants_info is needed to get phone numbers.
- The result of the above step is a look up table for each message. Joining this result
onto the messages table lets us know which participant a message originated from and
everyone else that received it.
"""
super(ViberMessagesParser, self).__init__(message_db.runQuery(
"""
SELECT convo_participants.from_number AS from_number,
convo_participants.recipients AS recipients,
M.conversation_id AS thread_id,
M.body AS msg_content,
M.send_type AS direction,
M.msg_date AS msg_date,
M.unread AS read_status
FROM (SELECT *,
group_concat(TO_RESULT.number) AS recipients
FROM (SELECT P._id AS FROM_ID,
P.conversation_id,
PI.number AS FROM_NUMBER
FROM participants AS P
JOIN participants_info AS PI
ON P.participant_info_id = PI._id) AS FROM_RESULT
JOIN (SELECT P._id AS TO_ID,
P.conversation_id,
PI.number
FROM participants AS P
JOIN participants_info AS PI
ON P.participant_info_id = PI._id) AS TO_RESULT
ON FROM_RESULT.from_id != TO_RESULT.to_id
AND FROM_RESULT.conversation_id = TO_RESULT.conversation_id
GROUP BY FROM_RESULT.from_id) AS convo_participants
JOIN messages AS M
ON M.participant_id = convo_participants.from_id
AND M.conversation_id = convo_participants.conversation_id
"""
)
)
self._VIBER_MESSAGE_TYPE = "Viber Message"
self._INCOMING_MESSAGE_TYPE = 0
self._OUTGOING_MESSAGE_TYPE = 1
def get_message_type(self):
return self._VIBER_MESSAGE_TYPE
def get_phone_number_from(self):
return Account.Address(self.result_set.getString("from_number"),
self.result_set.getString("from_number"))
def get_message_direction(self):
direction = self.result_set.getInt("direction")
if direction == self._INCOMING_MESSAGE_TYPE:
return self.INCOMING
return self.OUTGOING
def get_phone_number_to(self):
recipients = []
for token in self.result_set.getString("recipients").split(","):
recipients.append(Account.Address(token, token))
return recipients
def get_message_date_time(self):
#transform from ms to seconds
return self.result_set.getLong("msg_date") / 1000
def get_message_read_status(self):
if self.get_message_direction() == self.INCOMING:
if self.result_set.getInt("read_status") == 0:
return self.READ
else:
return self.UNREAD
return super(ViberMessagesParser, self).get_message_read_status()
def get_message_text(self):
return self.result_set.getString("msg_content")
def get_thread_id(self):
return str(self.result_set.getInt("thread_id"))

View File

@@ -0,0 +1,458 @@
"""
Autopsy Forensic Browser
Copyright 2019 Basis Technology Corp.
Contact: carrier <at> sleuthkit <dot> org
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from java.io import File
from java.lang import Class
from java.lang import ClassNotFoundException
from java.lang import Long
from java.lang import String
from java.sql import ResultSet
from java.sql import SQLException
from java.sql import Statement
from java.util.logging import Level
from java.util import ArrayList
from org.apache.commons.codec.binary import Base64
from org.sleuthkit.autopsy.casemodule import Case
from org.sleuthkit.autopsy.coreutils import Logger
from org.sleuthkit.autopsy.coreutils import MessageNotifyUtil
from org.sleuthkit.autopsy.coreutils import AppSQLiteDB
from org.sleuthkit.autopsy.datamodel import ContentUtils
from org.sleuthkit.autopsy.ingest import IngestJobContext
from org.sleuthkit.datamodel import AbstractFile
from org.sleuthkit.datamodel import BlackboardArtifact
from org.sleuthkit.datamodel import BlackboardAttribute
from org.sleuthkit.datamodel import Content
from org.sleuthkit.datamodel import TskCoreException
from org.sleuthkit.datamodel.Blackboard import BlackboardException
from org.sleuthkit.autopsy.casemodule import NoCurrentCaseException
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 TskMessagesParser import TskMessagesParser
from TskContactsParser import TskContactsParser
from TskCallLogsParser import TskCallLogsParser
import traceback
import general
class WhatsAppAnalyzer(general.AndroidComponentAnalyzer):
"""
Parses the WhatsApp databases for TSK contact, message
and calllog artifacts.
"""
def __init__(self):
self._logger = Logger.getLogger(self.__class__.__name__)
self._WHATSAPP_PACKAGE_NAME = "com.whatsapp"
self._PARSER_NAME = "WhatsApp Parser"
self._VERSION = "2.19.244"
def analyze(self, dataSource, fileManager, context):
"""
Extract, Transform and Load all TSK contact, message
and calllog artifacts from the WhatsApp databases.
"""
try:
contact_dbs = AppSQLiteDB.findAppDatabases(dataSource,
"wa.db", True, self._WHATSAPP_PACKAGE_NAME)
calllog_and_message_dbs = AppSQLiteDB.findAppDatabases(dataSource,
"msgstore.db", True, self._WHATSAPP_PACKAGE_NAME)
#Extract TSK_CONTACT information
for contact_db in contact_dbs:
current_case = Case.getCurrentCaseThrows()
helper = CommunicationArtifactsHelper(
current_case.getSleuthkitCase(), self._PARSER_NAME,
contact_db.getDBFile(), Account.Type.WHATSAPP)
self.parse_contacts(contact_db, helper)
for calllog_and_message_db in calllog_and_message_dbs:
current_case = Case.getCurrentCaseThrows()
helper = CommunicationArtifactsHelper(
current_case.getSleuthkitCase(), self._PARSER_NAME,
calllog_and_message_db.getDBFile(), Account.Type.WHATSAPP)
self.parse_calllogs(calllog_and_message_db, helper)
self.parse_messages(dataSource, calllog_and_message_db, helper)
except NoCurrentCaseException as ex:
#If there is no current case, bail out immediately.
self._logger.log(Level.WARNING, "No case currently open.", ex)
self._logger.log(Level.WARNING, traceback.format_exec())
#Clean up open file handles.
for contact_db in contact_dbs:
contact_db.close()
for calllog_and_message_db in calllog_and_message_dbs:
calllog_and_message_db.close()
def parse_contacts(self, contacts_db, helper):
try:
contacts_parser = WhatsAppContactsParser(contacts_db)
while contacts_parser.next():
helper.addContact(
contacts_parser.get_account_name(),
contacts_parser.get_contact_name(),
contacts_parser.get_phone(),
contacts_parser.get_home_phone(),
contacts_parser.get_mobile_phone(),
contacts_parser.get_email()
)
contacts_parser.close()
except SQLException as ex:
self._logger.log(Level.WARNING, "Error querying the whatsapp database for contacts.", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
except TskCoreException as ex:
self._logger.log(Level.SEVERE,
"Error adding whatsapp contact artifacts to the case database.", ex)
self._logger.log(Level.SEVERE, traceback.format_exc())
except BlackboardException as ex:
self._logger.log(Level.WARNING,
"Error posting contact artifact to the blackboard.", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
def parse_calllogs(self, calllogs_db, helper):
try:
single_calllogs_parser = WhatsAppSingleCallLogsParser(calllogs_db)
while single_calllogs_parser.next():
helper.addCalllog(
single_calllogs_parser.get_call_direction(),
single_calllogs_parser.get_phone_number_from(),
single_calllogs_parser.get_phone_number_to(),
single_calllogs_parser.get_call_start_date_time(),
single_calllogs_parser.get_call_end_date_time(),
single_calllogs_parser.get_call_type()
)
single_calllogs_parser.close()
group_calllogs_parser = WhatsAppGroupCallLogsParser(calllogs_db)
while group_calllogs_parser.next():
helper.addCalllog(
group_calllogs_parser.get_call_direction(),
group_calllogs_parser.get_phone_number_from(),
group_calllogs_parser.get_phone_number_to(),
group_calllogs_parser.get_call_start_date_time(),
group_calllogs_parser.get_call_end_date_time(),
group_calllogs_parser.get_call_type()
)
group_calllogs_parser.close()
except SQLException as ex:
self._logger.log(Level.WARNING, "Error querying the whatsapp database for calllogs.", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
except TskCoreException as ex:
self._logger.log(Level.SEVERE,
"Error adding whatsapp calllog artifacts to the case database.", ex)
self._logger.log(Level.SEVERE, traceback.format_exc())
except BlackboardException as ex:
self._logger.log(Level.WARNING,
"Error posting calllog artifact to the blackboard.", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
def parse_messages(self, dataSource, messages_db, helper):
try:
messages_db.attachDatabase(dataSource, "wa.db",
messages_db.getDBFile().getParentPath(), "wadb")
messages_parser = WhatsAppMessagesParser(messages_db)
while messages_parser.next():
helper.addMessage(
messages_parser.get_message_type(),
messages_parser.get_message_direction(),
messages_parser.get_phone_number_from(),
messages_parser.get_phone_number_to(),
messages_parser.get_message_date_time(),
messages_parser.get_message_read_status(),
messages_parser.get_message_subject(),
messages_parser.get_message_text(),
messages_parser.get_thread_id()
)
messages_parser.close()
except SQLException as ex:
self._logger.log(Level.WARNING, "Error querying the whatsapp database for contacts.", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
except TskCoreException as ex:
self._logger.log(Level.SEVERE,
"Error adding whatsapp contact artifacts to the case database.", ex)
self._logger.log(Level.SEVERE, traceback.format_exc())
except BlackboardException as ex:
self._logger.log(Level.WARNING,
"Error posting contact artifact to the blackboard.", ex)
self._logger.log(Level.WARNING, traceback.format_exc())
class WhatsAppGroupCallLogsParser(TskCallLogsParser):
"""
Extracts TSK_CALLLOG information from group call logs
in the WhatsApp database.
"""
def __init__(self, calllog_db):
super(WhatsAppGroupCallLogsParser, self).__init__(calllog_db.runQuery(
"""
SELECT CL.video_call,
CL.timestamp,
CL.duration,
CL.from_me,
J1.raw_string AS from_id,
group_concat(J.raw_string) AS group_members
FROM call_log_participant_v2 AS CLP
JOIN call_log AS CL
ON CL._id = CLP.call_log_row_id
JOIN jid AS J
ON J._id = CLP.jid_row_id
JOIN jid as J1
ON J1._id = CL.jid_row_id
GROUP BY CL._id
"""
)
)
self._INCOMING_CALL_TYPE = 0
self._OUTGOING_CALL_TYPE = 1
self._VIDEO_CALL_TYPE = 1
def get_call_direction(self):
if self.result_set.getInt("from_me") == self._INCOMING_CALL_TYPE:
return self.INCOMING_CALL
return self.OUTGOING_CALL
def get_phone_number_from(self):
if self.get_call_direction() == self.INCOMING_CALL:
sender = self.result_set.getString("from_id")
return Account.Address(sender, sender)
return super(WhatsAppGroupCallLogsParser, self).get_phone_number_from()
def get_phone_number_to(self):
if self.get_call_direction() == self.OUTGOING_CALL:
group = self.result_set.getString("group_members")
members = []
for token in group.split(","):
members.append(Account.Address(token, token))
return members
return super(WhatsAppGroupCallLogsParser, self).get_phone_number_to()
def get_call_start_date_time(self):
return self.result_set.getLong("timestamp") / 1000
def get_call_end_date_time(self):
start = self.get_call_start_date_time()
duration = self.result_set.getInt("duration")
return start + duration
def get_call_type(self):
if self.result_set.getInt("video_call") == self._VIDEO_CALL_TYPE:
return self.VIDEO_CALL
return self.AUDIO_CALL
class WhatsAppSingleCallLogsParser(TskCallLogsParser):
"""
Extracts TSK_CALLLOG information from 1 to 1 call logs
in the WhatsApp database.
"""
def __init__(self, calllog_db):
super(WhatsAppSingleCallLogsParser, self).__init__(calllog_db.runQuery(
"""
SELECT CL.timestamp,
CL.video_call,
CL.duration,
J.raw_string AS num,
CL.from_me
FROM call_log AS CL
JOIN jid AS J
ON J._id = CL.jid_row_id
WHERE CL._id NOT IN (SELECT DISTINCT call_log_row_id
FROM call_log_participant_v2)
"""
)
)
self._INCOMING_CALL_TYPE = 0
self._OUTGOING_CALL_TYPE = 1
self._VIDEO_CALL_TYPE = 1
def get_call_direction(self):
if self.result_set.getInt("from_me") == self._INCOMING_CALL_TYPE:
return self.INCOMING_CALL
return self.OUTGOING_CALL
def get_phone_number_from(self):
if self.get_call_direction() == self.INCOMING_CALL:
sender = self.result_set.getString("num")
return Account.Address(sender, sender)
return super(WhatsAppSingleCallLogsParser, self).get_phone_number_from()
def get_phone_number_to(self):
if self.get_call_direction() == self.OUTGOING_CALL:
to = self.result_set.getString("num")
return Account.Address(to, to)
return super(WhatsAppSingleCallLogsParser, self).get_phone_number_to()
def get_call_start_date_time(self):
return self.result_set.getLong("timestamp") / 1000
def get_call_end_date_time(self):
start = self.get_call_start_date_time()
duration = self.result_set.getInt("duration")
return start + duration
def get_call_type(self):
if self.result_set.getInt("video_call") == self._VIDEO_CALL_TYPE:
return self.VIDEO_CALL
return self.AUDIO_CALL
class WhatsAppContactsParser(TskContactsParser):
"""
Extracts TSK_CONTACT information from the WhatsApp database.
TSK_CONTACT fields that are not in the WhatsApp database are given
a default value inherited from the super class.
"""
def __init__(self, contact_db):
super(WhatsAppContactsParser, self).__init__(contact_db.runQuery(
"""
SELECT jid,
CASE
WHEN WC.number IS NULL THEN WC.jid
WHEN WC.number == "" THEN WC.jid
ELSE WC.number
END number,
CASE
WHEN WC.given_name IS NULL
AND WC.family_name IS NULL
AND WC.display_name IS NULL THEN WC.jid
WHEN WC.given_name IS NULL
AND WC.family_name IS NULL THEN WC.display_name
WHEN WC.given_name IS NULL THEN WC.family_name
WHEN WC.family_name IS NULL THEN WC.given_name
ELSE WC.given_name
|| " "
|| WC.family_name
END name
FROM wa_contacts AS WC
"""
)
)
def get_account_name(self):
return self.result_set.getString("jid")
def get_contact_name(self):
return self.result_set.getString("name")
def get_phone(self):
return self.result_set.getString("number")
class WhatsAppMessagesParser(TskMessagesParser):
"""
Extract TSK_MESSAGE information from the WhatsApp database.
TSK_CONTACT fields that are not in the WhatsApp database are given
a default value inherited from the super class.
"""
def __init__(self, message_db):
super(WhatsAppMessagesParser, self).__init__(message_db.runQuery(
"""
SELECT M.key_remote_jid AS id,
contact_info.recipients,
key_from_me AS direction,
CASE
WHEN M.data IS NULL THEN ""
ELSE M.data
END AS content,
M.timestamp AS send_timestamp,
M.received_timestamp,
M.remote_resource AS group_sender,
M.media_url As attachment
FROM (SELECT jid,
recipients
FROM wadb.wa_contacts AS WC
LEFT JOIN (SELECT gjid,
group_concat(CASE
WHEN jid == "" THEN NULL
ELSE jid
END) AS recipients
FROM group_participants
GROUP BY gjid) AS group_map
ON WC.jid = group_map.gjid
GROUP BY jid) AS contact_info
JOIN messages AS M
ON M.key_remote_jid = contact_info.jid
"""
)
)
self._WHATSAPP_MESSAGE_TYPE = "WhatsApp Message"
self._INCOMING_MESSAGE_TYPE = 0
self._OUTGOING_MESSAGE_TYPE = 1
self._message_db = message_db
def get_message_type(self):
return self._WHATSAPP_MESSAGE_TYPE
def get_phone_number_to(self):
if self.get_message_direction() == self.OUTGOING:
group = self.result_set.getString("recipients")
if group is not None:
group = group.split(",")
recipients = []
for token in group:
recipients.append(Account.Address(token, token))
return recipients
return Account.Address(self.result_set.getString("id"),
self.result_set.getString("id"))
return super(WhatsAppMessagesParser, self).get_phone_number_to()
def get_phone_number_from(self):
if self.get_message_direction() == self.INCOMING:
group_sender = self.result_set.getString("group_sender")
group = self.result_set.getString("recipients")
if group_sender is not None and group is not None:
return Account.Address(group_sender, group_sender)
else:
return Account.Address(self.result_set.getString("id"),
self.result_set.getString("id"))
return super(WhatsAppMessagesParser, self).get_phone_number_from()
def get_message_direction(self):
direction = self.result_set.getInt("direction")
if direction == self._INCOMING_MESSAGE_TYPE:
return self.INCOMING
return self.OUTGOING
def get_message_date_time(self):
#transform from ms to seconds
if self.get_message_direction() == self.OUTGOING:
return self.result_set.getLong("send_timestamp") / 1000
return self.result_set.getLong("received_timestamp") / 1000
def get_message_text(self):
message = self.result_set.getString("content")
attachment = self.result_set.getString("attachment")
if attachment is not None:
return general.appendAttachmentList(message, [attachment])
return message
def get_thread_id(self):
group = self.result_set.getString("recipients")
if group is not None:
return self.result_set.getString("id")
return super(WhatsAppMessagesParser, self).get_thread_id()

View File

@@ -29,6 +29,7 @@ from java.util.logging import Level
from java.util import ArrayList
from org.apache.commons.codec.binary import Base64
from org.sleuthkit.autopsy.casemodule import Case
from org.sleuthkit.autopsy.casemodule import NoCurrentCaseException
from org.sleuthkit.autopsy.coreutils import Logger
from org.sleuthkit.autopsy.coreutils import MessageNotifyUtil
from org.sleuthkit.autopsy.coreutils import AppSQLiteDB

View File

@@ -29,6 +29,7 @@ from java.util.logging import Level
from java.util import ArrayList
from org.apache.commons.codec.binary import Base64
from org.sleuthkit.autopsy.casemodule import Case
from org.sleuthkit.autopsy.casemodule import NoCurrentCaseException
from org.sleuthkit.autopsy.coreutils import Logger
from org.sleuthkit.autopsy.coreutils import MessageNotifyUtil
from org.sleuthkit.autopsy.coreutils import AppSQLiteDB