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.datasourceprocessorsorg.sleuthkit.autopsy.directorytreeorg.sleuthkit.autopsy.events
+ org.sleuthkit.autopsy.exceptionsorg.sleuthkit.autopsy.filesearchorg.sleuthkit.autopsy.guiutilsorg.sleuthkit.autopsy.healthmonitor
@@ -453,6 +454,10 @@
ext/commons-pool2-2.4.2.jarrelease/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.jarrelease/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 extends Content> 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=
+
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.
+
+
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.
+
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.
+
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.
+
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.
+
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.
+
shutdown() method: The shutDown() method either frees resources that were allocated or sends summary messages. For our module, it will do nothing.
+
+
+\subsection python_tutorial1_process The process() Method
+
+The process() method is passed in a reference to an AbstractFile Object. With this, you have access to all of a file's contents and metadata. We want to flag files that are larger than 10MB and that are a multiple of 4096 bytes. The following code does that:
+
+\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.
+
+
+
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.
+
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.
+
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.
+
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.
+
+
+\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:
+
+
Open the CSV file.
+
Query for all files.
+
Cycle through each of the files and print a line of text.
+
Add the report to the Case database.
+
+
+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'