From d9dea468f394addbf6bb946b9046763307d1d00f Mon Sep 17 00:00:00 2001 From: Ann Priestman Date: Fri, 11 Oct 2019 12:14:13 -0400 Subject: [PATCH 01/34] Added tutorial one to docs --- docs/doxygen/Doxyfile | 4 +- docs/doxygen/images/bigAndRoundFiles.png | Bin 0 -> 31278 bytes docs/doxygen/images/demoScript_folder.png | Bin 0 -> 7417 bytes docs/doxygen/images/ingest-modules.PNG | Bin 0 -> 43805 bytes docs/doxygen/{ => images}/viewer_image.JPG | Bin docs/doxygen/main.dox | 4 +- docs/doxygen/modDSIngestTutorial.dox | 5 + docs/doxygen/modFileIngestTutorial.dox | 154 +++++++++++++++++++++ 8 files changed, 164 insertions(+), 3 deletions(-) create mode 100644 docs/doxygen/images/bigAndRoundFiles.png create mode 100644 docs/doxygen/images/demoScript_folder.png create mode 100644 docs/doxygen/images/ingest-modules.PNG rename docs/doxygen/{ => images}/viewer_image.JPG (100%) mode change 100755 => 100644 create mode 100644 docs/doxygen/modDSIngestTutorial.dox create mode 100644 docs/doxygen/modFileIngestTutorial.dox diff --git a/docs/doxygen/Doxyfile b/docs/doxygen/Doxyfile index b640c41549..0037b15063 100644 --- a/docs/doxygen/Doxyfile +++ b/docs/doxygen/Doxyfile @@ -772,6 +772,8 @@ INPUT = main.dox \ regressionTesting.dox \ native_libs.dox \ modDevPython.dox \ + modFileIngestTutorial.dox \ + modDSIngestTutorial.dox \ debugTsk.dox \ ../../Core/src \ ../../CoreLibs/src \ @@ -867,7 +869,7 @@ EXAMPLE_RECURSIVE = NO # that contain images that are to be included in the documentation (see the # \image command). -IMAGE_PATH = . +IMAGE_PATH = images/ # The INPUT_FILTER tag can be used to specify a program that doxygen should # invoke to filter for each input file. Doxygen will invoke the filter program diff --git a/docs/doxygen/images/bigAndRoundFiles.png b/docs/doxygen/images/bigAndRoundFiles.png new file mode 100644 index 0000000000000000000000000000000000000000..fa6430e7d8f9e9d59f73d60bf63a4e07710912bc GIT binary patch literal 31278 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 JRf!k|{SU*u6|?{V literal 0 HcmV?d00001 diff --git a/docs/doxygen/images/demoScript_folder.png b/docs/doxygen/images/demoScript_folder.png new file mode 100644 index 0000000000000000000000000000000000000000..01d7d16f4c592b9d3f07c0c8bae604ef765b38d4 GIT binary patch literal 7417 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 HI^cf*s>lmm literal 0 HcmV?d00001 diff --git a/docs/doxygen/viewer_image.JPG b/docs/doxygen/images/viewer_image.JPG old mode 100755 new mode 100644 similarity index 100% rename from docs/doxygen/viewer_image.JPG rename to docs/doxygen/images/viewer_image.JPG diff --git a/docs/doxygen/main.dox b/docs/doxygen/main.dox index 22bcd097ad..094c61f612 100644 --- a/docs/doxygen/main.dox +++ b/docs/doxygen/main.dox @@ -9,8 +9,8 @@ If these pages don't answer your question, then send the question to the Writing Python or Java Modules If you want to write Java or Python modules, then there are some tutorials and detailed pages in this document. The Python tutorials include: -- File Ingest Modules: http://www.basistech.com/python-autopsy-module-tutorial-1-the-file-ingest-module/ -- Data Source Ingest Modules: http://www.basistech.com/python-autopsy-module-tutorial-2-the-data-source-ingest-module/ +- File Ingest Modules: \subpage mod_python_file_ingest_tutorial_page +- Data Source Ingest Modules: \subpage mod_python_ds_ingest_tutorial_page - Report Modules: http://www.basistech.com/python-autopsy-module-tutorial-3-the-report-module/ This document contains the following pages: diff --git a/docs/doxygen/modDSIngestTutorial.dox b/docs/doxygen/modDSIngestTutorial.dox new file mode 100644 index 0000000000..e457cbd7d0 --- /dev/null +++ b/docs/doxygen/modDSIngestTutorial.dox @@ -0,0 +1,5 @@ +/*! \page mod_python_ds_ingest_tutorial_page Python Tutorial #2: Writing a Data Source Ingest Module + + + +*/ \ No newline at end of file diff --git a/docs/doxygen/modFileIngestTutorial.dox b/docs/doxygen/modFileIngestTutorial.dox new file mode 100644 index 0000000000..def7e91c2a --- /dev/null +++ b/docs/doxygen/modFileIngestTutorial.dox @@ -0,0 +1,154 @@ +/*! \page mod_python_file_ingest_tutorial_page Python Tutorial #1: Writing a File Ingest Module + + +\section python_tutorial1_why Why Write a File Ingest Module? +
    +
  • Autopsy hides the fact that a file is coming from a file system, was carved, was from inside of a ZIP file, or was part of a local file. So, you don’t need to spend time supporting all of the ways that your user may want to get data to you. You just need to worry about analyzing the content.
  • +
  • Autopsy displays files automatically and can include them in reports if you use standard blackboard artifacts (described later). That means you don’t need to worry about UIs and reports.
  • +
  • Autopsy gives you access to results from other modules. So, you can build on top of their results instead of duplicating them.
  • +
+ +\section python_tutorial1_ingest_modules Ingest Modules + +For our first example, we’re going to write an ingest module. Ingest modules in Autopsy run on the data sources that are added to a case. When you add a disk image (or local drive or logical folder) in Autopsy, you’ll be presented with a list of modules to run (such as hash lookup and keyword search). + +\image html ingest-modules.PNG + +Those are all ingest modules. We’re going to write one of those. There are two types of ingest modules that we can build: +
    +
  • File Ingest Modules are the easiest to write. During their lifetime, they will get passed in each file in the data source. This includes files that are found via carving or inside of ZIP files (if those modules are also enabled).
  • +
  • Data Source Ingest Modules require slightly more work because you have to query the database for the files of interest. If you only care about a small number of files, know their name, and know they won’t be inside of ZIP files, then these are your best bet.
  • +
+ +For this first tutorial, we’re going to write a file ingest module. The \ref mod_python_ds_ingest_tutorial_page "second tutorial" will focus on data source ingest modules. Regardless of the type of ingest module you are writing, you will need to work with two classes: +
    +
  • The factory class provides Autopsy with module information such as display name and version. It also creates instances of ingest modules as needed.
  • +
  • The ingest module class will do the actual analysis. One of these will be created per thread. For file ingest modules, Autopsy will typically create two or more of these at a time so that it can analyze files in parallel. If you keep things simple, and don’t use static variables, then you don’t have to think about anything multithreaded.
  • +
+ +\section python_tutorial1_getting_started Getting Started + +To write your first file ingest module, you’ll need: +
+ +Some other general notes are that you will be writing in Jython, which converts Python-looking code into Java. It has some limitations, including: +
    +
  • You can’t use Python 3 (you are limited to Python 2.7)
  • +
  • You can’t use libraries that use native code
  • +
+ +But, Jython will give you access to all of the Java classes and services that Autopsy provides. So, if you want to stray from this example, then refer to the Developer docs on what classes and methods you have access to. The comments in the sample file will identify what type of object is being passed in along with a URL to its documentation. + +\subsection python_tutorial1_folder Making Your Module Folder + +Every Python module in Autopsy gets its own folder. This reduces naming collisions between modules. To find out where you should put your Python module, launch Autopsy and choose the Tools -> Python Plugins menu item. That will open a folder in your AppData folder, such as "C:\Users\JDoe\AppData\Roaming\Autopsy\python_modules". + +

Make a folder inside of there to store your module. Call it "DemoScript". Copy the fileIngestModule.py sample file listed above into the this new folder and rename it to FindBigRoundFiles.py. Your folder should look like this: + +\image html demoScript_folder.png + +\subsection python_tutorial1_writing Writing the Script + +We are going to write a script that flags any file that is larger than 10MB and whose size is a multiple of 4096. We’ll call these big and round files. This kind of technique could be useful for finding encrypted files. An additional check would be for entropy of the file, but we’ll keep the example simple. + +Open the FindBigRoundFiles.py file in your favorite python text editor. The sample Autopsy Python modules all have TODO entries in them to let you know what you should change. The below steps jump from one TODO to the next. +

    +
  1. Factory Class Name: The first thing to do is rename the sample class name from “SampleJythonFileIngestModuleFactory” to “FindBigRoundFilesIngestModuleFactory”. In the sample module, there are several uses of this class name, so you should search and replace for these strings.
  2. +
  3. Name and Description: The next TODO entries are for names and descriptions. These are shown to users. For this example, we’ll name it “Big and Round File Finder”. The description can be anything you want. Note that Autopsy requires that modules have unique names, so don’t make it too generic.
  4. +
  5. Ingest Module Class Name: The next thing to do is rename the ingest module class from “SampleJythonFileIngestModule” to “FindBigRoundFilesIngestModule”. Our usual naming convention is that this class is the same as the factory class with “Factory” removed from the end.
  6. +
  7. startUp() method: The startUp() method is where each module initializes. For our example, we don’t need to do anything special in here. Typically though, this is where you want to do stuff that could fail because throwing an exception here causes the entire ingest to stop.
  8. +
  9. process() method: This is where we do our analysis. The sample module is well documented with what it does. It ignores non-files, looks at the file name, and makes a blackboard artifact for “.txt” files. There are also a bunch of other things that it does to show examples for easy copy and pasting, but we don’t need them in our module. We’ll cover what goes into this method in the next section.
  10. +
  11. shutdown() method: The shutDown() method either frees resources that were allocated or sends summary messages. For our module, it will do nothing.
  12. +
+ +\subsection python_tutorial1_process The process() Method + +The process() method is passed in a reference to an AbstractFile Object. With this, you have access to all of a file’s contents and metadata. We want to flag files that are larger than 10MB and that are a multiple of 4096 bytes. The following code does that: + +\verbatim if ((file.getSize() > 10485760) and ((file.getSize() % 4096) == 0)): +\endverbatim + +Now that we have found the files, we want to do something with them. In our situation, we just want to alert the user to them. We do this by making an "Interesting Item" blackboard artifact. The Blackboard is where ingest modules can communicate with each other and with the Autopsy GUI. The blackboard has a set of artifacts on it and each artifact:

+
    +
  • Has a type
  • +
  • Is associated with a file
  • +
  • Has one or more attributes. Attributes are simply name and value pairs.
  • +
+ +For our example, we are going to make an artifact of type "TSK_INTERESTING_FILE" whenever we find a big and round file. These are one of the most generic artifact types and are simply a way of alerting the user that a file is interesting for some reason. Once you make the artifact, it will be shown in the UI. The below code makes an artifact for the file and puts it into the set of "Big and Round Files". You can create whatever set names you want. The Autopsy GUI organizes Interesting Files by their set name. +\verbatim + art = file.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT) + att = BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID(), + FindBigRoundFilesIngestModuleFactory.moduleName, "Big and Round Files") + art.addAttribute(att)\endverbatim + +The above code adds the artifact and a single attribute to the blackboard in the embedded database, but it does not notify other modules or the UI. The UI will eventually refresh, but it is faster to fire an event with this: +\verbatim + IngestServices.getInstance().fireModuleDataEvent( + ModuleDataEvent(FindBigRoundFilesIngestModuleFactory.moduleName, + BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT, None))\endverbatim + +That’s it. Your process() method should look something like this: +\verbatim + def process(self, file): + + # Skip non-files + + if ((file.getType() == TskData.TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS) or + + (file.getType() == TskData.TSK_DB_FILES_TYPE_ENUM.UNUSED_BLOCKS) or + + (file.isFile() == False)): + + return IngestModule.ProcessResult.OK + + + + # Look for files bigger than 10MB that are a multiple of 4096 + + if ((file.getSize() > 10485760) and ((file.getSize() % 4096) == 0)): + + + + # Make an artifact on the blackboard. TSK_INTERESTING_FILE_HIT is a generic type of + + # artifact. Refer to the developer docs for other examples. + + art = file.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT) + + att = BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID(), + + FindBigRoundFilesIngestModuleFactory.moduleName, "Big and Round Files") + + art.addAttribute(att) + + + + # Fire an event to notify the UI and others that there is a new artifact + + IngestServices.getInstance().fireModuleDataEvent( + + ModuleDataEvent(FindBigRoundFilesIngestModuleFactory.moduleName, + + BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT, None)) + + + + return IngestModule.ProcessResult.OK\endverbatim + +Save this file and run the module on some of your data. If you have any big and round files, you should see an entry under the “Interesting Items” node in the tree. + +\image html bigAndRoundFiles.png + +\subsection python_tutorial1_debug Debugging and Development Tips + +Whenever you have syntax errors or other errors in your script, you will get some form of dialog from Autopsy when you try to run ingest modules. If that happens, fix the problem and run ingest modules again. You don’t need to restart Autopsy each time! + +The sample module has some log statements in there to help debug what is going on since we don’t know of better ways to debug the scripts while running in Autopsy. + + +*/ From ba9e8c2b8ac179951b648d6cbb4c9fb3fd76c03d Mon Sep 17 00:00:00 2001 From: Eammon Date: Wed, 16 Oct 2019 16:42:33 -0400 Subject: [PATCH 02/34] Workarounds to ensure that dialogs are not hidden on macOS. --- .../casemodule/CaseInformationPanel.java | 2 ++ .../autopsy/casemodule/CaseOpenAction.java | 17 +++++++++++++++-- .../autopsy/casemodule/CueBannerPanel.java | 2 ++ .../autopsy/casemodule/NewCaseWizardAction.java | 2 ++ 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CaseInformationPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/CaseInformationPanel.java index a6494fe22b..76b56138d7 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/CaseInformationPanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CaseInformationPanel.java @@ -162,6 +162,8 @@ class CaseInformationPanel extends javax.swing.JPanel { editCasePropertiesDialog.setResizable(true); editCasePropertiesDialog.pack(); editCasePropertiesDialog.setLocationRelativeTo(this); + // Workaround to ensure dialog is not hidden on macOS + editCasePropertiesDialog.setAlwaysOnTop(true); editCasePropertiesDialog.setVisible(true); editCasePropertiesDialog.toFront(); caseDetailsPanel.updateCaseInfo(); diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java b/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java index cc07148ba0..b68d1a49f6 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java @@ -84,10 +84,16 @@ public final class CaseOpenAction extends CallableSystemAction implements Action fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); fileChooser.setMultiSelectionEnabled(false); fileChooser.setFileFilter(caseMetadataFileFilter); + if (null != ModuleSettings.getConfigSetting(ModuleSettings.MAIN_SETTINGS, PROP_BASECASE)) { fileChooser.setCurrentDirectory(new File(ModuleSettings.getConfigSetting("Case", PROP_BASECASE))); //NON-NLS } - + + /** + * If the open multi user case dialog is open make sure it's not set + * to always be on top as this hides the file chooser on macOS. + */ + OpenMultiUserCaseDialog.getInstance().setAlwaysOnTop(false); String optionsDlgTitle = NbBundle.getMessage(Case.class, "CloseCaseWhileIngesting.Warning.title"); String optionsDlgMessage = NbBundle.getMessage(Case.class, "CloseCaseWhileIngesting.Warning"); if (IngestRunningCheck.checkAndConfirmProceed(optionsDlgTitle, optionsDlgMessage)) { @@ -95,7 +101,12 @@ public final class CaseOpenAction extends CallableSystemAction implements Action * Pop up a file chooser to allow the user to select a case metadata * file (.aut file). */ - int retval = fileChooser.showOpenDialog(WindowManager.getDefault().getMainWindow()); + /** + * Passing the fileChooser as its own parent gets around an issue + * where the fileChooser was hidden behind the CueBannerPanel ("Welcome" dialog) + * on macOS. + */ + int retval = fileChooser.showOpenDialog(fileChooser); if (retval == JFileChooser.APPROVE_OPTION) { /* * Close the startup window, if it is open. @@ -159,6 +170,8 @@ public final class CaseOpenAction extends CallableSystemAction implements Action OpenMultiUserCaseDialog multiUserCaseWindow = OpenMultiUserCaseDialog.getInstance(); multiUserCaseWindow.setLocationRelativeTo(WindowManager.getDefault().getMainWindow()); + // Workaround to ensure that dialog is not hidden on macOS. + multiUserCaseWindow.setAlwaysOnTop(true); multiUserCaseWindow.setVisible(true); WindowManager.getDefault().getMainWindow().setCursor(null); diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CueBannerPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/CueBannerPanel.java index 3ddb97fcfd..37c6c7c8b8 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/CueBannerPanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CueBannerPanel.java @@ -249,6 +249,8 @@ public class CueBannerPanel extends javax.swing.JPanel { private void openRecentCaseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_openRecentCaseButtonActionPerformed recentCasesWindow.setLocationRelativeTo(this); OpenRecentCasePanel.getInstance(); //refreshes the recent cases table + // Workaround to ensure that dialog is not hidden on macOS. + recentCasesWindow.setAlwaysOnTop(true); recentCasesWindow.setVisible(true); }//GEN-LAST:event_openRecentCaseButtonActionPerformed diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseWizardAction.java b/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseWizardAction.java index 50688b1ac1..c5e6ece78d 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseWizardAction.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseWizardAction.java @@ -71,6 +71,8 @@ final class NewCaseWizardAction extends CallableSystemAction { wizardDescriptor.setTitleFormat(new MessageFormat("{0}")); wizardDescriptor.setTitle(NbBundle.getMessage(this.getClass(), "NewCaseWizardAction.newCase.windowTitle.text")); Dialog dialog = DialogDisplayer.getDefault().createDialog(wizardDescriptor); + // Workaround to ensure new case dialog is not hidden on macOS + dialog.setAlwaysOnTop(true); dialog.setVisible(true); dialog.toFront(); if (wizardDescriptor.getValue() == WizardDescriptor.FINISH_OPTION) { From f73eb23ad966d49d42e87f9030e369243f5ed1e8 Mon Sep 17 00:00:00 2001 From: Mark McKinnon Date: Wed, 16 Oct 2019 23:18:24 -0400 Subject: [PATCH 03/34] Update ThunderbirdMboxFileIngestModule.java Check threaded messageId if null and skip it if it is. Format code also. --- .../ThunderbirdMboxFileIngestModule.java | 161 +++++++++--------- 1 file changed, 81 insertions(+), 80 deletions(-) diff --git a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java index 5c42269a8a..b27f045398 100644 --- a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java +++ b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java @@ -65,12 +65,13 @@ import org.sleuthkit.datamodel.TskException; * structure and metadata. */ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { + private static final Logger logger = Logger.getLogger(ThunderbirdMboxFileIngestModule.class.getName()); private final IngestServices services = IngestServices.getInstance(); private FileManager fileManager; private IngestJobContext context; private Blackboard blackboard; - + private Case currentCase; /** @@ -80,7 +81,7 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { } @Override - @Messages ({"ThunderbirdMboxFileIngestModule.noOpenCase.errMsg=Exception while getting open case."}) + @Messages({"ThunderbirdMboxFileIngestModule.noOpenCase.errMsg=Exception while getting open case."}) public void startUp(IngestJobContext context) throws IngestModuleException { this.context = context; try { @@ -103,8 +104,8 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { } //skip unalloc - if ((abstractFile.getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS)) || - (abstractFile.getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.SLACK))) { + if ((abstractFile.getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS)) + || (abstractFile.getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.SLACK))) { return ProcessResult.OK; } @@ -115,7 +116,7 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { // check its signature boolean isMbox = false; boolean isEMLFile = false; - + try { byte[] t = new byte[64]; if (abstractFile.getSize() > 64) { @@ -132,7 +133,7 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { if (isMbox) { return processMBox(abstractFile); } - + if (isEMLFile) { return processEMLFile(abstractFile); } @@ -140,7 +141,7 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { if (PstParser.isPstFile(abstractFile)) { return processPst(abstractFile); } - + if (VcardParser.isVcardFile(abstractFile)) { return processVcard(abstractFile); } @@ -160,7 +161,7 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { String fileName; try { fileName = getTempPath() + File.separator + abstractFile.getName() - + "-" + String.valueOf(abstractFile.getId()); + + "-" + String.valueOf(abstractFile.getId()); } catch (NoCurrentCaseException ex) { logger.log(Level.SEVERE, "Exception while getting open case.", ex); //NON-NLS return ProcessResult.ERROR; @@ -188,11 +189,11 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { PstParser parser = new PstParser(services); PstParser.ParseResult result = parser.open(file, abstractFile.getId()); - switch( result) { + switch (result) { case OK: Iterator pstMsgIterator = parser.getEmailMessageIterator(); if (pstMsgIterator != null) { - processEmails(parser.getPartialEmailMessages(), pstMsgIterator , abstractFile); + processEmails(parser.getPartialEmailMessages(), pstMsgIterator, abstractFile); } else { // sometimes parser returns ParseResult=OK but there are no messages postErrorMessage( @@ -273,7 +274,7 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { String fileName; try { fileName = getTempPath() + File.separator + abstractFile.getName() - + "-" + String.valueOf(abstractFile.getId()); + + "-" + String.valueOf(abstractFile.getId()); } catch (NoCurrentCaseException ex) { logger.log(Level.SEVERE, "Exception while getting open case.", ex); //NON-NLS return ProcessResult.ERROR; @@ -298,16 +299,16 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { return ProcessResult.OK; } - MboxParser emailIterator = MboxParser.getEmailIterator( emailFolder, file, abstractFile.getId()); + MboxParser emailIterator = MboxParser.getEmailIterator(emailFolder, file, abstractFile.getId()); List emails = new ArrayList<>(); - if(emailIterator != null) { - while(emailIterator.hasNext()) { + if (emailIterator != null) { + while (emailIterator.hasNext()) { EmailMessage emailMessage = emailIterator.next(); - if(emailMessage != null) { + if (emailMessage != null) { emails.add(emailMessage); } } - + String errors = emailIterator.getErrors(); if (!errors.isEmpty()) { postErrorMessage( @@ -315,7 +316,7 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { abstractFile.getName()), errors); } } - processEmails(emails, MboxParser.getEmailIterator( emailFolder, file, abstractFile.getId()), abstractFile); + processEmails(emails, MboxParser.getEmailIterator(emailFolder, file, abstractFile.getId()), abstractFile); if (file.delete() == false) { logger.log(Level.INFO, "Failed to delete temp file: {0}", file.getName()); //NON-NLS @@ -323,7 +324,7 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { return ProcessResult.OK; } - + /** * Parse and extract data from a vCard file. * @@ -347,8 +348,8 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { } return ProcessResult.OK; } - - private ProcessResult processEMLFile(AbstractFile abstractFile) { + + private ProcessResult processEMLFile(AbstractFile abstractFile) { try { EmailMessage message = EMLParser.parse(abstractFile); @@ -400,7 +401,7 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { /** * Get a module output folder. - * + * * @throws NoCurrentCaseException if there is no open case. * * @return the module output folder @@ -435,38 +436,40 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { * @param abstractFile */ private void processEmails(List partialEmailsForThreading, Iterator fullMessageIterator, AbstractFile abstractFile) { - + // Putting try/catch around this to catch any exception and still allow // the creation of the artifacts to continue. - try{ + try { EmailMessageThreader.threadMessages(partialEmailsForThreading); - } catch(Exception ex) { + } catch (Exception ex) { logger.log(Level.WARNING, String.format("Exception thrown parsing emails from %s", abstractFile.getName()), ex); } - + List derivedFiles = new ArrayList<>(); int msgCnt = 0; - while(fullMessageIterator.hasNext()) { + while (fullMessageIterator.hasNext()) { EmailMessage current = fullMessageIterator.next(); - - if(current == null) { + + if (current == null) { continue; } - if(partialEmailsForThreading.size() > msgCnt) { + if (partialEmailsForThreading.size() > msgCnt) { EmailMessage threaded = partialEmailsForThreading.get(msgCnt++); - - if(threaded.getMessageID().equals(current.getMessageID()) && - threaded.getSubject().equals(current.getSubject())) { - current.setMessageThreadID(threaded.getMessageThreadID()); + + if (threaded.getMessageID() != null) { + if (threaded.getMessageID().equals(current.getMessageID()) + && threaded.getSubject().equals(current.getSubject())) { + current.setMessageThreadID(threaded.getMessageThreadID()); + } } } - + BlackboardArtifact msgArtifact = addEmailArtifact(current, abstractFile); - - if ((msgArtifact != null) && (current.hasAttachment())) { - derivedFiles.addAll(handleAttachments(current.getAttachments(), abstractFile, msgArtifact )); + + if ((msgArtifact != null) && (current.hasAttachment())) { + derivedFiles.addAll(handleAttachments(current.getAttachments(), abstractFile, msgArtifact)); } } @@ -477,6 +480,7 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { } context.addFilesToJob(derivedFiles); } + /** * Add the given attachments as derived files and reschedule them for * ingest. @@ -517,29 +521,30 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { } /** - * Finds and returns a set of unique email addresses found in the input string + * Finds and returns a set of unique email addresses found in the input + * string * * @param input - input string, like the To/CC line from an email header - * + * * @return Set: set of email addresses found in the input string */ private Set findEmailAddresess(String input) { Pattern p = Pattern.compile("\\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,4}\\b", - Pattern.CASE_INSENSITIVE); + Pattern.CASE_INSENSITIVE); Matcher m = p.matcher(input); Set emailAddresses = new HashSet<>(); while (m.find()) { - emailAddresses.add( m.group()); + emailAddresses.add(m.group()); } return emailAddresses; } - + /** * Add a blackboard artifact for the given e-mail message. * * @param email The e-mail message. * @param abstractFile The associated file. - * + * * @return The generated e-mail message artifact. */ @Messages({"ThunderbirdMboxFileIngestModule.addArtifact.indexError.message=Failed to index email message detected artifact for keyword search."}) @@ -563,73 +568,69 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { List senderAddressList = new ArrayList<>(); String senderAddress; senderAddressList.addAll(findEmailAddresess(from)); - + AccountFileInstance senderAccountInstance = null; if (senderAddressList.size() == 1) { senderAddress = senderAddressList.get(0); try { senderAccountInstance = currentCase.getSleuthkitCase().getCommunicationsManager().createAccountFileInstance(Account.Type.EMAIL, senderAddress, EmailParserModuleFactory.getModuleName(), abstractFile); + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "Failed to create account for email address " + senderAddress, ex); //NON-NLS } - catch(TskCoreException ex) { - logger.log(Level.WARNING, "Failed to create account for email address " + senderAddress, ex); //NON-NLS - } + } else { + logger.log(Level.WARNING, "Failed to find sender address, from = {0}", from); //NON-NLS } - else { - logger.log(Level.WARNING, "Failed to find sender address, from = {0}", from); //NON-NLS - } - + List recipientAddresses = new ArrayList<>(); recipientAddresses.addAll(findEmailAddresess(to)); recipientAddresses.addAll(findEmailAddresess(cc)); recipientAddresses.addAll(findEmailAddresess(bcc)); - + List recipientAccountInstances = new ArrayList<>(); recipientAddresses.forEach((addr) -> { try { - AccountFileInstance recipientAccountInstance = - currentCase.getSleuthkitCase().getCommunicationsManager().createAccountFileInstance(Account.Type.EMAIL, addr, - EmailParserModuleFactory.getModuleName(), abstractFile); + AccountFileInstance recipientAccountInstance + = currentCase.getSleuthkitCase().getCommunicationsManager().createAccountFileInstance(Account.Type.EMAIL, addr, + EmailParserModuleFactory.getModuleName(), abstractFile); recipientAccountInstances.add(recipientAccountInstance); - } - catch(TskCoreException ex) { + } catch (TskCoreException ex) { logger.log(Level.WARNING, "Failed to create account for email address " + addr, ex); //NON-NLS } }); - + addArtifactAttribute(headers, ATTRIBUTE_TYPE.TSK_HEADERS, bbattributes); addArtifactAttribute(from, ATTRIBUTE_TYPE.TSK_EMAIL_FROM, bbattributes); addArtifactAttribute(to, ATTRIBUTE_TYPE.TSK_EMAIL_TO, bbattributes); addArtifactAttribute(subject, ATTRIBUTE_TYPE.TSK_SUBJECT, bbattributes); - + addArtifactAttribute(dateL, ATTRIBUTE_TYPE.TSK_DATETIME_RCVD, bbattributes); addArtifactAttribute(dateL, ATTRIBUTE_TYPE.TSK_DATETIME_SENT, bbattributes); - + addArtifactAttribute(body, ATTRIBUTE_TYPE.TSK_EMAIL_CONTENT_PLAIN, bbattributes); - - addArtifactAttribute(((id < 0L) ? NbBundle.getMessage(this.getClass(), "ThunderbirdMboxFileIngestModule.notAvail") : String.valueOf(id)), + + addArtifactAttribute(((id < 0L) ? NbBundle.getMessage(this.getClass(), "ThunderbirdMboxFileIngestModule.notAvail") : String.valueOf(id)), ATTRIBUTE_TYPE.TSK_MSG_ID, bbattributes); - - addArtifactAttribute(((localPath.isEmpty() == false) ? localPath : ""), + + addArtifactAttribute(((localPath.isEmpty() == false) ? localPath : ""), ATTRIBUTE_TYPE.TSK_PATH, bbattributes); - + addArtifactAttribute(cc, ATTRIBUTE_TYPE.TSK_EMAIL_CC, bbattributes); addArtifactAttribute(bodyHTML, ATTRIBUTE_TYPE.TSK_EMAIL_CONTENT_HTML, bbattributes); addArtifactAttribute(rtf, ATTRIBUTE_TYPE.TSK_EMAIL_CONTENT_RTF, bbattributes); addArtifactAttribute(threadID, ATTRIBUTE_TYPE.TSK_THREAD_ID, bbattributes); - - + try { - + bbart = abstractFile.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_EMAIL_MSG); bbart.addAttributes(bbattributes); // Add account relationships - currentCase.getSleuthkitCase().getCommunicationsManager().addRelationships(senderAccountInstance, recipientAccountInstances, bbart,Relationship.Type.MESSAGE, dateL); - + currentCase.getSleuthkitCase().getCommunicationsManager().addRelationships(senderAccountInstance, recipientAccountInstances, bbart, Relationship.Type.MESSAGE, dateL); + try { // index the artifact for keyword search - blackboard.postArtifact(bbart, EmailParserModuleFactory.getModuleName()); + blackboard.postArtifact(bbart, EmailParserModuleFactory.getModuleName()); } catch (Blackboard.BlackboardException ex) { logger.log(Level.SEVERE, "Unable to index blackboard artifact " + bbart.getArtifactID(), ex); //NON-NLS MessageNotifyUtil.Notify.error(Bundle.ThunderbirdMboxFileIngestModule_addArtifact_indexError_message(), bbart.getDisplayName()); @@ -640,11 +641,11 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { return bbart; } - + /** * Add an attribute of a specified type to a supplied Collection. - * - * @param stringVal The attribute value. + * + * @param stringVal The attribute value. * @param attrType The type of attribute to be added. * @param bbattributes The Collection to which the attribute will be added. */ @@ -656,7 +657,7 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { /** * Add an attribute of a specified type to a supplied Collection. - * + * * @param stringVal The attribute value. * @param attrType The type of attribute to be added. * @param bbattributes The Collection to which the attribute will be added. @@ -666,10 +667,10 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { bbattributes.add(new BlackboardAttribute(attrType, EmailParserModuleFactory.getModuleName(), stringVal)); } } - + /** * Add an attribute of a specified type to a supplied Collection. - * + * * @param longVal The attribute value. * @param attrType The type of attribute to be added. * @param bbattributes The Collection to which the attribute will be added. @@ -679,10 +680,10 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { bbattributes.add(new BlackboardAttribute(attrType, EmailParserModuleFactory.getModuleName(), longVal)); } } - + /** * Post an error message for the user. - * + * * @param subj The error subject. * @param details The error details. */ @@ -693,7 +694,7 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { /** * Get the IngestServices object. - * + * * @return The IngestServices object. */ IngestServices getServices() { From 9f3e258837e9826fb8e14cea95049dd93d01c9e7 Mon Sep 17 00:00:00 2001 From: Mark McKinnon Date: Thu, 17 Oct 2019 02:13:46 -0400 Subject: [PATCH 04/34] Update ThunderbirdMboxFileIngestModule.java Make codacy happy. --- .../thunderbirdparser/ThunderbirdMboxFileIngestModule.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java index b27f045398..91bf391e63 100644 --- a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java +++ b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java @@ -458,12 +458,11 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { if (partialEmailsForThreading.size() > msgCnt) { EmailMessage threaded = partialEmailsForThreading.get(msgCnt++); - if (threaded.getMessageID() != null) { - if (threaded.getMessageID().equals(current.getMessageID()) - && threaded.getSubject().equals(current.getSubject())) { + if ((threaded.getMessageID() != null) && + (threaded.getMessageID().equals(current.getMessageID()) + && threaded.getSubject().equals(current.getSubject()))) { current.setMessageThreadID(threaded.getMessageThreadID()); } - } } BlackboardArtifact msgArtifact = addEmailArtifact(current, abstractFile); From e4299efff1116c279921fb8098df871c276441ff Mon Sep 17 00:00:00 2001 From: Ann Priestman Date: Fri, 18 Oct 2019 11:24:36 -0400 Subject: [PATCH 05/34] Finished second and third tutorials. --- docs/doxygen/Doxyfile | 5 +- docs/doxygen/images/reports_select.png | Bin 0 -> 23486 bytes docs/doxygen/main.dox | 2 +- docs/doxygen/modDSIngestTutorial.dox | 166 +++++++++++++++++++++++ docs/doxygen/modFileIngestTutorial.dox | 42 +++--- docs/doxygen/modReportModuleTutorial.dox | 123 +++++++++++++++++ 6 files changed, 314 insertions(+), 24 deletions(-) create mode 100644 docs/doxygen/images/reports_select.png create mode 100644 docs/doxygen/modReportModuleTutorial.dox diff --git a/docs/doxygen/Doxyfile b/docs/doxygen/Doxyfile index 0037b15063..d73868792a 100644 --- a/docs/doxygen/Doxyfile +++ b/docs/doxygen/Doxyfile @@ -772,8 +772,9 @@ INPUT = main.dox \ regressionTesting.dox \ native_libs.dox \ modDevPython.dox \ - modFileIngestTutorial.dox \ - modDSIngestTutorial.dox \ + modFileIngestTutorial.dox \ + modDSIngestTutorial.dox \ + modReportModuleTutorial.dox \ debugTsk.dox \ ../../Core/src \ ../../CoreLibs/src \ diff --git a/docs/doxygen/images/reports_select.png b/docs/doxygen/images/reports_select.png new file mode 100644 index 0000000000000000000000000000000000000000..fe4f1f10bd7f8622ca31f334fba3c323488db5d7 GIT binary patch literal 23486 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 literal 0 HcmV?d00001 diff --git a/docs/doxygen/main.dox b/docs/doxygen/main.dox index 094c61f612..230be1fd03 100644 --- a/docs/doxygen/main.dox +++ b/docs/doxygen/main.dox @@ -11,7 +11,7 @@ If these pages don't answer your question, then send the question to the Python Plugins menu item. That will open a subfolder in your AppData folder, such as "C:\Users\JDoe\AppData\Roaming\Autopsy\python_modules". + +Make a folder inside of there to store your module. Call it "DemoScript2". Copy the dataSourcengestModule.py sample file from github into the this new folder and rename it to FindContactsDb.py. + +\subsection python_tutorial2_script Writing The Script + +We are going to write a script that: +
    +
  • Queries the backend database for files of a given name
  • +
  • Opens the database
  • +
  • Queries data from the database and makes an artifact for each row
  • +
+ +Open the FindContactsDb.py script in your favorite text editor. The sample Autopsy Python modules all have TODO entries in them to let you know what you should change. The below steps jump from one TODO to the next. +
    +
  1. Factory Class Name: The first thing to do is rename the sample class name from "SampleJythonDataSourceIngestModuleFactory" to "ContactsDbIngestModuleFactory". In the sample module, there are several uses of this class name, so you should search and replace for these strings.
  2. +
  3. Name and Description: The next TODO entries are for names and descriptions. These are shown to users. For this example, we'll name it "Contacts Db Analyzer". The description can be anything you want. Note that Autopsy requires that modules have unique names, so don't make it too generic.
  4. +
  5. Ingest Module Class Name: The next thing to do is rename the ingest module class from "SampleJythonDataSourceIngestModule" to "ContactsDbIngestModule". Our usual naming convention is that this class is the same as the factory class with "Factory" removed from the end. There are a couple of places where this name is used, so do a search and replace in your code.
  6. +
  7. startUp() method: The startUp() method is where each module initializes. For our example, we don't need to do anything special in here except save a reference to the passed in context object. This is used later on to see if the module has been cancelled.
  8. +
  9. process() method: This is where we do our analysis and we'll focus on this more in the next section.
  10. +
+ +That's it. In the file-level ingest module, we had a shutdown() method, but we do not need that with data source-level ingest modules. When their process method is finished, it can shut itself down. The process() method will be called only once. + +\subsection python_tutorial2_process The process() Method + +The process method in a data source-level ingest module is passed in reference to the data source as a Content object and a Progress Bar class to update our progress.

+

For this tutorial, you can start by deleting the contents of the existing process() method in the sample module. The full source code is linked to at the end of this blog and shows more detail about a fully fledged module. We'll just cover the analytics in the blog.

+ +\subsubsection python_tutorial2_getting_files Getting Files +Because data source-level ingest modules are not passed in specific files to analyze, nearly all of these types of modules will need to use the org.sleuthkit.autopsy.casemodule.services.FileManager service to find relevant files. Check out the methods on that class to see the different ways that you can find files. + +NOTE: See the \ref python_tutorial2_running_exes section for an example of when you simply want to run a command line tool on a disk image instead of querying for files to analyze. + +For our example, we want to find all files named "contacts.db". The org.sleuthkit.autopsy.casemodule.services.FileManager class contains several findFiles() methods to help. You can search for all files with a given name or files with a given name in a particular folder. You can also use SQL syntax to match file patterns, such as "%.jpg" to find all files with a JPEG extension. + +Our example needs these two lines to get the FileManager for the current case and to find the files. +\verbatim +fileManager = Case.getCurrentCase().getServices().getFileManager() +files = fileManager.findFiles(dataSource, "contacts.db")\endverbatim + +findFiles() returns a list of AbstractFile objects. This gives you access to the file's metadata and content. + +For our example, we are going to open these SQLite files. That means that we need to save them to disk. This is less than ideal because it wastes time writing the data to disk and then reading it back in, but it is the only option with many libraries. If you are doing some other type analysis on the content, then you do not need to write it to disk. You can read directly from the AbstractFile (see the sample modules for specific code to do this). + +The org.sleuthkit.autopsy.datamodel.ContentUtils class provides a utility to save file content to disk. We'll make a path in the temp folder of our case directory. To prevent naming collisions, we'll name the file based on its unique ID. The following two lines save the file to lclDbPath. + +\verbatim +lclDbPath = os.path.join(Case.getCurrentCase().getTempDirectory(), str(file.getId()) + ".db") +ContentUtils.writeToFile(file, File(lclDbPath))\endverbatim + +\subsubsection python_tutorial2_analyzing_sqlite Analyzing SQLite +Next, we need to open the SQLite database. We are going to use the Java JDBC infrastructure for this. JDBC is Java's generic way of dealing with different types of databases. To open the database, we do this: +\verbatim +Class.forName("org.sqlite.JDBC").newInstance() +dbConn = DriverManager.getConnection("jdbc:sqlite:%s" % lclDbPath)\endverbatim + +With our connection in hand, we can do some queries. In our sample database, we have a single table named "contacts", which has columns for name, email, and phone. We first start by querying for all rows in our simple table: +\verbatim +stmt = dbConn.createStatement() +resultSet = stmt.executeQuery("SELECT * FROM contacts")\endverbatim + +For each row, we are going to get the values for the name, e-mail, and phone number and make a TSK_CONTACT artifact. Recall from the first tutorial that posting artifacts to the blackboard allows modules to communicate with each other and also allows you to easily display data to the user. The TSK_CONTACT artifact is for storing contact information. + +The basic approach in our example is to make an artifact of a given type (TSK_CONTACT) and have it be associated with the database it came from. We then make attributes for the name, email, and phone. The following code does this for each row in the database: +\verbatim +while resultSet.next(): + + # Make an artifact on the blackboard and give it attributes + art = file.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_CONTACT) + + name = resultSet.getString("name") + art.addAttribute(BlackboardAttribute( + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME_PERSON.getTypeID(), + ContactsDbIngestModuleFactory.moduleName, name)) + + email = resultSet.getString("email") + art.addAttribute(BlackboardAttribute( + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_EMAIL.getTypeID(), + ContactsDbIngestModuleFactory.moduleName, email)) + + phone = resultSet.getString("phone") + art.addAttribute(BlackboardAttribute( + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER.getTypeID(), + ContactsDbIngestModuleFactory.moduleName, phone))\endverbatim + +That's it. We've just found the databases, queried them, and made artifacts for the user to see. There are some final things though. First, we should fire off an event so that the UI updates and refreshes with the new artifacts. We can fire just one event after each database is parsed (or you could fire one for each artifact - it's up to you). + +\verbatim +IngestServices.getInstance().fireModuleDataEvent( + ModuleDataEvent(ContactsDbIngestModuleFactory.moduleName, + BlackboardArtifact.ARTIFACT_TYPE.TSK_CONTACT, None))\endverbatim + +And the final thing is to clean up. We should close the database connections and delete our temporary file. +\verbatim +stmt.close() +dbConn.close() +os.remove(lclDbPath)\endverbatim + +\subsection python_tutorial2_niceties Niceties + +Data source-level ingest modules can run for quite some time. Therefore, data source-level ingest modules should do some additional things that file-level ingest modules do not need to. +
    +
  • Progress bars: Each data source-level ingest module will have its own progress bar in the lower right. A reference to it is passed into the process() method. You should update it to provide user feedback.
  • +
  • Cancellation: A user could cancel ingest while your module is running. You should periodically check if that occurred so that you can bail out as soon as possible. You can do that with a check of: +\verbatim if self.context.isJobCancelled():\endverbatim
  • +
+ +\subsection python_tutorial2_tips Debugging and Development Tips + +You can find the full file along with a small sample database on github. To use the database, add it as a logical file and run your module on it. + +Whenever you have syntax errors or other errors in your script, you will get some form of dialog from Autopsy when you try to run ingest modules. If that happens, fix the problem and run ingest modules again. You don't need to restart Autopsy each time! + +The sample module has some log statements in there to help debug what is going on since we don't know of better ways to debug the scripts while running in Autopsy. + +\section python_tutorial2_running_exes Running Executables +While the above example outlined using the FileManager to find files to analyze, the other common use of data source-level ingest modules is to wrap a command line tool that takes a disk image as input. A sample program (RunExe.py) that does that can be found on github. I'll cover the big topics of that program in this section. There are more details in the script about error checking and such. + +\subsection python_tutorial2_finding_exe Finding The Executable + +To write this kind of data source-level ingest module, put the executable in your module's folder (the DemoScript2 folder we previously made). Use "__file__" to get the path to where your script is and then use some os.path methods to get to the executable in the same folder. +\verbatim +path_to_exe = os.path.join(os.path.dirname(os.path.abspath(__file__)), "img_stat.exe")\endverbatim + +In our sample program, we do this and verify we can find it in the startup() method so that if we don't, then ingest never starts. + +\subsection python_tutorial2_running_the_exe Running The Executable + +Data sources can be disk images, but they can also be a folder of files. We only want to run our executable on a disk image. So, verify that: +\verbatim +if not isinstance(dataSource, Image): + self.log(Level.INFO, "Ignoring data source. Not an image") + return IngestModule.ProcessResult.OK \endverbatim + +You can get the path to the disk image using dataSource.getPaths(). + +Once you have the EXE and the disk image, you can use the various subprocess methods to run them. + +\subsection python_tutorial2_showing_results Showing the User Results + +After the command line tool runs, you have the option of either showing the user the raw output of the tool or parsing it into individual artifacts. Refer to previous sections of this tutorial and the previous tutorial for making artifacts. If you want to simply show the user the output of the tool, then save the output to the Reports folder in the Case directory: +\verbatim +reportPath = os.path.join(Case.getCurrentCase().getCaseDirectory(), + "Reports", "img_stat-" + str(dataSource.getId()) + ".txt") \endverbatim + +Then you can add the report to the case so that it shows up in the tree in the main UI panel. +\verbatim Case.getCurrentCase().addReport(reportPath, "Run EXE", "img_stat output")\endverbatim + +\section python_tutorial2_conclusion Conclusion + +Data source-level ingest modules allow you to query for a subset of files by name or to run on an entire disk image. This tutorial has shown an example of both use cases and shown how to use SQLite in Jython. + */ \ No newline at end of file diff --git a/docs/doxygen/modFileIngestTutorial.dox b/docs/doxygen/modFileIngestTutorial.dox index def7e91c2a..7873513a4f 100644 --- a/docs/doxygen/modFileIngestTutorial.dox +++ b/docs/doxygen/modFileIngestTutorial.dox @@ -3,32 +3,32 @@ \section python_tutorial1_why Why Write a File Ingest Module?
    -
  • Autopsy hides the fact that a file is coming from a file system, was carved, was from inside of a ZIP file, or was part of a local file. So, you don’t need to spend time supporting all of the ways that your user may want to get data to you. You just need to worry about analyzing the content.
  • -
  • Autopsy displays files automatically and can include them in reports if you use standard blackboard artifacts (described later). That means you don’t need to worry about UIs and reports.
  • +
  • Autopsy hides the fact that a file is coming from a file system, was carved, was from inside of a ZIP file, or was part of a local file. So, you don't need to spend time supporting all of the ways that your user may want to get data to you. You just need to worry about analyzing the content.
  • +
  • Autopsy displays files automatically and can include them in reports if you use standard blackboard artifacts (described later). That means you don't need to worry about UIs and reports.
  • Autopsy gives you access to results from other modules. So, you can build on top of their results instead of duplicating them.
\section python_tutorial1_ingest_modules Ingest Modules -For our first example, we’re going to write an ingest module. Ingest modules in Autopsy run on the data sources that are added to a case. When you add a disk image (or local drive or logical folder) in Autopsy, you’ll be presented with a list of modules to run (such as hash lookup and keyword search). +For our first example, we're going to write an ingest module. Ingest modules in Autopsy run on the data sources that are added to a case. When you add a disk image (or local drive or logical folder) in Autopsy, you'll be presented with a list of modules to run (such as hash lookup and keyword search). \image html ingest-modules.PNG -Those are all ingest modules. We’re going to write one of those. There are two types of ingest modules that we can build: +Those are all ingest modules. We're going to write one of those. There are two types of ingest modules that we can build:
  • File Ingest Modules are the easiest to write. During their lifetime, they will get passed in each file in the data source. This includes files that are found via carving or inside of ZIP files (if those modules are also enabled).
  • -
  • Data Source Ingest Modules require slightly more work because you have to query the database for the files of interest. If you only care about a small number of files, know their name, and know they won’t be inside of ZIP files, then these are your best bet.
  • +
  • Data Source Ingest Modules require slightly more work because you have to query the database for the files of interest. If you only care about a small number of files, know their name, and know they won't be inside of ZIP files, then these are your best bet.
-For this first tutorial, we’re going to write a file ingest module. The \ref mod_python_ds_ingest_tutorial_page "second tutorial" will focus on data source ingest modules. Regardless of the type of ingest module you are writing, you will need to work with two classes: +For this first tutorial, we're going to write a file ingest module. The \ref mod_python_ds_ingest_tutorial_page "second tutorial" will focus on data source ingest modules. Regardless of the type of ingest module you are writing, you will need to work with two classes:
  • The factory class provides Autopsy with module information such as display name and version. It also creates instances of ingest modules as needed.
  • -
  • The ingest module class will do the actual analysis. One of these will be created per thread. For file ingest modules, Autopsy will typically create two or more of these at a time so that it can analyze files in parallel. If you keep things simple, and don’t use static variables, then you don’t have to think about anything multithreaded.
  • +
  • The ingest module class will do the actual analysis. One of these will be created per thread. For file ingest modules, Autopsy will typically create two or more of these at a time so that it can analyze files in parallel. If you keep things simple, and don't use static variables, then you don't have to think about anything multithreaded.
\section python_tutorial1_getting_started Getting Started -To write your first file ingest module, you’ll need: +To write your first file ingest module, you'll need:
  • An installed copy of Autopsy available from SleuthKit
  • A text editor.
  • @@ -37,8 +37,8 @@ To write your first file ingest module, you’ll need: Some other general notes are that you will be writing in Jython, which converts Python-looking code into Java. It has some limitations, including:
      -
    • You can’t use Python 3 (you are limited to Python 2.7)
    • -
    • You can’t use libraries that use native code
    • +
    • You can't use Python 3 (you are limited to Python 2.7)
    • +
    • You can't use libraries that use native code
    But, Jython will give you access to all of the Java classes and services that Autopsy provides. So, if you want to stray from this example, then refer to the Developer docs on what classes and methods you have access to. The comments in the sample file will identify what type of object is being passed in along with a URL to its documentation. @@ -53,21 +53,21 @@ Every Python module in Autopsy gets its own folder. This reduces naming collisio \subsection python_tutorial1_writing Writing the Script -We are going to write a script that flags any file that is larger than 10MB and whose size is a multiple of 4096. We’ll call these big and round files. This kind of technique could be useful for finding encrypted files. An additional check would be for entropy of the file, but we’ll keep the example simple. +We are going to write a script that flags any file that is larger than 10MB and whose size is a multiple of 4096. We'll call these big and round files. This kind of technique could be useful for finding encrypted files. An additional check would be for entropy of the file, but we'll keep the example simple. Open the FindBigRoundFiles.py file in your favorite python text editor. The sample Autopsy Python modules all have TODO entries in them to let you know what you should change. The below steps jump from one TODO to the next.
      -
    1. Factory Class Name: The first thing to do is rename the sample class name from “SampleJythonFileIngestModuleFactory” to “FindBigRoundFilesIngestModuleFactory”. In the sample module, there are several uses of this class name, so you should search and replace for these strings.
    2. -
    3. Name and Description: The next TODO entries are for names and descriptions. These are shown to users. For this example, we’ll name it “Big and Round File Finder”. The description can be anything you want. Note that Autopsy requires that modules have unique names, so don’t make it too generic.
    4. -
    5. Ingest Module Class Name: The next thing to do is rename the ingest module class from “SampleJythonFileIngestModule” to “FindBigRoundFilesIngestModule”. Our usual naming convention is that this class is the same as the factory class with “Factory” removed from the end.
    6. -
    7. startUp() method: The startUp() method is where each module initializes. For our example, we don’t need to do anything special in here. Typically though, this is where you want to do stuff that could fail because throwing an exception here causes the entire ingest to stop.
    8. -
    9. process() method: This is where we do our analysis. The sample module is well documented with what it does. It ignores non-files, looks at the file name, and makes a blackboard artifact for “.txt” files. There are also a bunch of other things that it does to show examples for easy copy and pasting, but we don’t need them in our module. We’ll cover what goes into this method in the next section.
    10. +
    11. Factory Class Name: The first thing to do is rename the sample class name from "SampleJythonFileIngestModuleFactory" to "FindBigRoundFilesIngestModuleFactory". In the sample module, there are several uses of this class name, so you should search and replace for these strings.
    12. +
    13. Name and Description: The next TODO entries are for names and descriptions. These are shown to users. For this example, we'll name it "Big and Round File Finder". The description can be anything you want. Note that Autopsy requires that modules have unique names, so don't make it too generic.
    14. +
    15. Ingest Module Class Name: The next thing to do is rename the ingest module class from "SampleJythonFileIngestModule" to "FindBigRoundFilesIngestModule". Our usual naming convention is that this class is the same as the factory class with "Factory" removed from the end.
    16. +
    17. startUp() method: The startUp() method is where each module initializes. For our example, we don't need to do anything special in here. Typically though, this is where you want to do stuff that could fail because throwing an exception here causes the entire ingest to stop.
    18. +
    19. process() method: This is where we do our analysis. The sample module is well documented with what it does. It ignores non-files, looks at the file name, and makes a blackboard artifact for ".txt" files. There are also a bunch of other things that it does to show examples for easy copy and pasting, but we don't need them in our module. We'll cover what goes into this method in the next section.
    20. shutdown() method: The shutDown() method either frees resources that were allocated or sends summary messages. For our module, it will do nothing.
    \subsection python_tutorial1_process The process() Method -The process() method is passed in a reference to an AbstractFile Object. With this, you have access to all of a file’s contents and metadata. We want to flag files that are larger than 10MB and that are a multiple of 4096 bytes. The following code does that: +The process() method is passed in a reference to an AbstractFile Object. With this, you have access to all of a file's contents and metadata. We want to flag files that are larger than 10MB and that are a multiple of 4096 bytes. The following code does that: \verbatim if ((file.getSize() > 10485760) and ((file.getSize() % 4096) == 0)): \endverbatim @@ -92,7 +92,7 @@ The above code adds the artifact and a single attribute to the blackboard in the ModuleDataEvent(FindBigRoundFilesIngestModuleFactory.moduleName, BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT, None))\endverbatim -That’s it. Your process() method should look something like this: +That's it. Your process() method should look something like this: \verbatim def process(self, file): @@ -140,15 +140,15 @@ That’s it. Your process() method should look something like this: return IngestModule.ProcessResult.OK\endverbatim -Save this file and run the module on some of your data. If you have any big and round files, you should see an entry under the “Interesting Items” node in the tree. +Save this file and run the module on some of your data. If you have any big and round files, you should see an entry under the "Interesting Items" node in the tree. \image html bigAndRoundFiles.png \subsection python_tutorial1_debug Debugging and Development Tips -Whenever you have syntax errors or other errors in your script, you will get some form of dialog from Autopsy when you try to run ingest modules. If that happens, fix the problem and run ingest modules again. You don’t need to restart Autopsy each time! +Whenever you have syntax errors or other errors in your script, you will get some form of dialog from Autopsy when you try to run ingest modules. If that happens, fix the problem and run ingest modules again. You don't need to restart Autopsy each time! -The sample module has some log statements in there to help debug what is going on since we don’t know of better ways to debug the scripts while running in Autopsy. +The sample module has some log statements in there to help debug what is going on since we don't know of better ways to debug the scripts while running in Autopsy. */ diff --git a/docs/doxygen/modReportModuleTutorial.dox b/docs/doxygen/modReportModuleTutorial.dox new file mode 100644 index 0000000000..26cb14564e --- /dev/null +++ b/docs/doxygen/modReportModuleTutorial.dox @@ -0,0 +1,123 @@ +/*! \page mod_python_report_tutorial_page Python Tutorial #3: Writing a Report Module + +In our last two tutorials, we built a Python Autopsy \ref mod_python_file_ingest_tutorial_page "file ingest modules" and \ref mod_python_ds_ingest_tutorial_page "data source ingest modules" that analyzed the data sources as they were added to cases. In our third post, we're going to make an entirely different kind of module, a report module. + +Report modules are typically run after the user has completed their analysis. Autopsy comes with report modules to generate HTML, Excel, KML, and other types of reports. We're going to make a report module that outputs data in CSV. + +Like in the second tutorial, we are going to assume that you've read at least the \ref mod_python_file_ingest_tutorial_page "first tutorial" to know how to get your environment set up. As a reminder, Python modules in Autopsy are written in Jython and have access to all of the Java classes (which is why we have links to Java documentation below). + +\section python_tutorial3_report_modules Report Modules + +Autopsy report modules are often run after the user has run some ingest modules, reviewed the results, and tagged some files of interest. The user will be given a list of report modules to choose from. + +\image html reports_select.png + +The main reasons for writing an Autopsy report module are: +
      +
    • You need the results in a custom output format, such as XML or JSON.
    • +
    • You want to upload results to a central location.
    • +
    • You want to perform additional analysis after all ingest modules have run. While the modules have the word "report" in them, there is no actual requirement that they produce a report or export data. The module can simply perform data analysis and post artifacts to the blackboard like ingest modules do.
    • +
    + +As we dive into the details, you will notice that the report module API is fairly generic. This is because reports are created at a case level, not a data source level. So, when a user chooses to run a report module, all Autopsy does is tell it to run and gives it a path to a directory to store its results in. The report module can store whatever it wants in the directory. + +Note that if you look at the \ref mod_report_page "full developer docs", there are other report module types that are supported in Java. These are not supported though in Python. + +\subsection python_tutorial3_getting_content Getting Content + +With report modules, it is up to you to find the content that you want to include in your report or analysis. Generally, you will want to access some or all of the files, tagged files, or blackboard artifacts. As you may recall from the previous tutorials, blackboard artifacts are how ingest modules in Autopsy store their results so that they can be shown in the UI, used by other modules, and included in the final report. In this tutorial, we will introduce the SleuthkitCase class, which we generally don't introduce to module writers because it has lots of methods, many of which are low-level, and there are other classes, such as FileManager, that are more focused and easier to use. + +\subsubsection python_tutorial3_getting_files Getting Files + +You have three choices for getting files to report on. You can use the FileManager, which we used in \ref mod_python_ds_ingest_tutorial_page "the last Data Source-level Ingest Module tutorial". The only change is that you will need to call it multiple times, one for each data source in the case. You will have code that looks something like this: +\verbatim +dataSources = Case.getCurrentCase().getDataSources() +fileManager = Case.getCurrentCase().getServices().getFileManager() + +for dataSource in dataSources: + files = fileManager.findFiles(dataSource, "%.txt")\endverbatim + +Another approach is to use the SleuthkitCase.findAllFilesWhere() method that allows you to specify a SQL query. To use this method, you must know the schema of the database (which makes this a bit more challenging, but more powerful). The schema is defined on the wiki. + +Usually, you just need to focus on the tsk_files table. You may run into memory problems and you can also use SleuthkitCase.findAllFileIdsWhere() to get just the IDs and then call SleuthkitCase.getAbstractFileById() to get files as needed. + +A third approach is to call org.sleuthkit.autopsy.casemodule.Case.getDataSources(), and then recursively call getChildren() on each Content object. This will traverse all of the folders and files in the case. This is the most memory efficient, but also more complex to code. + +\subsubsection python_tutorial3_getting_artifacts Getting Blackboard Artifacts + +The blackboard is where modules store their analysis results. If you want to include them in your report, then there are several methods that you could use. If you want all artifacts of a given type, then you can use SleuthkitCase.getBlackboardArtifacts(). There are many variations of this method that take different arguments. Look at them to find the one that is most convenient for you. + +\subsubsection python_tutorial3_getting_tags Getting Tagged Files or Artifacts + +If you want to find files or artifacts that are tagged, then you can use the org.sleuthkit.autopsy.casemodule.services.TagsManager. It has methods to get all tags of a given name, such as org.sleuthkit.autopsy.casemodule.services.TagsManager.getContentTagsByTagName(). + +\section python_tutorial3_getting_started Getting Started + +\subsection python_tutorial3_making_the_folder Making the Folder + +We'll start by making our module folder. As we learned in \ref mod_python_file_ingest_tutorial_page "the first tutorial", every Python module in Autopsy gets its own folder. To find out where you should put your Python module, launch Autopsy and choose the Tools->Python Plugins menu item. That will open a subfolder in your AppData folder, such as "C:\Users\JDoe\AppData\Roaming\Autopsy\python_modules". + +Make a folder inside of there to store your module. Call it "DemoScript3". Copy the reportmodule.py sample file into the this new folder and rename it to CSVReport.py. + +\subsection python_tutorial3_writing_script Writing the Script + +We are going to write a script that makes some basic CSV output: file name and MD5 hash. Open the CSVReport.py file in your favorite Python text editor. The sample Autopsy Python modules all have TODO entries in them to let you know what you should change. The below steps jump from one TODO to the next. + +
      +
    1. Factory Class Name: The first thing to do is rename the sample class name from "SampleGeneralReportModule" to "CSVReportModule". In the sample module, there are several uses of this class name, so you should search and replace for these strings.
    2. +
    3. Name and Description: The next TODO entries are for names and descriptions. These are shown to users. For this example, we'll name it "CSV Hash Report Module". The description can be anything you want. Note that Autopsy requires that modules have unique names, so don't make it too generic.
    4. +
    5. Relative File Path: The next step is to specify the filename that your module is going to use for the report. Autopsy will later provide you with a folder name to save your report in. If you have multiple file names, then pick the main one. This path will be shown to the user after the report has been generated so that they can open it. For this example, we'll call it "hashes.csv" in the getRelativeFilePath() method.
    6. +
    7. generateReport() Method: This method is what is called when the user wants to run the module. It gets passed in the base directory to store the results in and a progress bar. It is responsible for making the report and calling Case.addReport() so that it will be shown in the tree. We'll cover the details of this method in a later section.
    8. +
    + +\subsection python_tutorial3_generate_report The generateReport() method + +The generateReport() method is where the work is done. The baseReportDir argument is a string for the base directory to store results in. The progressBar argument is a org.sleuthkit.autopsy.report.ReportProgressPanel +that shows the user progress while making long reports and to make the progress bar red if an error occurs. + +We'll use one of the basic ideas from the sample, so you can copy and paste from that as you see fit to make this method. Our general approach is going to be this: +
      +
    1. Open the CSV file.
    2. +
    3. Query for all files.
    4. +
    5. Cycle through each of the files and print a line of text.
    6. +
    7. Add the report to the Case database.
    8. +
    + +To focus on the essential code, we'll skip the progress bar details. However, the final solution that we'll link to at the end contains the progress bar code. + +To open the report file in the right folder, we'll need a line such as this: +\verbatim +fileName = os.path.join(baseReportDir, self.getRelativeFilePath()) +report = open(fileName, 'w')\endverbatim + +Next we need to query for the files. In our case, we want all of the files, but can skip the directories. We'll use lines such as this to get the current case and then call the SleuthkitCase.findAllFilesWhere() method. +\verbatim +sleuthkitCase = Case.getCurrentCase().getSleuthkitCase() +files = sleuthkitCase.findAllFilesWhere("NOT meta_type = " + + str(TskData.TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_DIR.getValue()))\endverbatim + +Now, we want to print a line for each file. To do this, you'll need something like: +\verbatim +for file in files: + md5 = file.getMd5Hash() + + if md5 is None: + md5 = "" + + report.write(file.getParentPath() + file.getName() + "," + md5 + "n")\endverbatim + +Note that the file will only have an MD5 value if the Hash Lookup ingest module was run on the data source. + +Lastly, we want to add the report to the case database so that the user can later find it from the tree and we want to report that we completed successfully. +\verbatim +Case.getCurrentCase().addReport(fileName, self.moduleName, "Hashes CSV") +progressBar.complete(ReportStatus.COMPLETE)\endverbatim + +That's it. The final code can be found on github. + +\subsection python_tutorial3_conclusions Conclusions + +In this tutorial, we made a basic report module that creates a custom CSV file. The most challenging part of writing a report module is knowing how to get all of the data that you need. Hopefully, the \ref python_tutorial3_getting_content section above covered what you need, but if not, then go on the Sleuthkit forum and we'll try to point you in the right direction.

    + + +*/ \ No newline at end of file From e21a620a0dc66a6ba27e89db952c0b61f717a024 Mon Sep 17 00:00:00 2001 From: Ann Priestman Date: Fri, 18 Oct 2019 11:29:04 -0400 Subject: [PATCH 06/34] Update links on Python page --- docs/doxygen/modDevPython.dox | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/doxygen/modDevPython.dox b/docs/doxygen/modDevPython.dox index 8878d97cdb..85b420a4be 100644 --- a/docs/doxygen/modDevPython.dox +++ b/docs/doxygen/modDevPython.dox @@ -15,11 +15,10 @@ Using it is very easy though in Autopsy and it allows you to access all of the J To develop a module, you should follow this section to get your environment setup and then read the later sections on the different types of modules. -There are also a set of tutorials that Basis Technology published on their blog. While not as thorough as this documentation, they are an easy introduction to the general ideas. - -- File Ingest Modules: http://www.basistech.com/python-autopsy-module-tutorial-1-the-file-ingest-module/ -- Data Source Ingest Modules: http://www.basistech.com/python-autopsy-module-tutorial-2-the-data-source-ingest-module/ -- Report Modules: http://www.basistech.com/python-autopsy-module-tutorial-3-the-report-module/ +There are also a set of tutorials that provide an easy introduction to the general ideas. +- File Ingest Modules: \subpage mod_python_file_ingest_tutorial_page +- Data Source Ingest Modules: \subpage mod_python_ds_ingest_tutorial_page +- Report Modules: \subpage mod_python_report_tutorial_page \section mod_dev_py_setup Basic Setup From d39dc4232920963ecb8dc4ff5199eed964b8cd45 Mon Sep 17 00:00:00 2001 From: "U-BASIS\\dsmyda" Date: Mon, 21 Oct 2019 17:19:26 -0400 Subject: [PATCH 07/34] Created a new View for the JSlider. Made the slider thumb easier to drag. Added track highlighting. Adding thread safety to the progress slider. --- .../contentviewers/MediaPlayerPanel.form | 10 +- .../contentviewers/MediaPlayerPanel.java | 249 ++++++++++++++++-- 2 files changed, 240 insertions(+), 19 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.form b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.form index 605ed93697..d8433a907b 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.form +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.form @@ -16,8 +16,8 @@ - + @@ -106,6 +106,9 @@ + + + @@ -172,7 +175,7 @@ - + @@ -192,6 +195,9 @@ + + + diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java index dec8bf55b4..be600a0ea6 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java @@ -19,8 +19,16 @@ package org.sleuthkit.autopsy.contentviewers; import com.google.common.io.Files; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.RenderingHints; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; import java.io.File; import java.io.IOException; import java.util.Arrays; @@ -30,6 +38,7 @@ import java.util.SortedSet; import java.util.TreeSet; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; +import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import javax.swing.BoxLayout; @@ -52,13 +61,17 @@ import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.TskData; import javafx.embed.swing.JFXPanel; +import javax.swing.JComponent; +import javax.swing.JSlider; import javax.swing.SwingUtilities; import javax.swing.event.ChangeListener; +import javax.swing.plaf.basic.BasicSliderUI; import org.freedesktop.gstreamer.ClockTime; import org.freedesktop.gstreamer.Format; import org.freedesktop.gstreamer.GstException; import org.freedesktop.gstreamer.event.SeekFlags; import org.freedesktop.gstreamer.event.SeekType; +import org.openide.util.Exceptions; /** * This is a video player that is part of the Media View layered pane. It uses @@ -188,6 +201,11 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie private static final int SKIP_IN_SECONDS = 30; private ExtractMedia extractMediaWorker; + + //Serialize setting the value of the Video progress slider. + //The slider is a shared resource between the VideoPanelUpdater + //and the TrackListener of the JSliderUI. + private final Semaphore sliderLock; /** * Creates new form MediaViewVideoPanel @@ -195,6 +213,7 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie public MediaPlayerPanel() throws GstException, UnsatisfiedLinkError { initComponents(); customizeComponents(); + sliderLock = new Semaphore(1); } private void customizeComponents() { @@ -531,26 +550,220 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie @Override public void actionPerformed(ActionEvent e) { - if (!progressSlider.getValueIsAdjusting()) { - long position = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS); - long duration = gstPlayBin.queryDuration(TimeUnit.NANOSECONDS); - /** - * Duration may not be known until there is video data in the - * pipeline. We start this updater when data-flow has just been - * initiated so buffering may still be in progress. - */ - if (duration >= 0 && position >= 0) { - double relativePosition = (double) position / duration; - progressSlider.setValue((int) (relativePosition * PROGRESS_SLIDER_SIZE)); - } + try { + if (!progressSlider.getValueIsAdjusting()) { + sliderLock.acquire(); + long position = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS); + long duration = gstPlayBin.queryDuration(TimeUnit.NANOSECONDS); + /** + * Duration may not be known until there is video data in the + * pipeline. We start this updater when data-flow has just been + * initiated so buffering may still be in progress. + */ + if (duration >= 0 && position >= 0) { + double relativePosition = (double) position / duration; + progressSlider.setValue((int) (relativePosition * PROGRESS_SLIDER_SIZE)); + } - SwingUtilities.invokeLater(() -> { - updateTimeLabel(position, duration); - }); + SwingUtilities.invokeLater(() -> { + updateTimeLabel(position, duration); + }); + sliderLock.release(); + } + } catch (InterruptedException ex) { + } } } + /** + * Represents the default configuration for the circular JSliderUI. + */ + private class CircularJSliderConfiguration { + + //Thumb configurations + private final Color thumbColor; + private final Dimension thumbDimension; + + //Track configurations + //Progress bar can be bisected into a seen group + //and an unseen group. + private final Color unseen; + private final Color seen; + + /** + * Default configuration + * + * JSlider is light blue RGB(0,130,255). Seen track is light blue + * RGB(0,130,255). Unseen track is light grey RGB(192, 192, 192). + * + * @param thumbDimension Size of the oval thumb. + */ + public CircularJSliderConfiguration(Dimension thumbDimension) { + Color lightBlue = new Color(0, 130, 255); + + seen = lightBlue; + unseen = Color.LIGHT_GRAY; + + thumbColor = lightBlue; + + this.thumbDimension = new Dimension(thumbDimension); + } + + public Color getThumbColor() { + return thumbColor; + } + + public Color getUnseenTrackColor() { + return unseen; + } + + public Color getSeenTrackColor() { + return seen; + } + + public Dimension getThumbDimension() { + return new Dimension(thumbDimension); + } + } + + /** + * Custom view for the JSlider. + */ + private class CircularJSliderUI extends BasicSliderUI { + + private final CircularJSliderConfiguration config; + + /** + * Creates a custom view for the JSlider. This view draws a blue oval + * thumb at the given width and height. It also paints the track blue as + * the thumb progresses. + * + * @param b JSlider component + * @param width Width of the oval + * @param height Height of the oval. + */ + public CircularJSliderUI(JSlider b, CircularJSliderConfiguration config) { + super(b); + this.config = config; + } + + @Override + protected Dimension getThumbSize() { + return config.getThumbDimension(); + } + + /** + * Modifies the View to be an oval rather than the + * rectangle Controller. + */ + @Override + public void paintThumb(Graphics g) { + Rectangle thumb = this.thumbRect; + + Color original = g.getColor(); + + //Change the thumb view from the rectangle + //controller to an oval. + g.setColor(config.getThumbColor()); + Dimension thumbDimension = config.getThumbDimension(); + g.fillOval(thumb.x, thumb.y, thumbDimension.width, thumbDimension.height); + + //Preserve the graphics original color + g.setColor(original); + } + + @Override + public void paintTrack(Graphics g) { + //This rectangle is the bounding box for the progress bar + //portion of the slider. The track is painted in the middle + //of this rectangle and the thumb laid overtop. + Rectangle track = this.trackRect; + + //Get the location of the thumb, this point splits the + //progress bar into 2 line segments, seen and unseen. + Rectangle thumb = this.thumbRect; + int thumbX = thumb.x; + int thumbY = thumb.y; + + Color original = g.getColor(); + + //Paint the seen side + g.setColor(config.getSeenTrackColor()); + g.drawLine(track.x, track.y + track.height / 2, + thumbX, thumbY + track.height / 2); + + //Paint the unseen side + g.setColor(config.getUnseenTrackColor()); + g.drawLine(thumbX, thumbY + track.height / 2, + track.x + track.width, track.y + track.height / 2); + + //Preserve the graphics color. + g.setColor(original); + } + + @Override + protected TrackListener createTrackListener(JSlider slider) { + return new CustomTrackListener(); + } + + @Override + protected void scrollDueToClickInTrack(int direction) { + try { + //Set the thumb position to the mouse press location, as opposed + //to the closest "block" which is the default behavior. + Point mousePosition = slider.getMousePosition(); + if (mousePosition == null) { + return; + } + int value = this.valueForXPosition(mousePosition.x); + + //Lock the slider down, which is a shared resource. + //The VideoPanelUpdater (dedicated thread) keeps the + //slider in sync with the video position, so without + //proper locking our change could be overwritten. + sliderLock.acquire(); + slider.setValueIsAdjusting(true); + slider.setValue(value); + slider.setValueIsAdjusting(false); + sliderLock.release(); + } catch (InterruptedException ex) { + } + } + + /** + * Applies anti-aliasing if available. + */ + @Override + public void update(Graphics g, JComponent c) { + if (g instanceof Graphics2D) { + Graphics2D g2 = (Graphics2D) g; + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + } + + super.update(g, c); + } + + /** + * This track listener will force the thumb to be snapped to the + * mouse location. This makes grabbing and dragging the JSlider much + * easier. Using the default track listener, the user would have to + * click exactly on the slider thumb to drag it. Now the thumb positions + * itself under the mouse so that it can always be dragged. + */ + private class CustomTrackListener extends CircularJSliderUI.TrackListener { + @Override + public void mousePressed(MouseEvent e) { + //Snap the thumb to position of the mouse + scrollDueToClickInTrack(0); + + //Handle the event as normal. + super.mousePressed(e); + } + } + } + /** * This method is called from within the constructor to initialize the form. * WARNING: Do NOT modify this code. The content of this method is always @@ -592,6 +805,7 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie progressSlider.setDoubleBuffered(true); progressSlider.setMinimumSize(new java.awt.Dimension(36, 21)); progressSlider.setPreferredSize(new java.awt.Dimension(200, 21)); + progressSlider.setUI(new CircularJSliderUI(progressSlider, new CircularJSliderConfiguration(new Dimension(18,18)))); org.openide.awt.Mnemonics.setLocalizedText(progressLabel, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.progressLabel.text")); // NOI18N @@ -645,7 +859,7 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie gridBagConstraints.ipadx = 8; gridBagConstraints.ipady = 7; gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; - gridBagConstraints.insets = new java.awt.Insets(6, 6, 0, 0); + gridBagConstraints.insets = new java.awt.Insets(6, 14, 0, 0); buttonPanel.add(VolumeIcon, gridBagConstraints); audioSlider.setMajorTickSpacing(10); @@ -655,6 +869,7 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie audioSlider.setValue(25); audioSlider.setMinimumSize(new java.awt.Dimension(200, 21)); audioSlider.setPreferredSize(new java.awt.Dimension(200, 21)); + audioSlider.setUI(new CircularJSliderUI(audioSlider, new CircularJSliderConfiguration(new Dimension(15,15)))); gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 4; gridBagConstraints.gridy = 0; @@ -739,8 +954,8 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie this.setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(videoPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(controlPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(videoPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) From 0573fc4a15292f80a081dced2cdc6cc1e4dd0003 Mon Sep 17 00:00:00 2001 From: "U-BASIS\\dsmyda" Date: Mon, 21 Oct 2019 17:32:13 -0400 Subject: [PATCH 08/34] Updated javadoc and cleared out all 1 letter variable names --- .../contentviewers/MediaPlayerPanel.java | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java index be600a0ea6..95634f0435 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java @@ -71,7 +71,6 @@ import org.freedesktop.gstreamer.Format; import org.freedesktop.gstreamer.GstException; import org.freedesktop.gstreamer.event.SeekFlags; import org.freedesktop.gstreamer.event.SeekType; -import org.openide.util.Exceptions; /** * This is a video player that is part of the Media View layered pane. It uses @@ -571,7 +570,7 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie sliderLock.release(); } } catch (InterruptedException ex) { - + //Video panel thread interrupted while waiting on lock. } } } @@ -640,11 +639,11 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie * the thumb progresses. * * @param b JSlider component - * @param width Width of the oval - * @param height Height of the oval. + * @param config Configuration object. Contains info about thumb dimensions + * and colors. */ - public CircularJSliderUI(JSlider b, CircularJSliderConfiguration config) { - super(b); + public CircularJSliderUI(JSlider slider, CircularJSliderConfiguration config) { + super(slider); this.config = config; } @@ -658,23 +657,23 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie * rectangle Controller. */ @Override - public void paintThumb(Graphics g) { + public void paintThumb(Graphics graphic) { Rectangle thumb = this.thumbRect; - Color original = g.getColor(); + Color original = graphic.getColor(); //Change the thumb view from the rectangle //controller to an oval. - g.setColor(config.getThumbColor()); + graphic.setColor(config.getThumbColor()); Dimension thumbDimension = config.getThumbDimension(); - g.fillOval(thumb.x, thumb.y, thumbDimension.width, thumbDimension.height); + graphic.fillOval(thumb.x, thumb.y, thumbDimension.width, thumbDimension.height); //Preserve the graphics original color - g.setColor(original); + graphic.setColor(original); } @Override - public void paintTrack(Graphics g) { + public void paintTrack(Graphics graphic) { //This rectangle is the bounding box for the progress bar //portion of the slider. The track is painted in the middle //of this rectangle and the thumb laid overtop. @@ -686,20 +685,20 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie int thumbX = thumb.x; int thumbY = thumb.y; - Color original = g.getColor(); + Color original = graphic.getColor(); //Paint the seen side - g.setColor(config.getSeenTrackColor()); - g.drawLine(track.x, track.y + track.height / 2, + graphic.setColor(config.getSeenTrackColor()); + graphic.drawLine(track.x, track.y + track.height / 2, thumbX, thumbY + track.height / 2); //Paint the unseen side - g.setColor(config.getUnseenTrackColor()); - g.drawLine(thumbX, thumbY + track.height / 2, + graphic.setColor(config.getUnseenTrackColor()); + graphic.drawLine(thumbX, thumbY + track.height / 2, track.x + track.width, track.y + track.height / 2); //Preserve the graphics color. - g.setColor(original); + graphic.setColor(original); } @Override @@ -728,6 +727,7 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie slider.setValueIsAdjusting(false); sliderLock.release(); } catch (InterruptedException ex) { + //Thread (EDT) interrupted while waiting on lock. } } @@ -735,14 +735,14 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie * Applies anti-aliasing if available. */ @Override - public void update(Graphics g, JComponent c) { - if (g instanceof Graphics2D) { - Graphics2D g2 = (Graphics2D) g; - g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + public void update(Graphics graphic, JComponent component) { + if (graphic instanceof Graphics2D) { + Graphics2D graphic2 = (Graphics2D) graphic; + graphic2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); } - super.update(g, c); + super.update(graphic, component); } /** From 756c82256f904996e7021d98359efa1ebb9892c5 Mon Sep 17 00:00:00 2001 From: "U-BASIS\\dsmyda" Date: Tue, 22 Oct 2019 10:57:16 -0400 Subject: [PATCH 09/34] Paused the video when users click in the track. Unpause when mouse is released. This is the behavior of YouTube. It also keeps the video from ending during a drag. --- .../autopsy/contentviewers/MediaPlayerPanel.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java index 95634f0435..56feadbb82 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java @@ -757,10 +757,21 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie public void mousePressed(MouseEvent e) { //Snap the thumb to position of the mouse scrollDueToClickInTrack(0); + + //Pause the video for convenience + gstPlayBin.pause(); //Handle the event as normal. super.mousePressed(e); - } + } + + @Override + public void mouseReleased(MouseEvent e) { + super.mouseReleased(e); + + //Unpause once the mouse has been released. + gstPlayBin.play(); + } } } From f1b9d2ab85419a49379c572c564e05d07893086a Mon Sep 17 00:00:00 2001 From: "U-BASIS\\dsmyda" Date: Tue, 22 Oct 2019 11:08:10 -0400 Subject: [PATCH 10/34] Ran source formatting and disable track listening when the slider is not enabled. --- .../contentviewers/MediaPlayerPanel.java | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java index 56feadbb82..966d5829e6 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java @@ -200,7 +200,7 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie private static final int SKIP_IN_SECONDS = 30; private ExtractMedia extractMediaWorker; - + //Serialize setting the value of the Video progress slider. //The slider is a shared resource between the VideoPanelUpdater //and the TrackListener of the JSliderUI. @@ -555,9 +555,10 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie long position = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS); long duration = gstPlayBin.queryDuration(TimeUnit.NANOSECONDS); /** - * Duration may not be known until there is video data in the - * pipeline. We start this updater when data-flow has just been - * initiated so buffering may still be in progress. + * Duration may not be known until there is video data in + * the pipeline. We start this updater when data-flow has + * just been initiated so buffering may still be in + * progress. */ if (duration >= 0 && position >= 0) { double relativePosition = (double) position / duration; @@ -639,8 +640,8 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie * the thumb progresses. * * @param b JSlider component - * @param config Configuration object. Contains info about thumb dimensions - * and colors. + * @param config Configuration object. Contains info about thumb + * dimensions and colors. */ public CircularJSliderUI(JSlider slider, CircularJSliderConfiguration config) { super(slider); @@ -653,8 +654,7 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie } /** - * Modifies the View to be an oval rather than the - * rectangle Controller. + * Modifies the View to be an oval rather than the rectangle Controller. */ @Override public void paintThumb(Graphics graphic) { @@ -700,7 +700,7 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie //Preserve the graphics color. graphic.setColor(original); } - + @Override protected TrackListener createTrackListener(JSlider slider) { return new CustomTrackListener(); @@ -716,7 +716,7 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie return; } int value = this.valueForXPosition(mousePosition.x); - + //Lock the slider down, which is a shared resource. //The VideoPanelUpdater (dedicated thread) keeps the //slider in sync with the video position, so without @@ -744,31 +744,39 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie super.update(graphic, component); } - + /** - * This track listener will force the thumb to be snapped to the - * mouse location. This makes grabbing and dragging the JSlider much - * easier. Using the default track listener, the user would have to - * click exactly on the slider thumb to drag it. Now the thumb positions + * This track listener will force the thumb to be snapped to the mouse + * location. This makes grabbing and dragging the JSlider much easier. + * Using the default track listener, the user would have to click + * exactly on the slider thumb to drag it. Now the thumb positions * itself under the mouse so that it can always be dragged. */ private class CustomTrackListener extends CircularJSliderUI.TrackListener { - @Override + + @Override public void mousePressed(MouseEvent e) { + if (!slider.isEnabled()) { + return; + } //Snap the thumb to position of the mouse scrollDueToClickInTrack(0); - + //Pause the video for convenience gstPlayBin.pause(); //Handle the event as normal. super.mousePressed(e); } - + @Override public void mouseReleased(MouseEvent e) { - super.mouseReleased(e); + if (!slider.isEnabled()) { + return; + } + super.mouseReleased(e); + //Unpause once the mouse has been released. gstPlayBin.play(); } From 30c18c15bfdb829a4c1807b37cd316afdde50029 Mon Sep 17 00:00:00 2001 From: "U-BASIS\\dsmyda" Date: Tue, 22 Oct 2019 13:05:47 -0400 Subject: [PATCH 11/34] Added fairness to semaphore and changed the acquire to not be interruptable. --- .../contentviewers/MediaPlayerPanel.java | 82 +++++++++---------- 1 file changed, 38 insertions(+), 44 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java index 966d5829e6..1f02708f64 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java @@ -66,6 +66,7 @@ import javax.swing.JSlider; import javax.swing.SwingUtilities; import javax.swing.event.ChangeListener; import javax.swing.plaf.basic.BasicSliderUI; +import javax.swing.plaf.basic.BasicSliderUI.TrackListener; import org.freedesktop.gstreamer.ClockTime; import org.freedesktop.gstreamer.Format; import org.freedesktop.gstreamer.GstException; @@ -212,7 +213,9 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie public MediaPlayerPanel() throws GstException, UnsatisfiedLinkError { initComponents(); customizeComponents(); - sliderLock = new Semaphore(1); + //True for fairness. In other words, + //acquire() calls are processed in order of invocation. + sliderLock = new Semaphore(1, true); } private void customizeComponents() { @@ -549,29 +552,24 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie @Override public void actionPerformed(ActionEvent e) { - try { - if (!progressSlider.getValueIsAdjusting()) { - sliderLock.acquire(); - long position = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS); - long duration = gstPlayBin.queryDuration(TimeUnit.NANOSECONDS); - /** - * Duration may not be known until there is video data in - * the pipeline. We start this updater when data-flow has - * just been initiated so buffering may still be in - * progress. - */ - if (duration >= 0 && position >= 0) { - double relativePosition = (double) position / duration; - progressSlider.setValue((int) (relativePosition * PROGRESS_SLIDER_SIZE)); - } - - SwingUtilities.invokeLater(() -> { - updateTimeLabel(position, duration); - }); - sliderLock.release(); + if (!progressSlider.getValueIsAdjusting()) { + sliderLock.acquireUninterruptibly(); + long position = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS); + long duration = gstPlayBin.queryDuration(TimeUnit.NANOSECONDS); + /** + * Duration may not be known until there is video data in the + * pipeline. We start this updater when data-flow has just been + * initiated so buffering may still be in progress. + */ + if (duration >= 0 && position >= 0) { + double relativePosition = (double) position / duration; + progressSlider.setValue((int) (relativePosition * PROGRESS_SLIDER_SIZE)); } - } catch (InterruptedException ex) { - //Video panel thread interrupted while waiting on lock. + + SwingUtilities.invokeLater(() -> { + updateTimeLabel(position, duration); + }); + sliderLock.release(); } } } @@ -708,27 +706,23 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie @Override protected void scrollDueToClickInTrack(int direction) { - try { - //Set the thumb position to the mouse press location, as opposed - //to the closest "block" which is the default behavior. - Point mousePosition = slider.getMousePosition(); - if (mousePosition == null) { - return; - } - int value = this.valueForXPosition(mousePosition.x); - - //Lock the slider down, which is a shared resource. - //The VideoPanelUpdater (dedicated thread) keeps the - //slider in sync with the video position, so without - //proper locking our change could be overwritten. - sliderLock.acquire(); - slider.setValueIsAdjusting(true); - slider.setValue(value); - slider.setValueIsAdjusting(false); - sliderLock.release(); - } catch (InterruptedException ex) { - //Thread (EDT) interrupted while waiting on lock. + //Set the thumb position to the mouse press location, as opposed + //to the closest "block" which is the default behavior. + Point mousePosition = slider.getMousePosition(); + if (mousePosition == null) { + return; } + int value = this.valueForXPosition(mousePosition.x); + + //Lock the slider down, which is a shared resource. + //The VideoPanelUpdater (dedicated thread) keeps the + //slider in sync with the video position, so without + //proper locking our change could be overwritten. + sliderLock.acquireUninterruptibly(); + slider.setValueIsAdjusting(true); + slider.setValue(value); + slider.setValueIsAdjusting(false); + sliderLock.release(); } /** @@ -774,7 +768,7 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie if (!slider.isEnabled()) { return; } - + super.mouseReleased(e); //Unpause once the mouse has been released. From e9890a72679597d1c8bb0b3a68a2e59345839f42 Mon Sep 17 00:00:00 2001 From: "U-BASIS\\dsmyda" Date: Tue, 22 Oct 2019 14:42:15 -0400 Subject: [PATCH 12/34] Cleaned up line queries and tested for regressions --- InternalPythonModules/android/line.py | 85 ++++++++++----------------- 1 file changed, 31 insertions(+), 54 deletions(-) diff --git a/InternalPythonModules/android/line.py b/InternalPythonModules/android/line.py index ab8e24c9f9..6edcf158fa 100644 --- a/InternalPythonModules/android/line.py +++ b/InternalPythonModules/android/line.py @@ -236,28 +236,23 @@ class LineCallLogsParser(TskCallLogsParser): def __init__(self, calllog_db): super(LineCallLogsParser, self).__init__(calllog_db.runQuery( """ - SELECT Substr(CH.call_type, -1) AS direction, - CH.start_time AS start_time, - CH.end_time AS end_time, - contacts_list_with_groups.members AS group_members, - contacts_list_with_groups.member_names AS names, - CH.caller_mid, - CH.voip_type AS call_type, - CH.voip_gc_media_type AS group_call_type + SELECT Substr(calls.call_type, -1) AS direction, + calls.start_time AS start_time, + calls.end_time AS end_time, + contact_book_w_groups.members AS group_members, + calls.caller_mid, + calls.voip_type AS call_type, + calls.voip_gc_media_type AS group_call_type FROM (SELECT id, - Group_concat(M.m_id) AS members, - Group_concat(Replace(C.server_name, ",", "")) AS member_names + Group_concat(M.m_id) AS members FROM membership AS M - JOIN naver.contacts AS C - ON M.m_id = C.m_id GROUP BY id UNION SELECT m_id, - NULL, - server_name - FROM naver.contacts) AS contacts_list_with_groups - JOIN call_history AS CH - ON CH.caller_mid = contacts_list_with_groups.id + NULL + FROM naver.contacts) AS contact_book_w_groups + JOIN call_history AS calls + ON calls.caller_mid = contact_book_w_groups.id """ ) ) @@ -355,43 +350,25 @@ class LineMessagesParser(TskMessagesParser): 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 + SELECT contact_book_w_groups.id, + contact_book_w_groups.members, + messages.from_mid, + messages.content, + messages.created_time, + messages.attachement_type, + messages.attachement_local_uri, + messages.status + FROM (SELECT id, + Group_concat(M.m_id) AS members + FROM membership AS M + GROUP BY id + UNION + SELECT m_id, + NULL + FROM contacts) AS contact_book_w_groups + JOIN chat_history AS messages + ON messages.chat_id = contact_book_w_groups.id + WHERE attachement_type != 6 """ ) ) From 77aa843d862c55c861d41cd77ecd5580be71e839 Mon Sep 17 00:00:00 2001 From: Eammon Date: Tue, 22 Oct 2019 14:43:30 -0400 Subject: [PATCH 13/34] Added mechanism to get the startup window and to use it or the multi user case dialog as the parent of the file chooser. --- .../autopsy/casemodule/CaseOpenAction.java | 14 ++++++++------ .../autopsy/casemodule/StartupWindowProvider.java | 9 +++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java b/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java index b68d1a49f6..0ba92c7bce 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java @@ -18,6 +18,7 @@ */ package org.sleuthkit.autopsy.casemodule; +import java.awt.Component; import java.awt.Cursor; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -93,7 +94,8 @@ public final class CaseOpenAction extends CallableSystemAction implements Action * If the open multi user case dialog is open make sure it's not set * to always be on top as this hides the file chooser on macOS. */ - OpenMultiUserCaseDialog.getInstance().setAlwaysOnTop(false); + OpenMultiUserCaseDialog multiUserCaseDialog = OpenMultiUserCaseDialog.getInstance(); + multiUserCaseDialog.setAlwaysOnTop(false); String optionsDlgTitle = NbBundle.getMessage(Case.class, "CloseCaseWhileIngesting.Warning.title"); String optionsDlgMessage = NbBundle.getMessage(Case.class, "CloseCaseWhileIngesting.Warning"); if (IngestRunningCheck.checkAndConfirmProceed(optionsDlgTitle, optionsDlgMessage)) { @@ -102,11 +104,11 @@ public final class CaseOpenAction extends CallableSystemAction implements Action * file (.aut file). */ /** - * Passing the fileChooser as its own parent gets around an issue - * where the fileChooser was hidden behind the CueBannerPanel ("Welcome" dialog) - * on macOS. + * The parent of the fileChooser will either be the multi user + * case dialog or the startup window. */ - int retval = fileChooser.showOpenDialog(fileChooser); + int retval = fileChooser.showOpenDialog(multiUserCaseDialog.isVisible() + ? multiUserCaseDialog : (Component) StartupWindowProvider.getInstance().getStartupWindow()); if (retval == JFileChooser.APPROVE_OPTION) { /* * Close the startup window, if it is open. @@ -116,7 +118,7 @@ public final class CaseOpenAction extends CallableSystemAction implements Action /* * Close the Open Multi-User Case window, if it is open. */ - OpenMultiUserCaseDialog.getInstance().setVisible(false); + multiUserCaseDialog.setVisible(false); /* * Try to open the case associated with the case metadata file diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/StartupWindowProvider.java b/Core/src/org/sleuthkit/autopsy/casemodule/StartupWindowProvider.java index 13aae1c0de..8bec55f53c 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/StartupWindowProvider.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/StartupWindowProvider.java @@ -144,4 +144,13 @@ public class StartupWindowProvider implements StartupWindowInterface { startupWindowToUse.close(); } } + + /** + * Get the chosen startup window. + * + * @return The startup window. + */ + public StartupWindowInterface getStartupWindow() { + return startupWindowToUse; + } } From 7e745617c0cb5009ad8e50d6053e3e7bc6b02a9b Mon Sep 17 00:00:00 2001 From: "U-BASIS\\dsmyda" Date: Tue, 22 Oct 2019 15:38:16 -0400 Subject: [PATCH 14/34] Simplifed and tested the textnow queries --- InternalPythonModules/android/textnow.py | 78 +++++++++++------------- 1 file changed, 37 insertions(+), 41 deletions(-) diff --git a/InternalPythonModules/android/textnow.py b/InternalPythonModules/android/textnow.py index ad9704cece..1890c7ae42 100644 --- a/InternalPythonModules/android/textnow.py +++ b/InternalPythonModules/android/textnow.py @@ -290,53 +290,50 @@ class TextNowMessagesParser(TskMessagesParser): """ 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 ) + SELECT CASE + WHEN messages.message_direction == 2 THEN NULL + WHEN contact_book_w_groups.to_addresses IS NULL THEN + messages.contact_value + END from_address, + CASE + WHEN messages.message_direction == 1 THEN NULL + WHEN contact_book_w_groups.to_addresses IS NULL THEN + messages.contact_value + ELSE contact_book_w_groups.to_addresses + END to_address, + messages.message_direction, + messages.message_text, + messages.READ, + messages.DATE, + messages.attach, + thread_id + FROM (SELECT GM.contact_value, + Group_concat(GM.member_contact_value) AS to_addresses, + G.contact_value AS thread_id + FROM group_members AS GM + join GROUPS AS G + ON G.contact_value = GM.contact_value + GROUP BY GM.contact_value + UNION + SELECT contact_value, + NULL, + NULL + FROM contacts) AS contact_book_w_groups + join messages + ON messages.contact_value = contact_book_w_groups.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") == "": + if self.result_set.getString("from_address") is None: return super(TextNowMessagesParser, self).get_phone_number_from() return self.result_set.getString("from_address") @@ -347,10 +344,9 @@ class TextNowMessagesParser(TskMessagesParser): return self.OUTGOING def get_phone_number_to(self): - if self.result_set.getString("to_address") == "": + if self.result_set.getString("to_address") is None: return super(TextNowMessagesParser, self).get_phone_number_to() - recipients = self.result_set.getString("to_address").split(",") - return recipients + return self.result_set.getString("to_address").split(",") def get_message_date_time(self): #convert ms to s @@ -359,7 +355,7 @@ class TextNowMessagesParser(TskMessagesParser): def get_message_read_status(self): read = self.result_set.getBoolean("read") if self.get_message_direction() == self.INCOMING: - if read == True: + if read: return self.READ return self.UNREAD @@ -375,6 +371,6 @@ class TextNowMessagesParser(TskMessagesParser): def get_thread_id(self): thread_id = self.result_set.getString("thread_id") - if thread_id == self._UNKNOWN_THREAD_ID: + if thread_id is None: return super(TextNowMessagesParser, self).get_thread_id() return thread_id From b46fe4d52432ed47e77589cccee69df604c2b1e2 Mon Sep 17 00:00:00 2001 From: "U-BASIS\\dsmyda" Date: Tue, 22 Oct 2019 16:56:54 -0400 Subject: [PATCH 15/34] Cleaned up the whatsapp query and tested changes --- InternalPythonModules/android/whatsapp.py | 49 +++++++++++------------ 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/InternalPythonModules/android/whatsapp.py b/InternalPythonModules/android/whatsapp.py index 9cb7ea3d73..438784f3e2 100644 --- a/InternalPythonModules/android/whatsapp.py +++ b/InternalPythonModules/android/whatsapp.py @@ -433,31 +433,28 @@ class WhatsAppMessagesParser(TskMessagesParser): 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 + SELECT messages.key_remote_jid AS id, + contact_book_w_groups.recipients, + key_from_me AS direction, + messages.data AS content, + messages.timestamp AS send_timestamp, + messages.received_timestamp, + messages.remote_resource AS group_sender, + messages.media_url AS attachment + FROM (SELECT jid, + recipients + FROM wadb.wa_contacts AS contacts + left join (SELECT gjid, + Group_concat(CASE + WHEN jid == "" THEN NULL + ELSE jid + END) AS recipients + FROM group_participants + GROUP BY gjid) AS groups + ON contacts.jid = groups.gjid + GROUP BY jid) AS contact_book_w_groups + join messages + ON messages.key_remote_jid = contact_book_w_groups.jid """ ) ) @@ -503,6 +500,8 @@ class WhatsAppMessagesParser(TskMessagesParser): def get_message_text(self): message = self.result_set.getString("content") + if message is None: + message = super(WhatsAppMessagesParser, self).get_message_text() attachment = self.result_set.getString("attachment") if attachment is not None: return general.appendAttachmentList(message, [attachment]) From cd7942af8905fc399f94e130466f95aa9c5c21d5 Mon Sep 17 00:00:00 2001 From: Richard Cordovano Date: Tue, 22 Oct 2019 17:06:52 -0400 Subject: [PATCH 16/34] 5506 More robust machine trans --- .../ui/TranslatedTextViewer.java | 158 +++++++++--------- 1 file changed, 75 insertions(+), 83 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java index 8c49c23ed7..883b36c2fb 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java @@ -53,6 +53,7 @@ import org.sleuthkit.autopsy.texttranslation.TranslationException; import org.sleuthkit.datamodel.Content; import java.util.List; import java.util.logging.Level; +import javax.swing.SwingUtilities; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.PlatformUtil; import org.sleuthkit.autopsy.texttranslation.ui.TranslationContentPanel.DisplayDropdownOptions; @@ -63,7 +64,7 @@ import org.sleuthkit.autopsy.texttranslation.ui.TranslationContentPanel.DisplayD @ServiceProvider(service = TextViewer.class, position = 4) public final class TranslatedTextViewer implements TextViewer { - private static final Logger logger = Logger.getLogger(TranslatedTextViewer.class.getName()); + private static final Logger logger = Logger.getLogger(TranslatedTextViewer.class.getName()); private static final boolean OCR_ENABLED = true; private static final boolean OCR_DISABLED = false; @@ -72,7 +73,7 @@ public final class TranslatedTextViewer implements TextViewer { private final TranslationContentPanel panel = new TranslationContentPanel(); private volatile Node node; - private volatile BackgroundTranslationTask updateTask; + private volatile ExtractAndTranslateTextTask backgroundTask; private final ThreadFactory translationThreadFactory = new ThreadFactoryBuilder().setNameFormat("translation-content-viewer-%d").build(); private final ExecutorService executorService = Executors.newSingleThreadExecutor(translationThreadFactory); @@ -95,7 +96,7 @@ public final class TranslatedTextViewer implements TextViewer { panel.addLanguagePackNames(INSTALLED_LANGUAGE_PACKS); } } - + int payloadMaxInKB = TextTranslationService.getInstance().getMaxTextChars() / 1000; panel.setWarningLabelMsg(String.format(Bundle.TranslatedTextViewer_maxPayloadSize(), payloadMaxInKB)); @@ -129,10 +130,10 @@ public final class TranslatedTextViewer implements TextViewer { public void resetComponent() { panel.reset(); this.node = null; - if (updateTask != null) { - updateTask.cancel(true); + if (backgroundTask != null) { + backgroundTask.cancel(true); } - updateTask = null; + backgroundTask = null; } @Override @@ -157,62 +158,62 @@ public final class TranslatedTextViewer implements TextViewer { } /** - * Fetches file text and performs translation. + * Extracts text from a file in the currently selected display node and + * optionally translates it. */ - private class BackgroundTranslationTask extends SwingWorker { + private class ExtractAndTranslateTextTask extends SwingWorker { + + private final AbstractFile file; + private final boolean translateText; + + private ExtractAndTranslateTextTask(AbstractFile file, boolean translateText) { + this.file = file; + this.translateText = translateText; + } @NbBundle.Messages({ - "TranslatedContentViewer.noIndexedTextMsg=Run the Keyword Search Ingest Module to get text for translation.", - "TranslatedContentViewer.textAlreadyIndexed=Please view the original text in the Indexed Text viewer.", - "TranslatedContentViewer.errorMsg=Error encountered while getting file text.", - "TranslatedContentViewer.errorExtractingText=Could not extract text from file.", - "TranslatedContentViewer.translatingText=Translating text, please wait..." + "TranslatedContentViewer.translatingText=Translating text, please wait...", + "TranslatedContentViewer.errorExtractingText=Error encountered while extracting text from file.", + "TranslatedContentViewer.fileHasNoText=File has no text.", + "TranslatedContentViewer.errorTranslatingText=Could not translate text from file." }) @Override public String doInBackground() throws InterruptedException { if (this.isCancelled()) { throw new InterruptedException(); } - String dropdownSelection = panel.getDisplayDropDownSelection(); - if (dropdownSelection.equals(DisplayDropdownOptions.ORIGINAL_TEXT.toString())) { - try { - return getFileText(node); - } catch (IOException ex) { - logger.log(Level.WARNING, "Error getting text", ex); - return Bundle.TranslatedContentViewer_errorMsg(); - } catch (TextExtractor.InitReaderException ex) { - logger.log(Level.WARNING, "Error getting text", ex); - return Bundle.TranslatedContentViewer_errorExtractingText(); - } - } else { - try { - return translate(getFileText(node)); - } catch (IOException ex) { - logger.log(Level.WARNING, "Error translating text", ex); - return Bundle.TranslatedContentViewer_errorMsg(); - } catch (TextExtractor.InitReaderException ex) { - logger.log(Level.WARNING, "Error translating text", ex); - return Bundle.TranslatedContentViewer_errorExtractingText(); - } - } - } + SwingUtilities.invokeLater(() -> { + panel.display(Bundle.TranslatedContentViewer_translatingText(), + ComponentOrientation.LEFT_TO_RIGHT, Font.ITALIC); + }); - /** - * Update the extraction loading message depending on the file type. - * - * @param isImage Boolean indicating if the selecting node is an image - */ - @NbBundle.Messages({"TranslatedContentViewer.extractingImageText=Extracting text from image, please wait...", - "TranslatedContentViewer.extractingFileText=Extracting text from file, please wait...",}) - private void updateExtractionLoadingMessage(boolean isImage) { - if (isImage) { - panel.display(Bundle.TranslatedContentViewer_extractingImageText(), - ComponentOrientation.LEFT_TO_RIGHT, Font.ITALIC); - } else { - panel.display(Bundle.TranslatedContentViewer_extractingFileText(), - ComponentOrientation.LEFT_TO_RIGHT, Font.ITALIC); + String fileText; + try { + fileText = getFileText(file); + } catch (IOException | TextExtractor.InitReaderException ex) { + logger.log(Level.WARNING, String.format("Error getting text for file %s (objId=%d)", file.getName(), file.getId()), ex); + return Bundle.TranslatedContentViewer_errorExtractingText(); } + + if (this.isCancelled()) { + throw new InterruptedException(); + } + + if (fileText == null || fileText.isEmpty()) { + return Bundle.TranslatedContentViewer_fileHasNoText(); + } + + if (this.translateText) { + String translation = translate(fileText); + if (this.isCancelled()) { + throw new InterruptedException(); + } + return translation; + } else { + return fileText; + } + } @Override @@ -227,8 +228,10 @@ public final class TranslatedTextViewer implements TextViewer { String orientDetectSubstring = result.substring(0, maxOrientChars); ComponentOrientation orientation = TextUtil.getTextDirection(orientDetectSubstring); panel.display(result, orientation, Font.PLAIN); - } catch (InterruptedException | ExecutionException | CancellationException ignored) { - //InterruptedException & CancellationException - User cancelled, no error. + } catch (InterruptedException | CancellationException ignored) { + // Task cancelled, no error. + } catch (ExecutionException ex) { + logger.log(Level.WARNING, "Error occurred during background task execution", ex); } } @@ -243,14 +246,7 @@ public final class TranslatedTextViewer implements TextViewer { "TranslatedContentViewer.emptyTranslation=The resulting translation was empty.", "TranslatedContentViewer.noServiceProvider=Machine Translation software was not found.", "TranslatedContentViewer.translationException=Error encountered while attempting translation."}) - private String translate(String input) throws InterruptedException { - if (this.isCancelled()) { - throw new InterruptedException(); - } - - panel.display(Bundle.TranslatedContentViewer_translatingText(), - ComponentOrientation.LEFT_TO_RIGHT, Font.ITALIC); - + private String translate(String input) { try { TextTranslationService translatorInstance = TextTranslationService.getInstance(); String translatedResult = translatorInstance.translate(input); @@ -277,33 +273,22 @@ public final class TranslatedTextViewer implements TextViewer { * @throws InterruptedException * @throws * org.sleuthkit.autopsy.textextractors.TextExtractor.InitReaderException - * @throws NoOpenCoreException - * @throws KeywordSearchModuleException */ - private String getFileText(Node node) throws IOException, + private String getFileText(AbstractFile file) throws IOException, InterruptedException, TextExtractor.InitReaderException { - AbstractFile source = (AbstractFile) DataContentViewerUtility.getDefaultContent(node); - boolean isImage = false; - - if (source != null) { - isImage = source.getMIMEType().toLowerCase().startsWith("image/"); - } - - updateExtractionLoadingMessage(isImage); - + final boolean isImage = file.getMIMEType().toLowerCase().startsWith("image/"); // NON-NLS String result; - if (isImage) { - result = extractText(source, OCR_ENABLED); + result = extractText(file, OCR_ENABLED); } else { - result = extractText(source, OCR_DISABLED); + result = extractText(file, OCR_DISABLED); } //Correct for UTF-8 byte[] resultInUTF8Bytes = result.getBytes("UTF8"); - byte[] trimToArraySize = Arrays.copyOfRange(resultInUTF8Bytes, 0, - Math.min(resultInUTF8Bytes.length, MAX_EXTRACT_SIZE_BYTES) ); + byte[] trimToArraySize = Arrays.copyOfRange(resultInUTF8Bytes, 0, + Math.min(resultInUTF8Bytes.length, MAX_EXTRACT_SIZE_BYTES)); return new String(trimToArraySize, "UTF-8"); } @@ -348,7 +333,7 @@ public final class TranslatedTextViewer implements TextViewer { textBuilder.append(cbuf, 0, read); bytesRead += read; } - + return textBuilder.toString(); } @@ -399,7 +384,7 @@ public final class TranslatedTextViewer implements TextViewer { */ private abstract class SelectionChangeListener implements ActionListener { - public String currentSelection = null; + public String currentSelection; public abstract String getSelection(); @@ -408,14 +393,21 @@ public final class TranslatedTextViewer implements TextViewer { String selection = getSelection(); if (!selection.equals(currentSelection)) { currentSelection = selection; - if (updateTask != null && !updateTask.isDone()) { - updateTask.cancel(true); + + if (backgroundTask != null && !backgroundTask.isDone()) { + backgroundTask.cancel(true); } - updateTask = new BackgroundTranslationTask(); + + AbstractFile file = node.getLookup().lookup(AbstractFile.class); + if (file == null) { + return; + } + boolean translateText = currentSelection.equals(DisplayDropdownOptions.ORIGINAL_TEXT.toString()); + backgroundTask = new ExtractAndTranslateTextTask(file, translateText); //Pass the background task to a single threaded pool to keep //the number of jobs running to one. - executorService.execute(updateTask); + executorService.execute(backgroundTask); } } } From ec58218cb21addcf7139ab51650cf1566b08cf55 Mon Sep 17 00:00:00 2001 From: Richard Cordovano Date: Tue, 22 Oct 2019 17:16:11 -0400 Subject: [PATCH 17/34] 5506 More robust machine trans --- .../autopsy/texttranslation/ui/TranslatedTextViewer.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java index 883b36c2fb..ee8d4d96b4 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java @@ -158,8 +158,7 @@ public final class TranslatedTextViewer implements TextViewer { } /** - * Extracts text from a file in the currently selected display node and - * optionally translates it. + * Extracts text from a file and optionally translates it. */ private class ExtractAndTranslateTextTask extends SwingWorker { @@ -183,6 +182,11 @@ public final class TranslatedTextViewer implements TextViewer { throw new InterruptedException(); } + /* + * This message is only written to the viewer once this task starts + * and any previous task has been completed by the single-threaded + * executor. + */ SwingUtilities.invokeLater(() -> { panel.display(Bundle.TranslatedContentViewer_translatingText(), ComponentOrientation.LEFT_TO_RIGHT, Font.ITALIC); From a58de8029febce29f4f454a79d0dc0fe6f7811aa Mon Sep 17 00:00:00 2001 From: "U-BASIS\\dsmyda" Date: Tue, 22 Oct 2019 17:28:13 -0400 Subject: [PATCH 18/34] Cleaned up skype queries and tested them --- InternalPythonModules/android/skype.py | 141 ++++++++----------------- 1 file changed, 45 insertions(+), 96 deletions(-) diff --git a/InternalPythonModules/android/skype.py b/InternalPythonModules/android/skype.py index 5044898d5f..d8b79ac7fe 100644 --- a/InternalPythonModules/android/skype.py +++ b/InternalPythonModules/android/skype.py @@ -76,11 +76,8 @@ class SkypeAnalyzer(general.AndroidComponentAnalyzer): 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 + with chatItem. This result is consistently labeled contact_book_w_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): @@ -93,7 +90,12 @@ class SkypeAnalyzer(general.AndroidComponentAnalyzer): account_query_result = skype_db.runQuery( """ SELECT entry_id, - """+_format_user_name()+""" AS name + 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 AS name FROM user """ ) @@ -251,14 +253,6 @@ class SkypeCallLogsParser(TskCallLogsParser): 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. @@ -266,37 +260,23 @@ class SkypeCallLogsParser(TskCallLogsParser): """ 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 + SELECT contact_book_w_groups.conversation_id, + contact_book_w_groups.participant_ids, + messages.time, + messages.duration, + messages.is_sender_me, + messages.person_id AS sender_id 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_concat(person_id) AS participant_ids + FROM particiapnt 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 + SELECT entry_id AS conversation_id, + NULL + FROM person) AS contact_book_w_groups + join chatitem AS messages + ON messages.conversation_link = contact_book_w_groups.conversation_id + WHERE message_type == 3 """ ) ) @@ -347,7 +327,12 @@ class SkypeContactsParser(TskContactsParser): super(SkypeContactsParser, self).__init__(contact_db.runQuery( """ SELECT entry_id, - """+_format_user_name()+""" AS name + 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 AS name FROM person """ ) @@ -379,39 +364,25 @@ class SkypeMessagesParser(TskMessagesParser): """ 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 + SELECT contact_book_w_groups.conversation_id, + contact_book_w_groups.participant_ids, + messages.time, + messages.content, + messages.device_gallery_path, + messages.is_sender_me, + messages.person_id as sender_id + FROM (SELECT conversation_id, + Group_concat(person_id) AS participant_ids + FROM particiapnt + GROUP BY conversation_id + UNION + SELECT entry_id as conversation_id, + NULL + FROM person) AS contact_book_w_groups + JOIN chatitem AS messages + ON messages.conversation_link = contact_book_w_groups.conversation_id WHERE message_type != 3 - """ + """ ) ) self._SKYPE_MESSAGE_TYPE = "Skype Message" @@ -469,25 +440,3 @@ class SkypeMessagesParser(TskMessagesParser): 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 - """ - - From 1acd1ccfd1c81d2d6137d0fbd398cb095dff5040 Mon Sep 17 00:00:00 2001 From: Richard Cordovano Date: Tue, 22 Oct 2019 17:30:35 -0400 Subject: [PATCH 19/34] 5506 More robust machine trans --- .../ui/Bundle.properties-MERGED | 16 ++++++------- .../ui/TranslatedTextViewer.java | 23 +++++++++---------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties-MERGED index 141ca2f7ef..bbf7046f47 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties-MERGED @@ -2,16 +2,14 @@ OptionsCategory_Name_Machine_Translation=Machine Translation OptionsCategory_Keywords_Machine_Translation_Settings=Machine Translation Settings TranslatedContentPanel.comboBoxOption.originalText=Original Text (Up to 25KB) TranslatedContentPanel.comboBoxOption.translatedText=Translated Text -TranslatedContentViewer.emptyTranslation=The resulting translation was empty. -TranslatedContentViewer.errorExtractingText=Could not extract text from file. -TranslatedContentViewer.errorMsg=Error encountered while getting file text. -TranslatedContentViewer.extractingFileText=Extracting text from file, please wait... -TranslatedContentViewer.extractingImageText=Extracting text from image, please wait... -TranslatedContentViewer.noIndexedTextMsg=Run the Keyword Search Ingest Module to get text for translation. -TranslatedContentViewer.noServiceProvider=Machine Translation software was not found. -TranslatedContentViewer.textAlreadyIndexed=Please view the original text in the Indexed Text viewer. +TranslatedContentViewer.emptyTranslation=The translation is empty. +TranslatedContentViewer.errorExtractingText=Error encountered while extracting text from file. +TranslatedContentViewer.errorTranslatingText=Could not translate text from file. +TranslatedContentViewer.fileHasNoText=File has no text. +TranslatedContentViewer.noServiceProvider=Machine translation software was not found. TranslatedContentViewer.translatingText=Translating text, please wait... -TranslatedContentViewer.translationException=Error encountered while attempting translation. +# {0} - exception message +TranslatedContentViewer.translationException=Error encountered while attempting translation ({0}). TranslatedTextViewer.maxPayloadSize=Up to the first %dKB of text will be translated TranslatedTextViewer.title=Translation TranslatedTextViewer.toolTip=Displays translated file text. diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java index ee8d4d96b4..035d9f6398 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java @@ -188,8 +188,7 @@ public final class TranslatedTextViewer implements TextViewer { * executor. */ SwingUtilities.invokeLater(() -> { - panel.display(Bundle.TranslatedContentViewer_translatingText(), - ComponentOrientation.LEFT_TO_RIGHT, Font.ITALIC); + panel.display(Bundle.TranslatedContentViewer_translatingText(), ComponentOrientation.LEFT_TO_RIGHT, Font.ITALIC); }); String fileText; @@ -235,7 +234,7 @@ public final class TranslatedTextViewer implements TextViewer { } catch (InterruptedException | CancellationException ignored) { // Task cancelled, no error. } catch (ExecutionException ex) { - logger.log(Level.WARNING, "Error occurred during background task execution", ex); + logger.log(Level.WARNING, String.format("Error occurred during background task execution for file %s (objId=%d)", file.getName(), file.getId()), ex); } } @@ -247,9 +246,9 @@ public final class TranslatedTextViewer implements TextViewer { * @return Translated text or error message */ @NbBundle.Messages({ - "TranslatedContentViewer.emptyTranslation=The resulting translation was empty.", - "TranslatedContentViewer.noServiceProvider=Machine Translation software was not found.", - "TranslatedContentViewer.translationException=Error encountered while attempting translation."}) + "TranslatedContentViewer.emptyTranslation=The translation is empty.", + "TranslatedContentViewer.noServiceProvider=Machine translation software was not found.", + "# {0} - exception message", "TranslatedContentViewer.translationException=Error encountered while attempting translation ({0})."}) private String translate(String input) { try { TextTranslationService translatorInstance = TextTranslationService.getInstance(); @@ -261,8 +260,8 @@ public final class TranslatedTextViewer implements TextViewer { } catch (NoServiceProviderException ex) { return Bundle.TranslatedContentViewer_noServiceProvider(); } catch (TranslationException ex) { - logger.log(Level.WARNING, "Error translating text", ex); - return Bundle.TranslatedContentViewer_translationException() + " (" + ex.getMessage() + ")"; + logger.log(Level.WARNING, String.format("Error occurred translating text for file %s (objId=%d)", file.getName(), file.getId()), ex); + return Bundle.TranslatedContentViewer_translationException(ex.getMessage()); } } @@ -388,9 +387,9 @@ public final class TranslatedTextViewer implements TextViewer { */ private abstract class SelectionChangeListener implements ActionListener { - public String currentSelection; + private String currentSelection; - public abstract String getSelection(); + abstract String getSelection(); @Override public final void actionPerformed(ActionEvent e) { @@ -422,7 +421,7 @@ public final class TranslatedTextViewer implements TextViewer { private class DisplayDropDownChangeListener extends SelectionChangeListener { @Override - public String getSelection() { + String getSelection() { return panel.getDisplayDropDownSelection(); } } @@ -433,7 +432,7 @@ public final class TranslatedTextViewer implements TextViewer { private class OCRDropdownChangeListener extends SelectionChangeListener { @Override - public String getSelection() { + String getSelection() { return panel.getSelectedOcrLanguagePack(); } } From 3d19a07ba4bd75d4e7150216d492a1e5d193fdfa Mon Sep 17 00:00:00 2001 From: Richard Cordovano Date: Tue, 22 Oct 2019 17:48:58 -0400 Subject: [PATCH 20/34] 5506 More robust machine trans --- .../ui/Bundle.properties-MERGED | 4 +- .../ui/TranslatedTextViewer.java | 59 ++++++++++--------- 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties-MERGED index bbf7046f47..cfa9af0fb7 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties-MERGED @@ -3,8 +3,8 @@ OptionsCategory_Keywords_Machine_Translation_Settings=Machine Translation Settin TranslatedContentPanel.comboBoxOption.originalText=Original Text (Up to 25KB) TranslatedContentPanel.comboBoxOption.translatedText=Translated Text TranslatedContentViewer.emptyTranslation=The translation is empty. -TranslatedContentViewer.errorExtractingText=Error encountered while extracting text from file. -TranslatedContentViewer.errorTranslatingText=Could not translate text from file. +# {0} - exception message +TranslatedContentViewer.errorExtractingText=Error encountered while extracting text from file ({0}). TranslatedContentViewer.fileHasNoText=File has no text. TranslatedContentViewer.noServiceProvider=Machine translation software was not found. TranslatedContentViewer.translatingText=Translating text, please wait... diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java index 035d9f6398..b6490b70bd 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java @@ -172,9 +172,11 @@ public final class TranslatedTextViewer implements TextViewer { @NbBundle.Messages({ "TranslatedContentViewer.translatingText=Translating text, please wait...", - "TranslatedContentViewer.errorExtractingText=Error encountered while extracting text from file.", + "# {0} - exception message", "TranslatedContentViewer.errorExtractingText=Error encountered while extracting text from file ({0}).", "TranslatedContentViewer.fileHasNoText=File has no text.", - "TranslatedContentViewer.errorTranslatingText=Could not translate text from file." + "TranslatedContentViewer.emptyTranslation=The translation is empty.", + "TranslatedContentViewer.noServiceProvider=Machine translation software was not found.", + "# {0} - exception message", "TranslatedContentViewer.translationException=Error encountered while attempting translation ({0})." }) @Override public String doInBackground() throws InterruptedException { @@ -184,8 +186,8 @@ public final class TranslatedTextViewer implements TextViewer { /* * This message is only written to the viewer once this task starts - * and any previous task has been completed by the single-threaded - * executor. + * and any previous task has therefore been completed by the + * single-threaded executor. */ SwingUtilities.invokeLater(() -> { panel.display(Bundle.TranslatedContentViewer_translatingText(), ComponentOrientation.LEFT_TO_RIGHT, Font.ITALIC); @@ -196,7 +198,7 @@ public final class TranslatedTextViewer implements TextViewer { fileText = getFileText(file); } catch (IOException | TextExtractor.InitReaderException ex) { logger.log(Level.WARNING, String.format("Error getting text for file %s (objId=%d)", file.getName(), file.getId()), ex); - return Bundle.TranslatedContentViewer_errorExtractingText(); + return Bundle.TranslatedContentViewer_errorExtractingText(ex.getMessage()); } if (this.isCancelled()) { @@ -207,16 +209,26 @@ public final class TranslatedTextViewer implements TextViewer { return Bundle.TranslatedContentViewer_fileHasNoText(); } - if (this.translateText) { - String translation = translate(fileText); - if (this.isCancelled()) { - throw new InterruptedException(); - } - return translation; - } else { + if (!this.translateText) { return fileText; } + String translation; + try { + translation = translate(fileText); + } catch (NoServiceProviderException ex) { + logger.log(Level.WARNING, String.format("Error occurred translating text for file %s (objId=%d)", file.getName(), file.getId()), ex); + translation = Bundle.TranslatedContentViewer_noServiceProvider(); + } catch (TranslationException ex) { + logger.log(Level.WARNING, String.format("Error occurred translating text for file %s (objId=%d)", file.getName(), file.getId()), ex); + translation = Bundle.TranslatedContentViewer_translationException(ex.getMessage()); + } + + if (this.isCancelled()) { + throw new InterruptedException(); + } + + return translation; } @Override @@ -245,24 +257,13 @@ public final class TranslatedTextViewer implements TextViewer { * * @return Translated text or error message */ - @NbBundle.Messages({ - "TranslatedContentViewer.emptyTranslation=The translation is empty.", - "TranslatedContentViewer.noServiceProvider=Machine translation software was not found.", - "# {0} - exception message", "TranslatedContentViewer.translationException=Error encountered while attempting translation ({0})."}) - private String translate(String input) { - try { - TextTranslationService translatorInstance = TextTranslationService.getInstance(); - String translatedResult = translatorInstance.translate(input); - if (translatedResult.isEmpty()) { - return Bundle.TranslatedContentViewer_emptyTranslation(); - } - return translatedResult; - } catch (NoServiceProviderException ex) { - return Bundle.TranslatedContentViewer_noServiceProvider(); - } catch (TranslationException ex) { - logger.log(Level.WARNING, String.format("Error occurred translating text for file %s (objId=%d)", file.getName(), file.getId()), ex); - return Bundle.TranslatedContentViewer_translationException(ex.getMessage()); + private String translate(String input) throws NoServiceProviderException, TranslationException { + TextTranslationService translatorInstance = TextTranslationService.getInstance(); + String translatedResult = translatorInstance.translate(input); + if (translatedResult.isEmpty()) { + return Bundle.TranslatedContentViewer_emptyTranslation(); } + return translatedResult; } /** From 5a9c630d4b3284de7f0564c73496a99763e16b6f Mon Sep 17 00:00:00 2001 From: Richard Cordovano Date: Tue, 22 Oct 2019 17:50:19 -0400 Subject: [PATCH 21/34] 5506 More robust machine trans --- .../autopsy/texttranslation/ui/TranslatedTextViewer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java index b6490b70bd..65d65f4ad5 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java @@ -176,7 +176,7 @@ public final class TranslatedTextViewer implements TextViewer { "TranslatedContentViewer.fileHasNoText=File has no text.", "TranslatedContentViewer.emptyTranslation=The translation is empty.", "TranslatedContentViewer.noServiceProvider=Machine translation software was not found.", - "# {0} - exception message", "TranslatedContentViewer.translationException=Error encountered while attempting translation ({0})." + "# {0} - exception message", "TranslatedContentViewer.translationException=Error encountered while translating file ({0})." }) @Override public String doInBackground() throws InterruptedException { @@ -243,6 +243,7 @@ public final class TranslatedTextViewer implements TextViewer { String orientDetectSubstring = result.substring(0, maxOrientChars); ComponentOrientation orientation = TextUtil.getTextDirection(orientDetectSubstring); panel.display(result, orientation, Font.PLAIN); + } catch (InterruptedException | CancellationException ignored) { // Task cancelled, no error. } catch (ExecutionException ex) { From 0de0c7d852bae5dfba0e0a085e494202a458e62b Mon Sep 17 00:00:00 2001 From: esaunders Date: Tue, 22 Oct 2019 17:53:07 -0400 Subject: [PATCH 22/34] Add note about location of Options dialog on Mac. --- docs/doxygen-user/main.dox | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/doxygen-user/main.dox b/docs/doxygen-user/main.dox index 122e14fb0f..61d555ace4 100644 --- a/docs/doxygen-user/main.dox +++ b/docs/doxygen-user/main.dox @@ -6,6 +6,8 @@ Overview This is the User's Guide for the open source Autopsy platform. Autopsy allows you to examine a hard drive or mobile device and recover evidence from it. This guide should help you with using Autopsy. The developer's guide will help you develop your own Autopsy modules. +Note: For those users running Autopsy on Mac devices, the functionality available through the "Tools" -> "Options" dialog as described in this documentation can be accessed through the system menu bar under "Preferences" or through the Cmd + , (command-comma) shortcut. + Help Topics ------- The following topics are available here: From 9e01f63d5008b18d90a8fa360cfa36fb1760a412 Mon Sep 17 00:00:00 2001 From: Richard Cordovano Date: Tue, 22 Oct 2019 17:53:42 -0400 Subject: [PATCH 23/34] 5506 More robust machine trans --- .../autopsy/texttranslation/ui/TranslatedTextViewer.java | 1 + 1 file changed, 1 insertion(+) diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java index 65d65f4ad5..811dc229cc 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java @@ -248,6 +248,7 @@ public final class TranslatedTextViewer implements TextViewer { // Task cancelled, no error. } catch (ExecutionException ex) { logger.log(Level.WARNING, String.format("Error occurred during background task execution for file %s (objId=%d)", file.getName(), file.getId()), ex); + panel.display(Bundle.TranslatedContentViewer_translationException(ex.getMessage()), ComponentOrientation.LEFT_TO_RIGHT, Font.ITALIC); } } From 08ccb2a9c95dd0ff1decac9124318ab8a5fce53a Mon Sep 17 00:00:00 2001 From: Mark McKinnon Date: Wed, 23 Oct 2019 11:39:13 -0400 Subject: [PATCH 24/34] Update EmailMessage.java If messageid is null then set it to "" in setMessageID --- .../thunderbirdparser/EmailMessage.java | 74 ++++++++++--------- 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/EmailMessage.java b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/EmailMessage.java index 40f2fc0933..09a6637e6e 100644 --- a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/EmailMessage.java +++ b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/EmailMessage.java @@ -83,7 +83,7 @@ class EmailMessage { void setSubject(String subject) { if (subject != null) { this.subject = subject; - if(subject.matches("^[R|r][E|e].*?:.*")) { + if (subject.matches("^[R|r][E|e].*?:.*")) { this.simplifiedSubject = subject.replaceAll("[R|r][E|e].*?:", "").trim(); replySubject = true; } else { @@ -93,19 +93,19 @@ class EmailMessage { this.simplifiedSubject = ""; } } - + /** * Returns the orginal subject with the "RE:" stripped off". - * + * * @return Message subject with the "RE" stripped off */ String getSimplifiedSubject() { return simplifiedSubject; } - + /** * Returns whether or not the message subject started with "RE:" - * + * * @return true if the original subject started with RE otherwise false. */ boolean isReplySubject() { @@ -121,6 +121,7 @@ class EmailMessage { this.headers = headers; } } + String getTextBody() { return textBody; } @@ -211,75 +212,80 @@ class EmailMessage { this.localPath = localPath; } } - + /** - * Returns the value of the Message-ID header field of this message or - * empty string if it is not present. - * + * Returns the value of the Message-ID header field of this message or empty + * string if it is not present. + * * @return the identifier of this message. */ String getMessageID() { return messageID; } - + /** * Sets the identifier of this message. - * + * * @param messageID identifer of this message */ void setMessageID(String messageID) { - this.messageID = messageID; + if (messageID != null) { + this.messageID = messageID; + } else { + this.messageID = ""; + } } - + /** - * Returns the messageID of the parent message or empty String if not present. - * + * Returns the messageID of the parent message or empty String if not + * present. + * * @return the idenifier of the message parent */ String getInReplyToID() { return inReplyToID; } - + /** * Sets the messageID of the parent message. - * + * * @param inReplyToID messageID of the parent message. */ void setInReplyToID(String inReplyToID) { this.inReplyToID = inReplyToID; } - + /** - * Returns a list of Message-IDs listing the parent, grandparent, - * great-grandparent, and so on, of this message. - * + * Returns a list of Message-IDs listing the parent, grandparent, + * great-grandparent, and so on, of this message. + * * @return The reference list or empty string if none is available. */ List getReferences() { return references; } - + /** * Set the list of reference message-IDs from the email message header. - * - * @param references + * + * @param references */ void setReferences(List references) { this.references = references; } - + /** * Sets the ThreadID of this message. - * + * * @param threadID - the thread ID to set */ void setMessageThreadID(String threadID) { this.messageThreadID = threadID; } - + /** * Returns the ThreadID for this message. - * + * * @return - the message thread ID or "" is non is available */ String getMessageThreadID() { @@ -308,7 +314,7 @@ class EmailMessage { private long aTime = 0L; private long mTime = 0L; - + private TskData.EncodingType encodingType = TskData.EncodingType.NONE; String getName() { @@ -394,14 +400,14 @@ class EmailMessage { this.mTime = mTime.getTime() / 1000; } } - - void setEncodingType(TskData.EncodingType encodingType){ + + void setEncodingType(TskData.EncodingType encodingType) { this.encodingType = encodingType; } - - TskData.EncodingType getEncodingType(){ + + TskData.EncodingType getEncodingType() { return encodingType; } - + } } From 0b8ea73f7f52f3968a90aaab94b7a7c0a6cefd98 Mon Sep 17 00:00:00 2001 From: Mark McKinnon Date: Wed, 23 Oct 2019 11:45:25 -0400 Subject: [PATCH 25/34] Update ThunderbirdMboxFileIngestModule.java Revert changes from prior commit. --- .../ThunderbirdMboxFileIngestModule.java | 162 +++++++++--------- 1 file changed, 81 insertions(+), 81 deletions(-) diff --git a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java index 91bf391e63..5c42269a8a 100644 --- a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java +++ b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java @@ -65,13 +65,12 @@ import org.sleuthkit.datamodel.TskException; * structure and metadata. */ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { - private static final Logger logger = Logger.getLogger(ThunderbirdMboxFileIngestModule.class.getName()); private final IngestServices services = IngestServices.getInstance(); private FileManager fileManager; private IngestJobContext context; private Blackboard blackboard; - + private Case currentCase; /** @@ -81,7 +80,7 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { } @Override - @Messages({"ThunderbirdMboxFileIngestModule.noOpenCase.errMsg=Exception while getting open case."}) + @Messages ({"ThunderbirdMboxFileIngestModule.noOpenCase.errMsg=Exception while getting open case."}) public void startUp(IngestJobContext context) throws IngestModuleException { this.context = context; try { @@ -104,8 +103,8 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { } //skip unalloc - if ((abstractFile.getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS)) - || (abstractFile.getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.SLACK))) { + if ((abstractFile.getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS)) || + (abstractFile.getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.SLACK))) { return ProcessResult.OK; } @@ -116,7 +115,7 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { // check its signature boolean isMbox = false; boolean isEMLFile = false; - + try { byte[] t = new byte[64]; if (abstractFile.getSize() > 64) { @@ -133,7 +132,7 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { if (isMbox) { return processMBox(abstractFile); } - + if (isEMLFile) { return processEMLFile(abstractFile); } @@ -141,7 +140,7 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { if (PstParser.isPstFile(abstractFile)) { return processPst(abstractFile); } - + if (VcardParser.isVcardFile(abstractFile)) { return processVcard(abstractFile); } @@ -161,7 +160,7 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { String fileName; try { fileName = getTempPath() + File.separator + abstractFile.getName() - + "-" + String.valueOf(abstractFile.getId()); + + "-" + String.valueOf(abstractFile.getId()); } catch (NoCurrentCaseException ex) { logger.log(Level.SEVERE, "Exception while getting open case.", ex); //NON-NLS return ProcessResult.ERROR; @@ -189,11 +188,11 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { PstParser parser = new PstParser(services); PstParser.ParseResult result = parser.open(file, abstractFile.getId()); - switch (result) { + switch( result) { case OK: Iterator pstMsgIterator = parser.getEmailMessageIterator(); if (pstMsgIterator != null) { - processEmails(parser.getPartialEmailMessages(), pstMsgIterator, abstractFile); + processEmails(parser.getPartialEmailMessages(), pstMsgIterator , abstractFile); } else { // sometimes parser returns ParseResult=OK but there are no messages postErrorMessage( @@ -274,7 +273,7 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { String fileName; try { fileName = getTempPath() + File.separator + abstractFile.getName() - + "-" + String.valueOf(abstractFile.getId()); + + "-" + String.valueOf(abstractFile.getId()); } catch (NoCurrentCaseException ex) { logger.log(Level.SEVERE, "Exception while getting open case.", ex); //NON-NLS return ProcessResult.ERROR; @@ -299,16 +298,16 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { return ProcessResult.OK; } - MboxParser emailIterator = MboxParser.getEmailIterator(emailFolder, file, abstractFile.getId()); + MboxParser emailIterator = MboxParser.getEmailIterator( emailFolder, file, abstractFile.getId()); List emails = new ArrayList<>(); - if (emailIterator != null) { - while (emailIterator.hasNext()) { + if(emailIterator != null) { + while(emailIterator.hasNext()) { EmailMessage emailMessage = emailIterator.next(); - if (emailMessage != null) { + if(emailMessage != null) { emails.add(emailMessage); } } - + String errors = emailIterator.getErrors(); if (!errors.isEmpty()) { postErrorMessage( @@ -316,7 +315,7 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { abstractFile.getName()), errors); } } - processEmails(emails, MboxParser.getEmailIterator(emailFolder, file, abstractFile.getId()), abstractFile); + processEmails(emails, MboxParser.getEmailIterator( emailFolder, file, abstractFile.getId()), abstractFile); if (file.delete() == false) { logger.log(Level.INFO, "Failed to delete temp file: {0}", file.getName()); //NON-NLS @@ -324,7 +323,7 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { return ProcessResult.OK; } - + /** * Parse and extract data from a vCard file. * @@ -348,8 +347,8 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { } return ProcessResult.OK; } - - private ProcessResult processEMLFile(AbstractFile abstractFile) { + + private ProcessResult processEMLFile(AbstractFile abstractFile) { try { EmailMessage message = EMLParser.parse(abstractFile); @@ -401,7 +400,7 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { /** * Get a module output folder. - * + * * @throws NoCurrentCaseException if there is no open case. * * @return the module output folder @@ -436,39 +435,38 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { * @param abstractFile */ private void processEmails(List partialEmailsForThreading, Iterator fullMessageIterator, AbstractFile abstractFile) { - + // Putting try/catch around this to catch any exception and still allow // the creation of the artifacts to continue. - try { + try{ EmailMessageThreader.threadMessages(partialEmailsForThreading); - } catch (Exception ex) { + } catch(Exception ex) { logger.log(Level.WARNING, String.format("Exception thrown parsing emails from %s", abstractFile.getName()), ex); } - + List derivedFiles = new ArrayList<>(); int msgCnt = 0; - while (fullMessageIterator.hasNext()) { + while(fullMessageIterator.hasNext()) { EmailMessage current = fullMessageIterator.next(); - - if (current == null) { + + if(current == null) { continue; } - if (partialEmailsForThreading.size() > msgCnt) { + if(partialEmailsForThreading.size() > msgCnt) { EmailMessage threaded = partialEmailsForThreading.get(msgCnt++); - - if ((threaded.getMessageID() != null) && - (threaded.getMessageID().equals(current.getMessageID()) - && threaded.getSubject().equals(current.getSubject()))) { - current.setMessageThreadID(threaded.getMessageThreadID()); - } + + if(threaded.getMessageID().equals(current.getMessageID()) && + threaded.getSubject().equals(current.getSubject())) { + current.setMessageThreadID(threaded.getMessageThreadID()); + } } - + BlackboardArtifact msgArtifact = addEmailArtifact(current, abstractFile); - - if ((msgArtifact != null) && (current.hasAttachment())) { - derivedFiles.addAll(handleAttachments(current.getAttachments(), abstractFile, msgArtifact)); + + if ((msgArtifact != null) && (current.hasAttachment())) { + derivedFiles.addAll(handleAttachments(current.getAttachments(), abstractFile, msgArtifact )); } } @@ -479,7 +477,6 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { } context.addFilesToJob(derivedFiles); } - /** * Add the given attachments as derived files and reschedule them for * ingest. @@ -520,30 +517,29 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { } /** - * Finds and returns a set of unique email addresses found in the input - * string + * Finds and returns a set of unique email addresses found in the input string * * @param input - input string, like the To/CC line from an email header - * + * * @return Set: set of email addresses found in the input string */ private Set findEmailAddresess(String input) { Pattern p = Pattern.compile("\\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,4}\\b", - Pattern.CASE_INSENSITIVE); + Pattern.CASE_INSENSITIVE); Matcher m = p.matcher(input); Set emailAddresses = new HashSet<>(); while (m.find()) { - emailAddresses.add(m.group()); + emailAddresses.add( m.group()); } return emailAddresses; } - + /** * Add a blackboard artifact for the given e-mail message. * * @param email The e-mail message. * @param abstractFile The associated file. - * + * * @return The generated e-mail message artifact. */ @Messages({"ThunderbirdMboxFileIngestModule.addArtifact.indexError.message=Failed to index email message detected artifact for keyword search."}) @@ -567,69 +563,73 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { List senderAddressList = new ArrayList<>(); String senderAddress; senderAddressList.addAll(findEmailAddresess(from)); - + AccountFileInstance senderAccountInstance = null; if (senderAddressList.size() == 1) { senderAddress = senderAddressList.get(0); try { senderAccountInstance = currentCase.getSleuthkitCase().getCommunicationsManager().createAccountFileInstance(Account.Type.EMAIL, senderAddress, EmailParserModuleFactory.getModuleName(), abstractFile); - } catch (TskCoreException ex) { - logger.log(Level.WARNING, "Failed to create account for email address " + senderAddress, ex); //NON-NLS } - } else { - logger.log(Level.WARNING, "Failed to find sender address, from = {0}", from); //NON-NLS + catch(TskCoreException ex) { + logger.log(Level.WARNING, "Failed to create account for email address " + senderAddress, ex); //NON-NLS + } } - + else { + logger.log(Level.WARNING, "Failed to find sender address, from = {0}", from); //NON-NLS + } + List recipientAddresses = new ArrayList<>(); recipientAddresses.addAll(findEmailAddresess(to)); recipientAddresses.addAll(findEmailAddresess(cc)); recipientAddresses.addAll(findEmailAddresess(bcc)); - + List recipientAccountInstances = new ArrayList<>(); recipientAddresses.forEach((addr) -> { try { - AccountFileInstance recipientAccountInstance - = currentCase.getSleuthkitCase().getCommunicationsManager().createAccountFileInstance(Account.Type.EMAIL, addr, - EmailParserModuleFactory.getModuleName(), abstractFile); + AccountFileInstance recipientAccountInstance = + currentCase.getSleuthkitCase().getCommunicationsManager().createAccountFileInstance(Account.Type.EMAIL, addr, + EmailParserModuleFactory.getModuleName(), abstractFile); recipientAccountInstances.add(recipientAccountInstance); - } catch (TskCoreException ex) { + } + catch(TskCoreException ex) { logger.log(Level.WARNING, "Failed to create account for email address " + addr, ex); //NON-NLS } }); - + addArtifactAttribute(headers, ATTRIBUTE_TYPE.TSK_HEADERS, bbattributes); addArtifactAttribute(from, ATTRIBUTE_TYPE.TSK_EMAIL_FROM, bbattributes); addArtifactAttribute(to, ATTRIBUTE_TYPE.TSK_EMAIL_TO, bbattributes); addArtifactAttribute(subject, ATTRIBUTE_TYPE.TSK_SUBJECT, bbattributes); - + addArtifactAttribute(dateL, ATTRIBUTE_TYPE.TSK_DATETIME_RCVD, bbattributes); addArtifactAttribute(dateL, ATTRIBUTE_TYPE.TSK_DATETIME_SENT, bbattributes); - + addArtifactAttribute(body, ATTRIBUTE_TYPE.TSK_EMAIL_CONTENT_PLAIN, bbattributes); - - addArtifactAttribute(((id < 0L) ? NbBundle.getMessage(this.getClass(), "ThunderbirdMboxFileIngestModule.notAvail") : String.valueOf(id)), + + addArtifactAttribute(((id < 0L) ? NbBundle.getMessage(this.getClass(), "ThunderbirdMboxFileIngestModule.notAvail") : String.valueOf(id)), ATTRIBUTE_TYPE.TSK_MSG_ID, bbattributes); - - addArtifactAttribute(((localPath.isEmpty() == false) ? localPath : ""), + + addArtifactAttribute(((localPath.isEmpty() == false) ? localPath : ""), ATTRIBUTE_TYPE.TSK_PATH, bbattributes); - + addArtifactAttribute(cc, ATTRIBUTE_TYPE.TSK_EMAIL_CC, bbattributes); addArtifactAttribute(bodyHTML, ATTRIBUTE_TYPE.TSK_EMAIL_CONTENT_HTML, bbattributes); addArtifactAttribute(rtf, ATTRIBUTE_TYPE.TSK_EMAIL_CONTENT_RTF, bbattributes); addArtifactAttribute(threadID, ATTRIBUTE_TYPE.TSK_THREAD_ID, bbattributes); - + + try { - + bbart = abstractFile.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_EMAIL_MSG); bbart.addAttributes(bbattributes); // Add account relationships - currentCase.getSleuthkitCase().getCommunicationsManager().addRelationships(senderAccountInstance, recipientAccountInstances, bbart, Relationship.Type.MESSAGE, dateL); - + currentCase.getSleuthkitCase().getCommunicationsManager().addRelationships(senderAccountInstance, recipientAccountInstances, bbart,Relationship.Type.MESSAGE, dateL); + try { // index the artifact for keyword search - blackboard.postArtifact(bbart, EmailParserModuleFactory.getModuleName()); + blackboard.postArtifact(bbart, EmailParserModuleFactory.getModuleName()); } catch (Blackboard.BlackboardException ex) { logger.log(Level.SEVERE, "Unable to index blackboard artifact " + bbart.getArtifactID(), ex); //NON-NLS MessageNotifyUtil.Notify.error(Bundle.ThunderbirdMboxFileIngestModule_addArtifact_indexError_message(), bbart.getDisplayName()); @@ -640,11 +640,11 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { return bbart; } - + /** * Add an attribute of a specified type to a supplied Collection. - * - * @param stringVal The attribute value. + * + * @param stringVal The attribute value. * @param attrType The type of attribute to be added. * @param bbattributes The Collection to which the attribute will be added. */ @@ -656,7 +656,7 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { /** * Add an attribute of a specified type to a supplied Collection. - * + * * @param stringVal The attribute value. * @param attrType The type of attribute to be added. * @param bbattributes The Collection to which the attribute will be added. @@ -666,10 +666,10 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { bbattributes.add(new BlackboardAttribute(attrType, EmailParserModuleFactory.getModuleName(), stringVal)); } } - + /** * Add an attribute of a specified type to a supplied Collection. - * + * * @param longVal The attribute value. * @param attrType The type of attribute to be added. * @param bbattributes The Collection to which the attribute will be added. @@ -679,10 +679,10 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { bbattributes.add(new BlackboardAttribute(attrType, EmailParserModuleFactory.getModuleName(), longVal)); } } - + /** * Post an error message for the user. - * + * * @param subj The error subject. * @param details The error details. */ @@ -693,7 +693,7 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { /** * Get the IngestServices object. - * + * * @return The IngestServices object. */ IngestServices getServices() { From 166366e2410e468a9070b8689f04cd00ea1ab96c Mon Sep 17 00:00:00 2001 From: esaunders Date: Wed, 23 Oct 2019 14:12:52 -0400 Subject: [PATCH 26/34] Don't make building dependent on downloading test data files. Out Travis build occasionally fails when attempting to dowmload these files. Ideally these files should be downloaded only if tests are to be run that require them. --- Core/build.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/build.xml b/Core/build.xml index 28e64b83e5..0e5c90ef04 100644 --- a/Core/build.xml +++ b/Core/build.xml @@ -137,7 +137,7 @@ - + From f306fc28ad13da320f545ff51525efeae750c39a Mon Sep 17 00:00:00 2001 From: Richard Cordovano Date: Wed, 23 Oct 2019 14:43:35 -0400 Subject: [PATCH 27/34] 5506 More robust machine trans --- .../ui/Bundle.properties-MERGED | 3 ++- .../ui/TranslatedTextViewer.java | 25 ++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties-MERGED index cfa9af0fb7..c4da06114c 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties-MERGED @@ -5,11 +5,12 @@ TranslatedContentPanel.comboBoxOption.translatedText=Translated Text TranslatedContentViewer.emptyTranslation=The translation is empty. # {0} - exception message TranslatedContentViewer.errorExtractingText=Error encountered while extracting text from file ({0}). +TranslatedContentViewer.extractingText=Getting text, please wait... TranslatedContentViewer.fileHasNoText=File has no text. TranslatedContentViewer.noServiceProvider=Machine translation software was not found. TranslatedContentViewer.translatingText=Translating text, please wait... # {0} - exception message -TranslatedContentViewer.translationException=Error encountered while attempting translation ({0}). +TranslatedContentViewer.translationException=Error encountered while translating file ({0}). TranslatedTextViewer.maxPayloadSize=Up to the first %dKB of text will be translated TranslatedTextViewer.title=Translation TranslatedTextViewer.toolTip=Displays translated file text. diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java index 811dc229cc..aed254c9e0 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java @@ -171,6 +171,7 @@ public final class TranslatedTextViewer implements TextViewer { } @NbBundle.Messages({ + "TranslatedContentViewer.extractingText=Getting text, please wait...", "TranslatedContentViewer.translatingText=Translating text, please wait...", "# {0} - exception message", "TranslatedContentViewer.errorExtractingText=Error encountered while extracting text from file ({0}).", "TranslatedContentViewer.fileHasNoText=File has no text.", @@ -184,15 +185,14 @@ public final class TranslatedTextViewer implements TextViewer { throw new InterruptedException(); } - /* - * This message is only written to the viewer once this task starts - * and any previous task has therefore been completed by the - * single-threaded executor. - */ SwingUtilities.invokeLater(() -> { - panel.display(Bundle.TranslatedContentViewer_translatingText(), ComponentOrientation.LEFT_TO_RIGHT, Font.ITALIC); + /* + * This message is only written to the viewer once this task + * starts and any previous task has therefore been completed by + * the single-threaded executor. + */ + panel.display(Bundle.TranslatedContentViewer_extractingText(), ComponentOrientation.LEFT_TO_RIGHT, Font.ITALIC); }); - String fileText; try { fileText = getFileText(file); @@ -213,6 +213,9 @@ public final class TranslatedTextViewer implements TextViewer { return fileText; } + SwingUtilities.invokeLater(() -> { + panel.display(Bundle.TranslatedContentViewer_translatingText(), ComponentOrientation.LEFT_TO_RIGHT, Font.ITALIC); + }); String translation; try { translation = translate(fileText); @@ -243,7 +246,7 @@ public final class TranslatedTextViewer implements TextViewer { String orientDetectSubstring = result.substring(0, maxOrientChars); ComponentOrientation orientation = TextUtil.getTextDirection(orientDetectSubstring); panel.display(result, orientation, Font.PLAIN); - + } catch (InterruptedException | CancellationException ignored) { // Task cancelled, no error. } catch (ExecutionException ex) { @@ -405,10 +408,8 @@ public final class TranslatedTextViewer implements TextViewer { } AbstractFile file = node.getLookup().lookup(AbstractFile.class); - if (file == null) { - return; - } - boolean translateText = currentSelection.equals(DisplayDropdownOptions.ORIGINAL_TEXT.toString()); + String textDisplaySelection = panel.getDisplayDropDownSelection(); + boolean translateText = !textDisplaySelection.equals(DisplayDropdownOptions.ORIGINAL_TEXT.toString()); backgroundTask = new ExtractAndTranslateTextTask(file, translateText); //Pass the background task to a single threaded pool to keep From a416f04a46b5baf7cd6145975181b5a536fe9348 Mon Sep 17 00:00:00 2001 From: Richard Cordovano Date: Wed, 23 Oct 2019 14:45:13 -0400 Subject: [PATCH 28/34] 5506 More robust machine trans --- .../autopsy/texttranslation/ui/TranslatedTextViewer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java index aed254c9e0..242c32684d 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java @@ -171,7 +171,7 @@ public final class TranslatedTextViewer implements TextViewer { } @NbBundle.Messages({ - "TranslatedContentViewer.extractingText=Getting text, please wait...", + "TranslatedContentViewer.extractingText=Extracting text, please wait...", "TranslatedContentViewer.translatingText=Translating text, please wait...", "# {0} - exception message", "TranslatedContentViewer.errorExtractingText=Error encountered while extracting text from file ({0}).", "TranslatedContentViewer.fileHasNoText=File has no text.", From 4c7d14683aa6d01b97e78584c21b7b33f459402e Mon Sep 17 00:00:00 2001 From: Richard Cordovano Date: Wed, 23 Oct 2019 15:11:44 -0400 Subject: [PATCH 29/34] 5506 More robust machine trans --- .../texttranslation/ui/Bundle.properties-MERGED | 10 +++++----- .../texttranslation/ui/TranslatedTextViewer.java | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties-MERGED index c4da06114c..fa1a250e44 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/Bundle.properties-MERGED @@ -2,15 +2,15 @@ OptionsCategory_Name_Machine_Translation=Machine Translation OptionsCategory_Keywords_Machine_Translation_Settings=Machine Translation Settings TranslatedContentPanel.comboBoxOption.originalText=Original Text (Up to 25KB) TranslatedContentPanel.comboBoxOption.translatedText=Translated Text -TranslatedContentViewer.emptyTranslation=The translation is empty. +TranslatedContentViewer.emptyTranslation=The machine translation software did not return any text. # {0} - exception message -TranslatedContentViewer.errorExtractingText=Error encountered while extracting text from file ({0}). -TranslatedContentViewer.extractingText=Getting text, please wait... +TranslatedContentViewer.errorExtractingText=An error occurred while extracting the text ({0}). +TranslatedContentViewer.extractingText=Extracting text, please wait... TranslatedContentViewer.fileHasNoText=File has no text. -TranslatedContentViewer.noServiceProvider=Machine translation software was not found. +TranslatedContentViewer.noServiceProvider=The machine translation software was not found. TranslatedContentViewer.translatingText=Translating text, please wait... # {0} - exception message -TranslatedContentViewer.translationException=Error encountered while translating file ({0}). +TranslatedContentViewer.translationException=An error occurred while translating the text ({0}). TranslatedTextViewer.maxPayloadSize=Up to the first %dKB of text will be translated TranslatedTextViewer.title=Translation TranslatedTextViewer.toolTip=Displays translated file text. diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java index 242c32684d..e3993e629b 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java @@ -173,11 +173,11 @@ public final class TranslatedTextViewer implements TextViewer { @NbBundle.Messages({ "TranslatedContentViewer.extractingText=Extracting text, please wait...", "TranslatedContentViewer.translatingText=Translating text, please wait...", - "# {0} - exception message", "TranslatedContentViewer.errorExtractingText=Error encountered while extracting text from file ({0}).", + "# {0} - exception message", "TranslatedContentViewer.errorExtractingText=An error occurred while extracting the text ({0}).", "TranslatedContentViewer.fileHasNoText=File has no text.", - "TranslatedContentViewer.emptyTranslation=The translation is empty.", - "TranslatedContentViewer.noServiceProvider=Machine translation software was not found.", - "# {0} - exception message", "TranslatedContentViewer.translationException=Error encountered while translating file ({0})." + "TranslatedContentViewer.emptyTranslation=The machine translation software did not return any text.", + "TranslatedContentViewer.noServiceProvider=The machine translation software was not found.", + "# {0} - exception message", "TranslatedContentViewer.translationException=An error occurred while translating the text ({0})." }) @Override public String doInBackground() throws InterruptedException { From 264a827f21b4c650d6ff54ccce87b3fa18e4097f Mon Sep 17 00:00:00 2001 From: Richard Cordovano Date: Wed, 23 Oct 2019 15:17:53 -0400 Subject: [PATCH 30/34] 5506 More robust machine trans --- .../autopsy/texttranslation/ui/TranslatedTextViewer.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java index e3993e629b..13dda67f5f 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java @@ -186,11 +186,6 @@ public final class TranslatedTextViewer implements TextViewer { } SwingUtilities.invokeLater(() -> { - /* - * This message is only written to the viewer once this task - * starts and any previous task has therefore been completed by - * the single-threaded executor. - */ panel.display(Bundle.TranslatedContentViewer_extractingText(), ComponentOrientation.LEFT_TO_RIGHT, Font.ITALIC); }); String fileText; From 6702fdb6acc6933753f992942410e80539d74336 Mon Sep 17 00:00:00 2001 From: Richard Cordovano Date: Wed, 23 Oct 2019 15:20:00 -0400 Subject: [PATCH 31/34] 5506 More robust machine trans --- .../autopsy/texttranslation/ui/TranslatedTextViewer.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java index 13dda67f5f..21e95cc329 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java @@ -192,7 +192,7 @@ public final class TranslatedTextViewer implements TextViewer { try { fileText = getFileText(file); } catch (IOException | TextExtractor.InitReaderException ex) { - logger.log(Level.WARNING, String.format("Error getting text for file %s (objId=%d)", file.getName(), file.getId()), ex); + logger.log(Level.WARNING, String.format("Error extracting text for file %s (objId=%d)", file.getName(), file.getId()), ex); return Bundle.TranslatedContentViewer_errorExtractingText(ex.getMessage()); } @@ -215,10 +215,10 @@ public final class TranslatedTextViewer implements TextViewer { try { translation = translate(fileText); } catch (NoServiceProviderException ex) { - logger.log(Level.WARNING, String.format("Error occurred translating text for file %s (objId=%d)", file.getName(), file.getId()), ex); + logger.log(Level.WARNING, String.format("Error translating text for file %s (objId=%d)", file.getName(), file.getId()), ex); translation = Bundle.TranslatedContentViewer_noServiceProvider(); } catch (TranslationException ex) { - logger.log(Level.WARNING, String.format("Error occurred translating text for file %s (objId=%d)", file.getName(), file.getId()), ex); + logger.log(Level.WARNING, String.format("Error translating text for file %s (objId=%d)", file.getName(), file.getId()), ex); translation = Bundle.TranslatedContentViewer_translationException(ex.getMessage()); } From 565a0296fba715f80adc2b907dfbd7f3fbf0a45d Mon Sep 17 00:00:00 2001 From: Richard Cordovano Date: Wed, 23 Oct 2019 15:26:18 -0400 Subject: [PATCH 32/34] 5506 More robust machine trans --- .../autopsy/texttranslation/ui/TranslatedTextViewer.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java index 21e95cc329..bed42e2287 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java @@ -175,7 +175,6 @@ public final class TranslatedTextViewer implements TextViewer { "TranslatedContentViewer.translatingText=Translating text, please wait...", "# {0} - exception message", "TranslatedContentViewer.errorExtractingText=An error occurred while extracting the text ({0}).", "TranslatedContentViewer.fileHasNoText=File has no text.", - "TranslatedContentViewer.emptyTranslation=The machine translation software did not return any text.", "TranslatedContentViewer.noServiceProvider=The machine translation software was not found.", "# {0} - exception message", "TranslatedContentViewer.translationException=An error occurred while translating the text ({0})." }) @@ -257,6 +256,9 @@ public final class TranslatedTextViewer implements TextViewer { * * @return Translated text or error message */ + @NbBundle.Messages({ + "TranslatedContentViewer.emptyTranslation=The machine translation software did not return any text." + }) private String translate(String input) throws NoServiceProviderException, TranslationException { TextTranslationService translatorInstance = TextTranslationService.getInstance(); String translatedResult = translatorInstance.translate(input); From 338f49a9af7843ac224b700196895f07eba20e66 Mon Sep 17 00:00:00 2001 From: Richard Cordovano Date: Wed, 23 Oct 2019 15:33:09 -0400 Subject: [PATCH 33/34] Minor clean up of DataContentViewerUtility.java --- .../DataContentViewerUtility.java | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerUtility.java b/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerUtility.java index ea1e7b58a0..d460a16f03 100755 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerUtility.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerUtility.java @@ -23,32 +23,42 @@ import org.openide.nodes.Node; import org.sleuthkit.datamodel.BlackboardArtifact; /** - * Utility classes for content viewers. In theory, this would live in the - * contentviewer package, but the initial method was needed only be viewers in - * corecomponents and therefore can stay out of public API. + * Utility methods for content viewers. */ public class DataContentViewerUtility { /** - * Returns the first non-Blackboard Artifact from a Node. Needed for (at - * least) Hex and Strings that want to view all types of content (not just - * AbstractFile), but don't want to display an artifact unless that's the - * only thing there. Scenario is hash hit or interesting item hit. + * Gets a Content object from the Lookup of a display Node object, + * preferring to return any Content object other than a BlackboardArtifact + * object. * - * @param node Node passed into content viewer + * This method was written with the needs of the hex and strings content + * viewers in mind - the algorithm is exactly what those viewers require. * - * @return highest priority content or null if there is no content + * @param node A display Node object. + * + * @return If there are multiple Content objects associated with the Node, + * the first Content object that is not a BlackboardArtifact object + * is returned. If no Content objects other than artifacts are found, + * the first BlackboardArtifact object found is returned. If no + * Content objects are found, null is returned. */ public static Content getDefaultContent(Node node) { - Content bbContentSeen = null; - for (Content content : (node).getLookup().lookupAll(Content.class)) { - if (content instanceof BlackboardArtifact) { - bbContentSeen = content; + Content artifact = null; + for (Content content : node.getLookup().lookupAll(Content.class)) { + if (content instanceof BlackboardArtifact && artifact == null) { + artifact = content; } else { return content; } } - return bbContentSeen; + return artifact; + } + + /* + * Private constructor to prevent instantiation of utility class. + */ + private DataContentViewerUtility() { } } From 6c54c81c7746b976a2438b94d06943645fb09b5f Mon Sep 17 00:00:00 2001 From: Ann Priestman Date: Thu, 24 Oct 2019 09:14:14 -0400 Subject: [PATCH 34/34] Update copyright for API docs. Fix doxygen warnings. --- .../centralrepository/eventlisteners/IngestEventsListener.java | 1 + .../org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java | 2 +- .../autopsy/texttranslation/ui/TranslatedTextViewer.java | 2 +- docs/doxygen/footer.html | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/IngestEventsListener.java b/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/IngestEventsListener.java index 282e225135..355c7c8dbc 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/IngestEventsListener.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/IngestEventsListener.java @@ -224,6 +224,7 @@ public class IngestEventsListener { * in the central repository. * * @param originalArtifact the artifact to create the interesting item for + * @param caseDisplayNames the case names the artifact was previously seen in */ @NbBundle.Messages({"IngestEventsListener.prevExists.text=Previously Seen Devices (Central Repository)", "# {0} - typeName", diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java index 1f02708f64..84f7da6c36 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java @@ -637,7 +637,7 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie * thumb at the given width and height. It also paints the track blue as * the thumb progresses. * - * @param b JSlider component + * @param slider JSlider component * @param config Configuration object. Contains info about thumb * dimensions and colors. */ diff --git a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java index bed42e2287..7e07545b49 100644 --- a/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java +++ b/Core/src/org/sleuthkit/autopsy/texttranslation/ui/TranslatedTextViewer.java @@ -271,7 +271,7 @@ public final class TranslatedTextViewer implements TextViewer { /** * Extracts text from the given node * - * @param node Selected node in UI + * @param file Selected node in UI * * @return Extracted text * diff --git a/docs/doxygen/footer.html b/docs/doxygen/footer.html index ac0c0a8d1c..8bf1577a45 100644 --- a/docs/doxygen/footer.html +++ b/docs/doxygen/footer.html @@ -1,5 +1,5 @@
    -

    Copyright © 2012-2018 Basis Technology. Generated on: $date
    +

    Copyright © 2012-2019 Basis Technology. Generated on: $date
    This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.