diff --git a/.travis.yml b/.travis.yml index 56c8b7bbbd..dd6b50f34a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,11 @@ language: java sudo: required -dist: bionic -os: - - linux + +jobs: + include: + - os: linux + dist: bionic + - os: osx env: global: @@ -12,6 +15,7 @@ addons: apt: update: true packages: + - testdisk - libafflib-dev - libewf-dev - libpq-dev @@ -29,11 +33,13 @@ addons: update: true packages: - ant - - ant-optional + - wget + - libpq - libewf - gettext - cppunit - afflib + - testdisk python: - "2.7" @@ -43,9 +49,7 @@ before_install: - python setupSleuthkitBranch.py install: - - sudo apt-get install testdisk - - cd sleuthkit/sleuthkit - - ./travis_install_libs.sh + - pushd sleuthkit/sleuthkit && ./travis_install_libs.sh && popd before_script: - if [ $TRAVIS_OS_NAME = linux ]; then @@ -54,13 +58,13 @@ before_script: export PATH=/usr/bin:$PATH; unset JAVA_HOME; fi + - if [ $TRAVIS_OS_NAME = osx ]; then + brew uninstall java --force; + brew cask uninstall java --force; + brew tap homebrew/cask-versions; + brew cask install corretto8; + export JAVA_HOME=/Library/Java/JavaVirtualMachines/amazon-corretto-8.jdk/Contents/Home; + fi + - java -version -script: - - set -e - - echo "Building TSK..." - - ./bootstrap && ./configure --prefix=/usr && make - - pushd bindings/java/ && ant -q dist-PostgreSQL && popd - - echo "Building Autopsy..." && echo -en 'travis_fold:start:script.build\\r' - - cd $TRAVIS_BUILD_DIR/ - - ant build - - echo -en 'travis_fold:end:script.build\\r' +script: ./travis_build.sh 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 @@ - + diff --git a/Core/ivy.xml b/Core/ivy.xml index e6c513fdf6..d875087942 100644 --- a/Core/ivy.xml +++ b/Core/ivy.xml @@ -1,56 +1,59 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/nbproject/project.properties b/Core/nbproject/project.properties index 541ef8e4c6..aa5e50279c 100644 --- a/Core/nbproject/project.properties +++ b/Core/nbproject/project.properties @@ -50,6 +50,7 @@ file.reference.jsoup-1.11.3.jar=release\\modules\\ext\\jsoup-1.11.3.jar file.reference.jul-to-slf4j-1.7.25.jar=release\\modules\\ext\\jul-to-slf4j-1.7.25.jar file.reference.juniversalchardet-1.0.3.jar=release\\modules\\ext\\juniversalchardet-1.0.3.jar file.reference.junrar-2.0.0.jar=release\\modules\\ext\\junrar-2.0.0.jar +file.reference.jxmapviewer2-2.4.jar=release/modules/ext/jxmapviewer2-2.4.jar file.reference.jython-standalone-2.7.0.jar=release/modules/ext/jython-standalone-2.7.0.jar file.reference.libphonenumber-3.5.jar=release/modules/ext/libphonenumber-3.5.jar file.reference.mchange-commons-java-0.2.9.jar=release/modules/ext/mchange-commons-java-0.2.9.jar diff --git a/Core/nbproject/project.xml b/Core/nbproject/project.xml index 8f2eb52c85..7fe269c0fb 100644 --- a/Core/nbproject/project.xml +++ b/Core/nbproject/project.xml @@ -327,6 +327,7 @@ org.sleuthkit.autopsy.datasourceprocessors org.sleuthkit.autopsy.directorytree org.sleuthkit.autopsy.events + org.sleuthkit.autopsy.exceptions org.sleuthkit.autopsy.filesearch org.sleuthkit.autopsy.guiutils org.sleuthkit.autopsy.healthmonitor @@ -453,6 +454,10 @@ ext/commons-pool2-2.4.2.jar release/modules/ext/commons-pool2-2.4.2.jar + + ext/jxmapviewer2-2.4.jar + release/modules/ext/jxmapviewer2-2.4.jar + ext/jdom-2.0.5-contrib.jar release/modules/ext/jdom-2.0.5-contrib.jar diff --git a/Core/src/org/sleuthkit/autopsy/actions/AddBlackboardArtifactTagAction.java b/Core/src/org/sleuthkit/autopsy/actions/AddBlackboardArtifactTagAction.java index 9015d9a6ad..26d1c7a9bc 100644 --- a/Core/src/org/sleuthkit/autopsy/actions/AddBlackboardArtifactTagAction.java +++ b/Core/src/org/sleuthkit/autopsy/actions/AddBlackboardArtifactTagAction.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2018 Basis Technology Corp. + * Copyright 2011-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -30,6 +30,7 @@ import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.TagName; import org.sleuthkit.datamodel.TskCoreException; @@ -72,15 +73,24 @@ public class AddBlackboardArtifactTagAction extends AddTagAction { @Override protected void addTag(TagName tagName, String comment) { - /* - * The documentation for Lookup.lookupAll() explicitly says that the - * collection it returns may contain duplicates. Within this invocation - * of addTag(), we don't want to tag the same BlackboardArtifact more - * than once, so we dedupe the BlackboardArtifacts by stuffing them into - * a HashSet. - */ - final Collection selectedArtifacts = new HashSet<>(Utilities.actionsGlobalContext().lookupAll(BlackboardArtifact.class)); - + final Collection selectedArtifacts = new HashSet<>(); + //If the contentToTag is empty look up the selected content + if (getContentToTag().isEmpty()) { + /* + * The documentation for Lookup.lookupAll() explicitly says that the + * collection it returns may contain duplicates. Within this + * invocation of addTag(), we don't want to tag the same + * BlackboardArtifact more than once, so we dedupe the + * BlackboardArtifacts by stuffing them into a HashSet. + */ + selectedArtifacts.addAll(Utilities.actionsGlobalContext().lookupAll(BlackboardArtifact.class)); + } else { + for (Content content : getContentToTag()) { + if (content instanceof BlackboardArtifact) { + selectedArtifacts.add((BlackboardArtifact) content); + } + } + } new Thread(() -> { for (BlackboardArtifact artifact : selectedArtifacts) { try { diff --git a/Core/src/org/sleuthkit/autopsy/actions/AddContentTagAction.java b/Core/src/org/sleuthkit/autopsy/actions/AddContentTagAction.java index 34e7b2a110..c32533b0cd 100644 --- a/Core/src/org/sleuthkit/autopsy/actions/AddContentTagAction.java +++ b/Core/src/org/sleuthkit/autopsy/actions/AddContentTagAction.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2018 Basis Technology Corp. + * Copyright 2011-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -76,16 +76,26 @@ public class AddContentTagAction extends AddTagAction { @Override protected void addTag(TagName tagName, String comment) { - /* - * The documentation for Lookup.lookupAll() explicitly says that the - * collection it returns may contain duplicates. Within this invocation - * of addTag(), we don't want to tag the same AbstractFile more than - * once, so we dedupe the AbstractFiles by stuffing them into a HashSet. - * - * We don't want VirtualFile and DerivedFile objects to be tagged. - */ - final Collection selectedFiles = new HashSet<>(Utilities.actionsGlobalContext().lookupAll(AbstractFile.class)); - + final Collection selectedFiles = new HashSet<>(); + //If the contentToTag is empty look up the selected content + if (getContentToTag().isEmpty()) { + /* + * The documentation for Lookup.lookupAll() explicitly says that the + * collection it returns may contain duplicates. Within this + * invocation of addTag(), we don't want to tag the same + * AbstractFile more than once, so we dedupe the AbstractFiles by + * stuffing them into a HashSet. + * + * We don't want VirtualFile and DerivedFile objects to be tagged. + */ + selectedFiles.addAll(Utilities.actionsGlobalContext().lookupAll(AbstractFile.class)); + } else { + for (Content content : getContentToTag()) { + if (content instanceof AbstractFile) { + selectedFiles.add((AbstractFile) content); + } + } + } new Thread(() -> { for (AbstractFile file : selectedFiles) { try { diff --git a/Core/src/org/sleuthkit/autopsy/actions/AddTagAction.java b/Core/src/org/sleuthkit/autopsy/actions/AddTagAction.java index 6606185e24..3709592e62 100644 --- a/Core/src/org/sleuthkit/autopsy/actions/AddTagAction.java +++ b/Core/src/org/sleuthkit/autopsy/actions/AddTagAction.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2018 Basis Technology Corp. + * Copyright 2011-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,6 +20,9 @@ package org.sleuthkit.autopsy.actions; import java.awt.event.ActionEvent; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -33,6 +36,7 @@ import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.casemodule.services.TagsManager; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.TagName; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData; @@ -45,6 +49,7 @@ abstract class AddTagAction extends AbstractAction implements Presenter.Popup { private static final long serialVersionUID = 1L; private static final String NO_COMMENT = ""; + private final Collection content = new HashSet<>(); AddTagAction(String menuText) { super(menuText); @@ -52,6 +57,32 @@ abstract class AddTagAction extends AbstractAction implements Presenter.Popup { @Override public JMenuItem getPopupPresenter() { + content.clear(); + return new TagMenu(); + } + + /** + * Get the collection of content which may have been specified for this + * action. Empty collection returned when no content was specified. + * + * @return The specified content for this action. + */ + Collection getContentToTag() { + return Collections.unmodifiableCollection(content); + } + + /** + * Get the menu for adding tags to the specified collection of Content. + * + * @param contentToTag The collection of Content the menu actions will be + * applied to. + * + * @return The menu which will allow users to choose the tag they want to + * apply to the Content specified. + */ + public JMenuItem getMenuForContent(Collection contentToTag) { + content.clear(); + content.addAll(contentToTag); return new TagMenu(); } @@ -118,26 +149,26 @@ abstract class AddTagAction extends AbstractAction implements Presenter.Popup { tagNameItem.addActionListener((ActionEvent e) -> { getAndAddTag(entry.getKey(), entry.getValue(), NO_COMMENT); }); - - // Show custom tags before predefined tags in the menu + + // Show custom tags before predefined tags in the menu if (standardTagNames.contains(tagDisplayName)) { standardTagMenuitems.add(tagNameItem); } else { add(tagNameItem); } } - } - + } + if (getItemCount() > 0) { addSeparator(); } - + standardTagMenuitems.forEach((menuItem) -> { add(menuItem); }); - + addSeparator(); - + // Create a "Choose Tag and Comment..." menu item. Selecting this item initiates // a dialog that can be used to create or select a tag name with an // optional comment and adds a tag with the resulting name. @@ -150,7 +181,7 @@ abstract class AddTagAction extends AbstractAction implements Presenter.Popup { } }); add(tagAndCommentItem); - + // Create a "New Tag..." menu item. // Selecting this item initiates a dialog that can be used to create // or select a tag name and adds a tag with the resulting name. @@ -162,7 +193,7 @@ abstract class AddTagAction extends AbstractAction implements Presenter.Popup { } }); add(newTagMenuItem); - + } /** @@ -194,10 +225,10 @@ abstract class AddTagAction extends AbstractAction implements Presenter.Popup { tagName = openCase.getServices().getTagsManager().getDisplayNamesToTagNamesMap().get(tagDisplayName); } catch (TskCoreException ex1) { Logger.getLogger(AddTagAction.class.getName()).log(Level.SEVERE, tagDisplayName + " already exists in database but an error occurred in retrieving it.", ex1); //NON-NLS - } + } } catch (TskCoreException ex) { Logger.getLogger(AddTagAction.class.getName()).log(Level.SEVERE, "Error adding " + tagDisplayName + " tag name", ex); //NON-NLS - } + } } addTag(tagName, comment); } diff --git a/Core/src/org/sleuthkit/autopsy/actions/DeleteFileContentTagAction.java b/Core/src/org/sleuthkit/autopsy/actions/DeleteFileContentTagAction.java index c8a8a69fdc..f68017de19 100644 --- a/Core/src/org/sleuthkit/autopsy/actions/DeleteFileContentTagAction.java +++ b/Core/src/org/sleuthkit/autopsy/actions/DeleteFileContentTagAction.java @@ -1,15 +1,15 @@ /* * Autopsy Forensic Browser - * - * Copyright 2017-2018 Basis Technology Corp. + * + * Copyright 2017-2019 Basis Technology Corp. * Contact: carrier sleuthkit org - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -53,7 +53,7 @@ import org.sleuthkit.datamodel.TskData; "DeleteFileContentTagAction.deleteTag=Remove File Tag" }) public class DeleteFileContentTagAction extends AbstractAction implements Presenter.Popup { - + private static final Logger logger = Logger.getLogger(DeleteFileContentTagAction.class.getName()); private static final long serialVersionUID = 1L; @@ -81,6 +81,19 @@ public class DeleteFileContentTagAction extends AbstractAction implements Presen return new TagMenu(); } + /** + * Get the menu for removing tags from the specified collection of Files. + * + * @param selectedFiles The collection of AbstractFiles the menu actions + * will be applied to. + * + * @return The menu which will allow users to remove tags from the specified + * collection of Files. + */ + public JMenuItem getMenuForFiles(Collection selectedFiles) { + return new TagMenu(selectedFiles); + } + @Override public void actionPerformed(ActionEvent e) { } @@ -102,19 +115,19 @@ public class DeleteFileContentTagAction extends AbstractAction implements Presen tagsManager = Case.getCurrentCaseThrows().getServices().getTagsManager(); } catch (NoCurrentCaseException ex) { logger.log(Level.SEVERE, "Error untagging file. No open case found.", ex); //NON-NLS - Platform.runLater(() -> - new Alert(Alert.AlertType.ERROR, Bundle.DeleteFileContentTagAction_deleteTag_alert(fileId)).show() + Platform.runLater(() + -> new Alert(Alert.AlertType.ERROR, Bundle.DeleteFileContentTagAction_deleteTag_alert(fileId)).show() ); return null; } - + try { logger.log(Level.INFO, "Removing tag {0} from {1}", new Object[]{tagName.getDisplayName(), contentTag.getContent().getName()}); //NON-NLS tagsManager.deleteContentTag(contentTag); } catch (TskCoreException tskCoreException) { logger.log(Level.SEVERE, "Error untagging file", tskCoreException); //NON-NLS - Platform.runLater(() -> - new Alert(Alert.AlertType.ERROR, Bundle.DeleteFileContentTagAction_deleteTag_alert(fileId)).show() + Platform.runLater(() + -> new Alert(Alert.AlertType.ERROR, Bundle.DeleteFileContentTagAction_deleteTag_alert(fileId)).show() ); } return null; @@ -141,15 +154,24 @@ public class DeleteFileContentTagAction extends AbstractAction implements Presen private static final long serialVersionUID = 1L; + /** + * Construct an TagMenu object using the specified collection of files + * as the files to remove a tag from. + */ TagMenu() { + this(new HashSet<>(Utilities.actionsGlobalContext().lookupAll(AbstractFile.class))); + } + + /** + * Construct an TagMenu object using the specified collection of files + * as the files to remove a tag from. + */ + TagMenu(Collection selectedFiles) { super(getActionDisplayName()); - - final Collection selectedAbstractFilesList = - new HashSet<>(Utilities.actionsGlobalContext().lookupAll(AbstractFile.class)); - - if(!selectedAbstractFilesList.isEmpty()) { - AbstractFile file = selectedAbstractFilesList.iterator().next(); - + + if (!selectedFiles.isEmpty()) { + AbstractFile file = selectedFiles.iterator().next(); + Map tagNamesMap = null; List standardTagNames = TagsManager.getStandardTagNames(); List standardTagMenuitems = new ArrayList<>(); @@ -167,22 +189,22 @@ public class DeleteFileContentTagAction extends AbstractAction implements Presen // a tag with the associated tag name. if (null != tagNamesMap && !tagNamesMap.isEmpty()) { try { - List existingTagsList = - Case.getCurrentCaseThrows().getServices().getTagsManager() + List existingTagsList + = Case.getCurrentCaseThrows().getServices().getTagsManager() .getContentTagsByContent(file); for (Map.Entry entry : tagNamesMap.entrySet()) { String tagDisplayName = entry.getKey(); TagName tagName = entry.getValue(); - for(ContentTag contentTag : existingTagsList) { - if(tagDisplayName.equals(contentTag.getName().getDisplayName())) { + for (ContentTag contentTag : existingTagsList) { + if (tagDisplayName.equals(contentTag.getName().getDisplayName())) { String notableString = tagName.getKnownStatus() == TskData.FileKnown.BAD ? TagsManager.getNotableTagLabel() : ""; JMenuItem tagNameItem = new JMenuItem(tagDisplayName + notableString); tagNameItem.addActionListener((ActionEvent e) -> { deleteTag(tagName, contentTag, file.getId()); }); - + // Show custom tags before predefined tags in the menu if (standardTagNames.contains(tagDisplayName)) { standardTagMenuitems.add(tagNameItem); @@ -198,14 +220,14 @@ public class DeleteFileContentTagAction extends AbstractAction implements Presen } } - if ((getItemCount() > 0) && !standardTagMenuitems.isEmpty() ){ + if ((getItemCount() > 0) && !standardTagMenuitems.isEmpty()) { addSeparator(); } standardTagMenuitems.forEach((menuItem) -> { add(menuItem); }); - - if(getItemCount() == 0) { + + if (getItemCount() == 0) { setEnabled(false); } } 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/CaseMetadata.java b/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java index 20dfabd93c..5f7ffe9ea7 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java @@ -24,6 +24,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.StringWriter; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.Paths; import java.text.DateFormat; @@ -427,9 +428,10 @@ public final class CaseMetadata { transformer.transform(source, streamResult); /* - * Write the DOM to the metadata file. + * Write the DOM to the metadata file. Add UTF-8 Characterset so it writes to the file + * correctly for non-latin characters */ - try (BufferedWriter fileWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(metadataFilePath.toFile())))) { + try (BufferedWriter fileWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(metadataFilePath.toFile()), StandardCharsets.UTF_8))) { fileWriter.write(stringWriter.toString()); fileWriter.flush(); } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java b/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java index cc07148ba0..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; @@ -84,10 +85,17 @@ 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 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)) { @@ -95,7 +103,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()); + /** + * The parent of the fileChooser will either be the multi user + * case dialog or the startup window. + */ + int retval = fileChooser.showOpenDialog(multiUserCaseDialog.isVisible() + ? multiUserCaseDialog : (Component) StartupWindowProvider.getInstance().getStartupWindow()); if (retval == JFileChooser.APPROVE_OPTION) { /* * Close the startup window, if it is open. @@ -105,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 @@ -159,6 +172,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) { 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; + } } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/AbstractSqlEamDb.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/AbstractSqlEamDb.java index 6e77c317a7..65e6230443 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/AbstractSqlEamDb.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/AbstractSqlEamDb.java @@ -3298,10 +3298,10 @@ abstract class AbstractSqlEamDb implements EamDb { try { minorVersion = Integer.parseInt(minorVersionStr); } catch (NumberFormatException ex) { - throw new EamDbException(Bundle.AbstractSqlEamDb_badMinorSchema_message(minorVersionStr), ex); + throw new EamDbException("Bad value for schema minor version (" + minorVersionStr + ") - database is corrupt", Bundle.AbstractSqlEamDb_badMinorSchema_message(minorVersionStr), ex); } } else { - throw new EamDbException(Bundle.AbstractSqlEamDb_failedToReadMinorVersion_message()); + throw new EamDbException("Failed to read schema minor version from db_info table", Bundle.AbstractSqlEamDb_failedToReadMinorVersion_message()); } int majorVersion = 0; @@ -3312,10 +3312,10 @@ abstract class AbstractSqlEamDb implements EamDb { try { majorVersion = Integer.parseInt(majorVersionStr); } catch (NumberFormatException ex) { - throw new EamDbException(Bundle.AbstractSqlEamDb_badMajorSchema_message(majorVersionStr), ex); + throw new EamDbException("Bad value for schema version (" + majorVersionStr + ") - database is corrupt", Bundle.AbstractSqlEamDb_badMajorSchema_message(majorVersionStr), ex); } } else { - throw new EamDbException(Bundle.AbstractSqlEamDb_failedToReadMajorVersion_message()); + throw new EamDbException("Failed to read schema major version from db_info table", Bundle.AbstractSqlEamDb_failedToReadMajorVersion_message()); } /* @@ -3393,7 +3393,7 @@ abstract class AbstractSqlEamDb implements EamDb { addObjectIdIndexTemplate = SqliteEamDbSettings.getAddObjectIdIndexTemplate(); break; default: - throw new EamDbException(Bundle.AbstractSqlEamDb_cannotUpgrage_message(selectedPlatform.name())); + throw new EamDbException("Currently selected database platform \"" + selectedPlatform.name() + "\" can not be upgraded.", Bundle.AbstractSqlEamDb_cannotUpgrage_message(selectedPlatform.name())); } final String dataSourcesTableName = "data_sources"; final String dataSourceObjectIdColumnName = "datasource_obj_id"; @@ -3553,7 +3553,7 @@ abstract class AbstractSqlEamDb implements EamDb { statement.execute("DROP TABLE old_data_sources"); break; default: - throw new EamDbException(Bundle.AbstractSqlEamDb_cannotUpgrage_message(selectedPlatform.name())); + throw new EamDbException("Currently selected database platform \"" + selectedPlatform.name() + "\" can not be upgraded.", Bundle.AbstractSqlEamDb_cannotUpgrage_message(selectedPlatform.name())); } } updateSchemaVersion(conn); diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/Bundle.properties-MERGED index c94c442588..9b43833ee2 100755 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/Bundle.properties-MERGED @@ -39,6 +39,7 @@ EamDbUtil.centralRepoUpgradeFailed.message=Failed to upgrade Central Repository. EamDbUtil.exclusiveLockAquisitionFailure.message=Unable to acquire exclusive lock for Central Repository. PostgresEamDb.centralRepoDisabled.message=Central Repository module is not enabled. PostgresEamDb.connectionFailed.message=Error getting connection to database. +PostgresEamDb.multiUserLockError.message=Error acquiring database lock SqliteEamDb.centralRepositoryDisabled.message=Central Repository module is not enabled. SqliteEamDb.connectionFailedMessage.message=Error getting connection to database. SqliteEamDb.databaseMissing.message=Central repository database missing diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeInstance.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeInstance.java index 3cbc510e4c..727b5ecb0b 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeInstance.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeInstance.java @@ -277,10 +277,10 @@ public class CorrelationAttributeInstance implements Serializable { * @param enabled Is this Type currently enabled. */ @Messages({"CorrelationAttributeInstance.nullName.message=Database name is null.", - "CorrelationAttributeInstance.invalidName.message=Invalid database table name. Name must start with a lowercase letter and can only contain lowercase letters, numbers, and '_'."}) + "CorrelationAttributeInstance.invalidName.message=Invalid database table name. Name must start with a lowercase letter and can only contain lowercase letters, numbers, and '_'."}) public Type(int typeId, String displayName, String dbTableName, Boolean supported, Boolean enabled) throws EamDbException { if (dbTableName == null) { - throw new EamDbException(Bundle.CorrelationAttributeInstance_nullName_message()); + throw new EamDbException("dbTableName is null", Bundle.CorrelationAttributeInstance_nullName_message()); } this.typeId = typeId; this.displayName = displayName; @@ -288,7 +288,7 @@ public class CorrelationAttributeInstance implements Serializable { this.supported = supported; this.enabled = enabled; if (!Pattern.matches(DB_NAMES_REGEX, dbTableName)) { - throw new EamDbException(Bundle.CorrelationAttributeInstance_invalidName_message()); // NON-NLS + throw new EamDbException("Invalid database table name. Name must start with a lowercase letter and can only contain lowercase letters, numbers, and '_'.", Bundle.CorrelationAttributeInstance_invalidName_message()); // NON-NLS } } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/EamDbException.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/EamDbException.java index a410f7f182..262b550f5c 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/EamDbException.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/EamDbException.java @@ -1,7 +1,7 @@ /* * Central Repository * - * Copyright 2015-2017 Basis Technology Corp. + * Copyright 2015-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,10 +18,12 @@ */ package org.sleuthkit.autopsy.centralrepository.datamodel; -/* +import org.sleuthkit.autopsy.exceptions.AutopsyException; + +/** * An exception to be thrown by an artifact manager. */ -public class EamDbException extends Exception { +public class EamDbException extends AutopsyException { private static final long serialVersionUID = 1L; @@ -34,6 +36,28 @@ public class EamDbException extends Exception { super(message); } + /** + * Constructs an exception to be thrown by an artifact manager with a user exception. + * + * @param message Exception message. + * @param userMessage the user friendly message to include in this exception + */ + public EamDbException(String message, String userMessage) { + super(message, userMessage); + } + + /** + * Constructs an exception to be thrown by an artifact manager with a user + * exception. + * + * @param message Exception message. + * @param userMessage the user friendly message to include in this exception + * @param cause Exception cause. + */ + public EamDbException(String message, String userMessage, Throwable cause) { + super(message, userMessage, cause); + } + /** * Constructs an exception to be thrown by an artifact manager. * diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/EamDbUtil.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/EamDbUtil.java index ad198b1744..c287990d79 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/EamDbUtil.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/EamDbUtil.java @@ -181,32 +181,39 @@ public class EamDbUtil { } EamDb db = null; CoordinationService.Lock lock = null; - String messageForDialog = ""; + //get connection try { - db = EamDb.getInstance(); - } catch (EamDbException ex) { - LOGGER.log(Level.SEVERE, "Error updating central repository, unable to make connection", ex); - messageForDialog = Bundle.EamDbUtil_centralRepoConnectionFailed_message() + Bundle.EamDbUtil_centralRepoDisabled_message(); - } - //get lock necessary for upgrade - if (db != null) { try { - // This may return null if locking isn't supported, which is fine. It will - // throw an exception if locking is supported but we can't get the lock - // (meaning the database is in use by another user) - lock = db.getExclusiveMultiUserDbLock(); - //perform upgrade + db = EamDb.getInstance(); + } catch (EamDbException ex) { + LOGGER.log(Level.SEVERE, "Error updating central repository, unable to make connection", ex); + throw new EamDbException("Error updating central repository, unable to make connection", Bundle.EamDbUtil_centralRepoConnectionFailed_message() + Bundle.EamDbUtil_centralRepoDisabled_message(), ex); + } + //get lock necessary for upgrade + if (db != null) { + try { + // This may return null if locking isn't supported, which is fine. It will + // throw an exception if locking is supported but we can't get the lock + // (meaning the database is in use by another user) + lock = db.getExclusiveMultiUserDbLock(); + //perform upgrade + } catch (EamDbException ex) { + LOGGER.log(Level.SEVERE, "Error updating central repository, unable to acquire exclusive lock", ex); + throw new EamDbException("Error updating central repository, unable to acquire exclusive lock", Bundle.EamDbUtil_exclusiveLockAquisitionFailure_message() + Bundle.EamDbUtil_centralRepoDisabled_message(), ex); + } + try { db.upgradeSchema(); - } catch (EamDbException | SQLException | IncompatibleCentralRepoException ex) { + } catch (EamDbException ex) { LOGGER.log(Level.SEVERE, "Error updating central repository", ex); - messageForDialog = Bundle.EamDbUtil_centralRepoUpgradeFailed_message() + Bundle.EamDbUtil_centralRepoDisabled_message(); - if (ex instanceof IncompatibleCentralRepoException) { - messageForDialog = ex.getMessage() + "\n\n" + messageForDialog; - } else if (ex instanceof EamDbException) { - messageForDialog = ex.getMessage() + Bundle.EamDbUtil_centralRepoDisabled_message(); - } + throw new EamDbException("Error updating central repository", ex.getUserMessage() + Bundle.EamDbUtil_centralRepoDisabled_message(), ex); + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "Error updating central repository", ex); + throw new EamDbException("Error updating central repository", Bundle.EamDbUtil_centralRepoUpgradeFailed_message() + Bundle.EamDbUtil_centralRepoDisabled_message(), ex); + } catch (IncompatibleCentralRepoException ex) { + LOGGER.log(Level.SEVERE, "Error updating central repository", ex); + throw new EamDbException("Error updating central repository", ex.getMessage() + "\n\n" + Bundle.EamDbUtil_centralRepoUpgradeFailed_message() + Bundle.EamDbUtil_centralRepoDisabled_message(), ex); } finally { if (lock != null) { try { @@ -216,16 +223,11 @@ public class EamDbUtil { } } } - } catch (EamDbException ex) { - LOGGER.log(Level.SEVERE, "Error updating central repository, unable to acquire exclusive lock", ex); - messageForDialog = Bundle.EamDbUtil_exclusiveLockAquisitionFailure_message() + Bundle.EamDbUtil_centralRepoDisabled_message(); + } else { + throw new EamDbException("Unable to connect to database", Bundle.EamDbUtil_centralRepoConnectionFailed_message() + Bundle.EamDbUtil_centralRepoDisabled_message()); } - - } else { - messageForDialog = Bundle.EamDbUtil_centralRepoConnectionFailed_message() + Bundle.EamDbUtil_centralRepoDisabled_message(); - } - // Disable the central repo and clear the current settings. - if (!messageForDialog.isEmpty()) { + } catch (EamDbException ex) { + // Disable the central repo and clear the current settings. try { if (null != EamDb.getInstance()) { EamDb.getInstance().shutdownConnections(); @@ -235,8 +237,7 @@ public class EamDbUtil { } EamDbPlatformEnum.setSelectedPlatform(EamDbPlatformEnum.DISABLED.name()); EamDbPlatformEnum.saveSelectedPlatform(); - - throw new EamDbException(messageForDialog); + throw ex; } } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PostgresEamDb.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PostgresEamDb.java index c6b0d15669..dabfeba7a1 100755 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PostgresEamDb.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PostgresEamDb.java @@ -190,7 +190,7 @@ final class PostgresEamDb extends AbstractSqlEamDb { protected Connection connect() throws EamDbException { synchronized (this) { if (!EamDb.isEnabled()) { - throw new EamDbException(Bundle.PostgresEamDb_centralRepoDisabled_message()); // NON-NLS + throw new EamDbException("Central Repository module is not enabled", Bundle.PostgresEamDb_centralRepoDisabled_message()); // NON-NLS } if (connectionPool == null) { @@ -200,7 +200,7 @@ final class PostgresEamDb extends AbstractSqlEamDb { try { return connectionPool.getConnection(); } catch (SQLException ex) { - throw new EamDbException(Bundle.PostgresEamDb_connectionFailed_message(), ex); // NON-NLS + throw new EamDbException("Error getting connection from connection pool.", Bundle.PostgresEamDb_connectionFailed_message(), ex); // NON-NLS } } @@ -221,6 +221,7 @@ final class PostgresEamDb extends AbstractSqlEamDb { * to get the lock */ @Override + @Messages({"PostgresEamDb.multiUserLockError.message=Error acquiring database lock"}) public CoordinationService.Lock getExclusiveMultiUserDbLock() throws EamDbException { try { // First check if multi user mode is enabled - if not there's no point trying to get a lock @@ -234,9 +235,9 @@ final class PostgresEamDb extends AbstractSqlEamDb { if (lock != null) { return lock; } - throw new EamDbException("Error acquiring database lock"); + throw new EamDbException("Error acquiring database lock", Bundle.PostgresEamDb_multiUserLockError_message()); } catch (InterruptedException ex) { - throw new EamDbException("Error acquiring database lock"); + throw new EamDbException("Error acquiring database lock", Bundle.PostgresEamDb_multiUserLockError_message(), ex); } catch (CoordinationService.CoordinationServiceException ex) { // This likely just means the coordination service isn't running, which is ok return null; diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/SqliteEamDb.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/SqliteEamDb.java index b2df5f0fd9..7c36a2fd9e 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/SqliteEamDb.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/SqliteEamDb.java @@ -158,7 +158,7 @@ final class SqliteEamDb extends AbstractSqlEamDb { private void setupConnectionPool(boolean foreignKeysEnabled) throws EamDbException { if (dbSettings.dbFileExists() == false) { - throw new EamDbException(Bundle.SqliteEamDb_databaseMissing_message()); + throw new EamDbException("Central repository database missing", Bundle.SqliteEamDb_databaseMissing_message()); } connectionPool = new BasicDataSource(); @@ -194,7 +194,7 @@ final class SqliteEamDb extends AbstractSqlEamDb { protected Connection connect(boolean foreignKeys) throws EamDbException { synchronized (this) { if (!EamDb.isEnabled()) { - throw new EamDbException(Bundle.SqliteEamDb_centralRepositoryDisabled_message()); // NON-NLS + throw new EamDbException("Central repository database missing", Bundle.SqliteEamDb_centralRepositoryDisabled_message()); // NON-NLS } if (connectionPool == null) { setupConnectionPool(foreignKeys); @@ -202,7 +202,7 @@ final class SqliteEamDb extends AbstractSqlEamDb { try { return connectionPool.getConnection(); } catch (SQLException ex) { - throw new EamDbException(Bundle.SqliteEamDb_connectionFailedMessage_message(), ex); // NON-NLS + throw new EamDbException("Error getting connection from connection pool.", Bundle.SqliteEamDb_connectionFailedMessage_message(), ex); // NON-NLS } } } 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/centralrepository/eventlisteners/Installer.java b/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/Installer.java index a96f09fa78..690fb9189d 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/Installer.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/Installer.java @@ -64,7 +64,7 @@ public class Installer extends ModuleInstall { if (RuntimeProperties.runningWithGUI()) { WindowManager.getDefault().invokeWhenUIReady(() -> { JOptionPane.showMessageDialog(null, - ex.getMessage(), + ex.getUserMessage(), NbBundle.getMessage(this.getClass(), "Installer.centralRepoUpgradeFailed.title"), JOptionPane.ERROR_MESSAGE); diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/Bundle.properties-MERGED index dc42aa3b68..7277910e7d 100755 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/Bundle.properties-MERGED @@ -33,7 +33,7 @@ EamDbSettingsDialog.validation.finished=Click OK to save your database settings EamDbSettingsDialog.validation.incompleteFields=Fill in all values for the selected database. EamOptionsController.moduleErr=Error processing value changes. EamOptionsController.moduleErr.msg=Value change processing failed. -GlobalSettingsPanel.updateFailed.title=Central repository upgrade failed +GlobalSettingsPanel.updateFailed.title=Central repository disabled GlobalSettingsPanel.validationErrMsg.ingestRunning=You cannot change settings while ingest is running. GlobalSettingsPanel.validationerrMsg.mustConfigure=Configure the database to enable this module. ManageCasesDialog.title.text=Manage Cases diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/GlobalSettingsPanel.java b/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/GlobalSettingsPanel.java index c388d081a9..37718a8d7c 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/GlobalSettingsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/GlobalSettingsPanel.java @@ -77,7 +77,7 @@ public final class GlobalSettingsPanel extends IngestModuleGlobalSettingsPanel i ingestStateUpdated(Case.isCaseOpen()); } - @Messages({"GlobalSettingsPanel.updateFailed.title=Central repository upgrade failed"}) + @Messages({"GlobalSettingsPanel.updateFailed.title=Central repository disabled"}) private void updateDatabase() { if (EamDbPlatformEnum.getSelectedPlatform().equals(DISABLED)) { @@ -91,7 +91,7 @@ public final class GlobalSettingsPanel extends IngestModuleGlobalSettingsPanel i } catch (EamDbException ex) { setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); JOptionPane.showMessageDialog(this, - ex.getMessage(), + ex.getUserMessage(), NbBundle.getMessage(this.getClass(), "GlobalSettingsPanel.updateFailed.title"), JOptionPane.WARNING_MESSAGE); diff --git a/Core/src/org/sleuthkit/autopsy/communications/relationships/Bundle.properties b/Core/src/org/sleuthkit/autopsy/communications/relationships/Bundle.properties index 01d5316454..b14d8a2688 100755 --- a/Core/src/org/sleuthkit/autopsy/communications/relationships/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/communications/relationships/Bundle.properties @@ -26,3 +26,4 @@ SummaryViewer.attachmentsLabel.text=Total Attachments: SummaryViewer.referencesLabel.text=Communication References: SummaryViewer.referencesDataLabel.text= SummaryViewer.contactsLabel.text=Book Entries: +SummaryViewer.accountCountry.text= diff --git a/Core/src/org/sleuthkit/autopsy/communications/relationships/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/communications/relationships/Bundle.properties-MERGED index 73d807796e..b1b72054ca 100755 --- a/Core/src/org/sleuthkit/autopsy/communications/relationships/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/communications/relationships/Bundle.properties-MERGED @@ -47,6 +47,7 @@ SummaryViewer_Account_Description=This account represents a device in the case. SummaryViewer_Account_Description_MuliSelect=Summary information is not available when multiple accounts are selected. SummaryViewer_CaseRefNameColumn_Title=Case Name SummaryViewer_CentralRepository_Message= +SummaryViewer_Country_Code=Country: SummaryViewer_Creation_Date_Title=Creation Date SummaryViewer_Device_Account_Description=This account was referenced by a device in the case. SummaryViewer_FileRef_Message=", "SummaryViewer_Device_Account_Description=This account was referenced by a device in the case.", "SummaryViewer_Account_Description=This account represents a device in the case.", - "SummaryViewer_Account_Description_MuliSelect=Summary information is not available when multiple accounts are selected." + "SummaryViewer_Account_Description_MuliSelect=Summary information is not available when multiple accounts are selected.", + "SummaryViewer_Country_Code=Country: " }) /** @@ -110,7 +112,16 @@ public class SummaryViewer extends javax.swing.JPanel implements RelationshipsVi Account[] accountArray = info.getAccounts().toArray(new Account[1]); Account account = accountArray[0]; - accountLabel.setText(account.getTypeSpecificID()); + if (account.getAccountType().getTypeName().contains("PHONE")) { + String countryCode = PhoneNumUtil.getCountryCode(account.getTypeSpecificID()); + accountLabel.setText(PhoneNumUtil.convertToInternational(account.getTypeSpecificID())); + accountCountry.setText(Bundle.SummaryViewer_Country_Code() + countryCode); + accountCountry.setEnabled(true); + } else { + accountLabel.setText(account.getTypeSpecificID()); + accountCountry.setText(""); + accountCountry.setEnabled(false); + } if (account.getAccountType().equals(Account.Type.DEVICE)) { accoutDescriptionLabel.setText(Bundle.SummaryViewer_Account_Description()); @@ -173,6 +184,7 @@ public class SummaryViewer extends javax.swing.JPanel implements RelationshipsVi accountLabel.setText(""); accoutDescriptionLabel.setText(""); referencesDataLabel.setText(""); + accountCountry.setText(""); fileReferencesPanel.setNode(new AbstractNode(Children.LEAF)); caseReferencesPanel.setNode(new AbstractNode(Children.LEAF)); @@ -210,6 +222,7 @@ public class SummaryViewer extends javax.swing.JPanel implements RelationshipsVi summaryPanel = new javax.swing.JPanel(); accountLabel = new javax.swing.JLabel(); + accountCountry = new javax.swing.JLabel(); accoutDescriptionLabel = new javax.swing.JLabel(); countsPanel = new javax.swing.JPanel(); messagesLabel = new javax.swing.JLabel(); @@ -220,13 +233,15 @@ public class SummaryViewer extends javax.swing.JPanel implements RelationshipsVi callLogsDataLabel = new javax.swing.JLabel(); attachmentsLabel = new javax.swing.JLabel(); attachmentDataLabel = new javax.swing.JLabel(); - fileReferencesPanel = new org.sleuthkit.autopsy.communications.relationships.OutlineViewPanel(); - caseReferencesPanel = new org.sleuthkit.autopsy.communications.relationships.OutlineViewPanel(); contanctsPanel = new javax.swing.JPanel(); contactsLabel = new javax.swing.JLabel(); contactsDataLabel = new javax.swing.JLabel(); referencesLabel = new javax.swing.JLabel(); referencesDataLabel = new javax.swing.JLabel(); + fileReferencesPanel = new org.sleuthkit.autopsy.communications.relationships.OutlineViewPanel(); + caseReferencesPanel = new org.sleuthkit.autopsy.communications.relationships.OutlineViewPanel(); + + setLayout(new java.awt.GridBagLayout()); summaryPanel.setLayout(new java.awt.GridBagLayout()); @@ -238,15 +253,27 @@ public class SummaryViewer extends javax.swing.JPanel implements RelationshipsVi gridBagConstraints.insets = new java.awt.Insets(15, 9, 0, 9); summaryPanel.add(accountLabel, gridBagConstraints); - org.openide.awt.Mnemonics.setLocalizedText(accoutDescriptionLabel, org.openide.util.NbBundle.getMessage(SummaryViewer.class, "SummaryViewer.accoutDescriptionLabel.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(accountCountry, org.openide.util.NbBundle.getMessage(SummaryViewer.class, "SummaryViewer.accountCountry.text")); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 1; gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(0, 9, 0, 9); + summaryPanel.add(accountCountry, gridBagConstraints); + + org.openide.awt.Mnemonics.setLocalizedText(accoutDescriptionLabel, org.openide.util.NbBundle.getMessage(SummaryViewer.class, "SummaryViewer.accoutDescriptionLabel.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; gridBagConstraints.weightx = 1.0; gridBagConstraints.insets = new java.awt.Insets(15, 9, 15, 9); summaryPanel.add(accoutDescriptionLabel, gridBagConstraints); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + add(summaryPanel, gridBagConstraints); + countsPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(org.openide.util.NbBundle.getMessage(SummaryViewer.class, "SummaryViewer.countsPanel.border.title"))); // NOI18N countsPanel.setLayout(new java.awt.GridBagLayout()); @@ -315,31 +342,12 @@ public class SummaryViewer extends javax.swing.JPanel implements RelationshipsVi gridBagConstraints.insets = new java.awt.Insets(0, 0, 9, 15); countsPanel.add(attachmentDataLabel, gridBagConstraints); - summaryPanel.add(countsPanel, new java.awt.GridBagConstraints()); - - setLayout(new java.awt.GridBagLayout()); - - fileReferencesPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(org.openide.util.NbBundle.getMessage(SummaryViewer.class, "SummaryViewer.fileReferencesPanel.border.title"))); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; - gridBagConstraints.gridy = 3; + gridBagConstraints.gridy = 1; gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; - gridBagConstraints.weightx = 1.0; - gridBagConstraints.weighty = 1.0; - gridBagConstraints.insets = new java.awt.Insets(9, 0, 0, 0); - add(fileReferencesPanel, gridBagConstraints); - - caseReferencesPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(org.openide.util.NbBundle.getMessage(SummaryViewer.class, "SummaryViewer.caseReferencesPanel.border.title"))); // NOI18N - gridBagConstraints = new java.awt.GridBagConstraints(); - gridBagConstraints.gridx = 0; - gridBagConstraints.gridy = 4; - gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; - gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; - gridBagConstraints.weightx = 1.0; - gridBagConstraints.weighty = 1.0; - gridBagConstraints.insets = new java.awt.Insets(9, 0, 0, 0); - add(caseReferencesPanel, gridBagConstraints); + add(countsPanel, gridBagConstraints); contanctsPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(org.openide.util.NbBundle.getMessage(SummaryViewer.class, "SummaryViewer.contanctsPanel.border.title"))); // NOI18N contanctsPanel.setLayout(new java.awt.GridBagLayout()); @@ -381,14 +389,37 @@ public class SummaryViewer extends javax.swing.JPanel implements RelationshipsVi gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; - gridBagConstraints.gridy = 1; - gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; add(contanctsPanel, gridBagConstraints); + + fileReferencesPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(org.openide.util.NbBundle.getMessage(SummaryViewer.class, "SummaryViewer.fileReferencesPanel.border.title"))); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(9, 0, 0, 0); + add(fileReferencesPanel, gridBagConstraints); + + caseReferencesPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(org.openide.util.NbBundle.getMessage(SummaryViewer.class, "SummaryViewer.caseReferencesPanel.border.title"))); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 4; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(9, 0, 0, 0); + add(caseReferencesPanel, gridBagConstraints); }// //GEN-END:initComponents // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JLabel accountCountry; private javax.swing.JLabel accountLabel; private javax.swing.JLabel accoutDescriptionLabel; private javax.swing.JLabel attachmentDataLabel; diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties index c799a17d61..b3a384ce48 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties @@ -84,9 +84,12 @@ MediaViewImagePanel.zoomTextField.text= MediaViewImagePanel.rotationTextField.text= MediaViewImagePanel.rotateLeftButton.toolTipText= HtmlPanel.showImagesToggleButton.text=Download Images -MediaPlayerPanel.audioSlider.toolTipText= -MediaPlayerPanel.VolumeIcon.text=\ \ \ \ \ Volume +MediaViewImagePanel.tagsMenu.text_1=Tags Menu MediaPlayerPanel.progressLabel.text=00:00:00/00:00:00 +MediaPlayerPanel.audioSlider.toolTipText= +MediaPlayerPanel.rewindButton.text=\u2bc7\u2bc7 +MediaPlayerPanel.fastForwardButton.text=\u2bc8\u2bc8 MediaPlayerPanel.playButton.text=\u25ba MediaPlayerPanel.infoLabel.text=No Errors -MediaViewImagePanel.tagsMenu.text_1=Tags Menu +MediaPlayerPanel.VolumeIcon.text=Volume +MediaPlayerPanel.playBackSpeedLabel.text=Speed: diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED index ff3341b60f..2d9df0ae33 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED @@ -23,6 +23,7 @@ GstVideoPanel.noOpenCase.errMsg=No open case available. Html_text_display_error=The HTML text cannot be displayed, it may not be correctly formed HTML. HtmlPanel_showImagesToggleButton_hide=Hide Images HtmlPanel_showImagesToggleButton_show=Download Images +HtmlViewer_encoding_error=This file has unsupported encoding HtmlViewer_file_error=This file is missing or unreadable. MediaFileViewer.initGst.gstException.msg=Error initializing gstreamer for audio/video viewing and frame extraction capabilities. Video and audio viewing will be disabled. GstVideoPanel.setupVideo.infoLabel.text=Playback of deleted videos is not supported, use an external player. @@ -154,12 +155,15 @@ MediaViewImagePanel.zoomTextField.text= MediaViewImagePanel.rotationTextField.text= MediaViewImagePanel.rotateLeftButton.toolTipText= HtmlPanel.showImagesToggleButton.text=Download Images -MediaPlayerPanel.audioSlider.toolTipText= -MediaPlayerPanel.VolumeIcon.text=\ \ \ \ \ Volume +MediaViewImagePanel.tagsMenu.text_1=Tags Menu MediaPlayerPanel.progressLabel.text=00:00:00/00:00:00 +MediaPlayerPanel.audioSlider.toolTipText= +MediaPlayerPanel.rewindButton.text=\u2bc7\u2bc7 +MediaPlayerPanel.fastForwardButton.text=\u2bc8\u2bc8 MediaPlayerPanel.playButton.text=\u25ba MediaPlayerPanel.infoLabel.text=No Errors -MediaViewImagePanel.tagsMenu.text_1=Tags Menu +MediaPlayerPanel.VolumeIcon.text=Volume +MediaPlayerPanel.playBackSpeedLabel.text=Speed: # {0} - tableName SQLiteViewer.readTable.errorText=Error getting rows for table: {0} # {0} - tableName diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlViewer.java b/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlViewer.java index fb88ed9312..138e70ae47 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlViewer.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlViewer.java @@ -20,9 +20,12 @@ package org.sleuthkit.autopsy.contentviewers; import java.awt.Component; import java.awt.Cursor; +import java.io.UnsupportedEncodingException; import java.util.Arrays; import java.util.List; import java.util.logging.Level; +import org.apache.tika.parser.txt.CharsetDetector; +import org.apache.tika.parser.txt.CharsetMatch; import org.openide.util.NbBundle; import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.coreutils.Logger; @@ -59,17 +62,27 @@ final class HtmlViewer extends javax.swing.JPanel implements FileTypeViewer { * @return The text content of the file. */ @NbBundle.Messages({ - "HtmlViewer_file_error=This file is missing or unreadable.",}) + "HtmlViewer_file_error=This file is missing or unreadable.", + "HtmlViewer_encoding_error=This file has unsupported encoding"}) private String getHtmlText(AbstractFile abstractFile) { try { int fileSize = (int) abstractFile.getSize(); byte[] buffer = new byte[fileSize]; abstractFile.read(buffer, 0, fileSize); - return new String(buffer); + CharsetMatch match = new CharsetDetector().setText(buffer).detect(); + if (match != null) { + return new String(buffer, match.getName()); + } else { + return new String(buffer); + } } catch (TskCoreException ex) { logger.log(Level.SEVERE, String.format("Unable to read from file '%s' (id=%d).", abstractFile.getName(), abstractFile.getId()), ex); return String.format("

%s

", Bundle.HtmlViewer_file_error()); + } catch (UnsupportedEncodingException ex) { + logger.log(Level.SEVERE, String.format("Unsupported encoding for file '%s' (id=%d).", + abstractFile.getName(), abstractFile.getId()), ex); + return String.format("

%s

", Bundle.HtmlViewer_encoding_error()); } } diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.form b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.form index 880528e787..d8433a907b 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.form +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.form @@ -1,6 +1,6 @@ -
+ @@ -16,8 +16,8 @@ - + @@ -41,7 +41,7 @@ - + @@ -51,47 +51,42 @@ - - + + - - - - - - - - - - - - - + + + + + + + + + + + + - + - - - - - - + + + + - - - - - - - + + + + - + + @@ -111,26 +106,9 @@ - - - - - - - - - - - - - - - - - - - - + + + @@ -139,31 +117,174 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java index 1b30c49ef4..84f7da6c36 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java @@ -19,16 +19,26 @@ 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; +import java.util.EnumSet; import java.util.List; 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; @@ -37,7 +47,6 @@ import javax.swing.SwingWorker; import javax.swing.Timer; import javax.swing.event.ChangeEvent; import org.freedesktop.gstreamer.Bus; -import org.freedesktop.gstreamer.ClockTime; import org.freedesktop.gstreamer.Gst; import org.freedesktop.gstreamer.GstObject; import org.freedesktop.gstreamer.State; @@ -52,8 +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 javax.swing.plaf.basic.BasicSliderUI.TrackListener; +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; /** * This is a video player that is part of the Media View layered pane. It uses @@ -177,17 +195,27 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie private Bus.EOS endOfStreamListener; //Update progress bar and time label during video playback - private final Timer timer = new Timer(75, new VideoPanelUpdater()); + //Updating every 16 MS = 62.5 FPS. + private final Timer timer = new Timer(16, new VideoPanelUpdater()); private static final int PROGRESS_SLIDER_SIZE = 2000; + 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 */ public MediaPlayerPanel() throws GstException, UnsatisfiedLinkError { initComponents(); customizeComponents(); + //True for fairness. In other words, + //acquire() calls are processed in order of invocation. + sliderLock = new Semaphore(1, true); } private void customizeComponents() { @@ -202,11 +230,20 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie if (progressSlider.getValueIsAdjusting()) { long duration = gstPlayBin.queryDuration(TimeUnit.NANOSECONDS); double relativePosition = progressSlider.getValue() * 1.0 / PROGRESS_SLIDER_SIZE; - long newPos = (long) (relativePosition * duration); - gstPlayBin.seek(newPos, TimeUnit.NANOSECONDS); + long newStartTime = (long) (relativePosition * duration); + double playBackRate = getPlayBackRate(); + gstPlayBin.seek(playBackRate, + Format.TIME, + //FLUSH - flushes the pipeline + //ACCURATE - video will seek exactly to the position requested + EnumSet.of(SeekFlags.FLUSH, SeekFlags.ACCURATE), + //Set the start position to newTime + SeekType.SET, newStartTime, + //Do nothing for the end position + SeekType.NONE, -1); //Keep constantly updating the time label so users have a sense of //where the slider they are dragging is in relation to the video time - updateTimeLabel(newPos, duration); + updateTimeLabel(newStartTime, duration); } } }); @@ -220,10 +257,12 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie errorListener = new Bus.ERROR() { @Override public void errorMessage(GstObject go, int i, String string) { - enableComponents(false); - infoLabel.setText(String.format( - "%s", - MEDIA_PLAYER_ERROR_STRING)); + SwingUtilities.invokeLater(() -> { + enableComponents(false); + infoLabel.setText(String.format( + "%s", + MEDIA_PLAYER_ERROR_STRING)); + }); timer.stop(); } }; @@ -231,9 +270,13 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie @Override public void stateChanged(GstObject go, State oldState, State currentState, State pendingState) { if (State.PLAYING.equals(currentState)) { - playButton.setText("||"); + SwingUtilities.invokeLater(() -> { + playButton.setText("||"); + }); } else { - playButton.setText("â–º"); + SwingUtilities.invokeLater(() -> { + playButton.setText("â–º"); + }); } } }; @@ -241,7 +284,6 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie @Override public void endOfStream(GstObject go) { gstPlayBin.seek(ClockTime.ZERO); - progressSlider.setValue(0); /** * Keep the video from automatically playing */ @@ -268,7 +310,7 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie try { //Pushing off initialization to the background extractMediaWorker = new ExtractMedia(file, VideoUtils.getVideoFileInTempDir(file)); - extractMediaWorker.execute(); + extractMediaWorker.execute(); } catch (NoCurrentCaseException ex) { logger.log(Level.SEVERE, "Exception while getting open case.", ex); //NON-NLS infoLabel.setText(String.format("%s", Bundle.GstVideoPanel_noOpenCase_errMsg())); @@ -301,8 +343,8 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie if (gstPlayBin != null) { gstPlayBin.stop(); gstPlayBin.getBus().disconnect(endOfStreamListener); - gstPlayBin.getBus().disconnect(endOfStreamListener); - gstPlayBin.getBus().disconnect(endOfStreamListener); + gstPlayBin.getBus().disconnect(stateChangeListener); + gstPlayBin.getBus().disconnect(errorListener); gstPlayBin.dispose(); fxAppSink.clear(); gstPlayBin = null; @@ -317,6 +359,9 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie progressSlider.setEnabled(isEnabled); videoPanel.setEnabled(isEnabled); audioSlider.setEnabled(isEnabled); + rewindButton.setEnabled(isEnabled); + fastForwardButton.setEnabled(isEnabled); + playBackSpeedComboBox.setEnabled(isEnabled); } @Override @@ -373,7 +418,18 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie * @param total */ private void updateTimeLabel(long start, long total) { - progressLabel.setText(formatTime(start, false) + "/" + formatTime(total, true)); + progressLabel.setText(formatTime(start) + "/" + formatTime(total)); + } + + /** + * Reads the current selected playback rate from the speed combo box. + * + * @return The selected rate. + */ + private double getPlayBackRate() { + int selectIndex = playBackSpeedComboBox.getSelectedIndex(); + String selectText = playBackSpeedComboBox.getItemAt(selectIndex); + return Double.valueOf(selectText.substring(0, selectText.length() - 1)); } /** @@ -383,24 +439,18 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie "MediaPlayerPanel.unknownTime=Unknown", "MediaPlayerPanel.timeFormat=%02d:%02d:%02d" }) - private String formatTime(long ns, boolean ceiling) { + private String formatTime(long ns) { if (ns == -1) { return Bundle.MediaPlayerPanel_unknownTime(); } - double millis = ns / 1000000.0; - double seconds; - if (ceiling) { - seconds = Math.ceil(millis / 1000); - } else { - seconds = millis / 1000; - } - double hours = seconds / 3600; - seconds -= (int) hours * 3600; - double minutes = seconds / 60; - seconds -= (int) minutes * 60; + long seconds = TimeUnit.SECONDS.convert(ns, TimeUnit.NANOSECONDS); + long hours = TimeUnit.HOURS.convert(seconds, TimeUnit.SECONDS); + seconds -= TimeUnit.SECONDS.convert(hours, TimeUnit.HOURS); + long minutes = TimeUnit.MINUTES.convert(seconds, TimeUnit.SECONDS); + seconds -= TimeUnit.SECONDS.convert(minutes, TimeUnit.MINUTES); - return String.format(Bundle.MediaPlayerPanel_timeFormat(), (int) hours, (int) minutes, (int) seconds); + return String.format(Bundle.MediaPlayerPanel_timeFormat(), hours, minutes, seconds); } /** @@ -422,7 +472,11 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie protected Void doInBackground() throws Exception { if (!tempFile.exists() || tempFile.length() < sourceFile.getSize()) { progress = ProgressHandle.createHandle(NbBundle.getMessage(MediaPlayerPanel.class, "GstVideoPanel.ExtractMedia.progress.buffering", sourceFile.getName()), () -> this.cancel(true)); - progressLabel.setText(NbBundle.getMessage(this.getClass(), "GstVideoPanel.progress.buffering")); + + SwingUtilities.invokeLater(() -> { + progressLabel.setText(NbBundle.getMessage(this.getClass(), "GstVideoPanel.progress.buffering")); + }); + progress.start(100); try { Files.createParentDirs(tempFile); @@ -443,8 +497,8 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie protected void done() { try { super.get(); - - if(this.isCancelled()) { + + if (this.isCancelled()) { return; } @@ -460,8 +514,8 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie playBinBus.connect(endOfStreamListener); playBinBus.connect(stateChangeListener); playBinBus.connect(errorListener); - - if(this.isCancelled()) { + + if (this.isCancelled()) { return; } @@ -471,14 +525,14 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie videoPanel.add(fxPanel); fxAppSink = new JavaFxAppSink("JavaFxAppSink", fxPanel); gstPlayBin.setVideoSink(fxAppSink); - - if(this.isCancelled()) { + + if (this.isCancelled()) { return; } gstPlayBin.setVolume((audioSlider.getValue() * 2.0) / 100.0); gstPlayBin.pause(); - + timer.start(); enableComponents(true); } catch (CancellationException ex) { @@ -499,6 +553,7 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie @Override public void actionPerformed(ActionEvent e) { if (!progressSlider.getValueIsAdjusting()) { + sliderLock.acquireUninterruptibly(); long position = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS); long duration = gstPlayBin.queryDuration(TimeUnit.NANOSECONDS); /** @@ -506,12 +561,218 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie * pipeline. We start this updater when data-flow has just been * initiated so buffering may still be in progress. */ - if (duration != -1) { + if (duration >= 0 && position >= 0) { double relativePosition = (double) position / duration; progressSlider.setValue((int) (relativePosition * PROGRESS_SLIDER_SIZE)); } - updateTimeLabel(position, duration); + SwingUtilities.invokeLater(() -> { + updateTimeLabel(position, duration); + }); + sliderLock.release(); + } + } + } + + /** + * 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 slider JSlider component + * @param config Configuration object. Contains info about thumb + * dimensions and colors. + */ + public CircularJSliderUI(JSlider slider, CircularJSliderConfiguration config) { + super(slider); + 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 graphic) { + Rectangle thumb = this.thumbRect; + + Color original = graphic.getColor(); + + //Change the thumb view from the rectangle + //controller to an oval. + graphic.setColor(config.getThumbColor()); + Dimension thumbDimension = config.getThumbDimension(); + graphic.fillOval(thumb.x, thumb.y, thumbDimension.width, thumbDimension.height); + + //Preserve the graphics original color + graphic.setColor(original); + } + + @Override + 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. + 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 = graphic.getColor(); + + //Paint the seen side + graphic.setColor(config.getSeenTrackColor()); + graphic.drawLine(track.x, track.y + track.height / 2, + thumbX, thumbY + track.height / 2); + + //Paint the unseen side + graphic.setColor(config.getUnseenTrackColor()); + graphic.drawLine(thumbX, thumbY + track.height / 2, + track.x + track.width, track.y + track.height / 2); + + //Preserve the graphics color. + graphic.setColor(original); + } + + @Override + protected TrackListener createTrackListener(JSlider slider) { + return new CustomTrackListener(); + } + + @Override + protected void scrollDueToClickInTrack(int direction) { + //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(); + } + + /** + * Applies anti-aliasing if available. + */ + @Override + 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(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 + * itself under the mouse so that it can always be dragged. + */ + private class CustomTrackListener extends CircularJSliderUI.TrackListener { + + @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) { + if (!slider.isEnabled()) { + return; + } + + super.mouseReleased(e); + + //Unpause once the mouse has been released. + gstPlayBin.play(); } } } @@ -524,15 +785,22 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie @SuppressWarnings("unchecked") // //GEN-BEGIN:initComponents private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; videoPanel = new javax.swing.JPanel(); controlPanel = new javax.swing.JPanel(); progressSlider = new javax.swing.JSlider(); - infoLabel = new javax.swing.JLabel(); - playButton = new javax.swing.JButton(); progressLabel = new javax.swing.JLabel(); + buttonPanel = new javax.swing.JPanel(); + playButton = new javax.swing.JButton(); + fastForwardButton = new javax.swing.JButton(); + rewindButton = new javax.swing.JButton(); VolumeIcon = new javax.swing.JLabel(); audioSlider = new javax.swing.JSlider(); + infoLabel = new javax.swing.JLabel(); + playBackPanel = new javax.swing.JPanel(); + playBackSpeedComboBox = new javax.swing.JComboBox<>(); + playBackSpeedLabel = new javax.swing.JLabel(); javax.swing.GroupLayout videoPanelLayout = new javax.swing.GroupLayout(videoPanel); videoPanel.setLayout(videoPanelLayout); @@ -542,7 +810,7 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie ); videoPanelLayout.setVerticalGroup( videoPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGap(0, 259, Short.MAX_VALUE) + .addGap(0, 131, Short.MAX_VALUE) ); progressSlider.setValue(0); @@ -550,9 +818,11 @@ 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(infoLabel, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.infoLabel.text")); // NOI18N - infoLabel.setCursor(new java.awt.Cursor(java.awt.Cursor.DEFAULT_CURSOR)); + org.openide.awt.Mnemonics.setLocalizedText(progressLabel, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.progressLabel.text")); // NOI18N + + buttonPanel.setLayout(new java.awt.GridBagLayout()); org.openide.awt.Mnemonics.setLocalizedText(playButton, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.playButton.text")); // NOI18N playButton.addActionListener(new java.awt.event.ActionListener() { @@ -560,64 +830,145 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie playButtonActionPerformed(evt); } }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.ipadx = 21; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(5, 6, 0, 0); + buttonPanel.add(playButton, gridBagConstraints); - org.openide.awt.Mnemonics.setLocalizedText(progressLabel, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.progressLabel.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(fastForwardButton, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.fastForwardButton.text")); // NOI18N + fastForwardButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + fastForwardButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(5, 6, 0, 0); + buttonPanel.add(fastForwardButton, gridBagConstraints); + + org.openide.awt.Mnemonics.setLocalizedText(rewindButton, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.rewindButton.text")); // NOI18N + rewindButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + rewindButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(5, 0, 1, 0); + buttonPanel.add(rewindButton, gridBagConstraints); org.openide.awt.Mnemonics.setLocalizedText(VolumeIcon, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.VolumeIcon.text")); // NOI18N + VolumeIcon.setHorizontalTextPosition(javax.swing.SwingConstants.LEFT); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 3; + gridBagConstraints.gridy = 0; + gridBagConstraints.ipadx = 8; + gridBagConstraints.ipady = 7; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(6, 14, 0, 0); + buttonPanel.add(VolumeIcon, gridBagConstraints); audioSlider.setMajorTickSpacing(10); audioSlider.setMaximum(50); audioSlider.setMinorTickSpacing(5); - audioSlider.setPaintTicks(true); audioSlider.setToolTipText(org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.audioSlider.toolTipText")); // NOI18N 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; + gridBagConstraints.ipadx = -116; + gridBagConstraints.ipady = 7; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(3, 1, 0, 10); + buttonPanel.add(audioSlider, gridBagConstraints); + + infoLabel.setHorizontalAlignment(javax.swing.SwingConstants.LEFT); + org.openide.awt.Mnemonics.setLocalizedText(infoLabel, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.infoLabel.text")); // NOI18N + infoLabel.setCursor(new java.awt.Cursor(java.awt.Cursor.DEFAULT_CURSOR)); + + playBackSpeedComboBox.setModel(new javax.swing.DefaultComboBoxModel<>(new String[] { "0.25x", "0.50x", "0.75x", "1x", "1.25x", "1.50x", "1.75x", "2x" })); + playBackSpeedComboBox.setSelectedIndex(3); + playBackSpeedComboBox.setMaximumSize(new java.awt.Dimension(53, 23)); + playBackSpeedComboBox.setMinimumSize(new java.awt.Dimension(53, 23)); + playBackSpeedComboBox.setPreferredSize(new java.awt.Dimension(53, 23)); + playBackSpeedComboBox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + playBackSpeedComboBoxActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(playBackSpeedLabel, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.playBackSpeedLabel.text")); // NOI18N + + javax.swing.GroupLayout playBackPanelLayout = new javax.swing.GroupLayout(playBackPanel); + playBackPanel.setLayout(playBackPanelLayout); + playBackPanelLayout.setHorizontalGroup( + playBackPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(playBackPanelLayout.createSequentialGroup() + .addComponent(playBackSpeedLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(playBackSpeedComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(13, 13, 13)) + ); + playBackPanelLayout.setVerticalGroup( + playBackPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(playBackPanelLayout.createSequentialGroup() + .addGap(6, 6, 6) + .addGroup(playBackPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(playBackSpeedComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(playBackSpeedLabel)) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); javax.swing.GroupLayout controlPanelLayout = new javax.swing.GroupLayout(controlPanel); controlPanel.setLayout(controlPanelLayout); controlPanelLayout.setHorizontalGroup( controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, controlPanelLayout.createSequentialGroup() + .addGroup(controlPanelLayout.createSequentialGroup() .addContainerGap() .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(controlPanelLayout.createSequentialGroup() - .addComponent(playButton, javax.swing.GroupLayout.PREFERRED_SIZE, 64, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addComponent(progressSlider, javax.swing.GroupLayout.DEFAULT_SIZE, 680, Short.MAX_VALUE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addComponent(progressLabel)) - .addGroup(controlPanelLayout.createSequentialGroup() - .addComponent(infoLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGap(18, 18, 18) - .addComponent(VolumeIcon, javax.swing.GroupLayout.PREFERRED_SIZE, 64, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(2, 2, 2) - .addComponent(audioSlider, javax.swing.GroupLayout.PREFERRED_SIZE, 229, javax.swing.GroupLayout.PREFERRED_SIZE))) - .addContainerGap()) + .addComponent(infoLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, controlPanelLayout.createSequentialGroup() + .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(buttonPanel, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(progressSlider, javax.swing.GroupLayout.DEFAULT_SIZE, 623, Short.MAX_VALUE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addComponent(progressLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(playBackPanel, javax.swing.GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE)) + .addGap(10, 10, 10))) + .addGap(0, 0, 0)) ); controlPanelLayout.setVerticalGroup( controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(controlPanelLayout.createSequentialGroup() - .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) - .addComponent(progressLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(progressSlider, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) - .addComponent(playButton)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) - .addComponent(audioSlider, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(VolumeIcon, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(infoLabel))) - .addGap(13, 13, 13)) + .addGap(0, 0, 0) + .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addComponent(progressLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(progressSlider, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addGap(5, 5, 5) + .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addComponent(buttonPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(playBackPanel, javax.swing.GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE)) + .addGap(14, 14, 14) + .addComponent(infoLabel)) ); javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); 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) @@ -628,23 +979,96 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie ); }// //GEN-END:initComponents + private void rewindButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_rewindButtonActionPerformed + long currentTime = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS); + //Skip 30 seconds. + long rewindDelta = TimeUnit.NANOSECONDS.convert(SKIP_IN_SECONDS, TimeUnit.SECONDS); + //Ensure new video position is within bounds + long newTime = Math.max(currentTime - rewindDelta, 0); + double playBackRate = getPlayBackRate(); + gstPlayBin.seek(playBackRate, + Format.TIME, + //FLUSH - flushes the pipeline + //ACCURATE - video will seek exactly to the position requested + EnumSet.of(SeekFlags.FLUSH, SeekFlags.ACCURATE), + //Set the start position to newTime + SeekType.SET, newTime, + //Do nothing for the end position + SeekType.NONE, -1); + }//GEN-LAST:event_rewindButtonActionPerformed + + private void fastForwardButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_fastForwardButtonActionPerformed + long duration = gstPlayBin.queryDuration(TimeUnit.NANOSECONDS); + long currentTime = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS); + //Skip 30 seconds. + long fastForwardDelta = TimeUnit.NANOSECONDS.convert(SKIP_IN_SECONDS, TimeUnit.SECONDS); + + //Ignore fast forward requests if there are less than 30 seconds left. + if (currentTime + fastForwardDelta >= duration) { + return; + } + + long newTime = currentTime + fastForwardDelta; + double playBackRate = getPlayBackRate(); + gstPlayBin.seek(playBackRate, + Format.TIME, + //FLUSH - flushes the pipeline + //ACCURATE - video will seek exactly to the position requested + EnumSet.of(SeekFlags.FLUSH, SeekFlags.ACCURATE), + //Set the start position to newTime + SeekType.SET, newTime, + //Do nothing for the end position + SeekType.NONE, -1); + }//GEN-LAST:event_fastForwardButtonActionPerformed + private void playButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_playButtonActionPerformed if (gstPlayBin.isPlaying()) { gstPlayBin.pause(); } else { + double playBackRate = getPlayBackRate(); + long currentTime = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS); + //Set playback rate before play. + gstPlayBin.seek(playBackRate, + Format.TIME, + //FLUSH - flushes the pipeline + //ACCURATE - video will seek exactly to the position requested + EnumSet.of(SeekFlags.FLUSH, SeekFlags.ACCURATE), + //Set the start position to newTime + SeekType.SET, currentTime, + //Do nothing for the end position + SeekType.NONE, -1); gstPlayBin.play(); } }//GEN-LAST:event_playButtonActionPerformed + private void playBackSpeedComboBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_playBackSpeedComboBoxActionPerformed + double playBackRate = getPlayBackRate(); + long currentTime = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS); + gstPlayBin.seek(playBackRate, + Format.TIME, + //FLUSH - flushes the pipeline + //ACCURATE - video will seek exactly to the position requested + EnumSet.of(SeekFlags.FLUSH, SeekFlags.ACCURATE), + //Set the position to the currentTime, we are only adjusting the + //playback rate. + SeekType.SET, currentTime, + SeekType.NONE, 0); + }//GEN-LAST:event_playBackSpeedComboBoxActionPerformed // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JLabel VolumeIcon; private javax.swing.JSlider audioSlider; + private javax.swing.JPanel buttonPanel; private javax.swing.JPanel controlPanel; + private javax.swing.JButton fastForwardButton; private javax.swing.JLabel infoLabel; + private javax.swing.JPanel playBackPanel; + private javax.swing.JComboBox playBackSpeedComboBox; + private javax.swing.JLabel playBackSpeedLabel; private javax.swing.JButton playButton; private javax.swing.JLabel progressLabel; private javax.swing.JSlider progressSlider; + private javax.swing.JButton rewindButton; private javax.swing.JPanel videoPanel; // End of variables declaration//GEN-END:variables } diff --git a/Core/src/org/sleuthkit/autopsy/core/Bundle.properties b/Core/src/org/sleuthkit/autopsy/core/Bundle.properties index db8e9e9808..4fccbcb6fa 100644 --- a/Core/src/org/sleuthkit/autopsy/core/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/core/Bundle.properties @@ -11,7 +11,7 @@ OpenIDE-Module-Short-Description=Autopsy Core Module org_sleuthkit_autopsy_core_update_center=http://sleuthkit.org/autopsy/updates.xml Services/AutoupdateType/org_sleuthkit_autopsy_core_update_center.settings=Autopsy Update Center Installer.errorInitJavafx.msg=Error initializing JavaFX. -Installer.errorInitJavafx.details=\ Some features will not be available. Check that you have the right JRE installed (Oracle JRE > 1.7.10). +Installer.errorInitJavafx.details=\ Some features will not be available. Check that you have JavaFX installed (OpenJFX 8). ServicesMonitor.failedService.notify.title=Service Is Down ServicesMonitor.failedService.notify.msg=Connection to {0} is down ServicesMonitor.restoredService.notify.title=Service Is Up diff --git a/Core/src/org/sleuthkit/autopsy/core/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/core/Bundle.properties-MERGED index 087eaec314..0b16a9701f 100755 --- a/Core/src/org/sleuthkit/autopsy/core/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/core/Bundle.properties-MERGED @@ -15,7 +15,7 @@ OpenIDE-Module-Short-Description=Autopsy Core Module org_sleuthkit_autopsy_core_update_center=http://sleuthkit.org/autopsy/updates.xml Services/AutoupdateType/org_sleuthkit_autopsy_core_update_center.settings=Autopsy Update Center Installer.errorInitJavafx.msg=Error initializing JavaFX. -Installer.errorInitJavafx.details=\ Some features will not be available. Check that you have the right JRE installed (Oracle JRE > 1.7.10). +Installer.errorInitJavafx.details=\ Some features will not be available. Check that you have JavaFX installed (OpenJFX 8). ServicesMonitor.failedService.notify.title=Service Is Down ServicesMonitor.failedService.notify.msg=Connection to {0} is down ServicesMonitor.restoredService.notify.title=Service Is Up diff --git a/Core/src/org/sleuthkit/autopsy/core/Bundle_ja.properties b/Core/src/org/sleuthkit/autopsy/core/Bundle_ja.properties index 171ced6172..7bd07e089b 100644 --- a/Core/src/org/sleuthkit/autopsy/core/Bundle_ja.properties +++ b/Core/src/org/sleuthkit/autopsy/core/Bundle_ja.properties @@ -1,31 +1,30 @@ -OpenIDE-Module-Display-Category=\u30A4\u30F3\u30D5\u30E9\u30B9\u30C8\u30E9\u30AF\u30C1\u30E3\u30FC +OpenIDE-Module-Display-Category=\u30a4\u30f3\u30d5\u30e9\u30b9\u30c8\u30e9\u30af\u30c1\u30e3\u30fc OpenIDE-Module-Long-Description=\ - \u3053\u308C\u304CAutopsy\u306E\u30B3\u30A2\u30E2\u30B8\u30E5\u30FC\u30EB\u3067\u3059\u3002\n\n\ - \u30A2\u30D7\u30EA\u30B1\u30FC\u30B7\u30E7\u30F3\u306E\u307F\u3067\u5B9F\u884C\u3059\u308B\u306E\u306B\u5FC5\u8981\u306A\u4E3B\u8981\u30B3\u30F3\u30DD\u30FC\u30CD\u30F3\u30C8\u304C\u542B\u307E\u308C\u3066\u3044\u307E\u3059\uFF1ARCP\u30D7\u30E9\u30C3\u30C8\u30D5\u30A9\u30FC\u30E0\u3001\u30A6\u30A3\u30F3\u30C9\u30A6\u30A4\u30F3\u30B0GUI\u3001Sleuth Kit\u30D0\u30A4\u30F3\u30C7\u30A3\u30F3\u30B0\u3001\u30C7\u30FC\u30BF\u30E2\u30C7\u30EB\uFF0F\u30B9\u30C8\u30EC\u30FC\u30B8\u3001\u30A8\u30AF\u30B9\u30D7\u30ED\u30FC\u30E9\u3001\u7D50\u679C\u30D3\u30E5\u30FC\u30A2\u3001\u30B3\u30F3\u30C6\u30F3\u30C4\u30D3\u30E5\u30FC\u30A2\u3001\u30A4\u30F3\u30B8\u30A7\u30B9\u30C8\u7528\u30D5\u30EC\u30FC\u30E0\u30EF\u30FC\u30AF\u3001\u30EC\u30DD\u30FC\u30C8\u751F\u6210\u3001\u30D5\u30A1\u30A4\u30EB\u691C\u7D22\u7B49\u306E\u4E3B\u8981\u30C4\u30FC\u30EB\u3002\n\n\ - \u30E2\u30B8\u30E5\u30FC\u30EB\u5185\u306E\u30D5\u30EC\u30FC\u30E0\u30EF\u30FC\u30AF\u306B\u306F\u30A4\u30F3\u30B8\u30A7\u30B9\u30C8\u3001\u30D3\u30E5\u30FC\u30A2\u3001\u30EC\u30DD\u30FC\u30C8\u751F\u6210\u306E\u30E2\u30B8\u30E5\u30FC\u30EB\u958B\u767A\u7528\u306EAPI\u304C\u542B\u307E\u308C\u307E\u3059\u3002\ - \u30E2\u30B8\u30E5\u30FC\u30EB\u306FAutopsy\u30D7\u30E9\u30B0\u30A4\u30F3\u30A4\u30F3\u30B9\u30C8\u30FC\u30E9\u30FC\u3092\u4F7F\u7528\u3057\u3001\u30D7\u30E9\u30B0\u30A4\u30F3\u3068\u3057\u3066\u5B9F\u88C5\u3067\u304D\u307E\u3059\u3002\n\ - \u3053\u306E\u30E2\u30B8\u30E5\u30FC\u30EB\u306F\u30A2\u30F3\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u3057\u306A\u3044\u3067\u304F\u3060\u3055\u3044\u3002\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u3055\u308C\u3066\u3044\u306A\u3051\u308C\u3070\u3001Autopsy\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093\u3002\n\n\ - \u8A73\u7D30\u306F\u3053\u3061\u3089\u3067\u3054\u78BA\u8A8D\u304F\u3060\u3055\u3044\u3002http\://www.sleuthkit.org/autopsy/ -OpenIDE-Module-Name=Autopsy-\u30B3\u30A2 -OpenIDE-Module-Short-Description=Autopsy\u30B3\u30A2\u30E2\u30B8\u30E5\u30FC\u30EB + \u3053\u308c\u304cAutopsy\u306e\u30b3\u30a2\u30e2\u30b8\u30e5\u30fc\u30eb\u3067\u3059\u3002\n\n\ + \u30a2\u30d7\u30ea\u30b1\u30fc\u30b7\u30e7\u30f3\u306e\u307f\u3067\u5b9f\u884c\u3059\u308b\u306e\u306b\u5fc5\u8981\u306a\u4e3b\u8981\u30b3\u30f3\u30dd\u30fc\u30cd\u30f3\u30c8\u304c\u542b\u307e\u308c\u3066\u3044\u307e\u3059\uff1aRCP\u30d7\u30e9\u30c3\u30c8\u30d5\u30a9\u30fc\u30e0\u3001\u30a6\u30a3\u30f3\u30c9\u30a6\u30a4\u30f3\u30b0GUI\u3001Sleuth Kit\u30d0\u30a4\u30f3\u30c7\u30a3\u30f3\u30b0\u3001\u30c7\u30fc\u30bf\u30e2\u30c7\u30eb\uff0f\u30b9\u30c8\u30ec\u30fc\u30b8\u3001\u30a8\u30af\u30b9\u30d7\u30ed\u30fc\u30e9\u3001\u7d50\u679c\u30d3\u30e5\u30fc\u30a2\u3001\u30b3\u30f3\u30c6\u30f3\u30c4\u30d3\u30e5\u30fc\u30a2\u3001\u30a4\u30f3\u30b8\u30a7\u30b9\u30c8\u7528\u30d5\u30ec\u30fc\u30e0\u30ef\u30fc\u30af\u3001\u30ec\u30dd\u30fc\u30c8\u751f\u6210\u3001\u30d5\u30a1\u30a4\u30eb\u691c\u7d22\u7b49\u306e\u4e3b\u8981\u30c4\u30fc\u30eb\u3002\n\n\ + \u30e2\u30b8\u30e5\u30fc\u30eb\u5185\u306e\u30d5\u30ec\u30fc\u30e0\u30ef\u30fc\u30af\u306b\u306f\u30a4\u30f3\u30b8\u30a7\u30b9\u30c8\u3001\u30d3\u30e5\u30fc\u30a2\u3001\u30ec\u30dd\u30fc\u30c8\u751f\u6210\u306e\u30e2\u30b8\u30e5\u30fc\u30eb\u958b\u767a\u7528\u306eAPI\u304c\u542b\u307e\u308c\u307e\u3059\u3002\ + \u30e2\u30b8\u30e5\u30fc\u30eb\u306fAutopsy\u30d7\u30e9\u30b0\u30a4\u30f3\u30a4\u30f3\u30b9\u30c8\u30fc\u30e9\u30fc\u3092\u4f7f\u7528\u3057\u3001\u30d7\u30e9\u30b0\u30a4\u30f3\u3068\u3057\u3066\u5b9f\u88c5\u3067\u304d\u307e\u3059\u3002\n\ + \u3053\u306e\u30e2\u30b8\u30e5\u30fc\u30eb\u306f\u30a2\u30f3\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3057\u306a\u3044\u3067\u304f\u3060\u3055\u3044\u3002\u30a4\u30f3\u30b9\u30c8\u30fc\u30eb\u3055\u308c\u3066\u3044\u306a\u3051\u308c\u3070\u3001Autopsy\u306f\u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3002\n\n\ + \u8a73\u7d30\u306f\u3053\u3061\u3089\u3067\u3054\u78ba\u8a8d\u304f\u3060\u3055\u3044\u3002http\://www.sleuthkit.org/autopsy/ +OpenIDE-Module-Name=Autopsy-\u30b3\u30a2 +OpenIDE-Module-Short-Description=Autopsy\u30b3\u30a2\u30e2\u30b8\u30e5\u30fc\u30eb org_sleuthkit_autopsy_core_update_center=http\://sleuthkit.org/autopsy/updates_ja.xml -Services/AutoupdateType/org_sleuthkit_autopsy_core_update_center.settings=Autopsy\u30A2\u30C3\u30D7\u30C7\u30FC\u30C8\u30BB\u30F3\u30BF\u30FC -Installer.errorInitJavafx.msg=JavaFX\u521D\u671F\u5316\u30A8\u30E9\u30FC -Installer.errorInitJavafx.details=\u4E00\u90E8\u306E\u6A5F\u80FD\u304C\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093\u3002\u6B63\u3057\u3044JRE\u304C\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u3055\u308C\u3066\u3044\u308B\u304B\u3092\u78BA\u8A8D\u3057\u3066\u4E0B\u3055\u3044\u3002\uFF08Oracle JRE > 1.7.10\uFF09 -ServicesMonitor.failedService.notify.title=\u30B5\u30FC\u30D3\u30B9\u304C\u505C\u6B62\u3057\u3066\u3044\u307E\u3059 -ServicesMonitor.failedService.notify.msg={0}\u3078\u306E\u63A5\u7D9A\u304C\u30C0\u30A6\u30F3\u3057\u3066\u3044\u307E\u3059 -ServicesMonitor.restoredService.notify.title=\u30B5\u30FC\u30D3\u30B9\u304C\u7A3C\u50CD\u4E2D\u3067\u3059 -ServicesMonitor.restoredService.notify.msg={0}\u3078\u306E\u63A5\u7D9A\u304C\u5229\u7528\u3067\u304D\u307E\u3059 -ServicesMonitor.statusChange.notify.title=\u30B5\u30FC\u30D3\u30B9\u30B9\u30C6\u30FC\u30BF\u30B9\u30A2\u30C3\u30D7\u30C7\u30FC\u30C8 -ServicesMonitor.statusChange.notify.msg={0}\u306E\u30B9\u30C6\u30FC\u30BF\u30B9\u306F{1} -ServicesMonitor.nullServiceName.excepton.txt=\u30EA\u30AF\u30A8\u30B9\u30C8\u3055\u308C\u305F\u30B5\u30FC\u30D3\u30B9\u306F\u30CC\u30EB\u3067\u3059 -ServicesMonitor.unknownServiceName.excepton.txt=\u30EA\u30AF\u30A8\u30B9\u30C8\u3055\u308C\u305F\u30B5\u30FC\u30D3\u30B9{0}\u306F\u4E0D\u660E\u3067\u3059 -TextConverter.convert.exception.txt=\u30C6\u30AD\u30B9\u30C8{0}\u3092hex\u30C6\u30AD\u30B9\u30C8\u306B\u5909\u63DB\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F -TextConverter.convertFromHex.exception.txt=hex\u30C6\u30AD\u30B9\u30C8\u3092\u30C6\u30AD\u30B9\u30C8\u306B\u5909\u63DB\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F -ServicesMonitor.KeywordSearchNull=\u30AD\u30FC\u30EF\u30FC\u30C9\u691C\u7D22\u30B5\u30FC\u30D3\u30B9\u3092\u898B\u3064\u3051\u308C\u307E\u305B\u3093\u3067\u3057\u305F -ServicesMonitor.InvalidPortNumber=\u7121\u52B9\u306A\u30DD\u30FC\u30C8\u756A\u53F7 -ServicesMonitor.remoteCaseDatabase.displayName.text=\u8907\u6570\u306E\u30E6\u30FC\u30B6\u30FC\u306E\u30B1\u30FC\u30B9\u306E\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u30B5\u30FC\u30D3\u30B9 -ServicesMonitor.remoteKeywordSearch.displayName.text=\u8907\u6570\u306E\u30E6\u30FC\u30B6\u30FC\u306E\u30AD\u30FC\u30EF\u30FC\u30C9\u691C\u7D22\u30B5\u30FC\u30D3\u30B9 -ServicesMonitor.messaging.displayName.text=\u30E1\u30C3\u30BB\u30FC\u30B8\u30B5\u30FC\u30D3\u30B9 -ServicesMonitor.databaseConnectionInfo.error.msg=\u30B1\u30FC\u30B9\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u306E\u63A5\u7D9A\u60C5\u5831\u3092\u5165\u624B\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F -ServicesMonitor.messagingService.connErr.text=\u30E1\u30C3\u30BB\u30FC\u30B8\u30B5\u30FC\u30D3\u30B9\u306E\u63A5\u7D9A\u60C5\u5831\u3092\u5165\u624B\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F \ No newline at end of file +Services/AutoupdateType/org_sleuthkit_autopsy_core_update_center.settings=Autopsy\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u30bb\u30f3\u30bf\u30fc +Installer.errorInitJavafx.msg=JavaFX\u521d\u671f\u5316\u30a8\u30e9\u30fc +ServicesMonitor.failedService.notify.title=\u30b5\u30fc\u30d3\u30b9\u304c\u505c\u6b62\u3057\u3066\u3044\u307e\u3059 +ServicesMonitor.failedService.notify.msg={0}\u3078\u306e\u63a5\u7d9a\u304c\u30c0\u30a6\u30f3\u3057\u3066\u3044\u307e\u3059 +ServicesMonitor.restoredService.notify.title=\u30b5\u30fc\u30d3\u30b9\u304c\u7a3c\u50cd\u4e2d\u3067\u3059 +ServicesMonitor.restoredService.notify.msg={0}\u3078\u306e\u63a5\u7d9a\u304c\u5229\u7528\u3067\u304d\u307e\u3059 +ServicesMonitor.statusChange.notify.title=\u30b5\u30fc\u30d3\u30b9\u30b9\u30c6\u30fc\u30bf\u30b9\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8 +ServicesMonitor.statusChange.notify.msg={0}\u306e\u30b9\u30c6\u30fc\u30bf\u30b9\u306f{1} +ServicesMonitor.nullServiceName.excepton.txt=\u30ea\u30af\u30a8\u30b9\u30c8\u3055\u308c\u305f\u30b5\u30fc\u30d3\u30b9\u306f\u30cc\u30eb\u3067\u3059 +ServicesMonitor.unknownServiceName.excepton.txt=\u30ea\u30af\u30a8\u30b9\u30c8\u3055\u308c\u305f\u30b5\u30fc\u30d3\u30b9{0}\u306f\u4e0d\u660e\u3067\u3059 +TextConverter.convert.exception.txt=\u30c6\u30ad\u30b9\u30c8{0}\u3092hex\u30c6\u30ad\u30b9\u30c8\u306b\u5909\u63db\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f +TextConverter.convertFromHex.exception.txt=hex\u30c6\u30ad\u30b9\u30c8\u3092\u30c6\u30ad\u30b9\u30c8\u306b\u5909\u63db\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f +ServicesMonitor.KeywordSearchNull=\u30ad\u30fc\u30ef\u30fc\u30c9\u691c\u7d22\u30b5\u30fc\u30d3\u30b9\u3092\u898b\u3064\u3051\u308c\u307e\u305b\u3093\u3067\u3057\u305f +ServicesMonitor.InvalidPortNumber=\u7121\u52b9\u306a\u30dd\u30fc\u30c8\u756a\u53f7 +ServicesMonitor.remoteCaseDatabase.displayName.text=\u8907\u6570\u306e\u30e6\u30fc\u30b6\u30fc\u306e\u30b1\u30fc\u30b9\u306e\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u30b5\u30fc\u30d3\u30b9 +ServicesMonitor.remoteKeywordSearch.displayName.text=\u8907\u6570\u306e\u30e6\u30fc\u30b6\u30fc\u306e\u30ad\u30fc\u30ef\u30fc\u30c9\u691c\u7d22\u30b5\u30fc\u30d3\u30b9 +ServicesMonitor.messaging.displayName.text=\u30e1\u30c3\u30bb\u30fc\u30b8\u30b5\u30fc\u30d3\u30b9 +ServicesMonitor.databaseConnectionInfo.error.msg=\u30b1\u30fc\u30b9\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u306e\u63a5\u7d9a\u60c5\u5831\u3092\u5165\u624b\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f +ServicesMonitor.messagingService.connErr.text=\u30e1\u30c3\u30bb\u30fc\u30b8\u30b5\u30fc\u30d3\u30b9\u306e\u63a5\u7d9a\u60c5\u5831\u3092\u5165\u624b\u4e2d\u306b\u30a8\u30e9\u30fc\u304c\u767a\u751f\u3057\u307e\u3057\u305f \ No newline at end of file diff --git a/Core/src/org/sleuthkit/autopsy/core/geolocationWsmode.xml b/Core/src/org/sleuthkit/autopsy/core/geolocationWsmode.xml new file mode 100755 index 0000000000..4e29bb3e2d --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/core/geolocationWsmode.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/core/layer.xml b/Core/src/org/sleuthkit/autopsy/core/layer.xml index 6a9673a6d3..630af198a2 100644 --- a/Core/src/org/sleuthkit/autopsy/core/layer.xml +++ b/Core/src/org/sleuthkit/autopsy/core/layer.xml @@ -448,6 +448,7 @@ + diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerArtifact.java b/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerArtifact.java index 39d3203b13..8c8e722bce 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerArtifact.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerArtifact.java @@ -56,6 +56,10 @@ import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskException; import org.netbeans.swing.etable.ETable; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; /** * Instances of this class display the BlackboardArtifacts associated with the @@ -552,6 +556,16 @@ public class DataContentViewerArtifact extends javax.swing.JPanel implements Dat value = dateFormatter.format(new java.util.Date(epoch * 1000)); } break; + case JSON: + // @TODO: 5726 - return a multilevel bulleted list instead of prettyprint JSON + String jsonVal = attr.getValueString(); + + JsonParser parser = new JsonParser(); + JsonObject json = parser.parse(jsonVal).getAsJsonObject(); + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + + value = gson.toJson(json); + break; } /* * Attribute sources column. diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerUtility.java b/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerUtility.java index 24f54fa7ac..d460a16f03 100755 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerUtility.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerUtility.java @@ -1,15 +1,15 @@ /* * Autopsy Forensic Browser - * - * Copyright 2018 Basis Technology Corp. + * + * Copyright 2018-2019 Basis Technology Corp. * Contact: carrier sleuthkit org - * + * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -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. - * - * @param node Node passed into content viewer - * @return highest priority content or null if there is no content + * Gets a Content object from the Lookup of a display Node object, + * preferring to return any Content object other than a BlackboardArtifact + * object. + * + * This method was written with the needs of the hex and strings content + * viewers in mind - the algorithm is exactly what those viewers require. + * + * @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; - } - else { + 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() { + } + } diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/JLNK.java b/Core/src/org/sleuthkit/autopsy/coreutils/JLNK.java index 073b9c3107..5e8c680b9f 100644 --- a/Core/src/org/sleuthkit/autopsy/coreutils/JLNK.java +++ b/Core/src/org/sleuthkit/autopsy/coreutils/JLNK.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2012 Basis Technology Corp. + * Copyright 2012-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,6 +20,7 @@ package org.sleuthkit.autopsy.coreutils; import java.io.File; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import org.openide.util.NbBundle; @@ -35,43 +36,43 @@ import org.sleuthkit.autopsy.coreutils.LnkEnums.NetworkProviderType; */ public class JLNK { - private int header; - private byte[] linkClassIdentifier; - private List linkFlags; - private List fileAttributesFlags; - private long crtime; - private long atime; - private long mtime; - private int fileSize; - private int iconIndex; - private int showCommand; - private short hotKey; + private final int header; + private final byte[] linkClassIdentifier; + private final List linkFlags; + private final List fileAttributesFlags; + private final long crtime; + private final long atime; + private final long mtime; + private final int fileSize; + private final int iconIndex; + private final int showCommand; + private final short hotKey; - private List linkTargetIdList; + private final List linkTargetIdList; - private boolean hasUnicodeLocalBaseAndCommonSuffixOffset; - private String localBasePath; - private String commonPathSuffix; - private String localBasePathUnicode; - private String commonPathSuffixUnicode; + private final boolean hasUnicodeLocalBaseAndCommonSuffixOffset; + private final String localBasePath; + private final String commonPathSuffix; + private final String localBasePathUnicode; + private final String commonPathSuffixUnicode; - private String name; - private String relativePath; - private String workingDir; - private String arguments; - private String iconLocation; + private final String name; + private final String relativePath; + private final String workingDir; + private final String arguments; + private final String iconLocation; - private int driveSerialNumber; - private DriveType driveType; - private String volumeLabel; + private final int driveSerialNumber; + private final DriveType driveType; + private final String volumeLabel; - private List commonNetworkRelativeListFlags; - private NetworkProviderType networkProviderType; - private boolean unicodeNetAndDeviceName; - private String netName; - private String netNameUnicode; - private String deviceName; - private String deviceNameUnicode; + private final List commonNetworkRelativeListFlags; + private final NetworkProviderType networkProviderType; + private final boolean unicodeNetAndDeviceName; + private final String netName; + private final String netNameUnicode; + private final String deviceName; + private final String deviceNameUnicode; public JLNK(int header, byte[] linkClassIdentifier, int linkFlags, int fileAttributesFlags, long crtime, long atime, @@ -87,14 +88,14 @@ public class JLNK { String netName, String netNameUnicode, String deviceName, String deviceNameUnicode) { this.header = header; - this.linkClassIdentifier = linkClassIdentifier; - this.linkFlags = new ArrayList(); + this.linkClassIdentifier = linkClassIdentifier.clone(); + this.linkFlags = new ArrayList<>(); for (LnkEnums.LinkFlags enumVal : LnkEnums.LinkFlags.values()) { if ((linkFlags & enumVal.getFlag()) == enumVal.getFlag()) { this.linkFlags.add(enumVal); } } - this.fileAttributesFlags = new ArrayList(); + this.fileAttributesFlags = new ArrayList<>(); for (LnkEnums.FileAttributesFlags enumVal : LnkEnums.FileAttributesFlags.values()) { if ((fileAttributesFlags & enumVal.getFlag()) == enumVal.getFlag()) { this.fileAttributesFlags.add(enumVal); @@ -121,7 +122,7 @@ public class JLNK { this.driveSerialNumber = driveSerialNumber; this.driveType = driveType; this.volumeLabel = volumeLabel; - this.commonNetworkRelativeListFlags = new ArrayList(); + this.commonNetworkRelativeListFlags = new ArrayList<>(); for (LnkEnums.CommonNetworkRelativeLinkFlags enumVal : LnkEnums.CommonNetworkRelativeLinkFlags.values()) { if ((commonNetworkRelativeListFlags & enumVal.getFlag()) == enumVal.getFlag()) { this.commonNetworkRelativeListFlags.add(enumVal); @@ -140,7 +141,7 @@ public class JLNK { } public List getCommonNetworkRelativeListFlags() { - return commonNetworkRelativeListFlags; + return Collections.unmodifiableList(commonNetworkRelativeListFlags); } public String getCommonPathSuffix() { @@ -176,7 +177,7 @@ public class JLNK { } public List getFileAttributesFlags() { - return fileAttributesFlags; + return Collections.unmodifiableList(fileAttributesFlags); } public int getFileSize() { @@ -196,7 +197,7 @@ public class JLNK { } public List getLinkTargetIdList() { - return linkTargetIdList; + return Collections.unmodifiableList(linkTargetIdList); } public int getIconIndex() { @@ -208,11 +209,11 @@ public class JLNK { } public byte[] getLinkClassIdentifier() { - return linkClassIdentifier; + return linkClassIdentifier.clone(); } public List getLinkFlags() { - return linkFlags; + return Collections.unmodifiableList(linkFlags); } public String getLocalBasePath() { diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/LnkEnums.java b/Core/src/org/sleuthkit/autopsy/coreutils/LnkEnums.java index dcad3b318c..c154f5ac8a 100644 --- a/Core/src/org/sleuthkit/autopsy/coreutils/LnkEnums.java +++ b/Core/src/org/sleuthkit/autopsy/coreutils/LnkEnums.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2012 Basis Technology Corp. + * Copyright 2012-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -31,6 +31,10 @@ class LnkEnums { private static final byte[] IEFRAME = new byte[]{(byte) 0x80, 0x53, 0x1c, (byte) 0x87, (byte) 0xa0, 0x42, 0x69, 0x10, (byte) 0xa2, (byte) 0xea, 0x08, 0x00, 0x2b, 0x30, 0x30, (byte) 0x9d}; + private LnkEnums() { + //private constructor for utility class + } + public enum CommonCLSIDS { CDrivesFolder(CDRIVES), @@ -38,20 +42,24 @@ class LnkEnums { IEFrameDLL(IEFRAME), Unknown(new byte[16]); - private byte[] flag; + private final byte[] flag; private CommonCLSIDS(byte[] flag) { - this.flag = flag; + this.flag = flag.clone(); } static CommonCLSIDS valueOf(byte[] type) { for (CommonCLSIDS value : CommonCLSIDS.values()) { - if (java.util.Arrays.equals(value.flag, type)) { + if (java.util.Arrays.equals(value.getFlag(), type)) { return value; } } return Unknown; } + + byte[] getFlag() { + return flag.clone(); + } } public enum LinkFlags { @@ -84,7 +92,7 @@ class LnkEnums { PreferEnvironmentPath(0x02000000), KeepLocalIDListForUNCTarget(0x04000000); - private int flag; + private final int flag; private LinkFlags(int flag) { this.flag = flag; @@ -105,7 +113,7 @@ class LnkEnums { DRIVE_CDROM(0x00000005), DRIVE_RAMDISK(0x00000006); - private int flag; + private final int flag; private DriveType(int flag) { this.flag = flag; @@ -117,7 +125,7 @@ class LnkEnums { static DriveType valueOf(int type) { for (DriveType value : DriveType.values()) { - if (value.flag == type) { + if (value.getFlag() == type) { return value; } } @@ -143,7 +151,7 @@ class LnkEnums { NOT_CONTENT_INDEXED(0x00002000), ENCRYPTED(0x00004000); - private int flag; + private final int flag; private FileAttributesFlags(int flag) { this.flag = flag; @@ -159,7 +167,7 @@ class LnkEnums { VolumeIDAndLocalBasePath(0x00000001), CommonNetworkRelativeLinkAndPathSuffix(0x00000002); - private int flag; + private final int flag; private LinkInfoFlags(int flag) { this.flag = flag; @@ -175,7 +183,7 @@ class LnkEnums { ValidDevice(0x00000001), ValidNetType(0x00000002); - private int flag; + private final int flag; private CommonNetworkRelativeLinkFlags(int flag) { this.flag = flag; @@ -231,7 +239,7 @@ class LnkEnums { WNNC_NET_GOOGLE(0x00430000), WNNC_NET_UNKNOWN(0x00000000); - private int flag; + private final int flag; private NetworkProviderType(int flag) { this.flag = flag; @@ -239,7 +247,7 @@ class LnkEnums { static NetworkProviderType valueOf(int type) { for (NetworkProviderType value : NetworkProviderType.values()) { - if (value.flag == type) { + if (value.getFlag() == type) { return value; } } @@ -250,4 +258,5 @@ class LnkEnums { return flag; } } + } diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/PhoneNumUtil.java b/Core/src/org/sleuthkit/autopsy/coreutils/PhoneNumUtil.java new file mode 100644 index 0000000000..1fa56d1bfd --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/coreutils/PhoneNumUtil.java @@ -0,0 +1,109 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.coreutils; + +import com.google.i18n.phonenumbers.NumberParseException; +import com.google.i18n.phonenumbers.PhoneNumberUtil; +import com.google.i18n.phonenumbers.Phonenumber; +import java.util.logging.Level; + +/** + * + * Class to format and get information from a phone number + */ +public final class PhoneNumUtil { + + private static final Logger logger = Logger.getLogger(PhoneNumUtil.class.getName()); + + private PhoneNumUtil() { + } + + /** + * Get the country code from a phone number + * + * @param phoneNumber + * @return country code if can determine otherwise it will return "" + */ + public static String getCountryCode(String phoneNumber) { + String regionCode = null; + try { + PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.getInstance(); + Phonenumber.PhoneNumber phoneNum = phoneNumberUtil.parse(phoneNumber, ""); + regionCode = phoneNumberUtil.getRegionCodeForNumber(phoneNum); + + if (regionCode == null) { + return ""; + } else { + return regionCode; + } + } catch (NumberParseException ex) { + logger.log(Level.WARNING, "Error getting country code, for phone number: {0}", phoneNumber); + return ""; + } + } + + /** + * Convert a phone number to the E164 format + * + * @param phoneNumber + * + * @return formated phone number if successful or original phone number if + * unsuccessful + */ + public static String convertToE164(String phoneNumber) { + PhoneNumberUtil phone_util = PhoneNumberUtil.getInstance(); + try { + Phonenumber.PhoneNumber phoneProto = phone_util.parse(phoneNumber, getCountryCode(phoneNumber)); + if (phone_util.isValidNumber(phoneProto)) { + return phone_util.format(phoneProto, PhoneNumberUtil.PhoneNumberFormat.E164); + } else { + logger.log(Level.WARNING, "Invalid phone number: {0}", phoneNumber); + return phoneNumber; + } + } catch (NumberParseException e) { + logger.log(Level.WARNING, "Error parsing phone number: {0}", phoneNumber); + return phoneNumber; + } + } + + /** + * Convert a phone number to the International format + * + * @param phoneNumber + * + * @return formated phone number if successful or original phone number if + * unsuccessful + */ + public static String convertToInternational(String phoneNumber) { + PhoneNumberUtil phone_util = PhoneNumberUtil.getInstance(); + try { + Phonenumber.PhoneNumber phoneProto = phone_util.parse(phoneNumber, getCountryCode(phoneNumber)); + if (phone_util.isValidNumber(phoneProto)) { + return phone_util.format(phoneProto, PhoneNumberUtil.PhoneNumberFormat.INTERNATIONAL); + } else { + logger.log(Level.WARNING, "Invalid phone number: {0}", phoneNumber); + return phoneNumber; + } + } catch (NumberParseException e) { + logger.log(Level.WARNING, "Error parsing phone number: {0}", phoneNumber); + return phoneNumber; + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/ArtifactStringContent.java b/Core/src/org/sleuthkit/autopsy/datamodel/ArtifactStringContent.java index 774d1f2f9e..e2950e5b12 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/ArtifactStringContent.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/ArtifactStringContent.java @@ -122,6 +122,7 @@ public class ArtifactStringContent implements StringContent { case LONG: case DOUBLE: case BYTE: + case JSON: default: value = attr.getDisplayString(); break; diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFileReader.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFileReader.java new file mode 100755 index 0000000000..ff854a16ea --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFileReader.java @@ -0,0 +1,258 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourceprocessors.xry; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.nio.charset.MalformedInputException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Optional; +import java.util.logging.Level; +import java.util.NoSuchElementException; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.apache.commons.io.FilenameUtils; + +/** + * Extracts XRY entities and determines the report type. An example of an XRY + * entity would be: + * + * Calls # 1 + * Call Type: Missed + * Time: 1/2/2019 1:23:45 PM (Device) + * From + * Tel: 12345678 + */ +public final class XRYFileReader implements AutoCloseable { + + private static final Logger logger = Logger.getLogger(XRYFileReader.class.getName()); + + //Assume UTF_16LE + private static final Charset CHARSET = StandardCharsets.UTF_16LE; + + //Assume TXT extension + private static final String EXTENSION = "txt"; + + //Assume 0xFFFE is the BOM + private static final int[] BOM = {0xFF, 0xFE}; + + //Assume all XRY reports have the type on the 3rd line. + private static final int LINE_WITH_REPORT_TYPE = 3; + + //Assume all headers are 5 lines in length. + private static final int HEADER_LENGTH_IN_LINES = 5; + + //Underlying reader for the xry file. + private final BufferedReader reader; + + private final StringBuilder xryEntity; + + /** + * Creates an XRYFileReader. As part of construction, the XRY file is opened + * and the reader is advanced past the header. This leaves the reader + * positioned at the start of the first XRY entity. + * + * The file is assumed to be encoded in UTF-16LE. + * + * @param xryFile XRY file to read. It is assumed that the caller has read + * access to the path. + * @throws IOException if an I/O error occurs. + */ + public XRYFileReader(Path xryFile) throws IOException { + reader = Files.newBufferedReader(xryFile, CHARSET); + + //Advance the reader to the start of the first XRY entity. + for (int i = 0; i < HEADER_LENGTH_IN_LINES; i++) { + reader.readLine(); + } + + xryEntity = new StringBuilder(); + } + + /** + * Advances the reader until a valid XRY entity is detected or EOF is + * reached. + * + * @return Indication that there is another XRY entity to consume or that + * the file has been exhausted. + * @throws IOException if an I/O error occurs. + */ + public boolean hasNextEntity() throws IOException { + //Entity has yet to be consumed. + if (xryEntity.length() > 0) { + return true; + } + + String line; + while ((line = reader.readLine()) != null) { + if (marksEndOfEntity(line)) { + if (xryEntity.length() > 0) { + //Found a non empty XRY entity. + return true; + } + } else { + xryEntity.append(line).append("\n"); + } + } + + //Check if EOF was hit before an entity delimiter was found. + return xryEntity.length() > 0; + } + + /** + * Returns an XRY entity if there is one, otherwise an exception is thrown. + * + * @return A non-empty XRY entity. + * @throws IOException if an I/O error occurs. + * @throws NoSuchElementException if there are no more XRY entities to + * consume. + */ + public String nextEntity() throws IOException { + if (hasNextEntity()) { + String returnVal = xryEntity.toString(); + xryEntity.setLength(0); + return returnVal; + } else { + throw new NoSuchElementException(); + } + } + + /** + * Closes any file handles this reader may have open. + * + * @throws IOException + */ + @Override + public void close() throws IOException { + reader.close(); + } + + /** + * Determines if the line encountered during file reading signifies the end + * of an XRY entity. + * + * @param line + * @return + */ + private boolean marksEndOfEntity(String line) { + return line.isEmpty(); + } + + /** + * Checks if the Path is an XRY file. In order to be an XRY file, it must + * have a txt extension, a 0xFFFE BOM (for UTF-16LE), and a non-empty report + * type. The encoding is not verified any further than checking the BOM. To + * get the report type, the file is read with a UTF-16LE decoder. If a + * failure directly related to the decoding is encountered, it is logged and + * the file is assumed not to be an XRY file. A direct consequence is that + * there may be false positives. + * + * All other I/O exceptions are propagated up. If the Path represents a + * symbolic link, this function will not follow it. + * + * @param file Path to test. It is assumed that the caller has read access + * to the file. + * @return Indicates whether the Path is a XRY file. + * + * @throws IOException if an I/O error occurs + */ + public static boolean isXRYFile(Path file) throws IOException { + String parsedExtension = FilenameUtils.getExtension(file.toString()); + + //A XRY file should have a txt extension. + if (!EXTENSION.equals(parsedExtension)) { + return false; + } + + BasicFileAttributes attr = Files.readAttributes(file, + BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + + //Do not follow symbolic links. XRY files cannot be a directory. + if (attr.isSymbolicLink() || attr.isDirectory()) { + return false; + } + + //Check 0xFFFE BOM + if (!isXRYBOM(file)) { + return false; + } + + try { + Optional reportType = getType(file); + //All valid XRY reports should have a type. + return reportType.isPresent(); + } catch (MalformedInputException ex) { + logger.log(Level.WARNING, String.format("File at path [%s] had " + + "0xFFFE BOM but was not encoded in UTF-16LE.", file.toString()), ex); + return false; + } + } + + /** + * Checks the leading bytes of the Path to verify they match the expected + * 0xFFFE BOM. + * + * @param file Path to check. It is assumed that the caller has read access + * to the file. + * + * @return Indication if the leading bytes match. + * @throws IOException if an I/O error occurs. + */ + private static boolean isXRYBOM(Path file) throws IOException { + try (InputStream in = Files.newInputStream(file, StandardOpenOption.READ)) { + for (int bomByte : BOM) { + if (in.read() != bomByte) { + return false; + } + } + } + + return true; + } + + /** + * Reads the report type from the Path. It is assumed that the Path will + * have a UTF-16LE encoding. A MalformedInputException will be thrown if + * there is a decoding error. + * + * @param file + * @return + * @throws IOException + */ + private static Optional getType(Path file) throws IOException { + try (BufferedReader reader = Files.newBufferedReader(file, CHARSET)) { + //Advance the reader to the line before the report type. + for (int i = 0; i < LINE_WITH_REPORT_TYPE - 1; i++) { + reader.readLine(); + } + + String reportTypeLine = reader.readLine(); + if (reportTypeLine != null && !reportTypeLine.isEmpty()) { + return Optional.of(reportTypeLine); + } + return Optional.empty(); + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFolder.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFolder.java new file mode 100755 index 0000000000..b9b999f270 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFolder.java @@ -0,0 +1,77 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourceprocessors.xry; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Iterator; +import java.util.stream.Stream; + +/** + * Extracts XRY files and (optionally) non-XRY files from a XRY (Report) folder. + */ +public final class XRYFolder { + + //Depth that will contain XRY files. All XRY files will be immediate + //children of their parent folder. + private static final int XRY_FILES_DEPTH = 1; + + /** + * Searches for XRY files at the top level of a given folder. If at least + * one file matches, the entire directory is assumed to be an XRY report. + * + * This function will not follow any symbolic links, the directory is tested + * as is. + * + * @param folder Path to test. Assumes that caller has read access to the + * folder and all of the top level files. + * @return Indicates whether the Path is an XRY report. + * + * @throws IOException Error occurred during File I/O. + * @throws SecurityException If the security manager denies access any of + * the files. + */ + public static boolean isXRYFolder(Path folder) throws IOException { + BasicFileAttributes attr = Files.readAttributes(folder, + BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + + if (!attr.isDirectory()) { + return false; + } + + //Files.walk by default will not follow symbolic links. + try (Stream allFiles = Files.walk(folder, XRY_FILES_DEPTH)) { + Iterator allFilesIterator = allFiles.iterator(); + while (allFilesIterator.hasNext()) { + Path currentFile = allFilesIterator.next(); + if (XRYFileReader.isXRYFile(currentFile)) { + return true; + } + } + + return false; + } catch (UncheckedIOException ex) { + throw ex.getCause(); + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/Bundle.properties b/Core/src/org/sleuthkit/autopsy/directorytree/Bundle.properties index 4badcfe9d8..100b2fd67b 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/directorytree/Bundle.properties @@ -65,14 +65,6 @@ ExplorerNodeActionVisitor.volDetail.noVolMatchErr=Error: No Volume Matches. ExplorerNodeActionVisitor.imgDetail.noVolMatchesErr=Error: No Volume Matches. ExplorerNodeActionVisitor.exception.probGetParent.text=Problem getting parent from {0}: {1} ExtractAction.title.extractFiles.text=Extract File(s) -ExtractAction.extractFiles.cantCreateFolderErr.msg=Could not create selected folder. -ExtractAction.confDlg.destFileExist.msg=Destination file {0} already exists, overwrite? -ExtractAction.confDlg.destFileExist.title=File Exists -ExtractAction.msgDlg.cantOverwriteFile.msg=Could not overwrite existing file {0} -ExtractAction.notifyDlg.noFileToExtr.msg=No file(s) to extract. -ExtractAction.progress.extracting=Extracting -ExtractAction.progress.cancellingExtraction={0} (Cancelling...) -ExtractAction.done.notifyMsg.fileExtr.text=File(s) extracted. ExtractUnallocAction.notifyMsg.unallocAlreadyBeingExtr.msg=Unallocated Space is already being extracted on this Image. Please select a different Image. ExtractUnallocAction.msgDlg.folderDoesntExist.msg=Folder does not exist. Please choose a valid folder before continuing ExtractUnallocAction.dlgTitle.selectDirToSaveTo.msg=Select directory to save to diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/directorytree/Bundle.properties-MERGED index f70a374bc5..63b416b176 100755 --- a/Core/src/org/sleuthkit/autopsy/directorytree/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/directorytree/Bundle.properties-MERGED @@ -25,7 +25,6 @@ ExternalViewerAction.actionPerformed.failure.support.message=This platform (oper ExternalViewerAction.actionPerformed.failure.title=Open File Failure {0} ExternalViewerAction.actionPerformed.urlFailure.title=Open URL Failure ExternalViewerShortcutAction.title.text=Open in External Viewer Ctrl+E -ExtractAction.noOpenCase.errMsg=No open case available. ExtractUnallocAction.imageError=Error extracting unallocated space from image ExtractUnallocAction.noFiles=No unallocated files found on volume ExtractUnallocAction.noOpenCase.errMsg=No open case available. @@ -107,14 +106,6 @@ ExplorerNodeActionVisitor.volDetail.noVolMatchErr=Error: No Volume Matches. ExplorerNodeActionVisitor.imgDetail.noVolMatchesErr=Error: No Volume Matches. ExplorerNodeActionVisitor.exception.probGetParent.text=Problem getting parent from {0}: {1} ExtractAction.title.extractFiles.text=Extract File(s) -ExtractAction.extractFiles.cantCreateFolderErr.msg=Could not create selected folder. -ExtractAction.confDlg.destFileExist.msg=Destination file {0} already exists, overwrite? -ExtractAction.confDlg.destFileExist.title=File Exists -ExtractAction.msgDlg.cantOverwriteFile.msg=Could not overwrite existing file {0} -ExtractAction.notifyDlg.noFileToExtr.msg=No file(s) to extract. -ExtractAction.progress.extracting=Extracting -ExtractAction.progress.cancellingExtraction={0} (Cancelling...) -ExtractAction.done.notifyMsg.fileExtr.text=File(s) extracted. ExtractUnallocAction.notifyMsg.unallocAlreadyBeingExtr.msg=Unallocated Space is already being extracted on this Image. Please select a different Image. ExtractUnallocAction.msgDlg.folderDoesntExist.msg=Folder does not exist. Please choose a valid folder before continuing ExtractUnallocAction.dlgTitle.selectDirToSaveTo.msg=Select directory to save to diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/ExtractAction.java b/Core/src/org/sleuthkit/autopsy/directorytree/ExtractAction.java index 46c2161838..0efe3bdcfb 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/ExtractAction.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/ExtractAction.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-2018 Basis Technology Corp. + * Copyright 2013-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,32 +18,13 @@ */ package org.sleuthkit.autopsy.directorytree; -import java.awt.Component; +import org.sleuthkit.autopsy.directorytree.actionhelpers.ExtractActionHelper; import java.awt.event.ActionEvent; -import java.io.File; -import java.util.ArrayList; import java.util.Collection; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Set; -import java.util.concurrent.ExecutionException; -import java.util.logging.Level; import javax.swing.AbstractAction; -import javax.swing.JFileChooser; -import javax.swing.JOptionPane; -import javax.swing.SwingWorker; -import org.netbeans.api.progress.ProgressHandle; -import org.openide.util.Cancellable; import org.openide.util.NbBundle; import org.openide.util.Utilities; -import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; -import org.sleuthkit.autopsy.coreutils.FileUtil; -import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; -import org.sleuthkit.autopsy.datamodel.ContentUtils; -import org.sleuthkit.autopsy.datamodel.ContentUtils.ExtractFscContentVisitor; +import org.openide.util.Lookup; import org.sleuthkit.datamodel.AbstractFile; /** @@ -51,10 +32,6 @@ import org.sleuthkit.datamodel.AbstractFile; */ public final class ExtractAction extends AbstractAction { - private Logger logger = Logger.getLogger(ExtractAction.class.getName()); - - private String userDefinedExportPath; - // This class is a singleton to support multi-selection of nodes, since // org.openide.nodes.NodeOp.findActions(Node[] nodes) will only pick up an Action if every // node in the array returns a reference to the same action object from Node.getActions(boolean). @@ -82,300 +59,10 @@ public final class ExtractAction extends AbstractAction { */ @Override public void actionPerformed(ActionEvent e) { - Collection selectedFiles = Utilities.actionsGlobalContext().lookupAll(AbstractFile.class); - if (selectedFiles.size() > 1) { - extractFiles(e, selectedFiles); - } else if (selectedFiles.size() == 1) { - AbstractFile source = selectedFiles.iterator().next(); - if (source.isDir()) { - extractFiles(e, selectedFiles); - } else { - extractFile(e, selectedFiles.iterator().next()); - } - } - } + Lookup lookup = Utilities.actionsGlobalContext(); + Collection selectedFiles =lookup.lookupAll(AbstractFile.class); + ExtractActionHelper extractor = new ExtractActionHelper(); + extractor.extract(e, selectedFiles); - /** - * Called when user has selected a single file to extract - * - * @param event - * @param selectedFile Selected file - */ - @NbBundle.Messages({"ExtractAction.noOpenCase.errMsg=No open case available."}) - private void extractFile(ActionEvent event, AbstractFile selectedFile) { - Case openCase; - try { - openCase = Case.getCurrentCaseThrows(); - } catch (NoCurrentCaseException ex) { - JOptionPane.showMessageDialog((Component) event.getSource(), Bundle.ExtractAction_noOpenCase_errMsg()); - logger.log(Level.INFO, "Exception while getting open case.", ex); //NON-NLS - return; - } - JFileChooser fileChooser = new JFileChooser(); - fileChooser.setCurrentDirectory(new File(getExportDirectory(openCase))); - // If there is an attribute name, change the ":". Otherwise the extracted file will be hidden - fileChooser.setSelectedFile(new File(FileUtil.escapeFileName(selectedFile.getName()))); - if (fileChooser.showSaveDialog((Component) event.getSource()) == JFileChooser.APPROVE_OPTION) { - updateExportDirectory(fileChooser.getSelectedFile().getParent(), openCase); - - ArrayList fileExtractionTasks = new ArrayList<>(); - fileExtractionTasks.add(new FileExtractionTask(selectedFile, fileChooser.getSelectedFile())); - runExtractionTasks(event, fileExtractionTasks); - } - } - - /** - * Called when a user has selected multiple files to extract - * - * @param event - * @param selectedFiles Selected files - */ - private void extractFiles(ActionEvent event, Collection selectedFiles) { - Case openCase; - try { - openCase = Case.getCurrentCaseThrows(); - } catch (NoCurrentCaseException ex) { - JOptionPane.showMessageDialog((Component) event.getSource(), Bundle.ExtractAction_noOpenCase_errMsg()); - logger.log(Level.INFO, "Exception while getting open case.", ex); //NON-NLS - return; - } - JFileChooser folderChooser = new JFileChooser(); - folderChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); - folderChooser.setCurrentDirectory(new File(getExportDirectory(openCase))); - if (folderChooser.showSaveDialog((Component) event.getSource()) == JFileChooser.APPROVE_OPTION) { - File destinationFolder = folderChooser.getSelectedFile(); - if (!destinationFolder.exists()) { - try { - destinationFolder.mkdirs(); - } catch (Exception ex) { - JOptionPane.showMessageDialog((Component) event.getSource(), NbBundle.getMessage(this.getClass(), - "ExtractAction.extractFiles.cantCreateFolderErr.msg")); - logger.log(Level.INFO, "Unable to create folder(s) for user " + destinationFolder.getAbsolutePath(), ex); //NON-NLS - return; - } - } - updateExportDirectory(destinationFolder.getPath(), openCase); - - /* - * get the unique set of files from the list. A user once reported - * extraction taking days because it was extracting the same PST - * file 20k times. They selected 20k email messages in the tree and - * chose to extract them. - */ - Set uniqueFiles = new HashSet<>(selectedFiles); - - // make a task for each file - ArrayList fileExtractionTasks = new ArrayList<>(); - for (AbstractFile source : uniqueFiles) { - // If there is an attribute name, change the ":". Otherwise the extracted file will be hidden - fileExtractionTasks.add(new FileExtractionTask(source, new File(destinationFolder, source.getId() + "-" + FileUtil.escapeFileName(source.getName())))); - } - runExtractionTasks(event, fileExtractionTasks); - } - } - - /** - * Get the export directory path. - * - * @param openCase The current case. - * - * @return The export directory path. - */ - private String getExportDirectory(Case openCase) { - String caseExportPath = openCase.getExportDirectory(); - - if (userDefinedExportPath == null) { - return caseExportPath; - } - - File file = new File(userDefinedExportPath); - if (file.exists() == false || file.isDirectory() == false) { - return caseExportPath; - } - - return userDefinedExportPath; - } - - /** - * Update the default export directory. If the directory path matches the - * case export directory, then the directory used will always match the - * export directory of any given case. Otherwise, the path last used will be - * saved. - * - * @param exportPath The export path. - * @param openCase The current case. - */ - private void updateExportDirectory(String exportPath, Case openCase) { - if (exportPath.equalsIgnoreCase(openCase.getExportDirectory())) { - userDefinedExportPath = null; - } else { - userDefinedExportPath = exportPath; - } - } - - /** - * Execute a series of file extraction tasks. - * - * @param event ActionEvent whose source will be used for - * centering popup dialogs. - * @param fileExtractionTasks List of file extraction tasks. - */ - private void runExtractionTasks(ActionEvent event, List fileExtractionTasks) { - - // verify all of the sources and destinations are OK - for (Iterator it = fileExtractionTasks.iterator(); it.hasNext();) { - FileExtractionTask task = it.next(); - - if (ContentUtils.isDotDirectory(task.source)) { - //JOptionPane.showMessageDialog((Component) e.getSource(), "Cannot extract virtual " + task.source.getName() + " directory.", "File is Virtual Directory", JOptionPane.WARNING_MESSAGE); - it.remove(); - continue; - } - - /* - * This code assumes that each destination is unique. We previously - * satisfied that by adding the unique ID. - */ - if (task.destination.exists()) { - if (JOptionPane.showConfirmDialog((Component) event.getSource(), - NbBundle.getMessage(this.getClass(), "ExtractAction.confDlg.destFileExist.msg", task.destination.getAbsolutePath()), - NbBundle.getMessage(this.getClass(), "ExtractAction.confDlg.destFileExist.title"), - JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) { - if (!FileUtil.deleteFileDir(task.destination)) { - JOptionPane.showMessageDialog((Component) event.getSource(), - NbBundle.getMessage(this.getClass(), "ExtractAction.msgDlg.cantOverwriteFile.msg", task.destination.getAbsolutePath())); - it.remove(); - } - } else { - it.remove(); - } - } - } - - // launch a thread to do the work - if (!fileExtractionTasks.isEmpty()) { - try { - FileExtracter extracter = new FileExtracter(fileExtractionTasks); - extracter.execute(); - } catch (Exception ex) { - logger.log(Level.WARNING, "Unable to start background file extraction thread", ex); //NON-NLS - } - } else { - MessageNotifyUtil.Message.info( - NbBundle.getMessage(this.getClass(), "ExtractAction.notifyDlg.noFileToExtr.msg")); - } - } - - /** - * Stores source and destination for file extraction. - */ - private class FileExtractionTask { - - AbstractFile source; - File destination; - - /** - * Create an instance of the FileExtractionTask. - * - * @param source The file to be extracted. - * @param destination The destination for the extraction. - */ - FileExtractionTask(AbstractFile source, File destination) { - this.source = source; - this.destination = destination; - } - } - - /** - * Thread that does the actual extraction work - */ - private class FileExtracter extends SwingWorker { - - private final Logger logger = Logger.getLogger(FileExtracter.class.getName()); - private ProgressHandle progress; - private final List extractionTasks; - - /** - * Create an instance of the FileExtracter. - * - * @param extractionTasks List of file extraction tasks. - */ - FileExtracter(List extractionTasks) { - this.extractionTasks = extractionTasks; - } - - @Override - protected Object doInBackground() throws Exception { - if (extractionTasks.isEmpty()) { - return null; - } - - // Setup progress bar. - final String displayName = NbBundle.getMessage(this.getClass(), "ExtractAction.progress.extracting"); - progress = ProgressHandle.createHandle(displayName, new Cancellable() { - @Override - public boolean cancel() { - if (progress != null) { - progress.setDisplayName( - NbBundle.getMessage(this.getClass(), "ExtractAction.progress.cancellingExtraction", displayName)); - } - return ExtractAction.FileExtracter.this.cancel(true); - } - }); - progress.start(); - progress.switchToIndeterminate(); - - /* - * @@@ Add back in -> Causes exceptions int workUnits = 0; for - * (FileExtractionTask task : extractionTasks) { workUnits += - * calculateProgressBarWorkUnits(task.source); } - * progress.switchToDeterminate(workUnits); - */ - // Do the extraction tasks. - for (FileExtractionTask task : this.extractionTasks) { - // @@@ Note, we are no longer passing in progress - ExtractFscContentVisitor.extract(task.source, task.destination, null, this); - } - - return null; - } - - @Override - protected void done() { - boolean msgDisplayed = false; - try { - super.get(); - } catch (InterruptedException | ExecutionException ex) { - logger.log(Level.SEVERE, "Fatal error during file extraction", ex); //NON-NLS - MessageNotifyUtil.Message.info( - NbBundle.getMessage(this.getClass(), "ExtractAction.done.notifyMsg.extractErr", ex.getMessage())); - msgDisplayed = true; - } finally { - progress.finish(); - if (!this.isCancelled() && !msgDisplayed) { - MessageNotifyUtil.Message.info( - NbBundle.getMessage(this.getClass(), "ExtractAction.done.notifyMsg.fileExtr.text")); - } - } - } - - /** - * Calculate the number of work units for the progress bar. - * - * @param file File whose children will be reviewed to get the number of - * work units. - * - * @return The number of work units. - */ - /* - * private int calculateProgressBarWorkUnits(AbstractFile file) { int - * workUnits = 0; if (file.isFile()) { workUnits += file.getSize(); } - * else { try { for (Content child : file.getChildren()) { if (child - * instanceof AbstractFile) { workUnits += - * calculateProgressBarWorkUnits((AbstractFile) child); } } } catch - * (TskCoreException ex) { logger.log(Level.SEVERE, "Could not get - * children of content", ex); //NON-NLS } } return workUnits; - } - */ } } diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/ExtractUnallocAction.java b/Core/src/org/sleuthkit/autopsy/directorytree/ExtractUnallocAction.java index b2a3a93abf..58afb28fbe 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/ExtractUnallocAction.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/ExtractUnallocAction.java @@ -138,7 +138,7 @@ final class ExtractUnallocAction extends AbstractAction { try { openCase = Case.getCurrentCaseThrows(); } catch (NoCurrentCaseException ex) { - MessageNotifyUtil.Message.info(Bundle.ExtractAction_noOpenCase_errMsg()); + MessageNotifyUtil.Message.info(Bundle.ExtractUnallocAction_noOpenCase_errMsg()); return; } List copyList = new ArrayList() { diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/actionhelpers/Bundle.properties b/Core/src/org/sleuthkit/autopsy/directorytree/actionhelpers/Bundle.properties new file mode 100644 index 0000000000..310cf0a475 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/directorytree/actionhelpers/Bundle.properties @@ -0,0 +1,8 @@ +ExtractActionHelper.extractFiles.cantCreateFolderErr.msg=Could not create selected folder. +ExtractActionHelper.confDlg.destFileExist.msg=Destination file {0} already exists, overwrite? +ExtractActionHelper.confDlg.destFileExist.title=File Exists +ExtractActionHelper.msgDlg.cantOverwriteFile.msg=Could not overwrite existing file {0} +ExtractActionHelper.notifyDlg.noFileToExtr.msg=No file(s) to extract. +ExtractActionHelper.progress.extracting=Extracting +ExtractActionHelper.progress.cancellingExtraction={0} (Cancelling...) +ExtractActionHelper.done.notifyMsg.fileExtr.text=File(s) extracted. diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/actionhelpers/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/directorytree/actionhelpers/Bundle.properties-MERGED new file mode 100644 index 0000000000..f86fa5612d --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/directorytree/actionhelpers/Bundle.properties-MERGED @@ -0,0 +1,9 @@ +ExtractActionHelper.extractFiles.cantCreateFolderErr.msg=Could not create selected folder. +ExtractActionHelper.confDlg.destFileExist.msg=Destination file {0} already exists, overwrite? +ExtractActionHelper.confDlg.destFileExist.title=File Exists +ExtractActionHelper.msgDlg.cantOverwriteFile.msg=Could not overwrite existing file {0} +ExtractActionHelper.noOpenCase.errMsg=No open case available. +ExtractActionHelper.notifyDlg.noFileToExtr.msg=No file(s) to extract. +ExtractActionHelper.progress.extracting=Extracting +ExtractActionHelper.progress.cancellingExtraction={0} (Cancelling...) +ExtractActionHelper.done.notifyMsg.fileExtr.text=File(s) extracted. diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/actionhelpers/ExtractActionHelper.java b/Core/src/org/sleuthkit/autopsy/directorytree/actionhelpers/ExtractActionHelper.java new file mode 100644 index 0000000000..339a58142d --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/directorytree/actionhelpers/ExtractActionHelper.java @@ -0,0 +1,356 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013-2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this content except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.directorytree.actionhelpers; + +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import javax.swing.JFileChooser; +import javax.swing.JOptionPane; +import javax.swing.SwingWorker; +import org.netbeans.api.progress.ProgressHandle; +import org.openide.util.Cancellable; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.coreutils.FileUtil; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import org.sleuthkit.autopsy.datamodel.ContentUtils; +import org.sleuthkit.datamodel.AbstractFile; + +/** + * Helper class for methods needed by actions which extract files. + */ +public class ExtractActionHelper { + + private final Logger logger = Logger.getLogger(ExtractActionHelper.class.getName()); + private String userDefinedExportPath; + + /** + * Extract the specified collection of files with an event specified for + * context. + * + * @param event The event that caused the extract method to be + * called. + * @param selectedFiles The files to be extracted from the current case. + */ + public void extract(ActionEvent event, Collection selectedFiles) { + if (selectedFiles.size() > 1) { + extractFiles(event, selectedFiles); + } else if (selectedFiles.size() == 1) { + AbstractFile source = selectedFiles.iterator().next(); + if (source.isDir()) { + extractFiles(event, selectedFiles); + } else { + extractFile(event, selectedFiles.iterator().next()); + } + } + } + + /** + * Called when user has selected a single file to extract + * + * @param event + * @param selectedFile Selected file + */ + @NbBundle.Messages({"ExtractActionHelper.noOpenCase.errMsg=No open case available."}) + private void extractFile(ActionEvent event, AbstractFile selectedFile) { + Case openCase; + try { + openCase = Case.getCurrentCaseThrows(); + } catch (NoCurrentCaseException ex) { + JOptionPane.showMessageDialog((Component) event.getSource(), Bundle.ExtractActionHelper_noOpenCase_errMsg()); + logger.log(Level.INFO, "Exception while getting open case.", ex); //NON-NLS + return; + } + JFileChooser fileChooser = new JFileChooser(); + fileChooser.setCurrentDirectory(new File(getExportDirectory(openCase))); + // If there is an attribute name, change the ":". Otherwise the extracted file will be hidden + fileChooser.setSelectedFile(new File(FileUtil.escapeFileName(selectedFile.getName()))); + if (fileChooser.showSaveDialog((Component) event.getSource()) == JFileChooser.APPROVE_OPTION) { + updateExportDirectory(fileChooser.getSelectedFile().getParent(), openCase); + + ArrayList fileExtractionTasks = new ArrayList<>(); + fileExtractionTasks.add(new FileExtractionTask(selectedFile, fileChooser.getSelectedFile())); + runExtractionTasks(event, fileExtractionTasks); + } + } + + /** + * Called when a user has selected multiple files to extract + * + * @param event + * @param selectedFiles Selected files + */ + private void extractFiles(ActionEvent event, Collection selectedFiles) { + Case openCase; + try { + openCase = Case.getCurrentCaseThrows(); + } catch (NoCurrentCaseException ex) { + JOptionPane.showMessageDialog((Component) event.getSource(), Bundle.ExtractActionHelper_noOpenCase_errMsg()); + logger.log(Level.INFO, "Exception while getting open case.", ex); //NON-NLS + return; + } + JFileChooser folderChooser = new JFileChooser(); + folderChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + folderChooser.setCurrentDirectory(new File(getExportDirectory(openCase))); + if (folderChooser.showSaveDialog((Component) event.getSource()) == JFileChooser.APPROVE_OPTION) { + File destinationFolder = folderChooser.getSelectedFile(); + if (!destinationFolder.exists()) { + try { + destinationFolder.mkdirs(); + } catch (Exception ex) { + JOptionPane.showMessageDialog((Component) event.getSource(), NbBundle.getMessage(this.getClass(), + "ExtractAction.extractFiles.cantCreateFolderErr.msg")); + logger.log(Level.INFO, "Unable to create folder(s) for user " + destinationFolder.getAbsolutePath(), ex); //NON-NLS + return; + } + } + updateExportDirectory(destinationFolder.getPath(), openCase); + + /* + * get the unique set of files from the list. A user once reported + * extraction taking days because it was extracting the same PST + * file 20k times. They selected 20k email messages in the tree and + * chose to extract them. + */ + Set uniqueFiles = new HashSet<>(selectedFiles); + + // make a task for each file + ArrayList fileExtractionTasks = new ArrayList<>(); + for (AbstractFile source : uniqueFiles) { + // If there is an attribute name, change the ":". Otherwise the extracted file will be hidden + fileExtractionTasks.add(new FileExtractionTask(source, new File(destinationFolder, source.getId() + "-" + FileUtil.escapeFileName(source.getName())))); + } + runExtractionTasks(event, fileExtractionTasks); + } + } + + /** + * Get the export directory path. + * + * @param openCase The current case. + * + * @return The export directory path. + */ + private String getExportDirectory(Case openCase) { + String caseExportPath = openCase.getExportDirectory(); + + if (userDefinedExportPath == null) { + return caseExportPath; + } + + File file = new File(userDefinedExportPath); + if (file.exists() == false || file.isDirectory() == false) { + return caseExportPath; + } + + return userDefinedExportPath; + } + + /** + * Update the default export directory. If the directory path matches the + * case export directory, then the directory used will always match the + * export directory of any given case. Otherwise, the path last used will be + * saved. + * + * @param exportPath The export path. + * @param openCase The current case. + */ + private void updateExportDirectory(String exportPath, Case openCase) { + if (exportPath.equalsIgnoreCase(openCase.getExportDirectory())) { + userDefinedExportPath = null; + } else { + userDefinedExportPath = exportPath; + } + } + + /** + * Execute a series of file extraction tasks. + * + * @param event ActionEvent whose source will be used for + * centering popup dialogs. + * @param fileExtractionTasks List of file extraction tasks. + */ + private void runExtractionTasks(ActionEvent event, List fileExtractionTasks) { + + // verify all of the sources and destinations are OK + for (Iterator it = fileExtractionTasks.iterator(); it.hasNext();) { + FileExtractionTask task = it.next(); + + if (ContentUtils.isDotDirectory(task.source)) { + it.remove(); + continue; + } + + /* + * This code assumes that each destination is unique. We previously + * satisfied that by adding the unique ID. + */ + if (task.destination.exists()) { + if (JOptionPane.showConfirmDialog((Component) event.getSource(), + NbBundle.getMessage(this.getClass(), "ExtractActionHelper.confDlg.destFileExist.msg", task.destination.getAbsolutePath()), + NbBundle.getMessage(this.getClass(), "ExtractActionHelper.confDlg.destFileExist.title"), + JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) { + if (!FileUtil.deleteFileDir(task.destination)) { + JOptionPane.showMessageDialog((Component) event.getSource(), + NbBundle.getMessage(this.getClass(), "ExtractActionHelper.msgDlg.cantOverwriteFile.msg", task.destination.getAbsolutePath())); + it.remove(); + } + } else { + it.remove(); + } + } + } + + // launch a thread to do the work + if (!fileExtractionTasks.isEmpty()) { + try { + FileExtracter extracter = new FileExtracter(fileExtractionTasks); + extracter.execute(); + } catch (Exception ex) { + logger.log(Level.WARNING, "Unable to start background file extraction thread", ex); //NON-NLS + } + } else { + MessageNotifyUtil.Message.info( + NbBundle.getMessage(this.getClass(), "ExtractActionHelper.notifyDlg.noFileToExtr.msg")); + } + } + + /** + * Stores source and destination for file extraction. + */ + private class FileExtractionTask { + + AbstractFile source; + File destination; + + /** + * Create an instance of the FileExtractionTask. + * + * @param source The file to be extracted. + * @param destination The destination for the extraction. + */ + FileExtractionTask(AbstractFile source, File destination) { + this.source = source; + this.destination = destination; + } + } + + /** + * Thread that does the actual extraction work + */ + private class FileExtracter extends SwingWorker { + + private final Logger logger = Logger.getLogger(FileExtracter.class.getName()); + private ProgressHandle progress; + private final List extractionTasks; + + /** + * Create an instance of the FileExtracter. + * + * @param extractionTasks List of file extraction tasks. + */ + FileExtracter(List extractionTasks) { + this.extractionTasks = extractionTasks; + } + + @Override + protected Object doInBackground() throws Exception { + if (extractionTasks.isEmpty()) { + return null; + } + + // Setup progress bar. + final String displayName = NbBundle.getMessage(this.getClass(), "ExtractActionHelper.progress.extracting"); + progress = ProgressHandle.createHandle(displayName, new Cancellable() { + @Override + public boolean cancel() { + if (progress != null) { + progress.setDisplayName( + NbBundle.getMessage(this.getClass(), "ExtractActionHelper.progress.cancellingExtraction", displayName)); + } + return ExtractActionHelper.FileExtracter.this.cancel(true); + } + }); + progress.start(); + progress.switchToIndeterminate(); + + /* + * @@@ Add back in -> Causes exceptions int workUnits = 0; for + * (FileExtractionTask task : extractionTasks) { workUnits += + * calculateProgressBarWorkUnits(task.source); } + * progress.switchToDeterminate(workUnits); + */ + // Do the extraction tasks. + for (FileExtractionTask task : this.extractionTasks) { + // @@@ Note, we are no longer passing in progress + ContentUtils.ExtractFscContentVisitor.extract(task.source, task.destination, null, this); + } + + return null; + } + + @Override + protected void done() { + boolean msgDisplayed = false; + try { + super.get(); + } catch (InterruptedException | ExecutionException ex) { + logger.log(Level.SEVERE, "Fatal error during file extraction", ex); //NON-NLS + MessageNotifyUtil.Message.info( + NbBundle.getMessage(this.getClass(), "ExtractActionHelper.done.notifyMsg.extractErr", ex.getMessage())); + msgDisplayed = true; + } finally { + progress.finish(); + if (!this.isCancelled() && !msgDisplayed) { + MessageNotifyUtil.Message.info( + NbBundle.getMessage(this.getClass(), "ExtractActionHelper.done.notifyMsg.fileExtr.text")); + } + } + } + + /** + * Calculate the number of work units for the progress bar. + * + * @param file File whose children will be reviewed to get the number of + * work units. + * + * @return The number of work units. + */ + /* + * private int calculateProgressBarWorkUnits(AbstractFile file) { int + * workUnits = 0; if (file.isFile()) { workUnits += file.getSize(); } + * else { try { for (Content child : file.getChildren()) { if (child + * instanceof AbstractFile) { workUnits += + * calculateProgressBarWorkUnits((AbstractFile) child); } } } catch + * (TskCoreException ex) { logger.log(Level.SEVERE, "Could not get + * children of content", ex); //NON-NLS } } return workUnits; } + */ + } +} diff --git a/Core/src/org/sleuthkit/autopsy/exceptions/AutopsyException.java b/Core/src/org/sleuthkit/autopsy/exceptions/AutopsyException.java new file mode 100644 index 0000000000..1c91c6d773 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/exceptions/AutopsyException.java @@ -0,0 +1,88 @@ +/* + * Autopsy + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.exceptions; + +/** + * An exception to be thrown which can contain a user-friendly message. + */ +public abstract class AutopsyException extends Exception { + + private static final long serialVersionUID = 1L; + + private final String userMessage; + + /** + * Constructs an AutopsyException with identical exception and user-friendly + * messages. + * + * @param message Exception message. + */ + public AutopsyException(String message) { + super(message); + this.userMessage = message; + } + + /** + * Constructs an AutopsyException with an exception message and user-friendly message. + * + * @param message Exception message. + * @param userMessage The user-friendly message to include in this + * exception. + */ + public AutopsyException(String message, String userMessage) { + super(message); + this.userMessage = userMessage; + } + + /** + * Constructs an AutopsyException with identical exception and user-friendly + * messages. + * + * @param message Exception message. + * @param cause Exception cause. + */ + public AutopsyException(String message, Throwable cause) { + super(message, cause); + this.userMessage = message; + } + + /** + * Constructs an AutopsyException with an exception message, a user-friendly messages, and a cause. + * + * @param message Exception message. + * @param userMessage The user-friendly message to include in this + * exception. + * @param cause Exception cause. + */ + public AutopsyException(String message, String userMessage, Throwable cause) { + super(message, cause); + this.userMessage = userMessage; + } + + /** + * Get the user-friendly message if one exists. + * + * @return The user-friendly message if one was explicitly set, otherwise + * returns the exception message. + */ + public String getUserMessage() { + return userMessage; + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/Bundle.properties b/Core/src/org/sleuthkit/autopsy/geolocation/Bundle.properties new file mode 100755 index 0000000000..7a22608a3b --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/Bundle.properties @@ -0,0 +1,6 @@ +CTL_OpenGeolocation=Geolocation +CTL_GeolocationTopComponentAction=GeolocationTopComponent +CTL_GeolocationTopComponent=Geolocation +RefreshPanel.refreshLabel.text=The geolocation data has been updated, the visualization may be out of date. +RefreshPanel.refreshButton.text=Refresh View +RefreshPanel.closeButton.text= diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/geolocation/Bundle.properties-MERGED new file mode 100755 index 0000000000..f925437b0b --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/Bundle.properties-MERGED @@ -0,0 +1,10 @@ +CTL_OpenGeolocation=Geolocation +CTL_GeolocationTopComponentAction=GeolocationTopComponent +CTL_GeolocationTopComponent=Geolocation +GLTopComponent_initilzation_error=An error occurred during waypoint initilization. Geolocation data maybe incomplete. +GLTopComponent_name=Geolocation +OpenGeolocationAction_displayName=Geolocation +OpenGeolocationAction_name=Geolocation +RefreshPanel.refreshLabel.text=The geolocation data has been updated, the visualization may be out of date. +RefreshPanel.refreshButton.text=Refresh View +RefreshPanel.closeButton.text= diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/GeolocationTopComponent.form b/Core/src/org/sleuthkit/autopsy/geolocation/GeolocationTopComponent.form new file mode 100755 index 0000000000..bddebcbc0c --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/GeolocationTopComponent.form @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/GeolocationTopComponent.java b/Core/src/org/sleuthkit/autopsy/geolocation/GeolocationTopComponent.java new file mode 100755 index 0000000000..a33bc1d15e --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/GeolocationTopComponent.java @@ -0,0 +1,217 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.geolocation; + +import java.awt.BorderLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.beans.PropertyChangeListener; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import javax.swing.SwingWorker; +import org.jxmapviewer.viewer.Waypoint; +import org.openide.util.NbBundle.Messages; +import org.openide.windows.RetainLocation; +import org.openide.windows.TopComponent; +import org.openide.windows.WindowManager; +import org.sleuthkit.autopsy.casemodule.Case; +import static org.sleuthkit.autopsy.casemodule.Case.Events.CURRENT_CASE; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; +import org.sleuthkit.autopsy.ingest.IngestManager; +import static org.sleuthkit.autopsy.ingest.IngestManager.IngestModuleEvent.DATA_ADDED; +import org.sleuthkit.autopsy.ingest.ModuleDataEvent; +import org.sleuthkit.datamodel.BlackboardArtifact; + +/** + * Top component which displays the Geolocation Tool. + * + */ +@TopComponent.Description(preferredID = "GeolocationTopComponent", persistenceType = TopComponent.PERSISTENCE_NEVER) +@TopComponent.Registration(mode = "geolocation", openAtStartup = false) +@RetainLocation("geolocation") +@SuppressWarnings("PMD.SingularField") +public final class GeolocationTopComponent extends TopComponent { + + private static final long serialVersionUID = 1L; + + private static final Logger logger = Logger.getLogger(GeolocationTopComponent.class.getName()); + + private static final Set INGEST_MODULE_EVENTS_OF_INTEREST = EnumSet.of(DATA_ADDED); + + private final PropertyChangeListener ingestListener; + + final RefreshPanel refreshPanel = new RefreshPanel(); + + @Messages({ + "GLTopComponent_name=Geolocation", + "GLTopComponent_initilzation_error=An error occurred during waypoint initilization. Geolocation data maybe incomplete." + }) + + /** + * Creates new form GeoLocationTopComponent + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + public GeolocationTopComponent() { + initComponents(); + initWaypoints(); + setName(Bundle.GLTopComponent_name()); + + this.ingestListener = pce -> { + String eventType = pce.getPropertyName(); + if (eventType.equals(DATA_ADDED.toString())) { + // Indicate that a refresh may be needed for GPS data. + ModuleDataEvent eventData = (ModuleDataEvent) pce.getOldValue(); + if (null != eventData + && (eventData.getBlackboardArtifactType().getTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_TRACKPOINT.getTypeID() + || eventData.getBlackboardArtifactType().getTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_SEARCH.getTypeID() + || eventData.getBlackboardArtifactType().getTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_LAST_KNOWN_LOCATION.getTypeID() + || eventData.getBlackboardArtifactType().getTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_ROUTE.getTypeID() + || eventData.getBlackboardArtifactType().getTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_METADATA_EXIF.getTypeID() + || eventData.getBlackboardArtifactType().getTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_BOOKMARK.getTypeID())) { + + showRefreshPanel(true); + } + } + }; + + refreshPanel.addCloseActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + showRefreshPanel(false); + } + }); + + refreshPanel.addRefreshActionListner(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + mapPanel.clearWaypoints(); + initWaypoints(); + showRefreshPanel(false); + } + }); + } + + @Override + public void addNotify() { + super.addNotify(); + IngestManager.getInstance().addIngestModuleEventListener(INGEST_MODULE_EVENTS_OF_INTEREST, ingestListener); + Case.addEventTypeSubscriber(EnumSet.of(CURRENT_CASE), evt -> { + mapPanel.clearWaypoints(); + if (evt.getNewValue() != null) { + initWaypoints(); + } + }); + } + + @Override + public void removeNotify() { + super.removeNotify(); + IngestManager.getInstance().removeIngestModuleEventListener(ingestListener); + } + + @Override + public void componentOpened() { + super.componentOpened(); + WindowManager.getDefault().setTopComponentFloating(this, true); + } + + /** + * Set the state of the refresh panel at the top of the mapPanel. + * + * @param show Whether to show or hide the panel. + */ + private void showRefreshPanel(boolean show) { + if (show) { + mapPanel.add(refreshPanel, BorderLayout.NORTH); + } else { + mapPanel.remove(refreshPanel); + } + mapPanel.revalidate(); + } + + /** + * Use a SwingWorker thread to find all of the artifacts that have GPS + * coordinates. + * + */ + private void initWaypoints() { + SwingWorker, Waypoint> worker = new SwingWorker, Waypoint>() { + @Override + protected List doInBackground() throws Exception { + Case currentCase = Case.getCurrentCaseThrows(); + + return MapWaypoint.getWaypoints(currentCase.getSleuthkitCase()); + } + + @Override + protected void done() { + if (isDone() && !isCancelled()) { + try { + List waypoints = get(); + if (waypoints == null || waypoints.isEmpty()) { + return; + } + + for (Waypoint point : waypoints) { + mapPanel.addWaypoint(point); + } + + // There might be a better way to decide how to center + // but for now just use the first way point. + mapPanel.setCenterLocation(waypoints.get(0)); + + } catch (ExecutionException ex) { + logger.log(Level.WARNING, "An exception occured while initializing waypoints for geolocation window.", ex); + MessageNotifyUtil.Message.error(Bundle.GLTopComponent_initilzation_error()); + } catch (InterruptedException ex) { + logger.log(Level.WARNING, "The initializing thread for geolocation window was interrupted.", ex); + } + } + } + }; + + worker.execute(); + } + + + /** + * 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 + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + mapPanel = new org.sleuthkit.autopsy.geolocation.MapPanel(); + + setLayout(new java.awt.BorderLayout()); + add(mapPanel, java.awt.BorderLayout.CENTER); + }// //GEN-END:initComponents + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private org.sleuthkit.autopsy.geolocation.MapPanel mapPanel; + // End of variables declaration//GEN-END:variables +} diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/MapPanel.form b/Core/src/org/sleuthkit/autopsy/geolocation/MapPanel.form new file mode 100755 index 0000000000..ad0de45d6e --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/MapPanel.form @@ -0,0 +1,89 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/MapPanel.java b/Core/src/org/sleuthkit/autopsy/geolocation/MapPanel.java new file mode 100755 index 0000000000..fdc8b7c8f1 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/MapPanel.java @@ -0,0 +1,238 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.geolocation; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.HashSet; +import java.util.Set; +import javax.swing.DefaultListModel; +import javax.swing.event.ListDataEvent; +import javax.swing.event.ListDataListener; +import javax.swing.event.MouseInputListener; +import org.jxmapviewer.OSMTileFactoryInfo; +import org.jxmapviewer.input.CenterMapListener; +import org.jxmapviewer.input.PanMouseInputListener; +import org.jxmapviewer.input.ZoomMouseWheelListenerCursor; +import org.jxmapviewer.viewer.DefaultTileFactory; +import org.jxmapviewer.viewer.GeoPosition; +import org.jxmapviewer.viewer.TileFactoryInfo; +import org.jxmapviewer.viewer.Waypoint; +import org.jxmapviewer.viewer.WaypointPainter; + +/** + * Main panel with the JJXMapViewer object and its basic controls. + */ +final class MapPanel extends javax.swing.JPanel { + + private static final long serialVersionUID = 1L; + + private boolean zoomChanging; + + // Using a DefaultListModel to store the way points because we get + // a lot of functionality for free, like listeners. + private final DefaultListModel waypointListModel; + + /** + * Creates new form MapPanel + */ + MapPanel() { + waypointListModel = new DefaultListModel<>(); + zoomChanging = false; + + initComponents(); + initMap(); + } + + /** + * Initialize the map. + */ + private void initMap() { + + TileFactoryInfo info = new OSMTileFactoryInfo(); + DefaultTileFactory tileFactory = new DefaultTileFactory(info); + mapViewer.setTileFactory(tileFactory); + + // Add Mouse interactions + MouseInputListener mia = new PanMouseInputListener(mapViewer); + mapViewer.addMouseListener(mia); + mapViewer.addMouseMotionListener(mia); + + mapViewer.addMouseListener(new CenterMapListener(mapViewer)); + mapViewer.addMouseWheelListener(new ZoomMouseWheelListenerCursor(mapViewer)); + + // Listen to the map for a change in zoom so that we can update the slider. + mapViewer.addPropertyChangeListener("zoom", new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent evt) { + zoomSlider.setValue(mapViewer.getZoom()); + } + }); + + zoomSlider.setMinimum(tileFactory.getInfo().getMinimumZoomLevel()); + zoomSlider.setMaximum(tileFactory.getInfo().getMaximumZoomLevel()); + + + setZoom(tileFactory.getInfo().getMaximumZoomLevel()- 1); + mapViewer.setAddressLocation(new GeoPosition(0, 0)); + + // Listener for new way points being added to the map. + waypointListModel.addListDataListener(new ListDataListener() { + @Override + public void intervalAdded(ListDataEvent e) { + mapViewer.repaint(); + } + + @Override + public void intervalRemoved(ListDataEvent e) { + mapViewer.repaint(); + } + + @Override + public void contentsChanged(ListDataEvent e) { + mapViewer.repaint(); + } + + }); + + // Basic painters for the way points. + WaypointPainter waypointPainter = new WaypointPainter() { + @Override + public Set getWaypoints() { + Set set = new HashSet<>(); + for (int index = 0; index < waypointListModel.getSize(); index++) { + set.add(waypointListModel.get(index)); + } + return set; + } + }; + + mapViewer.setOverlayPainter(waypointPainter); + } + + /** + * Add a way point to the map. + * + * @param waypoint + */ + void addWaypoint(Waypoint waypoint) { + waypointListModel.addElement(waypoint); + } + + void setCenterLocation(Waypoint waypoint) { + mapViewer.setCenterPosition(waypoint.getPosition()); + } + + /** + * Set the current zoom level. + * + * @param zoom + */ + void setZoom(int zoom) { + zoomChanging = true; + mapViewer.setZoom(zoom); + zoomSlider.setValue((zoomSlider.getMaximum() + zoomSlider.getMinimum())- zoom); + zoomChanging = false; + } + + /** + * Remove all of the way points from the map. + */ + void clearWaypoints() { + waypointListModel.removeAllElements(); + } + + /** + * 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 + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + mapViewer = new org.jxmapviewer.JXMapViewer(); + zoomPanel = new javax.swing.JPanel(); + zoomSlider = new javax.swing.JSlider(); + + setFocusable(false); + setLayout(new java.awt.BorderLayout()); + + mapViewer.setLayout(new java.awt.GridBagLayout()); + + zoomPanel.setFocusable(false); + zoomPanel.setOpaque(false); + zoomPanel.setRequestFocusEnabled(false); + + zoomSlider.setMaximum(15); + zoomSlider.setMinimum(10); + zoomSlider.setMinorTickSpacing(1); + zoomSlider.setOrientation(javax.swing.JSlider.VERTICAL); + zoomSlider.setPaintTicks(true); + zoomSlider.setSnapToTicks(true); + zoomSlider.setMinimumSize(new java.awt.Dimension(35, 100)); + zoomSlider.setOpaque(false); + zoomSlider.setPreferredSize(new java.awt.Dimension(35, 190)); + zoomSlider.addChangeListener(new javax.swing.event.ChangeListener() { + public void stateChanged(javax.swing.event.ChangeEvent evt) { + zoomSliderStateChanged(evt); + } + }); + + javax.swing.GroupLayout zoomPanelLayout = new javax.swing.GroupLayout(zoomPanel); + zoomPanel.setLayout(zoomPanelLayout); + zoomPanelLayout.setHorizontalGroup( + zoomPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(zoomPanelLayout.createSequentialGroup() + .addGap(0, 0, 0) + .addComponent(zoomSlider, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + zoomPanelLayout.setVerticalGroup( + zoomPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, zoomPanelLayout.createSequentialGroup() + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(zoomSlider, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(0, 0, 0)) + ); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.anchor = java.awt.GridBagConstraints.SOUTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(4, 4, 4, 4); + mapViewer.add(zoomPanel, gridBagConstraints); + + add(mapViewer, java.awt.BorderLayout.CENTER); + }// //GEN-END:initComponents + + private void zoomSliderStateChanged(javax.swing.event.ChangeEvent evt) {//GEN-FIRST:event_zoomSliderStateChanged + if (!zoomChanging) { + setZoom(zoomSlider.getValue()); + } + }//GEN-LAST:event_zoomSliderStateChanged + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private org.jxmapviewer.JXMapViewer mapViewer; + private javax.swing.JPanel zoomPanel; + private javax.swing.JSlider zoomSlider; + // End of variables declaration//GEN-END:variables +} diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/MapWaypoint.java b/Core/src/org/sleuthkit/autopsy/geolocation/MapWaypoint.java new file mode 100755 index 0000000000..d765d17ff5 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/MapWaypoint.java @@ -0,0 +1,86 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.geolocation; + +import java.util.ArrayList; +import java.util.List; +import org.jxmapviewer.viewer.GeoPosition; +import org.sleuthkit.autopsy.geolocation.datamodel.GeoLocationDataException; +import org.sleuthkit.autopsy.geolocation.datamodel.Route; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.autopsy.geolocation.datamodel.Waypoint; + +/** + * A Wrapper for the datamodel Waypoint class that implements the jxmapviewer + * Waypoint interfact for use in the map. + * + */ +final class MapWaypoint implements org.jxmapviewer.viewer.Waypoint{ + + private final Waypoint dataModelWaypoint; + private final GeoPosition position; + + /** + * Private constructor for MapWaypoint + * + * @param dataModelWaypoint The datamodel waypoint to wrap + */ + private MapWaypoint(Waypoint dataModelWaypoint) { + this.dataModelWaypoint = dataModelWaypoint; + position = new GeoPosition(dataModelWaypoint.getLatitude(), dataModelWaypoint.getLongitude()); + } + + /** + * Gets a list of jxmapviewer waypoints from the current case. + * + * @param skCase Current case + * + * @return List of jxmapviewer waypoints + * + * @throws GeoLocationDataException + */ + static List getWaypoints(SleuthkitCase skCase) throws GeoLocationDataException{ + List points = Waypoint.getAllWaypoints(skCase); + + List routes = Route.getRoutes(skCase); + for(Route route: routes) { + points.addAll(route.getRoute()); + } + + List mapPoints = new ArrayList<>(); + + for(Waypoint point: points) { + mapPoints.add(new MapWaypoint(point)); + } + + return mapPoints; + } + + /** + * {@inheritDoc} + */ + @Override + public GeoPosition getPosition() { + return position; + } + + String getLabel() { + return dataModelWaypoint.getLabel(); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/OpenGeolocationAction.java b/Core/src/org/sleuthkit/autopsy/geolocation/OpenGeolocationAction.java new file mode 100755 index 0000000000..e0f8d42389 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/OpenGeolocationAction.java @@ -0,0 +1,95 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.geolocation; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import org.openide.awt.ActionID; +import org.openide.awt.ActionReference; +import org.openide.awt.ActionReferences; +import org.openide.awt.ActionRegistration; +import org.openide.util.HelpCtx; +import org.openide.util.NbBundle.Messages; +import org.openide.util.actions.CallableSystemAction; +import org.openide.windows.TopComponent; +import org.openide.windows.WindowManager; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.core.RuntimeProperties; + +/** + * Action that opens the Geolocation window. Available through the Tools menu. + * + */ +@ActionID(category = "Tools", + id = "org.sleuthkit.autopsy.geolocation.OpenGeolocationAction") +@ActionRegistration(displayName = "#CTL_OpenGeolocation", lazy = false) +@ActionReferences(value = { + @ActionReference(path = "Menu/Tools", position = 102)}) +public class OpenGeolocationAction extends CallableSystemAction { + + private static final long serialVersionUID = 1L; + + @Messages({ + "OpenGeolocationAction_name=Geolocation", + "OpenGeolocationAction_displayName=Geolocation" + }) + + /** + * Constructs the new action of opening the Geolocation window. + */ + public OpenGeolocationAction() { + setEnabled(false); //disabled by default. Will be enabled in Case.java when a case is opened. + + PropertyChangeListener caseChangeListener = (PropertyChangeEvent evt) -> { + if (evt.getPropertyName().equals(Case.Events.CURRENT_CASE.toString())) { + setEnabled(RuntimeProperties.runningWithGUI() && evt.getNewValue() != null); + } + }; + + Case.addPropertyChangeListener(caseChangeListener); + } + + @Override + public void performAction() { + final TopComponent topComponent = WindowManager.getDefault().findTopComponent("GeolocationTopComponent"); + if (topComponent != null) { + if (topComponent.isOpened() == false) { + topComponent.open(); + } + topComponent.toFront(); + topComponent.requestActive(); + } + } + + @Override + public String getName() { + return Bundle.OpenGeolocationAction_displayName(); + } + + @Override + public HelpCtx getHelpCtx() { + return HelpCtx.DEFAULT_HELP; + } + + @Override + public boolean asynchronous() { + return false; // run on edt + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/RefreshPanel.form b/Core/src/org/sleuthkit/autopsy/geolocation/RefreshPanel.form new file mode 100755 index 0000000000..400c788d42 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/RefreshPanel.form @@ -0,0 +1,86 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/RefreshPanel.java b/Core/src/org/sleuthkit/autopsy/geolocation/RefreshPanel.java new file mode 100755 index 0000000000..e7ff40f31c --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/RefreshPanel.java @@ -0,0 +1,115 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.geolocation; + +import java.awt.event.ActionListener; +import javax.swing.JPanel; + +/** + * Message panel to info user that the MapPanel may need to be refreshed + * due to new artifacts. + * + */ +final class RefreshPanel extends JPanel { + + private static final long serialVersionUID = 1L; + + /** + * Creates new form RefreshPanel + */ + RefreshPanel() { + initComponents(); + } + + void addCloseActionListener(ActionListener listener) { + closeButton.addActionListener(listener); + } + + void addRefreshActionListner(ActionListener listener) { + refreshButton.addActionListener(listener); + } + + void removeCloseActionListner(ActionListener listener) { + closeButton.removeActionListener(listener); + } + + void removeRefreshActionListner(ActionListener listener) { + refreshButton.removeActionListener(listener); + } + + /** + * 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 + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + refreshLabel = new javax.swing.JLabel(); + refreshButton = new javax.swing.JButton(); + closeButton = new javax.swing.JButton(); + + setBackground(new java.awt.Color(0, 51, 51)); + setLayout(new java.awt.GridBagLayout()); + + refreshLabel.setForeground(new java.awt.Color(255, 255, 255)); + refreshLabel.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/warning16.png"))); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(refreshLabel, org.openide.util.NbBundle.getMessage(RefreshPanel.class, "RefreshPanel.refreshLabel.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(15, 10, 15, 10); + add(refreshLabel, gridBagConstraints); + + refreshButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/geolocation/images/arrow-circle-double-135.png"))); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(refreshButton, org.openide.util.NbBundle.getMessage(RefreshPanel.class, "RefreshPanel.refreshButton.text")); // NOI18N + refreshButton.setMargin(new java.awt.Insets(2, 5, 2, 5)); + add(refreshButton, new java.awt.GridBagConstraints()); + + closeButton.setBackground(new java.awt.Color(0, 0, 0)); + closeButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/geolocation/images/cross-script.png"))); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(closeButton, org.openide.util.NbBundle.getMessage(RefreshPanel.class, "RefreshPanel.closeButton.text")); // NOI18N + closeButton.setMargin(new java.awt.Insets(0, 0, 0, 0)); + closeButton.setOpaque(false); + closeButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + closeButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.insets = new java.awt.Insets(0, 5, 0, 5); + add(closeButton, gridBagConstraints); + }// //GEN-END:initComponents + + private void closeButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_closeButtonActionPerformed + + }//GEN-LAST:event_closeButtonActionPerformed + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton closeButton; + private javax.swing.JButton refreshButton; + private javax.swing.JLabel refreshLabel; + // End of variables declaration//GEN-END:variables +} diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Bundle.properties-MERGED new file mode 100755 index 0000000000..86acb2fdb1 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Bundle.properties-MERGED @@ -0,0 +1,6 @@ +LastKnownWaypoint_Label=Last Known Location +Route_End_Label=End +Route_Label=As-the-crow-flies Route +Route_Start_Label=Start +SearchWaypoint_DisplayLabel=GPS Search +TrackpointWaypoint_DisplayLabel=GPS Trackpoint diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/EXIFWaypoint.java b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/EXIFWaypoint.java new file mode 100755 index 0000000000..3a1ec3e66e --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/EXIFWaypoint.java @@ -0,0 +1,85 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.geolocation.datamodel; + +import java.util.Map; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Waypoint wrapper class for TSK_METADATA_EXIF artifacts. + */ +final class EXIFWaypoint extends Waypoint { + + /** + * Construct a way point with the given artifact. + * + * @param artifact BlackboardArtifact for waypoint + * + * @throws GeoLocationDataException + */ + EXIFWaypoint(BlackboardArtifact artifact) throws GeoLocationDataException { + this(artifact, getAttributesFromArtifactAsMap(artifact), getImageFromArtifact(artifact)); + } + + /** + * Constructs new waypoint using the given artifact and attribute map. + * + * @param artifact Waypoint BlackboardArtifact + * @param attributeMap Map of artifact attributes + * @param image EXIF AbstractFile image + * + * @throws GeoLocationDataException + */ + private EXIFWaypoint(BlackboardArtifact artifact, Map attributeMap, AbstractFile image) throws GeoLocationDataException { + super(artifact, + image != null ? image.getName() : "", + attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_CREATED) != null ? attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_CREATED).getValueLong() : null, + attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE) != null ? attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE).getValueDouble() : null, + attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE) != null ? attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE).getValueDouble() : null, + attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_ALTITUDE) != null ? attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_ALTITUDE).getValueDouble() : null, + image, attributeMap, null); + } + + /** + * Gets the image from the given artifact. + * + * @param artifact BlackboardArtifact for waypoint + * + * @return AbstractFile image for this waypoint or null if one is not + * available + * + * @throws GeoLocationDataException + */ + private static AbstractFile getImageFromArtifact(BlackboardArtifact artifact) throws GeoLocationDataException { + AbstractFile abstractFile = null; + BlackboardArtifact.ARTIFACT_TYPE artifactType = BlackboardArtifact.ARTIFACT_TYPE.fromID(artifact.getArtifactTypeID()); + if (artifactType == BlackboardArtifact.ARTIFACT_TYPE.TSK_METADATA_EXIF) { + try { + abstractFile = artifact.getSleuthkitCase().getAbstractFileById(artifact.getObjectID()); + } catch (TskCoreException ex) { + throw new GeoLocationDataException(String.format("Unable to getAbstractFileByID for artifactID: %d", artifact.getArtifactID()), ex); + } + } + + return abstractFile; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/GeoLocationDataException.java b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/GeoLocationDataException.java new file mode 100755 index 0000000000..44aad62b58 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/GeoLocationDataException.java @@ -0,0 +1,47 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.geolocation.datamodel; + +/** + * An exception class for the Geolocation dateModel; + * + */ +public class GeoLocationDataException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Create exception containing the error message + * + * @param msg the message + */ + public GeoLocationDataException(String msg) { + super(msg); + } + + /** + * Create exception containing the error message and cause exception + * + * @param msg the message + * @param ex cause exception + */ + public GeoLocationDataException(String msg, Exception ex) { + super(msg, ex); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/LastKnownWaypoint.java b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/LastKnownWaypoint.java new file mode 100755 index 0000000000..7bf85874ff --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/LastKnownWaypoint.java @@ -0,0 +1,82 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.geolocation.datamodel; + +import java.util.Map; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; + +/** + * A Last Known Location Waypoint object. + */ +final class LastKnownWaypoint extends Waypoint { + + /** + * Constructs a new waypoint. + * + * @param artifact BlackboardArtifact from which to construct the waypoint + * + * @throws GeoLocationDataException + */ + LastKnownWaypoint(BlackboardArtifact artifact) throws GeoLocationDataException { + this(artifact, getAttributesFromArtifactAsMap(artifact)); + } + + /** + * Constructs a new waypoint with the given artifact and attribute map. + * + * @param artifact BlackboardArtifact from which to construct the + * waypoint + * @param attributeMap Map of artifact attributes + * + * @throws GeoLocationDataException + */ + private LastKnownWaypoint(BlackboardArtifact artifact, Map attributeMap) throws GeoLocationDataException { + super(artifact, + getLabelFromArtifact(attributeMap), + attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME) != null ? attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME).getValueLong() : null, + attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE) != null ? attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE).getValueDouble() : null, + attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE) != null ? attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE).getValueDouble() : null, + attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_ALTITUDE) != null ? attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_ALTITUDE).getValueDouble() : null, + null, attributeMap, null); + } + + /** + * Gets the label for a TSK_LAST_KNOWN_LOCATION. + * + * @param attributeMap Map of artifact attributes for this waypoint + * + * @return String value from attribute TSK_NAME or LastKnownWaypoint_Label + * + * @throws GeoLocationDataException + */ + @Messages({ + "LastKnownWaypoint_Label=Last Known Location",}) + private static String getLabelFromArtifact(Map attributeMap) throws GeoLocationDataException { + BlackboardAttribute attribute = attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME); + String label = attribute.getDisplayString(); + + if (label == null || label.isEmpty()) { + label = Bundle.LastKnownWaypoint_Label(); + } + + return label; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Route.java b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Route.java new file mode 100755 index 0000000000..29cfd5afe4 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Route.java @@ -0,0 +1,190 @@ +/* + * + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.geolocation.datamodel; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * A Route represents a TSK_GPS_ROUTE artifact which has a start and end point + * however the class was written with the assumption that routes may have + * more that two points. + * + */ +public final class Route { + private final List points; + private final Long timestamp; + + // This list is not expected to change after construction so the + // constructor will take care of creating an unmodifiable List + private final List immutablePropertiesList; + + /** + * Gets the list of Routes from the TSK_GPS_ROUTE artifacts. + * + * @param skCase Currently open SleuthkitCase + * + * @return List of Route objects, empty list will be returned if no Routes + * were found + * + * @throws GeoLocationDataException + */ + static public List getRoutes(SleuthkitCase skCase) throws GeoLocationDataException { + List artifacts = null; + try { + artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_ROUTE); + } catch (TskCoreException ex) { + throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_BOOKMARK", ex); + } + + List routes = new ArrayList<>(); + for (BlackboardArtifact artifact : artifacts) { + Route route = new Route(artifact); + routes.add(route); + } + return routes; + } + + /** + * Construct a route for the given artifact. + * + * @param artifact TSK_GPS_ROUTE artifact object + */ + Route(BlackboardArtifact artifact) throws GeoLocationDataException { + points = new ArrayList<>(); + + Map attributeMap = Waypoint.getAttributesFromArtifactAsMap(artifact); + points.add(getRouteStartPoint(artifact, attributeMap)); + points.add(getRouteEndPoint(artifact, attributeMap)); + + BlackboardAttribute attribute = attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME); + timestamp = attribute != null ? attribute.getValueLong() : null; + + immutablePropertiesList = Collections.unmodifiableList(Waypoint.createGeolocationProperties(attributeMap)); + } + + /** + * Get the list of way points for this route; + * + * @return List an unmodifiableList of ArtifactWaypoints for this route + */ + public List getRoute() { + return Collections.unmodifiableList(points); + } + + /** + * Get the "Other attributes" for this route. The map will contain display + * name, formatted value pairs. This list is unmodifiable. + * + * @return Map of key, value pairs. + */ + public List getOtherProperties() { + return immutablePropertiesList; + } + + /** + * Get the route label. + */ + @Messages({ + // This is the original static hardcoded label from the + // original kml-report code + "Route_Label=As-the-crow-flies Route" + }) + public String getLabel() { + return Bundle.Route_Label(); + } + + public Long getTimestamp() { + return timestamp; + } + + /** + * Get the route start point. + * + * @param attributeMap Map of artifact attributes for this waypoint. + * + * An exception will be thrown if longitude or latitude is null. + * + * @return Start waypoint + * + * @throws GeoLocationDataException. + */ + @Messages({ + "Route_Start_Label=Start" + }) + private Waypoint getRouteStartPoint(BlackboardArtifact artifact, Map attributeMap) throws GeoLocationDataException { + BlackboardAttribute latitude = attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_START); + BlackboardAttribute longitude = attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE_START); + BlackboardAttribute altitude = attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_ALTITUDE); + BlackboardAttribute pointTimestamp = attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME); + + if (latitude != null && longitude != null) { + return new Waypoint(artifact, + Bundle.Route_Start_Label(), + pointTimestamp != null ? pointTimestamp.getValueLong() : null, + latitude.getValueDouble(), + longitude.getValueDouble(), + altitude != null ? altitude.getValueDouble() : null, + null, attributeMap, this); + } else { + throw new GeoLocationDataException("Unable to create route start point, invalid longitude and/or latitude"); + } + } + + /** + * Get the route End point. + * + * An exception will be thrown if longitude or latitude is null. + * + * @param attributeMap Map of artifact attributes for this waypoint + * + * @return The end waypoint + * + * @throws GeoLocationDataException + */ + @Messages({ + "Route_End_Label=End" + }) + private Waypoint getRouteEndPoint(BlackboardArtifact artifact, Map attributeMap) throws GeoLocationDataException { + BlackboardAttribute latitude = attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_END); + BlackboardAttribute longitude = attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE_END); + BlackboardAttribute altitude = attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_ALTITUDE); + BlackboardAttribute pointTimestamp = attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME); + + if (latitude != null && longitude != null) { + return new Waypoint(artifact, + Bundle.Route_End_Label(), + pointTimestamp != null ? pointTimestamp.getValueLong() : null, + latitude.getValueDouble(), + longitude.getValueDouble(), + altitude != null ? altitude.getValueDouble() : null, + null, attributeMap, this); + } else { + throw new GeoLocationDataException("Unable to create route end point, invalid longitude and/or latitude"); + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/SearchWaypoint.java b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/SearchWaypoint.java new file mode 100755 index 0000000000..7f0746d6ca --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/SearchWaypoint.java @@ -0,0 +1,77 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.geolocation.datamodel; + +import java.util.Map; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; + +/** + * A SearchWaypoint is a subclass of Waypoint. + */ +final class SearchWaypoint extends Waypoint { + + /** + * Construct a waypoint for TSK_GPS_SEARCH artifact. + * + * @throws GeoLocationDataException + */ + SearchWaypoint(BlackboardArtifact artifact) throws GeoLocationDataException { + this(artifact, getAttributesFromArtifactAsMap(artifact)); + } + + private SearchWaypoint(BlackboardArtifact artifact, Map attributeMap) throws GeoLocationDataException { + super(artifact, + getLabelFromArtifact(attributeMap), + attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME) != null ? attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME).getValueLong() : null, + attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE) != null ? attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE).getValueDouble() : null, + attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE) != null ? attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE).getValueDouble() : null, + attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_ALTITUDE) != null ? attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_ALTITUDE).getValueDouble() : null, + null, attributeMap, null); + } + + /** + * Returns a Label for a GPS_SEARCH artifact. + * + * @param attributeMap Map of artifact attributes + * + * @return String label for the artifacts way point. + * + * @throws GeoLocationDataException + */ + @Messages({ + "SearchWaypoint_DisplayLabel=GPS Search" + }) + + private static String getLabelFromArtifact(Map attributeMap) throws GeoLocationDataException { + BlackboardAttribute attribute = attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME); + if (attribute != null) { + return attribute.getDisplayString(); + } + + attribute = attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_LOCATION); + if (attribute != null) { + return attribute.getDisplayString(); + } + + return Bundle.SearchWaypoint_DisplayLabel(); + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/TrackpointWaypoint.java b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/TrackpointWaypoint.java new file mode 100755 index 0000000000..7009aa63a1 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/TrackpointWaypoint.java @@ -0,0 +1,82 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.geolocation.datamodel; + +import java.util.Map; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; + +/** + * A wrapper class for TSK_GPS_TRACKPOINT artifacts. + */ +final class TrackpointWaypoint extends Waypoint { + /** + * Construct a waypoint for trackpoints. + * + * @throws GeoLocationDataException + */ + TrackpointWaypoint(BlackboardArtifact artifact) throws GeoLocationDataException { + this(artifact, getAttributesFromArtifactAsMap(artifact)); + } + + private TrackpointWaypoint(BlackboardArtifact artifact, Map attributeMap) throws GeoLocationDataException { + super(artifact, + getLabelFromArtifact(attributeMap), + attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME) != null ? attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME).getValueLong() : null, + attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE) != null ? attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE).getValueDouble() : null, + attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE) != null ? attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE).getValueDouble() : null, + attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_ALTITUDE) != null ? attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_ALTITUDE).getValueDouble() : null, + null, attributeMap, null); + } + + /** + * Returns a Label for a GPS_Trackpoint artifact. This function assumes the + * calling function has already checked TSK_NAME. + * + * @param artifact BlackboardArtifact for waypoint + * + * @return String label for the artifacts way point. + * + * @throws GeoLocationDataException + */ + @Messages({ + "TrackpointWaypoint_DisplayLabel=GPS Trackpoint" + }) + private static String getLabelFromArtifact(Map attributeMap) throws GeoLocationDataException { + + BlackboardAttribute attribute = attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME); + if (attribute != null) { + return attribute.getDisplayString(); + } + + attribute = attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME); + if (attribute != null) { + return attribute.getDisplayString(); + } + + attribute = attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_FLAG); + if (attribute != null) { + return attribute.getDisplayString(); + } + + return Bundle.TrackpointWaypoint_DisplayLabel(); + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Waypoint.java b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Waypoint.java new file mode 100755 index 0000000000..bd5b39b6b7 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Waypoint.java @@ -0,0 +1,514 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.geolocation.datamodel; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Representation of a Waypoint created from a BlackboardArtifact. + * + */ +public class Waypoint { + + final private Long timestamp; + final private Double longitude; + final private Double latitude; + final private Double altitude; + final private String label; + final private AbstractFile image; + final private BlackboardArtifact artifact; + final private Route route; + + // This list is not expected to change after construction. The + // constructor will take care of making an unmodifiable List + final private List immutablePropertiesList; + + /** + * This is a list of attributes that are already being handled by the + * by getter functions. + */ + static private BlackboardAttribute.ATTRIBUTE_TYPE[] ALREADY_HANDLED_ATTRIBUTES = { + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_ALTITUDE, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_CREATED, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_START, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE_START, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_END, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE_END,}; + + private static final Logger logger = Logger.getLogger(Waypoint.class.getName()); + + /** + * Construct a waypoint with the given artifact. + * + * @param artifact BlackboardArtifact for this waypoint + * + * @throws GeoLocationDataException Exception will be thrown if artifact did + * not have a valid longitude and latitude. + */ + Waypoint(BlackboardArtifact artifact) throws GeoLocationDataException { + this(artifact, + getAttributesFromArtifactAsMap(artifact)); + } + + /** + * Constructor that initializes all of the member variables. + * + * @param artifact BlackboardArtifact for this waypoint + * @param label String waypoint label + * @param timestamp Long timestamp, unix/java epoch seconds + * @param latitude Double waypoint latitude + * @param longitude Double waypoint longitude + * @param altitude Double waypoint altitude + * @param image AbstractFile image for waypoint, this maybe null + * @param attributeMap A Map of attributes for the given artifact + * + * @throws GeoLocationDataException Exception will be thrown if artifact did + * not have a valid longitude and latitude. + */ + Waypoint(BlackboardArtifact artifact, String label, Long timestamp, Double latitude, Double longitude, Double altitude, AbstractFile image, Map attributeMap, Route route) throws GeoLocationDataException { + if (longitude == null || latitude == null) { + throw new GeoLocationDataException("Invalid waypoint, null value passed for longitude or latitude"); + } + + this.artifact = artifact; + this.label = label; + this.image = image; + this.timestamp = timestamp; + this.longitude = longitude; + this.latitude = latitude; + this.altitude = altitude; + this.route = null; + + immutablePropertiesList = Collections.unmodifiableList(createGeolocationProperties(attributeMap)); + } + + /** + * Constructs a new ArtifactWaypoint. + * + * @param artifact BlackboardArtifact for this waypoint + * @param attributeMap A Map of the BlackboardAttributes for the given + * artifact. + * + * @throws GeoLocationDataException + */ + private Waypoint(BlackboardArtifact artifact, Map attributeMap) throws GeoLocationDataException { + this(artifact, + getLabelFromArtifact(attributeMap), + attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME) != null ? attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME).getValueLong() : null, + attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE) != null ? attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE).getValueDouble() : null, + attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE) != null ? attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE).getValueDouble() : null, + attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_ALTITUDE) != null ? attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_ALTITUDE).getValueDouble() : null, + null, attributeMap, null); + } + + /** + * Get the BlackboardArtifact that this waypoint represents. + * + * @return BlackboardArtifact for this waypoint. + */ + public BlackboardArtifact getArtifact() { + return artifact; + } + + /** + * Interface to describe a waypoint. A waypoint is made up of a longitude, + * latitude, label, timestamp, type, image and altitude. + * + * A good way point should have at minimum a longitude and latutude. + * + * @return Timestamp in java/unix epoch seconds or null if none was set. + */ + public Long getTimestamp() { + return timestamp; + } + + /** + * Get the label for this point object. + * + * @return String label for the point or null if none was set + */ + public String getLabel() { + return label; + } + + /** + * Get the latitude for this point. + * + * @return Returns the latitude for the point + */ + public Double getLatitude() { + return latitude; + } + + /** + * Get the longitude for this point. + * + * @return Returns the longitude for the point + */ + public Double getLongitude() { + return longitude; + } + + /** + * Get the altitude for this point. + * + * @return Returns the altitude for the point or null if none was set + */ + public Double getAltitude() { + return altitude; + } + + /** + * Get the image for this waypoint. + * + * @return AbstractFile image or null if one was not set + */ + public AbstractFile getImage() { + return image; + } + + /** + * Gets an unmodifiable List of other properties that may be interesting to + * this way point. The List will not include properties for which getter + * functions exist. + * + * @return A List of waypoint properties + */ + public List getOtherProperties() { + return immutablePropertiesList; + } + + /** + * Returns the route that this waypoint is apart of . + * + * @return The waypoint route or null if the waypoint is not apart of a route. + */ + public Route getRoute() { + return route; + } + + /** + * Gets the label for this waypoint. + * + * @param artifact BlackboardArtifact for waypoint + * + * @return Returns a label for the waypoint, or empty string if no label was + * found. + */ + private static String getLabelFromArtifact(Map attributeMap) { + BlackboardAttribute attribute = attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME); + if (attribute != null) { + return attribute.getDisplayString(); + } + + return ""; + } + + /** + * Gets the list of attributes from the artifact and puts them into a map + * with the ATRIBUTE_TYPE as the key. + * + * @param artifact BlackboardArtifact current artifact + * + * @return A Map of BlackboardAttributes for the given artifact with + * ATTRIBUTE_TYPE as the key. + * + * @throws GeoLocationDataException + */ + static Map getAttributesFromArtifactAsMap(BlackboardArtifact artifact) throws GeoLocationDataException { + Map attributeMap = new HashMap<>(); + try { + List attributeList = artifact.getAttributes(); + for (BlackboardAttribute attribute : attributeList) { + BlackboardAttribute.ATTRIBUTE_TYPE type = BlackboardAttribute.ATTRIBUTE_TYPE.fromID(attribute.getAttributeType().getTypeID()); + attributeMap.put(type, attribute); + } + } catch (TskCoreException ex) { + throw new GeoLocationDataException("Unable to get attributes from artifact", ex); + } + + return attributeMap; + } + + /** + * Returns a list of Waypoints for the artifacts with geolocation + * information. + * + * List will include artifacts of type: TSK_GPS_TRACKPOINT TSK_GPS_SEARCH + * TSK_GPS_LAST_KNOWN_LOCATION TSK_GPS_BOOKMARK TSK_METADATA_EXIF + * + * @param skCase Currently open SleuthkitCase + * + * @return List of Waypoint + * + * @throws GeoLocationDataException + */ + public static List getAllWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { + List points = new ArrayList<>(); + + points.addAll(getTrackpointWaypoints(skCase)); + points.addAll(getEXIFWaypoints(skCase)); + points.addAll(getSearchWaypoints(skCase)); + points.addAll(getLastKnownWaypoints(skCase)); + points.addAll(getBookmarkWaypoints(skCase)); + + return points; + } + + /** + * Gets a list of Waypoints for TSK_GPS_TRACKPOINT artifacts. + * + * @param skCase Currently open SleuthkitCase + * + * @return List of Waypoint + * + * @throws GeoLocationDataException + */ + public static List getTrackpointWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { + List artifacts = null; + try { + artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_TRACKPOINT); + } catch (TskCoreException ex) { + throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_TRACKPOINT", ex); + } + + List points = new ArrayList<>(); + for (BlackboardArtifact artifact : artifacts) { + try { + Waypoint point = new TrackpointWaypoint(artifact); + points.add(point); + } catch (GeoLocationDataException ex) { + logger.log(Level.WARNING, String.format("No longitude or latitude available for TSK_GPS_TRACKPOINT artifactID: %d", artifact.getArtifactID())); + } + } + return points; + } + + /** + * Gets a list of Waypoints for TSK_METADATA_EXIF artifacts. + * + * @param skCase Currently open SleuthkitCase + * + * @return List of Waypoint + * + * @throws GeoLocationDataException + */ + static public List getEXIFWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { + List artifacts = null; + try { + artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_METADATA_EXIF); + } catch (TskCoreException ex) { + throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_LAST_KNOWN_LOCATION", ex); + } + + List points = new ArrayList<>(); + if (artifacts != null) { + for (BlackboardArtifact artifact : artifacts) { + try { + Waypoint point = new EXIFWaypoint(artifact); + points.add(point); + } catch (GeoLocationDataException ex) { + // I am a little relucant to log this error because I suspect + // this will happen more often than not. It is valid for + // METADAT_EXIF to not have longitude and latitude + } + } + } + return points; + } + + /** + * Gets a list of Waypoints for TSK_GPS_SEARCH artifacts. + * + * @param skCase Currently open SleuthkitCase + * + * @return List of Waypoint + * + * @throws GeoLocationDataException + */ + public static List getSearchWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { + List artifacts = null; + try { + artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_SEARCH); + } catch (TskCoreException ex) { + throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_SEARCH", ex); + } + + List points = new ArrayList<>(); + if (artifacts != null) { + for (BlackboardArtifact artifact : artifacts) { + try { + Waypoint point = new SearchWaypoint(artifact); + points.add(point); + } catch (GeoLocationDataException ex) { + logger.log(Level.WARNING, String.format("No longitude or latitude available for TSK_GPS_SEARCH artifactID: %d", artifact.getArtifactID())); + } + } + } + return points; + } + + /** + * Gets a list of Waypoints for TSK_GPS_LAST_KNOWN_LOCATION artifacts. + * + * @param skCase Currently open SleuthkitCase + * + * @return List of Waypoint + * + * @throws GeoLocationDataException + */ + public static List getLastKnownWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { + List artifacts = null; + try { + artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_LAST_KNOWN_LOCATION); + } catch (TskCoreException ex) { + throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_LAST_KNOWN_LOCATION", ex); + } + + List points = new ArrayList<>(); + if (artifacts != null) { + for (BlackboardArtifact artifact : artifacts) { + try { + Waypoint point = new LastKnownWaypoint(artifact); + points.add(point); + } catch (GeoLocationDataException ex) { + logger.log(Level.WARNING, String.format("No longitude or latitude available for TSK_GPS_LAST_KNOWN_LOCATION artifactID: %d", artifact.getArtifactID())); + } + } + } + return points; + } + + /** + * Gets a list of Waypoints for TSK_GPS_BOOKMARK artifacts. + * + * @param skCase Currently open SleuthkitCase + * + * @return List of Waypoint + * + * @throws GeoLocationDataException + */ + public static List getBookmarkWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { + List artifacts = null; + try { + artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_BOOKMARK); + } catch (TskCoreException ex) { + throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_BOOKMARK", ex); + } + + List points = new ArrayList<>(); + if (artifacts != null) { + for (BlackboardArtifact artifact : artifacts) { + try { + Waypoint point = new Waypoint(artifact); + points.add(point); + } catch (GeoLocationDataException ex) { + logger.log(Level.WARNING, String.format("No longitude or latitude available for TSK_GPS_BOOKMARK artifactID: %d", artifact.getArtifactID())); + } + } + } + return points; + } + + /** + * Get a list of Waypoint.Property objects for the given artifact. This list + * will not include attributes that the Waypoint interfact has get functions + * for. + * + * @param artifact Blackboard artifact to get attributes\properties from + * + * @return A List of Waypoint.Property objects + * + * @throws GeoLocationDataException + */ + static public List createGeolocationProperties(Map attributeMap) throws GeoLocationDataException { + List list = new ArrayList<>(); + + Set keys = new HashSet<>(attributeMap.keySet()); + + for (BlackboardAttribute.ATTRIBUTE_TYPE type : ALREADY_HANDLED_ATTRIBUTES) { + keys.remove(type); + } + + for (BlackboardAttribute.ATTRIBUTE_TYPE type : keys) { + String key = type.getDisplayName(); + String value = attributeMap.get(type).getDisplayString(); + + list.add(new Waypoint.Property(key, value)); + } + return list; + } + + /** + * Simple property class for waypoint properties that a purely + * informational. + */ + public static final class Property { + + private final String displayName; + private final String value; + + /** + * Construct a Property object. + * + * @param displayName String display name for property. Ideally not null + * or empty string. + * @param value String value for property. Can be null. + */ + private Property(String displayName, String value) { + this.displayName = displayName; + this.value = value; + } + + /** + * Get the display name for this property. + * + * @return String display name. + */ + public String getDisplayName() { + return displayName; + } + + /** + * Get the property value. + * + * @return String value. + */ + public String getValue() { + return value; + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/images/arrow-circle-double-135.png b/Core/src/org/sleuthkit/autopsy/geolocation/images/arrow-circle-double-135.png new file mode 100755 index 0000000000..4f40ba5206 Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/geolocation/images/arrow-circle-double-135.png differ diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/images/cross-script.png b/Core/src/org/sleuthkit/autopsy/geolocation/images/cross-script.png new file mode 100755 index 0000000000..f37cf41830 Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/geolocation/images/cross-script.png differ diff --git a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/AddContentToHashDbAction.java b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/AddContentToHashDbAction.java index f80d791bf3..1868d3c57f 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/AddContentToHashDbAction.java +++ b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/AddContentToHashDbAction.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-2018 Basis Technology Corp. + * Copyright 2013-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,6 +21,7 @@ package org.sleuthkit.autopsy.modules.hashdatabase; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.logging.Level; import javax.swing.AbstractAction; @@ -42,7 +43,7 @@ import org.sleuthkit.datamodel.TskCoreException; /** * Instances of this Action allow users to content to a hash database. */ -final class AddContentToHashDbAction extends AbstractAction implements Presenter.Popup { +public final class AddContentToHashDbAction extends AbstractAction implements Presenter.Popup { private static AddContentToHashDbAction instance; @@ -66,12 +67,16 @@ final class AddContentToHashDbAction extends AbstractAction implements Presenter "AddContentToHashDbAction.singleSelectionNameNoMD5"); private final static String MULTI_SELECTION_NAME_NO_MD5 = NbBundle.getMessage(AddContentToHashDbAction.class, "AddContentToHashDbAction.multipleSelectionNameNoMD5"); + private static final long serialVersionUID = 1L; /** * AddContentToHashDbAction is a singleton to support multi-selection of * nodes, since org.openide.nodes.NodeOp.findActions(Node[] nodes) will only * pick up an Action from a node if every node in the nodes array returns a * reference to the same action object from Node.getActions(boolean). + * + * @return The AddContentToHashDbAction instance which is used to provide + * the menu for adding content to a HashDb. */ public static synchronized AddContentToHashDbAction getInstance() { if (null == instance) { @@ -83,6 +88,19 @@ final class AddContentToHashDbAction extends AbstractAction implements Presenter private AddContentToHashDbAction() { } + /** + * Get the menu for adding the specified collection of Files to a HashDb. + * + * @param selectedFiles The collection of AbstractFiles the menu actions + * will be applied to. + * + * @return The menu which will allow users to add the specified files to a + * HashDb. + */ + public JMenuItem getMenuForFiles(Collection selectedFiles) { + return new AddContentToHashDbMenu(selectedFiles); + } + @Override public JMenuItem getPopupPresenter() { return new AddContentToHashDbMenu(); @@ -96,10 +114,14 @@ final class AddContentToHashDbAction extends AbstractAction implements Presenter // action. private final class AddContentToHashDbMenu extends JMenu { - AddContentToHashDbMenu() { + private static final long serialVersionUID = 1L; + + /** + * Construct an AddContentToHashDbMenu object using the specified + * collection of files as the files to be added to a HashDb. + */ + AddContentToHashDbMenu(Collection selectedFiles) { super(SINGLE_SELECTION_NAME); - // Get any AbstractFile objects from the lookup of the currently focused top component. - final Collection selectedFiles = Utilities.actionsGlobalContext().lookupAll(AbstractFile.class); int numberOfFilesSelected = selectedFiles.size(); // Disable the menu if file ingest is in progress. @@ -109,16 +131,13 @@ final class AddContentToHashDbAction extends AbstractAction implements Presenter SINGLE_SELECTION_NAME_DURING_INGEST, MULTI_SELECTION_NAME_DURING_INGEST); return; - } - - if (selectedFiles.isEmpty()) { + } else if (numberOfFilesSelected == 0) { setEnabled(false); return; - } else { - setTextBasedOnNumberOfSelections(numberOfFilesSelected, - SINGLE_SELECTION_NAME, - MULTI_SELECTION_NAME); } + setTextBasedOnNumberOfSelections(numberOfFilesSelected, + SINGLE_SELECTION_NAME, + MULTI_SELECTION_NAME); // Disable the menu if md5 have not been computed or if the file size // is empty. Display the appropriate reason to the user. @@ -137,7 +156,26 @@ final class AddContentToHashDbAction extends AbstractAction implements Presenter return; } } + addExistingHashDatabases(selectedFiles); + // Add a "New Hash Set..." menu item. Selecting this item invokes a + // a hash database creation dialog and adds the selected files to the + // the new database. + addSeparator(); + JMenuItem newHashSetItem = new JMenuItem(NbBundle.getMessage(this.getClass(), + "AddContentToHashDbAction.ContentMenu.createDbItem")); + newHashSetItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + HashDb hashDb = new HashDbCreateDatabaseDialog().getHashDatabase(); + if (null != hashDb) { + addFilesToHashSet(selectedFiles, hashDb); + } + } + }); + add(newHashSetItem); + } + private void addExistingHashDatabases(Collection selectedFiles) { // Get the current set of updateable hash databases and add each // one to the menu as a separate menu item. Selecting a hash database // adds the selected files to the selected database. @@ -159,23 +197,16 @@ final class AddContentToHashDbAction extends AbstractAction implements Presenter empty.setEnabled(false); add(empty); } + } + + /** + * Construct an AddContentToHashDbMenu object using the currently + * selected files as the files to be added to a HashDb. + */ + AddContentToHashDbMenu() { + // Get any AbstractFile objects from the lookup of the currently focused top component. + this(new HashSet<>(Utilities.actionsGlobalContext().lookupAll(AbstractFile.class))); - // Add a "New Hash Set..." menu item. Selecting this item invokes a - // a hash database creation dialog and adds the selected files to the - // the new database. - addSeparator(); - JMenuItem newHashSetItem = new JMenuItem(NbBundle.getMessage(this.getClass(), - "AddContentToHashDbAction.ContentMenu.createDbItem")); - newHashSetItem.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - HashDb hashDb = new HashDbCreateDatabaseDialog().getHashDatabase(); - if (null != hashDb) { - addFilesToHashSet(selectedFiles, hashDb); - } - } - }); - add(newHashSetItem); } /** diff --git a/Core/src/org/sleuthkit/autopsy/progress/ModalDialogProgressIndicator.java b/Core/src/org/sleuthkit/autopsy/progress/ModalDialogProgressIndicator.java index 8d92e40d70..31fb900c56 100644 --- a/Core/src/org/sleuthkit/autopsy/progress/ModalDialogProgressIndicator.java +++ b/Core/src/org/sleuthkit/autopsy/progress/ModalDialogProgressIndicator.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2017 Basis Technology Corp. + * Copyright 2011-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,6 +25,7 @@ import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.ThreadSafe; import javax.swing.JDialog; import javax.swing.SwingUtilities; +import org.apache.commons.lang3.SerializationUtils; import org.openide.DialogDescriptor; import org.openide.DialogDisplayer; import org.openide.util.HelpCtx; @@ -65,7 +66,7 @@ public final class ModalDialogProgressIndicator implements ProgressIndicator { this.title = title; progressPanel = new ProgressPanel(); progressPanel.setIndeterminate(true); - this.buttonLabels = buttonLabels; + this.buttonLabels = SerializationUtils.clone(buttonLabels); this.focusedButtonLabel = focusedButtonLabel; this.buttonListener = buttonListener; } diff --git a/Core/src/org/sleuthkit/autopsy/report/modules/kml/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/report/modules/kml/Bundle.properties-MERGED index 0823cd171b..3cbb7f6506 100755 --- a/Core/src/org/sleuthkit/autopsy/report/modules/kml/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/report/modules/kml/Bundle.properties-MERGED @@ -23,3 +23,10 @@ ReportKML.genReport.srcModuleName.text=Geospatial Data ReportKML.genReport.reportName=KML Report ReportKML.latLongStartPoint={0};{1};;{2} (Start)\n ReportKML.latLongEndPoint={0};{1};;{2} (End)\n +Route_Details_Header=GPS Route +Waypoint_Bookmark_Display_String=GPS Bookmark +Waypoint_EXIF_Display_String=EXIF Metadata With Location +Waypoint_Last_Known_Display_String=GPS Last Known Location +Waypoint_Route_Point_Display_String=GPS Individual Route Point +Waypoint_Search_Display_String=GPS Search +Waypoint_Trackpoint_Display_String=GPS Trackpoint diff --git a/Core/src/org/sleuthkit/autopsy/report/modules/kml/KMLReport.java b/Core/src/org/sleuthkit/autopsy/report/modules/kml/KMLReport.java index c1a8ec4046..79fb65ec0c 100644 --- a/Core/src/org/sleuthkit/autopsy/report/modules/kml/KMLReport.java +++ b/Core/src/org/sleuthkit/autopsy/report/modules/kml/KMLReport.java @@ -21,13 +21,10 @@ package org.sleuthkit.autopsy.report.modules.kml; import org.sleuthkit.autopsy.report.GeneralReportModule; import javax.swing.JPanel; - import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.datamodel.*; import org.sleuthkit.autopsy.ingest.IngestManager; -import org.sleuthkit.datamodel.BlackboardArtifact; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -36,6 +33,7 @@ import java.io.OutputStream; import java.nio.file.Path; import java.nio.file.Paths; import java.text.SimpleDateFormat; +import java.util.List; import java.util.logging.Level; import org.jdom2.Document; import org.jdom2.Element; @@ -44,10 +42,18 @@ import org.jdom2.output.Format; import org.jdom2.output.XMLOutputter; import org.jdom2.CDATA; import org.openide.filesystems.FileUtil; +import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.geolocation.datamodel.GeoLocationDataException; +import org.sleuthkit.autopsy.geolocation.datamodel.Waypoint; +import org.sleuthkit.autopsy.geolocation.datamodel.Route; import org.sleuthkit.autopsy.report.ReportBranding; import org.sleuthkit.autopsy.report.ReportProgressPanel; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.ReadContentInputStream; import org.sleuthkit.datamodel.ReadContentInputStream.ReadContentInputStreamException; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; /** * Generates a KML file based on geospatial information from the BlackBoard. @@ -63,7 +69,14 @@ class KMLReport implements GeneralReportModule { private SleuthkitCase skCase; private final SimpleDateFormat kmlDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX"); private Namespace ns; - private final String SEP = "
"; + private final static String HTML_PROP_FORMAT = "%s: %s
"; + + private Element gpsExifMetadataFolder; + private Element gpsBookmarksFolder; + private Element gpsLastKnownLocationFolder; + private Element gpsRouteFolder; + private Element gpsSearchesFolder; + private Element gpsTrackpointsFolder; private enum FeatureColor { RED("style.kml#redFeature"), @@ -101,7 +114,7 @@ class KMLReport implements GeneralReportModule { * @param baseReportDir path to save the report * @param progressPanel panel to update the report's progress */ - @NbBundle.Messages({ + @Messages({ "KMLReport.unableToExtractPhotos=Could not extract photo information.", "KMLReport.exifPhotoError=Could not extract photos with EXIF metadata.", "KMLReport.bookmarkError=Could not extract Bookmark information.", @@ -111,14 +124,22 @@ class KMLReport implements GeneralReportModule { "KMLReport.gpsRouteError=Could not extract GPS Route information.", "KMLReport.gpsRouteDatabaseError=Could not get GPS Routes from database.", "KMLReport.gpsSearchDatabaseError=Could not get GPS Searches from database.", - "KMLReport.trackpointError=Could not extract Trackpoint information.", - "KMLReport.trackpointDatabaseError=Could not get GPS Trackpoints from database.", + "KMLReport.trackpointError=Could not extract Trackpoint information.", + "KMLReport.trackpointDatabaseError=Could not get GPS Trackpoints from database.", "KMLReport.stylesheetError=Error placing KML stylesheet. The .KML file will not function properly.", "KMLReport.kmlFileWriteError=Could not write the KML file.", "# {0} - filePath", "KMLReport.errorGeneratingReport=Error adding {0} to case as a report.", - "KMLReport.unableToOpenCase=Exception while getting open case." + "KMLReport.unableToOpenCase=Exception while getting open case.", + "Waypoint_Bookmark_Display_String=GPS Bookmark", + "Waypoint_Last_Known_Display_String=GPS Last Known Location", + "Waypoint_EXIF_Display_String=EXIF Metadata With Location", + "Waypoint_Route_Point_Display_String=GPS Individual Route Point", + "Waypoint_Search_Display_String=GPS Search", + "Waypoint_Trackpoint_Display_String=GPS Trackpoint", + "Route_Details_Header=GPS Route" }) + @Override public void generateReport(String baseReportDir, ReportProgressPanel progressPanel) { try { @@ -133,272 +154,20 @@ class KMLReport implements GeneralReportModule { progressPanel.updateStatusLabel(NbBundle.getMessage(this.getClass(), "ReportKML.progress.querying")); String kmlFileFullPath = baseReportDir + REPORT_KML; //NON-NLS String errorMessage = ""; - + skCase = currentCase.getSleuthkitCase(); progressPanel.updateStatusLabel(NbBundle.getMessage(this.getClass(), "ReportKML.progress.loading")); - ns = Namespace.getNamespace("", "http://www.opengis.net/kml/2.2"); //NON-NLS - - Element kml = new Element("kml", ns); //NON-NLS - kml.addNamespaceDeclaration(Namespace.getNamespace("gx", "http://www.google.com/kml/ext/2.2")); //NON-NLS - kml.addNamespaceDeclaration(Namespace.getNamespace("kml", "http://www.opengis.net/kml/2.2")); //NON-NLS - kml.addNamespaceDeclaration(Namespace.getNamespace("atom", "http://www.w3.org/2005/Atom")); //NON-NLS - Document kmlDocument = new Document(kml); - - Element document = new Element("Document", ns); //NON-NLS - kml.addContent(document); - - Element name = new Element("name", ns); //NON-NLS - ReportBranding rb = new ReportBranding(); - name.setText(rb.getReportTitle() + " KML"); //NON-NLS - document.addContent(name); - - // Check if ingest has finished - if (IngestManager.getInstance().isIngestRunning()) { - Element ingestwarning = new Element("snippet", ns); //NON-NLS - ingestwarning.addContent(NbBundle.getMessage(this.getClass(), "ReportBodyFile.ingestWarning.text")); //NON-NLS - document.addContent(ingestwarning); - } - - // Create folder structure - Element gpsExifMetadataFolder = new Element("Folder", ns); //NON-NLS - CDATA cdataExifMetadataFolder = new CDATA("https://raw.githubusercontent.com/sleuthkit/autopsy/develop/Core/src/org/sleuthkit/autopsy/images/camera-icon-16.png"); //NON-NLS - Element hrefExifMetadata = new Element("href", ns).addContent(cdataExifMetadataFolder); //NON-NLS - gpsExifMetadataFolder.addContent(new Element("Icon", ns).addContent(hrefExifMetadata)); //NON-NLS - - Element gpsBookmarksFolder = new Element("Folder", ns); //NON-NLS - CDATA cdataBookmarks = new CDATA("https://raw.githubusercontent.com/sleuthkit/autopsy/develop/Core/src/org/sleuthkit/autopsy/images/gpsfav.png"); //NON-NLS - Element hrefBookmarks = new Element("href", ns).addContent(cdataBookmarks); //NON-NLS - gpsBookmarksFolder.addContent(new Element("Icon", ns).addContent(hrefBookmarks)); //NON-NLS - - Element gpsLastKnownLocationFolder = new Element("Folder", ns); //NON-NLS - CDATA cdataLastKnownLocation = new CDATA("https://raw.githubusercontent.com/sleuthkit/autopsy/develop/Core/src/org/sleuthkit/autopsy/images/gps-lastlocation.png"); //NON-NLS - Element hrefLastKnownLocation = new Element("href", ns).addContent(cdataLastKnownLocation); //NON-NLS - gpsLastKnownLocationFolder.addContent(new Element("Icon", ns).addContent(hrefLastKnownLocation)); //NON-NLS - - Element gpsRouteFolder = new Element("Folder", ns); //NON-NLS - CDATA cdataRoute = new CDATA("https://raw.githubusercontent.com/sleuthkit/autopsy/develop/Core/src/org/sleuthkit/autopsy/images/gps-trackpoint.png"); //NON-NLS - Element hrefRoute = new Element("href", ns).addContent(cdataRoute); //NON-NLS - gpsRouteFolder.addContent(new Element("Icon", ns).addContent(hrefRoute)); //NON-NLS - - Element gpsSearchesFolder = new Element("Folder", ns); //NON-NLS - CDATA cdataSearches = new CDATA("https://raw.githubusercontent.com/sleuthkit/autopsy/develop/Core/src/org/sleuthkit/autopsy/images/gps-search.png"); //NON-NLS - Element hrefSearches = new Element("href", ns).addContent(cdataSearches); //NON-NLS - gpsSearchesFolder.addContent(new Element("Icon", ns).addContent(hrefSearches)); //NON-NLS - - Element gpsTrackpointsFolder = new Element("Folder", ns); //NON-NLS - CDATA cdataTrackpoints = new CDATA("https://raw.githubusercontent.com/sleuthkit/autopsy/develop/Core/src/org/sleuthkit/autopsy/images/gps-trackpoint.png"); //NON-NLS - Element hrefTrackpoints = new Element("href", ns).addContent(cdataTrackpoints); //NON-NLS - gpsTrackpointsFolder.addContent(new Element("Icon", ns).addContent(hrefTrackpoints)); //NON-NLS - - gpsExifMetadataFolder.addContent(new Element("name", ns).addContent("EXIF Metadata")); //NON-NLS - gpsBookmarksFolder.addContent(new Element("name", ns).addContent("GPS Bookmarks")); //NON-NLS - gpsLastKnownLocationFolder.addContent(new Element("name", ns).addContent("GPS Last Known Location")); //NON-NLS - gpsRouteFolder.addContent(new Element("name", ns).addContent("GPS Routes")); //NON-NLS - gpsSearchesFolder.addContent(new Element("name", ns).addContent("GPS Searches")); //NON-NLS - gpsTrackpointsFolder.addContent(new Element("name", ns).addContent("GPS Trackpoints")); //NON-NLS - - document.addContent(gpsExifMetadataFolder); - document.addContent(gpsBookmarksFolder); - document.addContent(gpsLastKnownLocationFolder); - document.addContent(gpsRouteFolder); - document.addContent(gpsSearchesFolder); - document.addContent(gpsTrackpointsFolder); + Document kmlDocument = setupReportDocument(); ReportProgressPanel.ReportStatus result = ReportProgressPanel.ReportStatus.COMPLETE; - /** - * In the following code, nulls are okay, and are handled when we go to - * write out the KML feature. Nulls are expected to be returned from any - * method where the artifact is not found and is handled in the - * individual feature creation methods. This is done because we don't - * know beforehand which attributes will be included for which artifact, - * as anyone could write a module that adds additional attributes to an - * artifact. - * - * If there are any issues reading the database getting artifacts and - * attributes, or any exceptions thrown during this process, a severe - * error is logged, the report is marked as "Incomplete KML Report", and - * we use a best-effort method to generate KML information on everything - * we can successfully pull out of the database. - */ try { - for (BlackboardArtifact artifact : skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_METADATA_EXIF)) { - String fileName = ""; - long fileId = 0; - try { - Long timestamp = getLong(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_CREATED); - String desc = getDescriptionFromArtifact(artifact, "EXIF Metadata With Locations"); //NON-NLS - Double lat = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE); - Double lon = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE); - Element point = makePoint(lat, lon, getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_ALTITUDE)); - - if (lat != null && lat != 0.0 && lon != null && lon != 0.0) { - AbstractFile abstractFile = artifact.getSleuthkitCase().getAbstractFileById(artifact.getObjectID()); - fileName = abstractFile.getName(); - fileId = abstractFile.getId(); - Path path; - copyFileUsingStream(abstractFile, Paths.get(baseReportDir, abstractFile.getName()).toFile()); - try { - path = Paths.get(removeLeadingImgAndVol(abstractFile.getUniquePath())); - } catch (TskCoreException ex) { - path = Paths.get(abstractFile.getParentPath(), abstractFile.getName()); - } - String formattedCoordinates = String.format("%.2f, %.2f", lat, lon); - if (path == null) { - path = Paths.get(abstractFile.getName()); - } - gpsExifMetadataFolder.addContent(makePlacemarkWithPicture(abstractFile.getName(), FeatureColor.RED, desc, timestamp, point, path, formattedCoordinates)); - } - } catch (ReadContentInputStreamException ex) { - logger.log(Level.WARNING, String.format("Error reading file '%s' (id=%d).", fileName, fileId), ex); - } catch (Exception ex) { - errorMessage = Bundle.KMLReport_unableToExtractPhotos(); - logger.log(Level.SEVERE, errorMessage, ex); //NON-NLS - result = ReportProgressPanel.ReportStatus.ERROR; - } - } - } catch (TskCoreException ex) { - errorMessage = Bundle.KMLReport_exifPhotoError(); - logger.log(Level.SEVERE, errorMessage, ex); //NON-NLS - result = ReportProgressPanel.ReportStatus.ERROR; - } - - try { - for (BlackboardArtifact artifact : skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_BOOKMARK)) { - try { - Long timestamp = getLong(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME); - String desc = getDescriptionFromArtifact(artifact, "GPS Bookmark"); //NON-NLS - Double lat = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE); - Double lon = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE); - Element point = makePoint(lat, lon, getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_ALTITUDE)); - String bookmarkName = getString(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME); - String formattedCoordinates = String.format("%.2f, %.2f", lat, lon); - gpsBookmarksFolder.addContent(makePlacemark(bookmarkName, FeatureColor.BLUE, desc, timestamp, point, formattedCoordinates)); - } catch (Exception ex) { - errorMessage = Bundle.KMLReport_bookmarkError(); - logger.log(Level.SEVERE, errorMessage, ex); //NON-NLS - result = ReportProgressPanel.ReportStatus.ERROR; - } - } - } catch (TskCoreException ex) { - errorMessage = Bundle.KMLReport_gpsBookmarkError(); - logger.log(Level.SEVERE, errorMessage, ex); //NON-NLS - result = ReportProgressPanel.ReportStatus.ERROR; - } - - try { - for (BlackboardArtifact artifact : skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_LAST_KNOWN_LOCATION)) { - try { - Long timestamp = getLong(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME); - String desc = getDescriptionFromArtifact(artifact, "GPS Last Known Location"); //NON-NLS - Double lat = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE); - Double lon = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE); - Double alt = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_ALTITUDE); - Element point = makePoint(lat, lon, alt); - String formattedCoordinates = String.format("%.2f, %.2f", lat, lon); - gpsLastKnownLocationFolder.addContent(makePlacemark("Last Known Location", FeatureColor.PURPLE, desc, timestamp, point, formattedCoordinates)); //NON-NLS - } catch (Exception ex) { - errorMessage = Bundle.KMLReport_locationError(); - logger.log(Level.SEVERE, errorMessage, ex); //NON-NLS - result = ReportProgressPanel.ReportStatus.ERROR; - } - } - } catch (TskCoreException ex) { - errorMessage = Bundle.KMLReport_locationDatabaseError(); - logger.log(Level.SEVERE, errorMessage, ex); //NON-NLS - result = ReportProgressPanel.ReportStatus.ERROR; - } - - try { - for (BlackboardArtifact artifact : skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_ROUTE)) { - try { - Long timestamp = getLong(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME); - String desc = getDescriptionFromArtifact(artifact, "GPS Route"); - Double latitudeStart = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_START); - Double longitudeStart = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE_START); - Double latitudeEnd = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_END); - Double longitudeEnd = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE_END); - Double altitude = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_ALTITUDE); - - Element route = makeLineString(latitudeStart, longitudeStart, altitude, latitudeEnd, longitudeEnd, altitude); - Element startingPoint = makePoint(latitudeStart, longitudeStart, altitude); - Element endingPoint = makePoint(latitudeEnd, longitudeEnd, altitude); - - String formattedCoordinates = String.format("%.2f, %.2f to %.2f, %.2f", latitudeStart, longitudeStart, latitudeEnd, longitudeEnd); - gpsRouteFolder.addContent(makePlacemark("As-the-crow-flies Route", FeatureColor.GREEN, desc, timestamp, route, formattedCoordinates)); //NON-NLS - - formattedCoordinates = String.format("%.2f, %.2f", latitudeStart, longitudeStart); - gpsRouteFolder.addContent(makePlacemark("Start", FeatureColor.GREEN, desc, timestamp, startingPoint, formattedCoordinates)); //NON-NLS - - formattedCoordinates = String.format("%.2f, %.2f", latitudeEnd, longitudeEnd); - gpsRouteFolder.addContent(makePlacemark("End", FeatureColor.GREEN, desc, timestamp, endingPoint, formattedCoordinates)); //NON-NLS - } catch (Exception ex) { - errorMessage = Bundle.KMLReport_gpsRouteError(); - logger.log(Level.SEVERE, errorMessage, ex); //NON-NLS - result = ReportProgressPanel.ReportStatus.ERROR; - } - } - } catch (TskCoreException ex) { - errorMessage = Bundle.KMLReport_gpsRouteDatabaseError(); - logger.log(Level.SEVERE, errorMessage, ex); //NON-NLS - result = ReportProgressPanel.ReportStatus.ERROR; - } - - try { - for (BlackboardArtifact artifact : skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_SEARCH)) { - Long timestamp = getLong(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME); - String desc = getDescriptionFromArtifact(artifact, "GPS Search"); //NON-NLS - Double lat = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE); - Double lon = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE); - Double alt = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_ALTITUDE); - Element point = makePoint(lat, lon, alt); - String formattedCoordinates = String.format("%.2f, %.2f", lat, lon); - String searchName = getString(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME); - if (searchName == null || searchName.isEmpty()) { - searchName = getString(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_LOCATION); - } - if (searchName == null || searchName.isEmpty()) { - searchName = "GPS Search"; - } - gpsSearchesFolder.addContent(makePlacemark(searchName, FeatureColor.WHITE, desc, timestamp, point, formattedCoordinates)); //NON-NLS - } - } catch (TskCoreException ex) { - errorMessage = Bundle.KMLReport_gpsSearchDatabaseError(); - logger.log(Level.SEVERE, errorMessage, ex); //NON-NLS - result = ReportProgressPanel.ReportStatus.ERROR; - } - - try { - for (BlackboardArtifact artifact : skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_TRACKPOINT)) { - try { - Long timestamp = getLong(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME); - String desc = getDescriptionFromArtifact(artifact, "GPS Trackpoint"); //NON-NLS - Double lat = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE); - Double lon = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE); - Double alt = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_ALTITUDE); - Element point = makePoint(lat, lon, alt); - String formattedCoordinates = String.format("%.2f, %.2f, %.2f", lat, lon, alt); - String trackName = getString(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME); - if (trackName == null || trackName.isEmpty()) { - trackName = getString(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME); - } - if (trackName == null || trackName.isEmpty()) { - trackName = getString(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_FLAG); - } - if (trackName == null || trackName.isEmpty()) { - trackName = "GPS Trackpoint"; - } - gpsTrackpointsFolder.addContent(makePlacemark(trackName, FeatureColor.YELLOW, desc, timestamp, point, formattedCoordinates)); - } catch (Exception ex) { - errorMessage = Bundle.KMLReport_trackpointError(); - logger.log(Level.SEVERE, errorMessage, ex); //NON-NLS - result = ReportProgressPanel.ReportStatus.ERROR; - } - } - } catch (TskCoreException ex) { - errorMessage = Bundle.KMLReport_trackpointDatabaseError(); + makeRoutes(skCase); + addLocationsToReport(skCase, baseReportDir); + } catch (GeoLocationDataException | IOException ex) { + errorMessage = "Failed to complete report."; logger.log(Level.SEVERE, errorMessage, ex); //NON-NLS result = ReportProgressPanel.ReportStatus.ERROR; } @@ -442,247 +211,256 @@ class KMLReport implements GeneralReportModule { } /** - * Get a Double from an artifact if it exists, return null otherwise. + * Do all of the setting up of elements needed for the report. * - * @param artifact The artifact to query - * @param type The attribute type we're looking for - * - * @return The Double if it exists, or null if not + * @return The report document object. */ - private Double getDouble(BlackboardArtifact artifact, BlackboardAttribute.ATTRIBUTE_TYPE type) { - Double returnValue = null; - try { - BlackboardAttribute bba = artifact.getAttribute(new BlackboardAttribute.Type(type)); - if (bba != null) { - Double value = bba.getValueDouble(); - returnValue = value; - } - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error getting Double value: " + type.toString(), ex); //NON-NLS + private Document setupReportDocument() { + ns = Namespace.getNamespace("", "http://www.opengis.net/kml/2.2"); //NON-NLS + + Element kml = new Element("kml", ns); //NON-NLS + kml.addNamespaceDeclaration(Namespace.getNamespace("gx", "http://www.google.com/kml/ext/2.2")); //NON-NLS + kml.addNamespaceDeclaration(Namespace.getNamespace("kml", "http://www.opengis.net/kml/2.2")); //NON-NLS + kml.addNamespaceDeclaration(Namespace.getNamespace("atom", "http://www.w3.org/2005/Atom")); //NON-NLS + Document kmlDocument = new Document(kml); + + Element document = new Element("Document", ns); //NON-NLS + kml.addContent(document); + + Element name = new Element("name", ns); //NON-NLS + ReportBranding rb = new ReportBranding(); + name.setText(rb.getReportTitle() + " KML"); //NON-NLS + document.addContent(name); + + // Check if ingest has finished + if (IngestManager.getInstance().isIngestRunning()) { + Element ingestwarning = new Element("snippet", ns); //NON-NLS + ingestwarning.addContent(NbBundle.getMessage(this.getClass(), "ReportBodyFile.ingestWarning.text")); //NON-NLS + document.addContent(ingestwarning); } - return returnValue; + + // Create folder structure + gpsExifMetadataFolder = new Element("Folder", ns); //NON-NLS + CDATA cdataExifMetadataFolder = new CDATA("https://raw.githubusercontent.com/sleuthkit/autopsy/develop/Core/src/org/sleuthkit/autopsy/images/camera-icon-16.png"); //NON-NLS + Element hrefExifMetadata = new Element("href", ns).addContent(cdataExifMetadataFolder); //NON-NLS + gpsExifMetadataFolder.addContent(new Element("Icon", ns).addContent(hrefExifMetadata)); //NON-NLS + + gpsBookmarksFolder = new Element("Folder", ns); //NON-NLS + CDATA cdataBookmarks = new CDATA("https://raw.githubusercontent.com/sleuthkit/autopsy/develop/Core/src/org/sleuthkit/autopsy/images/gpsfav.png"); //NON-NLS + Element hrefBookmarks = new Element("href", ns).addContent(cdataBookmarks); //NON-NLS + gpsBookmarksFolder.addContent(new Element("Icon", ns).addContent(hrefBookmarks)); //NON-NLS + + gpsLastKnownLocationFolder = new Element("Folder", ns); //NON-NLS + CDATA cdataLastKnownLocation = new CDATA("https://raw.githubusercontent.com/sleuthkit/autopsy/develop/Core/src/org/sleuthkit/autopsy/images/gps-lastlocation.png"); //NON-NLS + Element hrefLastKnownLocation = new Element("href", ns).addContent(cdataLastKnownLocation); //NON-NLS + gpsLastKnownLocationFolder.addContent(new Element("Icon", ns).addContent(hrefLastKnownLocation)); //NON-NLS + + gpsRouteFolder = new Element("Folder", ns); //NON-NLS + CDATA cdataRoute = new CDATA("https://raw.githubusercontent.com/sleuthkit/autopsy/develop/Core/src/org/sleuthkit/autopsy/images/gps-trackpoint.png"); //NON-NLS + Element hrefRoute = new Element("href", ns).addContent(cdataRoute); //NON-NLS + gpsRouteFolder.addContent(new Element("Icon", ns).addContent(hrefRoute)); //NON-NLS + + gpsSearchesFolder = new Element("Folder", ns); //NON-NLS + CDATA cdataSearches = new CDATA("https://raw.githubusercontent.com/sleuthkit/autopsy/develop/Core/src/org/sleuthkit/autopsy/images/gps-search.png"); //NON-NLS + Element hrefSearches = new Element("href", ns).addContent(cdataSearches); //NON-NLS + gpsSearchesFolder.addContent(new Element("Icon", ns).addContent(hrefSearches)); //NON-NLS + + gpsTrackpointsFolder = new Element("Folder", ns); //NON-NLS + CDATA cdataTrackpoints = new CDATA("https://raw.githubusercontent.com/sleuthkit/autopsy/develop/Core/src/org/sleuthkit/autopsy/images/gps-trackpoint.png"); //NON-NLS + Element hrefTrackpoints = new Element("href", ns).addContent(cdataTrackpoints); //NON-NLS + gpsTrackpointsFolder.addContent(new Element("Icon", ns).addContent(hrefTrackpoints)); //NON-NLS + + gpsExifMetadataFolder.addContent(new Element("name", ns).addContent("EXIF Metadata")); //NON-NLS + gpsBookmarksFolder.addContent(new Element("name", ns).addContent("GPS Bookmarks")); //NON-NLS + gpsLastKnownLocationFolder.addContent(new Element("name", ns).addContent("GPS Last Known Location")); //NON-NLS + gpsRouteFolder.addContent(new Element("name", ns).addContent("GPS Routes")); //NON-NLS + gpsSearchesFolder.addContent(new Element("name", ns).addContent("GPS Searches")); //NON-NLS + gpsTrackpointsFolder.addContent(new Element("name", ns).addContent("GPS Trackpoints")); //NON-NLS + + document.addContent(gpsExifMetadataFolder); + document.addContent(gpsBookmarksFolder); + document.addContent(gpsLastKnownLocationFolder); + document.addContent(gpsRouteFolder); + document.addContent(gpsSearchesFolder); + document.addContent(gpsTrackpointsFolder); + + return kmlDocument; } /** - * Get a Long from an artifact if it exists, return null otherwise. + * For the given point, create the data needed for the EXIF_METADATA * - * @param artifact The artifact to query - * @param type The attribute type we're looking for + * @param location The geolocation of the data + * @param baseReportDirectory The report directory where the image will be + * created. * - * @return The Long if it exists, or null if not + * @throws IOException */ - private Long getLong(BlackboardArtifact artifact, BlackboardAttribute.ATTRIBUTE_TYPE type) { - Long returnValue = null; - try { - BlackboardAttribute bba = artifact.getAttribute(new BlackboardAttribute.Type(type)); - if (bba != null) { - Long value = bba.getValueLong(); - returnValue = value; + void addExifMetadataContent(List points, String baseReportDirectory) throws IOException { + for(Waypoint point: points) { + Element mapPoint = makePoint(point); + if (mapPoint == null) { + return; } - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error getting Long value: " + type.toString(), ex); //NON-NLS + + AbstractFile abstractFile = point.getImage(); + String details = getFormattedDetails(point, Bundle.Waypoint_EXIF_Display_String()); + + Path path; + copyFileUsingStream(abstractFile, Paths.get(baseReportDirectory, abstractFile.getName()).toFile()); + try { + path = Paths.get(removeLeadingImgAndVol(abstractFile.getUniquePath())); + } catch (TskCoreException ex) { + path = Paths.get(abstractFile.getParentPath(), abstractFile.getName()); + } + if (path == null) { + path = Paths.get(abstractFile.getName()); + } + + gpsExifMetadataFolder.addContent(makePlacemarkWithPicture(abstractFile.getName(), FeatureColor.RED, details, point.getTimestamp(), mapPoint, path, formattedCoordinates(point.getLatitude(), point.getLongitude()))); } - return returnValue; } /** - * Get an Integer from an artifact if it exists, return null otherwise. + * Add the new location to the correct folder based on artifact type. * - * @param artifact The artifact to query - * @param type The attribute type we're looking for + * @param skCase Currently open case + * @param baseReportDir Output directory for the report. * - * @return The Integer if it exists, or null if not + * @throws TskCoreException + * @throws IOException */ - private Integer getInteger(BlackboardArtifact artifact, BlackboardAttribute.ATTRIBUTE_TYPE type) { - Integer returnValue = null; - try { - BlackboardAttribute bba = artifact.getAttribute(new BlackboardAttribute.Type(type)); - if (bba != null) { - Integer value = bba.getValueInt(); - returnValue = value; - } - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error getting Integer value: " + type.toString(), ex); //NON-NLS + void addLocationsToReport(SleuthkitCase skCase, String baseReportDir) throws GeoLocationDataException, IOException { + addExifMetadataContent(Waypoint.getEXIFWaypoints(skCase), baseReportDir); + addWaypoints(Waypoint.getBookmarkWaypoints(skCase), gpsBookmarksFolder, FeatureColor.BLUE, Bundle.Waypoint_Bookmark_Display_String()); + addWaypoints(Waypoint.getLastKnownWaypoints(skCase), gpsLastKnownLocationFolder, FeatureColor.PURPLE, Bundle.Waypoint_Last_Known_Display_String()); + addWaypoints(Waypoint.getSearchWaypoints(skCase), gpsSearchesFolder, FeatureColor.WHITE, Bundle.Waypoint_Search_Display_String()); + addWaypoints(Waypoint.getTrackpointWaypoints(skCase), gpsTrackpointsFolder, FeatureColor.WHITE, Bundle.Waypoint_Trackpoint_Display_String()); + } + + /** + * For each point in the waypoint list an Element to represent the given waypoint + * is created and added it to the given Element folder. + * + * @param points List of waypoints to add to the report + * @param folder The Element folder to add the points to + * @param waypointColor The color the waypoint should appear in the report + */ + void addWaypoints(List points, Element folder, FeatureColor waypointColor, String headerLabel) { + for(Waypoint point: points) { + addContent(folder, point.getLabel(), waypointColor, getFormattedDetails(point, headerLabel), point.getTimestamp(), makePoint(point), point.getLatitude(), point.getLongitude()); + } + } + + /** + * Adds the waypoint Element with details to the report in the given folder. + * + * @param folder Element folder to add the waypoint to + * @param waypointLabel String waypoint Label + * @param waypointColor FeatureColor for the waypoint + * @param formattedDetails String HTML formatted waypoint details + * @param timestamp Long timestamp (unix\jave epoch seconds) + * @param point Element point object + * @param latitude Double latitude value + * @param longitude Double longitude value + */ + void addContent(Element folder, String waypointLabel, FeatureColor waypointColor, String formattedDetails, Long timestamp, Element point, Double latitude, Double longitude) { + if(folder != null && point != null) { + String formattedCords = formattedCoordinates(latitude, longitude); + folder.addContent(makePlacemark(waypointLabel, waypointColor, formattedDetails, timestamp, point, formattedCords)); } - return returnValue; } /** - * Get a String from an artifact if it exists, return null otherwise. + * Add the route to the route folder in the document. * - * @param artifact The artifact to query - * @param type The attribute type we're looking for + * @param skCase Currently open case. * - * @return The String if it exists, or null if not + * @throws TskCoreException */ - private String getString(BlackboardArtifact artifact, BlackboardAttribute.ATTRIBUTE_TYPE type) { - String returnValue = null; - try { - BlackboardAttribute bba = artifact.getAttribute(new BlackboardAttribute.Type(type)); - if (bba != null) { - String value = bba.getValueString(); - if (value != null && !value.isEmpty()) { - returnValue = value; - } - } - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error getting String value: " + type.toString(), ex); //NON-NLS + void makeRoutes(SleuthkitCase skCase) throws GeoLocationDataException { + List routes = Route.getRoutes(skCase); + + if(routes == null) { + return; + } + + for (Route route : routes) { + addRouteToReport(route); + } + } + + void addRouteToReport(Route route) { + List routePoints = route.getRoute(); + Waypoint start = null; + Waypoint end = null; + // This is hardcoded knowledge that there is only two points + // a start and end. In the long run it would be nice to + // support the idea of a route with multiple points. The Route + // class supports that idea. Would be nice to figure out how to support + // for report. + if (routePoints != null && routePoints.size() > 1) { + start = routePoints.get(0); + end = routePoints.get(1); + } + + if (start == null || end == null) { + return; + } + + Element reportRoute = makeLineString(start.getLatitude(), start.getLongitude(), end.getLatitude(), end.getLongitude()); + Element startingPoint = makePoint(start.getLatitude(), start.getLongitude(), start.getAltitude()); + Element endingPoint = makePoint(end.getLatitude(), end.getLongitude(), end.getAltitude()); + + String formattedEnd = formattedCoordinates(end.getLatitude(), end.getLongitude()); + String formattedStart = formattedCoordinates(start.getLatitude(), start.getLongitude()); + + String formattedCoordinates = String.format("%s to %s", formattedStart, formattedEnd); + + if (reportRoute != null) { + gpsRouteFolder.addContent(makePlacemark(route.getLabel(), FeatureColor.GREEN, getFormattedDetails(route), route.getTimestamp(), reportRoute, formattedCoordinates)); //NON-NLS + } + + if (startingPoint != null) { + gpsRouteFolder.addContent(makePlacemark(start.getLabel(), + FeatureColor.GREEN, getFormattedDetails(start, Bundle.Waypoint_Route_Point_Display_String()), + start.getTimestamp(), startingPoint, formattedStart)); //NON-NLS + } + + if (endingPoint != null) { + gpsRouteFolder.addContent(makePlacemark(end.getLabel(), + FeatureColor.GREEN, + getFormattedDetails(end, Bundle.Waypoint_Route_Point_Display_String()), + end.getTimestamp(), endingPoint, formattedEnd)); //NON-NLS } - return returnValue; } /** - * This method creates a text description for a map feature using all the - * geospatial and time data we can for the Artifact. It queries the - * following attributes: + * Format a point time stamp (in seconds) to the report format. * - * TSK_GEO_LATITUDE 54; TSK_GEO_LONGITUDE 55; TSK_GEO_LATITUDE_START 98; - * TSK_GEO_LATITUDE_END 99; TSK_GEO_LONGITUDE_START 100; - * TSK_GEO_LONGITUDE_END 101; TSK_GEO_VELOCITY 56; TSK_GEO_ALTITUDE 57; - * TSK_GEO_BEARING 58; TSK_GEO_HPRECISION 59; TSK_GEO_VPRECISION 60; - * TSK_GEO_MAPDATUM 61; TSK_DATETIME_START 83; TSK_DATETIME_END 84; - * TSK_LOCATION 86; TSK_PATH_SOURCE 94; + * @param timeStamp The timestamp in epoch seconds. * - * @param artifact the artifact to query. - * @param featureType the type of Artifact we're working on. - * - * @return a String with the information we have available + * @return The formatted timestamp */ - private String getDescriptionFromArtifact(BlackboardArtifact artifact, String featureType) { - StringBuilder result = new StringBuilder("

" + featureType + "

"); //NON-NLS - - String name = getString(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME); - if (name != null && !name.isEmpty()) { - result.append("Name: ").append(name).append(SEP); //NON-NLS - } - - String location = getString(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_LOCATION); - if (location != null && !location.isEmpty()) { - result.append("Location: ").append(location).append(SEP); //NON-NLS - } - - Long timestamp = getLong(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME); - if (timestamp != null) { - result.append("Timestamp: ").append(getTimeStamp(timestamp)).append(SEP); //NON-NLS - result.append("Unix timestamp: ").append(timestamp).append(SEP); //NON-NLS - } - - Long startingTimestamp = getLong(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_START); - if (startingTimestamp != null) { - result.append("Starting Timestamp: ").append(getTimeStamp(startingTimestamp)).append(SEP); //NON-NLS - result.append("Starting Unix timestamp: ").append(startingTimestamp).append(SEP); //NON-NLS - } - - Long endingTimestamp = getLong(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_END); - if (endingTimestamp != null) { - result.append("Ending Timestamp: ").append(getTimeStamp(endingTimestamp)).append(SEP); //NON-NLS - result.append("Ending Unix timestamp: ").append(endingTimestamp).append(SEP); //NON-NLS - } - - Long createdTimestamp = getLong(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_CREATED); - if (createdTimestamp != null) { - result.append("Created Timestamp: ").append(getTimeStamp(createdTimestamp)).append(SEP); //NON-NLS - result.append("Created Unix timestamp: ").append(createdTimestamp).append(SEP); //NON-NLS - } - - Double latitude = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE); - if (latitude != null) { - result.append("Latitude: ").append(latitude).append(SEP); //NON-NLS - } - - Double longitude = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE); - if (longitude != null) { - result.append("Longitude: ").append(longitude).append(SEP); //NON-NLS - } - - Double latitudeStart = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_START); - if (latitudeStart != null) { - result.append("Latitude Start: ").append(latitudeStart).append(SEP); //NON-NLS - } - - Double longitudeStart = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE_START); - if (longitudeStart != null) { - result.append("Longitude Start: ").append(longitudeStart).append(SEP); //NON-NLS - } - - Double latitudeEnd = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_END); - if (latitudeEnd != null) { - result.append("Latitude End: ").append(latitudeEnd).append(SEP); //NON-NLS - } - - Double longitudeEnd = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE_END); - if (longitudeEnd != null) { - result.append("Longitude End: ").append(longitudeEnd).append(SEP); //NON-NLS - } - - Double velocity = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_VELOCITY); - if (velocity != null) { - result.append("Velocity: ").append(velocity).append(SEP); //NON-NLS - } - - Double altitude = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_ALTITUDE); - if (altitude != null) { - result.append("Altitude: ").append(altitude).append(SEP); //NON-NLS - } - - Double bearing = getDouble(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_BEARING); - if (bearing != null) { - result.append("Bearing: ").append(bearing).append(SEP); //NON-NLS - } - - Integer hPrecision = getInteger(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_HPRECISION); - if (hPrecision != null) { - result.append("Horizontal Precision Figure of Merit: ").append(hPrecision).append(SEP); //NON-NLS - } - - Integer vPrecision = getInteger(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_VPRECISION); - if (vPrecision != null) { - result.append("Vertical Precision Figure of Merit: ").append(vPrecision).append(SEP); //NON-NLS - } - - String mapDatum = getString(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_MAPDATUM); - if (mapDatum != null && !mapDatum.isEmpty()) { - result.append("Map Datum: ").append(mapDatum).append(SEP); //NON-NLS - } - - String programName = getString(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME); - if (programName != null && !programName.isEmpty()) { - result.append("Reported by: ").append(programName).append(SEP); //NON-NLS - } - - String flag = getString(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_FLAG); - if (flag != null && !flag.isEmpty()) { - result.append("Flag: ").append(flag).append(SEP); //NON-NLS - } - - String pathSource = getString(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH_SOURCE); - if (pathSource != null && !pathSource.isEmpty()) { - result.append("Source: ").append(pathSource).append(SEP); //NON-NLS - } - - String deviceMake = getString(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DEVICE_MAKE); - if (deviceMake != null && !deviceMake.isEmpty()) { - result.append("Device Make: ").append(deviceMake).append(SEP); //NON-NLS - } - - String deviceModel = getString(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DEVICE_MODEL); - if (deviceModel != null && !deviceModel.isEmpty()) { - result.append("Device Model: ").append(deviceModel).append(SEP); //NON-NLS - } - - return result.toString(); - } - private String getTimeStamp(long timeStamp) { return kmlDateFormat.format(new java.util.Date(timeStamp * 1000)); } + /** + * Create the point for the given artifact. + * + * @param point Artifact point. + * + * @return point element. + */ + private Element makePoint(Waypoint point) { + return makePoint(point.getLatitude(), point.getLongitude(), point.getAltitude()); + } + /** * Create a Point for use in a Placemark. Note in this method altitude is * ignored, as Google Earth apparently has trouble using altitudes for - * LineStrings, though the parameters are still in the call. Also note that - * any null value passed in will be set to 0.0, under the idea that it is - * better to show some data with gaps, than to show nothing at all. + * LineStrings, though the parameters are still in the call. * * @param latitude point latitude * @param longitude point longitude @@ -691,21 +469,16 @@ class KMLReport implements GeneralReportModule { * @return the Point as an Element */ private Element makePoint(Double latitude, Double longitude, Double altitude) { - if (latitude == null) { - latitude = 0.0; - } - if (longitude == null) { - longitude = 0.0; - } - if (altitude == null) { - altitude = 0.0; + if (latitude == null || longitude == null) { + return null; } + Element point = new Element("Point", ns); //NON-NLS - // KML uses lon, lat. Deliberately reversed. - Element coordinates = new Element("coordinates", ns).addContent(longitude + "," + latitude + "," + altitude); //NON-NLS + // KML uses lon, lat. Deliberately reversed.1 + Element coordinates = new Element("coordinates", ns).addContent(longitude + "," + latitude + "," + (altitude != null ? altitude : 0.0)); //NON-NLS - if (altitude != 0) { + if (altitude != null && altitude != 0) { /* * Though we are including a non-zero altitude, clamp it to the * ground because inaccuracies from the GPS data can cause the @@ -725,9 +498,10 @@ class KMLReport implements GeneralReportModule { * Create a LineString for use in a Placemark. Note in this method, start * and stop altitudes get ignored, as Google Earth apparently has trouble * using altitudes for LineStrings, though the parameters are still in the - * call. Also note that any null value passed in will be set to 0.0, under - * the idea that it is better to show some data with gaps, than to show - * nothing at all. + * call. + * + * If null values are pass for the latitudes or longitudes a line will not + * be drawn. * * @param startLatitude Starting latitude * @param startLongitude Starting longitude @@ -738,24 +512,9 @@ class KMLReport implements GeneralReportModule { * * @return the Line as an Element */ - private Element makeLineString(Double startLatitude, Double startLongitude, Double startAltitude, Double stopLatitude, Double stopLongitude, Double stopAltitude) { - if (startLatitude == null) { - startLatitude = 0.0; - } - if (startLongitude == null) { - startLongitude = 0.0; - } - if (startAltitude == null) { - startAltitude = 0.0; - } - if (stopLatitude == null) { - stopLatitude = 0.0; - } - if (stopLongitude == null) { - stopLongitude = 0.0; - } - if (stopAltitude == null) { - stopAltitude = 0.0; + private Element makeLineString(Double startLatitude, Double startLongitude, Double stopLatitude, Double stopLongitude) { + if (startLatitude == null || startLongitude == null || stopLatitude == null || stopLongitude == null) { + return null; } Element lineString = new Element("LineString", ns); //NON-NLS @@ -925,4 +684,111 @@ class KMLReport implements GeneralReportModule { } return strbuf.toString(); } + + /** + * Get the nicely formatted details for the given waypoint. + * + * @param point Waypoint object + * @param header String details header + * + * @return HTML formatted String of details for given waypoint + */ + private String getFormattedDetails(Waypoint point, String header) { + StringBuilder result = new StringBuilder(); //NON-NLS + result.append(String.format("

%s

", header)) + .append(formatAttribute("Name", point.getLabel())); + + Long timestamp = point.getTimestamp(); + if (timestamp != null) { + result.append(formatAttribute("Timestamp", getTimeStamp(timestamp))); + } + + result.append(formatAttribute("Latitude", point.getLatitude().toString())) + .append(formatAttribute("Longitude", point.getLongitude().toString())); + + if (point.getAltitude() != null) { + result.append(formatAttribute("Altitude", point.getAltitude().toString())); + } + + List list = point.getOtherProperties(); + for(Waypoint.Property prop: list) { + String value = prop.getValue(); + if(value != null && !value.isEmpty()) { + result.append(formatAttribute(prop.getDisplayName(), value)); + } + } + + return result.toString(); + } + + private String formatAttribute(String title, String value) { + return String.format(HTML_PROP_FORMAT, title, value); + } + + /** + * Returns an HTML formatted string of all the + * + * @param route + * + * @return A HTML formatted list of the Route attributes + */ + private String getFormattedDetails(Route route) { + List points = route.getRoute(); + StringBuilder result = new StringBuilder(); //NON-NLS + + result.append(String.format("

%s

", Bundle.Route_Details_Header())) + .append(formatAttribute("Name", route.getLabel())); + + Long timestamp = route.getTimestamp(); + if (timestamp != null) { + result.append(formatAttribute("Timestamp", getTimeStamp(timestamp))); + } + + if (points.size() > 1) { + Waypoint start = points.get(0); + Waypoint end = points.get(1); + + result.append(formatAttribute("Start Latitude", start.getLatitude().toString())) + .append(formatAttribute("Start Longitude", start.getLongitude().toString())); + + Double altitude = start.getAltitude(); + if(altitude != null) { + result.append(formatAttribute("Start Altitude", altitude.toString())); + } + + result.append(formatAttribute("End Latitude", end.getLatitude().toString())) + .append(formatAttribute("End Longitude", end.getLongitude().toString())); + + altitude = end.getAltitude(); + if(altitude != null) { + result.append(formatAttribute("End Altitude", altitude.toString())); + } + } + + List list = route.getOtherProperties(); + for(Waypoint.Property prop: list) { + String value = prop.getValue(); + if(value != null && !value.isEmpty()) { + result.append(formatAttribute(prop.getDisplayName(), value)); + } + } + + return result.toString(); + } + + /** + * Helper functions for consistently formatting longitude and latitude. + * + * @param latitude Double latitude value + * @param longitude Double longitude value + * + * @return String Nicely formatted double values separated by a comma + */ + private String formattedCoordinates(Double latitude, Double longitude) { + if (latitude == null || longitude == null) { + return ""; + } + + return String.format("%.2f, %.2f", latitude, longitude); + } } diff --git a/Core/src/org/sleuthkit/autopsy/report/modules/portablecase/PortableCaseReportModule.java b/Core/src/org/sleuthkit/autopsy/report/modules/portablecase/PortableCaseReportModule.java index de3350ad88..38e53b5eb6 100644 --- a/Core/src/org/sleuthkit/autopsy/report/modules/portablecase/PortableCaseReportModule.java +++ b/Core/src/org/sleuthkit/autopsy/report/modules/portablecase/PortableCaseReportModule.java @@ -768,6 +768,7 @@ public class PortableCaseReportModule implements ReportModule { oldAttr.getValueLong())); break; case STRING: + case JSON: newAttrs.add(new BlackboardAttribute(newAttributeType, String.join(",", oldAttr.getSources()), oldAttr.getValueString())); break; diff --git a/Core/src/org/sleuthkit/autopsy/test/CustomArtifactType.java b/Core/src/org/sleuthkit/autopsy/test/CustomArtifactType.java index b27fa29dd6..e584c6bdfc 100644 --- a/Core/src/org/sleuthkit/autopsy/test/CustomArtifactType.java +++ b/Core/src/org/sleuthkit/autopsy/test/CustomArtifactType.java @@ -50,6 +50,8 @@ final class CustomArtifactType { private static final String BYTES_ATTR_DISPLAY_NAME = "Custom Bytes"; private static final String STRING_ATTR_TYPE_NAME = "CUSTOM_STRING_ATTRIBUTE"; private static final String STRING_ATTR_DISPLAY_NAME = "Custom String"; + private static final String JSON_ATTR_TYPE_NAME = "CUSTOM_JSON_ATTRIBUTE"; + private static final String JSON_ATTR_DISPLAY_NAME = "Custom Json"; private static BlackboardArtifact.Type artifactType; private static BlackboardAttribute.Type intAttrType; private static BlackboardAttribute.Type doubleAttrType; @@ -57,6 +59,7 @@ final class CustomArtifactType { private static BlackboardAttribute.Type dateTimeAttrType; private static BlackboardAttribute.Type bytesAttrType; private static BlackboardAttribute.Type stringAttrType; + private static BlackboardAttribute.Type jsonAttrType; /** * Adds the custom artifact type, with its associated custom attribute @@ -73,6 +76,7 @@ final class CustomArtifactType { dateTimeAttrType = blackboard.getOrAddAttributeType(DATETIME_ATTR_TYPE_NAME, BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DATETIME, DATETIME_ATTR_DISPLAY_NAME); bytesAttrType = blackboard.getOrAddAttributeType(BYTES_ATTR_TYPE_NAME, BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.BYTE, BYTES_ATTR_DISPLAY_NAME); stringAttrType = blackboard.getOrAddAttributeType(STRING_ATTR_TYPE_NAME, BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING, STRING_ATTR_DISPLAY_NAME); + jsonAttrType = blackboard.getOrAddAttributeType(JSON_ATTR_TYPE_NAME, BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.JSON, JSON_ATTR_DISPLAY_NAME); } /** @@ -93,6 +97,7 @@ final class CustomArtifactType { attributes.add(new BlackboardAttribute(dateTimeAttrType, MODULE_NAME, 60L)); attributes.add(new BlackboardAttribute(bytesAttrType, MODULE_NAME, DatatypeConverter.parseHexBinary("ABCD"))); attributes.add(new BlackboardAttribute(stringAttrType, MODULE_NAME, "Zero")); + attributes.add(new BlackboardAttribute(jsonAttrType, MODULE_NAME, "{\"fruit\": \"Apple\",\"size\": \"Large\",\"color\": \"Red\"}")); artifact.addAttributes(attributes); /* diff --git a/Core/src/org/sleuthkit/autopsy/textextractors/SqliteTextExtractor.java b/Core/src/org/sleuthkit/autopsy/textextractors/SqliteTextExtractor.java index 889062189c..50151c9da9 100755 --- a/Core/src/org/sleuthkit/autopsy/textextractors/SqliteTextExtractor.java +++ b/Core/src/org/sleuthkit/autopsy/textextractors/SqliteTextExtractor.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2018-2018 Basis Technology Corp. + * Copyright 2018-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -44,7 +44,7 @@ final class SqliteTextExtractor implements TextExtractor { private static final Logger logger = Logger.getLogger(SqliteTextExtractor.class.getName()); private final AbstractFile file; - public SqliteTextExtractor(AbstractFile file) { + SqliteTextExtractor(AbstractFile file) { this.file = file; } /** @@ -101,7 +101,7 @@ final class SqliteTextExtractor implements TextExtractor { * * @param file Sqlite file */ - public SQLiteStreamReader(AbstractFile file) { + SQLiteStreamReader(AbstractFile file) { this.file = file; reader = new SQLiteTableReader.Builder(file) .forAllColumnNames(getColumnNameStrategy()) @@ -140,7 +140,7 @@ final class SqliteTextExtractor implements TextExtractor { } fillBuffer(objectStr); - columnIndex = columnIndex % totalColumns; + columnIndex %= totalColumns; } }; } @@ -171,7 +171,7 @@ final class SqliteTextExtractor implements TextExtractor { fillBuffer(columnName + ((columnIndex == totalColumns) ? "\n" : " ")); //Reset the columnCount to 0 for next table read - columnIndex = columnIndex % totalColumns; + columnIndex %= totalColumns; } }; } @@ -204,7 +204,7 @@ final class SqliteTextExtractor implements TextExtractor { */ @Override public int read(char[] cbuf, int off, int len) throws IOException { - buf = cbuf; + buf = cbuf; //needs to be the same memory address and not a copy of the contents since we are filling it in bufIndex = off; @@ -277,12 +277,12 @@ final class SqliteTextExtractor implements TextExtractor { private final String entity; private Integer pointer; - public ExcessBytes(String entity, Integer pointer) { + ExcessBytes(String entity, Integer pointer) { this.entity = entity; this.pointer = pointer; } - public boolean isFinished() { + boolean isFinished() { return entity.length() == pointer; } @@ -296,7 +296,7 @@ final class SqliteTextExtractor implements TextExtractor { * * @return number of characters read into the buffer */ - public int read(char[] buf, int off, int len) { + int read(char[] buf, int off, int len) { for (int i = off; i < len; i++) { if (isFinished()) { return i - off; 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..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,16 +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 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 machine translation software did not return any text. +# {0} - exception message +TranslatedContentViewer.errorExtractingText=An error occurred while extracting the text ({0}). +TranslatedContentViewer.extractingText=Extracting text, please wait... +TranslatedContentViewer.fileHasNoText=File has no text. +TranslatedContentViewer.noServiceProvider=The machine translation software was not found. TranslatedContentViewer.translatingText=Translating text, please wait... -TranslatedContentViewer.translationException=Error encountered while attempting translation. +# {0} - exception message +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 8c49c23ed7..7e07545b49 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,74 @@ public final class TranslatedTextViewer implements TextViewer { } /** - * Fetches file text and performs translation. + * Extracts text from a file 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.extractingText=Extracting text, please wait...", + "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.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 { 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_extractingText(), ComponentOrientation.LEFT_TO_RIGHT, Font.ITALIC); + }); + String fileText; + try { + fileText = getFileText(file); + } catch (IOException | TextExtractor.InitReaderException 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()); } - } - /** - * 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); + if (this.isCancelled()) { + throw new InterruptedException(); } + + if (fileText == null || fileText.isEmpty()) { + return Bundle.TranslatedContentViewer_fileHasNoText(); + } + + if (!this.translateText) { + return fileText; + } + + SwingUtilities.invokeLater(() -> { + panel.display(Bundle.TranslatedContentViewer_translatingText(), ComponentOrientation.LEFT_TO_RIGHT, Font.ITALIC); + }); + String translation; + try { + translation = translate(fileText); + } catch (NoServiceProviderException 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 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 @@ -227,8 +240,12 @@ 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, 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); } } @@ -240,36 +257,21 @@ 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."}) - private String translate(String input) throws InterruptedException { - if (this.isCancelled()) { - throw new InterruptedException(); - } - - panel.display(Bundle.TranslatedContentViewer_translatingText(), - ComponentOrientation.LEFT_TO_RIGHT, Font.ITALIC); - - 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, "Error translating text", ex); - return Bundle.TranslatedContentViewer_translationException() + " (" + ex.getMessage() + ")"; + "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); + if (translatedResult.isEmpty()) { + return Bundle.TranslatedContentViewer_emptyTranslation(); } + return translatedResult; } /** * Extracts text from the given node * - * @param node Selected node in UI + * @param file Selected node in UI * * @return Extracted text * @@ -277,33 +279,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 +339,7 @@ public final class TranslatedTextViewer implements TextViewer { textBuilder.append(cbuf, 0, read); bytesRead += read; } - + return textBuilder.toString(); } @@ -399,23 +390,28 @@ public final class TranslatedTextViewer implements TextViewer { */ private abstract class SelectionChangeListener implements ActionListener { - public String currentSelection = null; + private String currentSelection; - public abstract String getSelection(); + abstract String getSelection(); @Override public final void actionPerformed(ActionEvent e) { 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); + 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 //the number of jobs running to one. - executorService.execute(updateTask); + executorService.execute(backgroundTask); } } } @@ -426,7 +422,7 @@ public final class TranslatedTextViewer implements TextViewer { private class DisplayDropDownChangeListener extends SelectionChangeListener { @Override - public String getSelection() { + String getSelection() { return panel.getDisplayDropDownSelection(); } } @@ -437,7 +433,7 @@ public final class TranslatedTextViewer implements TextViewer { private class OCRDropdownChangeListener extends SelectionChangeListener { @Override - public String getSelection() { + String getSelection() { return panel.getSelectedOcrLanguagePack(); } } diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/FileExportRuleSet.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/FileExportRuleSet.java index 399ae574ba..198dd8c5bd 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/FileExportRuleSet.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/FileExportRuleSet.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2015-2018 Basis Technology Corp. + * Copyright 2015-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -178,6 +178,20 @@ final class FileExportRuleSet implements Serializable, Comparable(); } + /** + * Sort the file size conditions of this rule. + */ + void sortFileSizeConditions() { + this.fileSizeConditions.sort(null); + } + + /** + * Sort the artifact conditions of this rule. + */ + void sortArtifactConditions() { + this.artifactConditions.sort(null); + } + /** * Gets the name of the rule. * @@ -336,13 +350,13 @@ final class FileExportRuleSet implements Serializable, Comparable evaluate(long dataSourceId) throws ExportRulesException { try { SleuthkitCase db = Case.getCurrentCaseThrows().getSleuthkitCase(); - try (SleuthkitCase.CaseDbQuery queryResult = db.executeQuery(getQuery(dataSourceId))) { - ResultSet resultSet = queryResult.getResultSet(); + try (SleuthkitCase.CaseDbQuery queryResult = db.executeQuery(getQuery(dataSourceId)); + ResultSet resultSet = queryResult.getResultSet();) { List fileIds = new ArrayList<>(); while (resultSet.next()) { fileIds.add(resultSet.getLong("obj_id")); @@ -889,7 +903,7 @@ final class FileExportRuleSet implements Serializable, Comparable { + private static final Logger LOG = Logger.getLogger(BinaryCookieReader.class.getName()); private static final int MAGIC_SIZE = 4; private static final int SIZEOF_INT_BYTES = 4; private static final int PAGE_HEADER_VALUE = 256; @@ -53,8 +54,6 @@ final class BinaryCookieReader implements Iterable { private final int[] pageSizeArray; private final File cookieFile; - private static final Logger LOG = Logger.getLogger(BinaryCookieReader.class.getName()); - /** * The binary cookie reader encapsulates all the knowledge of how to read * the mac .binarycookie files into one class. @@ -62,7 +61,7 @@ final class BinaryCookieReader implements Iterable { */ private BinaryCookieReader(File cookieFile, int[] pageSizeArray) { this.cookieFile = cookieFile; - this.pageSizeArray = pageSizeArray; + this.pageSizeArray = pageSizeArray.clone(); } /** @@ -71,7 +70,9 @@ final class BinaryCookieReader implements Iterable { * open. * * @param cookieFile binarycookie file + * * @return An instance of the reader + * * @throws FileNotFoundException * @throws IOException */ @@ -229,6 +230,7 @@ final class BinaryCookieReader implements Iterable { * correct format by checking for the header value of 0x0100. * * @param page byte array representing a cookie page + * * @throws IOException */ CookiePage(byte[] page) throws IOException { @@ -414,7 +416,8 @@ final class BinaryCookieReader implements Iterable { * offset ending at the first null terminator found. * * @param byteArray Array of bytes - * @param offset starting offset in the array + * @param offset starting offset in the array + * * @return String with bytes converted to ascii */ private String decodeString(byte[] byteArray, int offset) { diff --git a/Running_Linux_OSX.txt b/Running_Linux_OSX.txt index 56ea6a5888..bb60569a56 100644 --- a/Running_Linux_OSX.txt +++ b/Running_Linux_OSX.txt @@ -14,21 +14,21 @@ The following need to be done at least once. They do not need to be repeated for 1. Download a 64 bit Java 8 JRE for your specific platform from https://www.azul.com/downloads/zulu-community 2. Install the JRE. e.g. % sudo apt install ./zulu8.40.0.25-ca-jre8.0.222-linux_amd64.deb 3. Download a 64 bit Java 8 JavaFX for your specific platform from the same location. + - Note that you may need to select "Older Zulu versions" for FX to become available in the "Java Package" dropdown. 4. Extract the contents of the JavaFX archive into the folder where the JRE was installed. e.g. % sudo tar xzf ~/Downloads/zulu8.40.0.25-ca-fx-jre8.0.222-linux_x64.tar.gz -C /usr/lib/jvm/zre-8-amd64 --strip-components=1 NOTE: You may need to log out and back in again after setting JAVA_HOME before the Autopsy unix_setup.sh script can see the value. --- OS X: Any Java 8 version of OpenJDK/OpenJFX distribution should suffice. +-- OS X: Any Java 8 version of OpenJDK/OpenJFX distribution should suffice. The following instructions use the AdoptOpenJDK distribution. 1. Install a 64 bit Java 8 JRE. - % brew tap adoptopenjdk/openjdk % brew cask install adoptopenjdk8 - 2. Download a 64 bit Java 8 JavaFX for macOS from https://www.azul.com/downloads/zulu-community - 3. Extract the contents of the JavaFX archive into the folder where the JRE was installed. - e.g. % sudo tar xf ~/Downloads/zulu8.40.0.25-ca-fx-jre8.0.222-macosx_x64.tar.gz -C /Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Home--strip-components=1 - 4. Confirm Java 8 is being found by running 'java -version' - 5. Set JAVA_HOME environment variable to location of JRE installation. + 2. Set JAVA_HOME environment variable to location of JRE installation. + e.g. add the following to ~/.bashrc + export JAVA_HOME=$(/usr/libexec/java_home -v 1.8) + 3. Confirm your version of Java by running + % java -version * Install The Sleuth Kit Java Bindings * 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: diff --git a/docs/doxygen/Doxyfile b/docs/doxygen/Doxyfile index b640c41549..d73868792a 100644 --- a/docs/doxygen/Doxyfile +++ b/docs/doxygen/Doxyfile @@ -772,6 +772,9 @@ INPUT = main.dox \ regressionTesting.dox \ native_libs.dox \ modDevPython.dox \ + modFileIngestTutorial.dox \ + modDSIngestTutorial.dox \ + modReportModuleTutorial.dox \ debugTsk.dox \ ../../Core/src \ ../../CoreLibs/src \ @@ -867,7 +870,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/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.

diff --git a/docs/doxygen/images/bigAndRoundFiles.png b/docs/doxygen/images/bigAndRoundFiles.png new file mode 100644 index 0000000000..fa6430e7d8 Binary files /dev/null and b/docs/doxygen/images/bigAndRoundFiles.png differ diff --git a/docs/doxygen/images/demoScript_folder.png b/docs/doxygen/images/demoScript_folder.png new file mode 100644 index 0000000000..01d7d16f4c Binary files /dev/null and b/docs/doxygen/images/demoScript_folder.png differ diff --git a/docs/doxygen/images/ingest-modules.PNG b/docs/doxygen/images/ingest-modules.PNG new file mode 100644 index 0000000000..9941a0dfb3 Binary files /dev/null and b/docs/doxygen/images/ingest-modules.PNG differ diff --git a/docs/doxygen/images/reports_select.png b/docs/doxygen/images/reports_select.png new file mode 100644 index 0000000000..fe4f1f10bd Binary files /dev/null and b/docs/doxygen/images/reports_select.png differ 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..230be1fd03 100644 --- a/docs/doxygen/main.dox +++ b/docs/doxygen/main.dox @@ -9,9 +9,9 @@ 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/ -- Report Modules: http://www.basistech.com/python-autopsy-module-tutorial-3-the-report-module/ +- 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 This document contains the following pages: - \subpage platform_page diff --git a/docs/doxygen/modDSIngestTutorial.dox b/docs/doxygen/modDSIngestTutorial.dox new file mode 100644 index 0000000000..c38f3d5814 --- /dev/null +++ b/docs/doxygen/modDSIngestTutorial.dox @@ -0,0 +1,171 @@ +/*! \page mod_python_ds_ingest_tutorial_page Python Tutorial #2: Writing a Data Source Ingest Module + + +In the \ref mod_python_file_ingest_tutorial_page "first tutorial" we built a basic Python Autopsy module that looked for big and round files. In this tutorial we're going to make two data source ingest modules. The first focuses on finding SQLite databases and parsing them, and the second focuses on running a command line tool on a disk image. + +The main difference from the first tutorial, which focused on file ingest modules, is that these are data source ingest modules. Data source-ingest modules are given a reference to a data source and the module needs to find the files to analyze, whereas file-level ingest modules are given a reference to each file in the data source. + +\section python_tutorial2_assumptions Assumptions + +This post assumes you've read the \ref mod_python_file_ingest_tutorial_page "first tutorial". That means that you should know why it is better to write an Autopsy module versus a stand-alone tool, and what you need to set up (Autopsy installed, text editor, etc.). You may also recall the limitations (and benefits) of data source ingest modules. The most notable difference between them is that data source-ingest modules may not have access to carved files or files that are inside of ZIP files. For our example in this post, we are looking for a SQLite database with a specific name, and it will not be inside of a ZIP file, so data source ingest modules are the most efficient and will get us results faster. + +The other assumption is that you know something about SQL queries. We have some example queries below and we don't go into detail about how they work. + +\section python_tutorial2_getting_started Getting Started + +\subsection python_tutorial2_folder Making Your Module Folder +We'll start by making our module folder. As we learned in the \ref mod_python_file_ingest_tutorial_page "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 "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/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 diff --git a/docs/doxygen/modFileIngestTutorial.dox b/docs/doxygen/modFileIngestTutorial.dox new file mode 100644 index 0000000000..7873513a4f --- /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: +
    +
  • An installed copy of Autopsy available from SleuthKit
  • +
  • A text editor.
  • +
  • A copy of the sample file ingest module from Github
  • +
+ +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. + + +*/ 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 diff --git a/pythonExamples/Aug2015DataSourceTutorial/FindContactsDb.py b/pythonExamples/Aug2015DataSourceTutorial/FindContactsDb.py index 69a103dcdf..e03ad34121 100644 --- a/pythonExamples/Aug2015DataSourceTutorial/FindContactsDb.py +++ b/pythonExamples/Aug2015DataSourceTutorial/FindContactsDb.py @@ -98,15 +98,15 @@ class ContactsDbIngestModule(DataSourceIngestModule): # Where any setup and configuration is done # 'context' is an instance of org.sleuthkit.autopsy.ingest.IngestJobContext. - # See: http://sleuthkit.org/autopsy/docs/api-docs/4.6.0/classorg_1_1sleuthkit_1_1autopsy_1_1ingest_1_1_ingest_job_context.html + # See: http://sleuthkit.org/autopsy/docs/api-docs/4.13.0/classorg_1_1sleuthkit_1_1autopsy_1_1ingest_1_1_ingest_job_context.html def startUp(self, context): self.context = context # Where the analysis is done. # The 'dataSource' object being passed in is of type org.sleuthkit.datamodel.Content. - # See: http://www.sleuthkit.org/sleuthkit/docs/jni-docs/4.6.0/interfaceorg_1_1sleuthkit_1_1datamodel_1_1_content.html + # See: http://www.sleuthkit.org/sleuthkit/docs/jni-docs/4.13.0/interfaceorg_1_1sleuthkit_1_1datamodel_1_1_content.html # 'progressBar' is of type org.sleuthkit.autopsy.ingest.DataSourceIngestModuleProgress - # See: http://sleuthkit.org/autopsy/docs/api-docs/4.6.0/classorg_1_1sleuthkit_1_1autopsy_1_1ingest_1_1_data_source_ingest_module_progress.html + # See: http://sleuthkit.org/autopsy/docs/api-docs/4.13.0/classorg_1_1sleuthkit_1_1autopsy_1_1ingest_1_1_data_source_ingest_module_progress.html def process(self, dataSource, progressBar): # we don't know how much work there is yet diff --git a/pythonExamples/July2015FileTutorial_BigRound/FindBigRoundFiles.py b/pythonExamples/July2015FileTutorial_BigRound/FindBigRoundFiles.py index 712cbd24c8..b2b13db96f 100644 --- a/pythonExamples/July2015FileTutorial_BigRound/FindBigRoundFiles.py +++ b/pythonExamples/July2015FileTutorial_BigRound/FindBigRoundFiles.py @@ -92,7 +92,7 @@ class FindBigRoundFilesIngestModule(FileIngestModule): # Where any setup and configuration is done # 'context' is an instance of org.sleuthkit.autopsy.ingest.IngestJobContext. - # See: http://sleuthkit.org/autopsy/docs/api-docs/4.6.0/classorg_1_1sleuthkit_1_1autopsy_1_1ingest_1_1_ingest_job_context.html + # See: http://sleuthkit.org/autopsy/docs/api-docs/4.13.0/classorg_1_1sleuthkit_1_1autopsy_1_1ingest_1_1_ingest_job_context.html # TODO: Add any setup code that you need here. def startUp(self, context): self.filesFound = 0 @@ -103,7 +103,7 @@ class FindBigRoundFilesIngestModule(FileIngestModule): # Where the analysis is done. Each file will be passed into here. # The 'file' object being passed in is of type org.sleuthkit.datamodel.AbstractFile. - # See: http://www.sleuthkit.org/sleuthkit/docs/jni-docs/4.6.0/classorg_1_1sleuthkit_1_1datamodel_1_1_abstract_file.html + # See: http://www.sleuthkit.org/sleuthkit/docs/jni-docs/4.13.0/classorg_1_1sleuthkit_1_1datamodel_1_1_abstract_file.html def process(self, file): # Use blackboard class to index blackboard artifacts for keyword search diff --git a/pythonExamples/Sept2015ReportTutorial_CSV/CsvReportModule.py b/pythonExamples/Sept2015ReportTutorial_CSV/CsvReportModule.py index 5e21c125bd..9253e0ee82 100644 --- a/pythonExamples/Sept2015ReportTutorial_CSV/CsvReportModule.py +++ b/pythonExamples/Sept2015ReportTutorial_CSV/CsvReportModule.py @@ -27,7 +27,7 @@ # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. -# See http://sleuthkit.org/autopsy/docs/api-docs/4.6.0/index.html for documentation +# See http://sleuthkit.org/autopsy/docs/api-docs/4.13.0/index.html for documentation # Simple report module for Autopsy. # Used as part of Python tutorials from Basis Technology - September 2015 @@ -71,7 +71,7 @@ class CSVReportModule(GeneralReportModuleAdapter): # TODO: Update this method to make a report # The 'baseReportDir' object being passed in is a string with the directory that reports are being stored in. Report should go into baseReportDir + getRelativeFilePath(). # The 'progressBar' object is of type ReportProgressPanel. - # See: http://sleuthkit.org/autopsy/docs/api-docs/4.6.0/classorg_1_1sleuthkit_1_1autopsy_1_1report_1_1_report_progress_panel.html + # See: http://sleuthkit.org/autopsy/docs/api-docs/4.13.0/classorg_1_1sleuthkit_1_1autopsy_1_1report_1_1_report_progress_panel.html def generateReport(self, baseReportDir, progressBar): # Open the output file. diff --git a/pythonExamples/dataSourceIngestModule.py b/pythonExamples/dataSourceIngestModule.py index c2edfcd8fb..07e5520e74 100644 --- a/pythonExamples/dataSourceIngestModule.py +++ b/pythonExamples/dataSourceIngestModule.py @@ -29,7 +29,7 @@ # Simple data source-level ingest module for Autopsy. # Search for TODO for the things that you need to change -# See http://sleuthkit.org/autopsy/docs/api-docs/4.6.0/index.html for documentation +# See http://sleuthkit.org/autopsy/docs/api-docs/4.13.0/index.html for documentation import jarray import inspect @@ -94,7 +94,7 @@ class SampleJythonDataSourceIngestModule(DataSourceIngestModule): # Where any setup and configuration is done # 'context' is an instance of org.sleuthkit.autopsy.ingest.IngestJobContext. - # See: http://sleuthkit.org/autopsy/docs/api-docs/4.6.0/classorg_1_1sleuthkit_1_1autopsy_1_1ingest_1_1_ingest_job_context.html + # See: http://sleuthkit.org/autopsy/docs/api-docs/4.13.0/classorg_1_1sleuthkit_1_1autopsy_1_1ingest_1_1_ingest_job_context.html # TODO: Add any setup code that you need here. def startUp(self, context): @@ -104,9 +104,9 @@ class SampleJythonDataSourceIngestModule(DataSourceIngestModule): # Where the analysis is done. # The 'dataSource' object being passed in is of type org.sleuthkit.datamodel.Content. - # See: http://www.sleuthkit.org/sleuthkit/docs/jni-docs/4.6.0/interfaceorg_1_1sleuthkit_1_1datamodel_1_1_content.html + # See: http://www.sleuthkit.org/sleuthkit/docs/jni-docs/4.13.0/interfaceorg_1_1sleuthkit_1_1datamodel_1_1_content.html # 'progressBar' is of type org.sleuthkit.autopsy.ingest.DataSourceIngestModuleProgress - # See: http://sleuthkit.org/autopsy/docs/api-docs/4.6.0/classorg_1_1sleuthkit_1_1autopsy_1_1ingest_1_1_data_source_ingest_module_progress.html + # See: http://sleuthkit.org/autopsy/docs/api-docs/4.13.0/classorg_1_1sleuthkit_1_1autopsy_1_1ingest_1_1_data_source_ingest_module_progress.html # TODO: Add your analysis code in here. def process(self, dataSource, progressBar): @@ -119,7 +119,7 @@ class SampleJythonDataSourceIngestModule(DataSourceIngestModule): # For our example, we will use FileManager to get all # files with the word "test" # in the name and then count and read them - # FileManager API: http://sleuthkit.org/autopsy/docs/api-docs/4.6.0/classorg_1_1sleuthkit_1_1autopsy_1_1casemodule_1_1services_1_1_file_manager.html + # FileManager API: http://sleuthkit.org/autopsy/docs/api-docs/4.13.0/classorg_1_1sleuthkit_1_1autopsy_1_1casemodule_1_1services_1_1_file_manager.html fileManager = Case.getCurrentCase().getServices().getFileManager() files = fileManager.findFiles(dataSource, "%test%") diff --git a/pythonExamples/fileIngestModule.py b/pythonExamples/fileIngestModule.py index b36eea7c57..078623c61d 100644 --- a/pythonExamples/fileIngestModule.py +++ b/pythonExamples/fileIngestModule.py @@ -29,7 +29,7 @@ # Simple file-level ingest module for Autopsy. # Search for TODO for the things that you need to change -# See http://sleuthkit.org/autopsy/docs/api-docs/4.6.0/index.html for documentation +# See http://sleuthkit.org/autopsy/docs/api-docs/4.13.0/index.html for documentation import jarray import inspect @@ -94,7 +94,7 @@ class SampleJythonFileIngestModule(FileIngestModule): # Where any setup and configuration is done # 'context' is an instance of org.sleuthkit.autopsy.ingest.IngestJobContext. - # See: http://sleuthkit.org/autopsy/docs/api-docs/4.6.0/classorg_1_1sleuthkit_1_1autopsy_1_1ingest_1_1_ingest_job_context.html + # See: http://sleuthkit.org/autopsy/docs/api-docs/4.13.0/classorg_1_1sleuthkit_1_1autopsy_1_1ingest_1_1_ingest_job_context.html # TODO: Add any setup code that you need here. def startUp(self, context): self.filesFound = 0 @@ -105,7 +105,7 @@ class SampleJythonFileIngestModule(FileIngestModule): # Where the analysis is done. Each file will be passed into here. # The 'file' object being passed in is of type org.sleuthkit.datamodel.AbstractFile. - # See: http://www.sleuthkit.org/sleuthkit/docs/jni-docs/4.6.0/classorg_1_1sleuthkit_1_1datamodel_1_1_abstract_file.html + # See: http://www.sleuthkit.org/sleuthkit/docs/jni-docs/4.13.0/classorg_1_1sleuthkit_1_1datamodel_1_1_abstract_file.html # TODO: Add your analysis code in here. def process(self, file): # Skip non-files diff --git a/pythonExamples/fileIngestModuleWithGui.py b/pythonExamples/fileIngestModuleWithGui.py index cdcaca3576..a7daf5d982 100644 --- a/pythonExamples/fileIngestModuleWithGui.py +++ b/pythonExamples/fileIngestModuleWithGui.py @@ -35,7 +35,7 @@ # don't need a configuration UI, start with the other sample module. # # Search for TODO for the things that you need to change -# See http://sleuthkit.org/autopsy/docs/api-docs/4.6.0/index.html for documentation +# See http://sleuthkit.org/autopsy/docs/api-docs/4.13.0/index.html for documentation import jarray diff --git a/pythonExamples/reportmodule.py b/pythonExamples/reportmodule.py index 1011cfe753..d1f0e927a6 100644 --- a/pythonExamples/reportmodule.py +++ b/pythonExamples/reportmodule.py @@ -31,7 +31,7 @@ # Sample report module for Autopsy. Use as a starting point for new modules. # # Search for TODO for the things that you need to change -# See http://sleuthkit.org/autopsy/docs/api-docs/4.6.0/index.html for documentation +# See http://sleuthkit.org/autopsy/docs/api-docs/4.13.0/index.html for documentation import os from java.lang import System @@ -69,7 +69,7 @@ class SampleGeneralReportModule(GeneralReportModuleAdapter): # TODO: Update this method to make a report # The 'baseReportDir' object being passed in is a string with the directory that reports are being stored in. Report should go into baseReportDir + getRelativeFilePath(). # The 'progressBar' object is of type ReportProgressPanel. - # See: http://sleuthkit.org/autopsy/docs/api-docs/4.6.0/classorg_1_1sleuthkit_1_1autopsy_1_1report_1_1_report_progress_panel.html + # See: http://sleuthkit.org/autopsy/docs/api-docs/4.13.0/classorg_1_1sleuthkit_1_1autopsy_1_1report_1_1_report_progress_panel.html def generateReport(self, baseReportDir, progressBar): # For an example, we write a file with the number of files created in the past 2 weeks 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; } - + } } diff --git a/travis_build.sh b/travis_build.sh new file mode 100755 index 0000000000..f7b7cfb72d --- /dev/null +++ b/travis_build.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +set -e +echo "Building TSK..." +cd sleuthkit/sleuthkit +./bootstrap && ./configure --prefix=/usr && make +pushd bindings/java && ant -q dist-PostgreSQL && popd + +echo "Building Autopsy..." && echo -en 'travis_fold:start:script.build\\r' +cd $TRAVIS_BUILD_DIR/ +ant build +echo -en 'travis_fold:end:script.build\\r'