mirror of
https://github.com/overcuriousity/autopsy-flatpak.git
synced 2025-07-12 16:06:15 +00:00
Merged release-4.13.0 and resolved conflicts
This commit is contained in:
commit
ddb9364d5d
@ -64,6 +64,8 @@ final class AddLogicalImageTask implements Runnable {
|
|||||||
private final static String MODULE_NAME = "Logical Imager"; //NON-NLS
|
private final static String MODULE_NAME = "Logical Imager"; //NON-NLS
|
||||||
private final static String ROOT_STR = "root"; // NON-NLS
|
private final static String ROOT_STR = "root"; // NON-NLS
|
||||||
private final static String VHD_EXTENSION = ".vhd"; // NON-NLS
|
private final static String VHD_EXTENSION = ".vhd"; // NON-NLS
|
||||||
|
private final static int REPORT_PROGRESS_INTERVAL = 100;
|
||||||
|
private final static int POST_ARTIFACT_INTERVAL = 1000;
|
||||||
private final String deviceId;
|
private final String deviceId;
|
||||||
private final String timeZone;
|
private final String timeZone;
|
||||||
private final File src;
|
private final File src;
|
||||||
@ -145,7 +147,7 @@ final class AddLogicalImageTask implements Runnable {
|
|||||||
callback.done(DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS, errorList, emptyDataSources);
|
callback.done(DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS, errorList, emptyDataSources);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the SearchResults.txt and users.txt to the case report
|
// Add the SearchResults.txt and users.txt to the case report
|
||||||
String resultsFilename;
|
String resultsFilename;
|
||||||
if (Paths.get(dest.toString(), SEARCH_RESULTS_TXT).toFile().exists()) {
|
if (Paths.get(dest.toString(), SEARCH_RESULTS_TXT).toFile().exists()) {
|
||||||
@ -200,7 +202,7 @@ final class AddLogicalImageTask implements Runnable {
|
|||||||
|
|
||||||
List<Content> newDataSources = new ArrayList<>();
|
List<Content> newDataSources = new ArrayList<>();
|
||||||
Map<String, List<Long>> interestingFileMap = new HashMap<>();
|
Map<String, List<Long>> interestingFileMap = new HashMap<>();
|
||||||
|
|
||||||
if (imagePaths.isEmpty()) {
|
if (imagePaths.isEmpty()) {
|
||||||
createVHD = false;
|
createVHD = false;
|
||||||
// No VHD in src directory, try ingest the root directory as local files
|
// No VHD in src directory, try ingest the root directory as local files
|
||||||
@ -241,7 +243,7 @@ final class AddLogicalImageTask implements Runnable {
|
|||||||
if (addMultipleImagesTask.getResult() == DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS) {
|
if (addMultipleImagesTask.getResult() == DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS) {
|
||||||
LOGGER.log(Level.SEVERE, "Failed to add VHD datasource"); // NON-NLS
|
LOGGER.log(Level.SEVERE, "Failed to add VHD datasource"); // NON-NLS
|
||||||
callback.done(DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS, addMultipleImagesTask.getErrorMessages(), emptyDataSources);
|
callback.done(DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS, addMultipleImagesTask.getErrorMessages(), emptyDataSources);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
interestingFileMap = getInterestingFileMapForVHD(Paths.get(dest.toString(), resultsFilename));
|
interestingFileMap = getInterestingFileMapForVHD(Paths.get(dest.toString(), resultsFilename));
|
||||||
@ -250,7 +252,7 @@ final class AddLogicalImageTask implements Runnable {
|
|||||||
LOGGER.log(Level.SEVERE, "Failed to add interesting files", ex); // NON-NLS
|
LOGGER.log(Level.SEVERE, "Failed to add interesting files", ex); // NON-NLS
|
||||||
callback.done(DataSourceProcessorCallback.DataSourceProcessorResult.NONCRITICAL_ERRORS, errorList, emptyDataSources);
|
callback.done(DataSourceProcessorCallback.DataSourceProcessorResult.NONCRITICAL_ERRORS, errorList, emptyDataSources);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (NoCurrentCaseException ex) {
|
} catch (NoCurrentCaseException ex) {
|
||||||
String msg = Bundle.AddLogicalImageTask_noCurrentCase();
|
String msg = Bundle.AddLogicalImageTask_noCurrentCase();
|
||||||
errorList.add(msg);
|
errorList.add(msg);
|
||||||
@ -261,7 +263,7 @@ final class AddLogicalImageTask implements Runnable {
|
|||||||
|
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
if (!createVHD) {
|
if (!createVHD) {
|
||||||
// TODO: When 5453 is fixed, we should be able to delete it when adding VHD.
|
// TODO: When 5453 is fixed, we should be able to delete it when adding VHD.
|
||||||
deleteDestinationDirectory();
|
deleteDestinationDirectory();
|
||||||
}
|
}
|
||||||
errorList.add(Bundle.AddLogicalImageTask_addImageCancelled());
|
errorList.add(Bundle.AddLogicalImageTask_addImageCancelled());
|
||||||
@ -345,7 +347,7 @@ final class AddLogicalImageTask implements Runnable {
|
|||||||
private void addInterestingFiles(Map<String, List<Long>> interestingFileMap) throws IOException, TskCoreException {
|
private void addInterestingFiles(Map<String, List<Long>> interestingFileMap) throws IOException, TskCoreException {
|
||||||
int lineNumber = 0;
|
int lineNumber = 0;
|
||||||
List<BlackboardArtifact> artifacts = new ArrayList<>();
|
List<BlackboardArtifact> artifacts = new ArrayList<>();
|
||||||
|
|
||||||
Iterator<Map.Entry<String, List<Long>>> iterator = interestingFileMap.entrySet().iterator();
|
Iterator<Map.Entry<String, List<Long>>> iterator = interestingFileMap.entrySet().iterator();
|
||||||
while (iterator.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
|
|
||||||
@ -365,21 +367,23 @@ final class AddLogicalImageTask implements Runnable {
|
|||||||
|
|
||||||
List<Long> fileIds = entry.getValue();
|
List<Long> fileIds = entry.getValue();
|
||||||
for (Long fileId: fileIds) {
|
for (Long fileId: fileIds) {
|
||||||
if (lineNumber % 100 == 0) {
|
if (cancelled) {
|
||||||
|
postArtifacts(artifacts);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (lineNumber % REPORT_PROGRESS_INTERVAL == 0) {
|
||||||
progressMonitor.setProgressText(Bundle.AddLogicalImageTask_addingInterestingFile(lineNumber, totalFiles));
|
progressMonitor.setProgressText(Bundle.AddLogicalImageTask_addingInterestingFile(lineNumber, totalFiles));
|
||||||
}
|
}
|
||||||
|
if (lineNumber % POST_ARTIFACT_INTERVAL == 0) {
|
||||||
|
postArtifacts(artifacts);
|
||||||
|
artifacts.clear();
|
||||||
|
}
|
||||||
addInterestingFileToArtifacts(fileId, ruleSetName, ruleName, artifacts);
|
addInterestingFileToArtifacts(fileId, ruleSetName, ruleName, artifacts);
|
||||||
lineNumber++;
|
lineNumber++;
|
||||||
}
|
}
|
||||||
iterator.remove();
|
iterator.remove();
|
||||||
}
|
}
|
||||||
|
postArtifacts(artifacts);
|
||||||
try {
|
|
||||||
// index the artifact for keyword search
|
|
||||||
blackboard.postArtifacts(artifacts, MODULE_NAME);
|
|
||||||
} catch (Blackboard.BlackboardException ex) {
|
|
||||||
LOGGER.log(Level.SEVERE, "Unable to post artifacts to blackboard", ex); //NON-NLS
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addInterestingFileToArtifacts(long fileId, String ruleSetName, String ruleName, List<BlackboardArtifact> artifacts) throws TskCoreException {
|
private void addInterestingFileToArtifacts(long fileId, String ruleSetName, String ruleName, List<BlackboardArtifact> artifacts) throws TskCoreException {
|
||||||
@ -400,7 +404,7 @@ final class AddLogicalImageTask implements Runnable {
|
|||||||
Map<Long, List<String>> objIdToimagePathsMap = currentCase.getSleuthkitCase().getImagePaths();
|
Map<Long, List<String>> objIdToimagePathsMap = currentCase.getSleuthkitCase().getImagePaths();
|
||||||
imagePathToObjIdMap = imagePathsToDataSourceObjId(objIdToimagePathsMap);
|
imagePathToObjIdMap = imagePathsToDataSourceObjId(objIdToimagePathsMap);
|
||||||
Map<String, List<Long>> interestingFileMap = new HashMap<>();
|
Map<String, List<Long>> interestingFileMap = new HashMap<>();
|
||||||
|
|
||||||
try (BufferedReader br = new BufferedReader(new InputStreamReader(
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(
|
||||||
new FileInputStream(resultsPath.toFile()), "UTF8"))) { // NON-NLS
|
new FileInputStream(resultsPath.toFile()), "UTF8"))) { // NON-NLS
|
||||||
String line;
|
String line;
|
||||||
@ -426,7 +430,7 @@ final class AddLogicalImageTask implements Runnable {
|
|||||||
String filename = fields[7];
|
String filename = fields[7];
|
||||||
String parentPath = fields[8];
|
String parentPath = fields[8];
|
||||||
|
|
||||||
if (lineNumber % 100 == 0) {
|
if (lineNumber % REPORT_PROGRESS_INTERVAL == 0) {
|
||||||
progressMonitor.setProgressText(Bundle.AddLogicalImageTask_searchingInterestingFile(lineNumber, totalFiles));
|
progressMonitor.setProgressText(Bundle.AddLogicalImageTask_searchingInterestingFile(lineNumber, totalFiles));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -443,11 +447,20 @@ final class AddLogicalImageTask implements Runnable {
|
|||||||
interestingFileMap.put(key, fileIds);
|
interestingFileMap.put(key, fileIds);
|
||||||
}
|
}
|
||||||
lineNumber++;
|
lineNumber++;
|
||||||
} // end reading file }
|
} // end reading file
|
||||||
}
|
}
|
||||||
return interestingFileMap;
|
return interestingFileMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void postArtifacts(List<BlackboardArtifact> artifacts) {
|
||||||
|
try {
|
||||||
|
// index the artifact for keyword search
|
||||||
|
blackboard.postArtifacts(artifacts, MODULE_NAME);
|
||||||
|
} catch (Blackboard.BlackboardException ex) {
|
||||||
|
LOGGER.log(Level.SEVERE, "Unable to post artifacts to blackboard", ex); //NON-NLS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Messages({
|
@Messages({
|
||||||
"# {0} - file number", "# {1} - total files", "AddLogicalImageTask.addingExtractedFile=Adding extracted files ({0}/{1})"
|
"# {0} - file number", "# {1} - total files", "AddLogicalImageTask.addingExtractedFile=Adding extracted files ({0}/{1})"
|
||||||
})
|
})
|
||||||
@ -455,7 +468,7 @@ final class AddLogicalImageTask implements Runnable {
|
|||||||
SleuthkitCase skCase = Case.getCurrentCase().getSleuthkitCase();
|
SleuthkitCase skCase = Case.getCurrentCase().getSleuthkitCase();
|
||||||
SleuthkitCase.CaseDbTransaction trans = null;
|
SleuthkitCase.CaseDbTransaction trans = null;
|
||||||
Map<String, List<Long>> interestingFileMap = new HashMap<>();
|
Map<String, List<Long>> interestingFileMap = new HashMap<>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
trans = skCase.beginTransaction();
|
trans = skCase.beginTransaction();
|
||||||
LocalFilesDataSource localFilesDataSource = skCase.addLocalFilesDataSource(deviceId, this.src.getName(), timeZone, trans);
|
LocalFilesDataSource localFilesDataSource = skCase.addLocalFilesDataSource(deviceId, this.src.getName(), timeZone, trans);
|
||||||
@ -492,7 +505,7 @@ final class AddLogicalImageTask implements Runnable {
|
|||||||
String ctime = fields[13];
|
String ctime = fields[13];
|
||||||
parentPath = ROOT_STR + "/" + vhdFilename + "/" + parentPath;
|
parentPath = ROOT_STR + "/" + vhdFilename + "/" + parentPath;
|
||||||
|
|
||||||
if (lineNumber % 100 == 0) {
|
if (lineNumber % REPORT_PROGRESS_INTERVAL == 0) {
|
||||||
progressMonitor.setProgressText(Bundle.AddLogicalImageTask_addingExtractedFile(lineNumber, totalFiles));
|
progressMonitor.setProgressText(Bundle.AddLogicalImageTask_addingExtractedFile(lineNumber, totalFiles));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,6 +29,7 @@ from java.util.logging import Level
|
|||||||
from java.util import ArrayList
|
from java.util import ArrayList
|
||||||
from org.apache.commons.codec.binary import Base64
|
from org.apache.commons.codec.binary import Base64
|
||||||
from org.sleuthkit.autopsy.casemodule import Case
|
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 Logger
|
||||||
from org.sleuthkit.autopsy.coreutils import MessageNotifyUtil
|
from org.sleuthkit.autopsy.coreutils import MessageNotifyUtil
|
||||||
from org.sleuthkit.autopsy.coreutils import AppSQLiteDB
|
from org.sleuthkit.autopsy.coreutils import AppSQLiteDB
|
||||||
@ -56,10 +57,14 @@ and adds artifacts to the case.
|
|||||||
class IMOAnalyzer(general.AndroidComponentAnalyzer):
|
class IMOAnalyzer(general.AndroidComponentAnalyzer):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._logger = Logger.getLogger(self.__class__.__name__)
|
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):
|
def analyze(self, dataSource, fileManager, context):
|
||||||
selfAccountAddress = None
|
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:
|
for accountDb in accountDbs:
|
||||||
try:
|
try:
|
||||||
accountResultSet = accountDb.runQuery("SELECT uid, name FROM account")
|
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"))
|
selfAccountAddress = Account.Address(accountResultSet.getString("uid"), accountResultSet.getString("name"))
|
||||||
|
|
||||||
except SQLException as ex:
|
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:
|
finally:
|
||||||
accountDb.close()
|
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:
|
for friendsDb in friendsDbs:
|
||||||
try:
|
try:
|
||||||
friendsDBHelper = CommunicationArtifactsHelper(Case.getCurrentCase().getSleuthkitCase(),
|
current_case = Case.getCurrentCaseThrows()
|
||||||
"IMO Parser", friendsDb.getDBFile(),
|
if selfAccountAddress is not None:
|
||||||
|
friendsDBHelper = CommunicationArtifactsHelper(current_case.getSleuthkitCase(),
|
||||||
|
self._PARSER_NAME,
|
||||||
|
friendsDb.getDBFile(),
|
||||||
Account.Type.IMO, Account.Type.IMO, selfAccountAddress )
|
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")
|
contactsResultSet = friendsDb.runQuery("SELECT buid, name FROM friends")
|
||||||
if contactsResultSet is not None:
|
if contactsResultSet is not None:
|
||||||
while contactsResultSet.next():
|
while contactsResultSet.next():
|
||||||
@ -121,7 +136,7 @@ class IMOAnalyzer(general.AndroidComponentAnalyzer):
|
|||||||
|
|
||||||
|
|
||||||
messageArtifact = friendsDBHelper.addMessage(
|
messageArtifact = friendsDBHelper.addMessage(
|
||||||
"IMO Message",
|
self._MESSAGE_TYPE,
|
||||||
direction,
|
direction,
|
||||||
fromAddress,
|
fromAddress,
|
||||||
toAddress,
|
toAddress,
|
||||||
@ -137,8 +152,16 @@ class IMOAnalyzer(general.AndroidComponentAnalyzer):
|
|||||||
|
|
||||||
except SQLException as ex:
|
except SQLException as ex:
|
||||||
self._logger.log(Level.WARNING, "Error processing query result for IMO friends", ex)
|
self._logger.log(Level.WARNING, "Error processing query result for IMO friends", ex)
|
||||||
except (TskCoreException, BlackboardException) as ex:
|
self._logger.log(Level.WARNING, traceback.format_exc())
|
||||||
self._logger.log(Level.WARNING, "Failed to message artifacts.", ex)
|
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:
|
finally:
|
||||||
friendsDb.close()
|
friendsDb.close()
|
||||||
|
|
||||||
|
@ -48,6 +48,7 @@ import textmessage
|
|||||||
import wwfmessage
|
import wwfmessage
|
||||||
import imo
|
import imo
|
||||||
import line
|
import line
|
||||||
|
import textnow
|
||||||
|
|
||||||
class AndroidModuleFactory(IngestModuleFactoryAdapter):
|
class AndroidModuleFactory(IngestModuleFactoryAdapter):
|
||||||
|
|
||||||
@ -92,7 +93,7 @@ class AndroidIngestModule(DataSourceIngestModule):
|
|||||||
analyzers = [contact.ContactAnalyzer(), calllog.CallLogAnalyzer(), textmessage.TextMessageAnalyzer(),
|
analyzers = [contact.ContactAnalyzer(), calllog.CallLogAnalyzer(), textmessage.TextMessageAnalyzer(),
|
||||||
tangomessage.TangoMessageAnalyzer(), wwfmessage.WWFMessageAnalyzer(),
|
tangomessage.TangoMessageAnalyzer(), wwfmessage.WWFMessageAnalyzer(),
|
||||||
googlemaplocation.GoogleMapLocationAnalyzer(), browserlocation.BrowserLocationAnalyzer(),
|
googlemaplocation.GoogleMapLocationAnalyzer(), browserlocation.BrowserLocationAnalyzer(),
|
||||||
cachelocation.CacheLocationAnalyzer(), imo.IMOAnalyzer(), line.LineAnalyzer()]
|
cachelocation.CacheLocationAnalyzer(), imo.IMOAnalyzer(), line.LineAnalyzer(), textnow.TextNowAnalyzer()]
|
||||||
self.log(Level.INFO, "running " + str(len(analyzers)) + " analyzers")
|
self.log(Level.INFO, "running " + str(len(analyzers)) + " analyzers")
|
||||||
progressBar.switchToDeterminate(len(analyzers))
|
progressBar.switchToDeterminate(len(analyzers))
|
||||||
|
|
||||||
|
392
InternalPythonModules/android/textnow.py
Normal file
392
InternalPythonModules/android/textnow.py
Normal 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
|
Loading…
x
Reference in New Issue
Block a user