diff --git a/.gitignore b/.gitignore index 6f6f3fb653..fd160c9744 100644 --- a/.gitignore +++ b/.gitignore @@ -82,6 +82,8 @@ hs_err_pid*.log /RecentActivity/release/ /CentralRepository/release/ +/.idea/ + *.img *.vhd *.E01 diff --git a/Core/build.xml b/Core/build.xml index bfde1a520b..3e2c4a0b58 100644 --- a/Core/build.xml +++ b/Core/build.xml @@ -109,27 +109,27 @@ - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/ivy.xml b/Core/ivy.xml index 9dfd0fcf85..54ed532feb 100644 --- a/Core/ivy.xml +++ b/Core/ivy.xml @@ -45,6 +45,9 @@ + + + diff --git a/Core/nbproject/project.properties b/Core/nbproject/project.properties index 955079d04a..5e07fadf28 100644 --- a/Core/nbproject/project.properties +++ b/Core/nbproject/project.properties @@ -75,7 +75,7 @@ file.reference.javax.ws.rs-api-2.0.1.jar=release/modules/ext/javax.ws.rs-api-2.0 file.reference.cxf-core-3.0.16.jar=release/modules/ext/cxf-core-3.0.16.jar file.reference.cxf-rt-frontend-jaxrs-3.0.16.jar=release/modules/ext/cxf-rt-frontend-jaxrs-3.0.16.jar file.reference.cxf-rt-transports-http-3.0.16.jar=release/modules/ext/cxf-rt-transports-http-3.0.16.jar -file.reference.sleuthkit-postgresql-4.6.6.jar=release/modules/ext/sleuthkit-postgresql-4.6.6.jar +file.reference.sleuthkit-postgresql-4.6.7.jar=release/modules/ext/sleuthkit-postgresql-4.6.7.jar file.reference.curator-client-2.8.0.jar=release/modules/ext/curator-client-2.8.0.jar file.reference.curator-framework-2.8.0.jar=release/modules/ext/curator-framework-2.8.0.jar file.reference.curator-recipes-2.8.0.jar=release/modules/ext/curator-recipes-2.8.0.jar @@ -111,6 +111,10 @@ file.reference.grpc-context-1.19.0.jar=release/modules/ext/grpc-context-1.19.0.j file.reference.opencensus-api-0.19.2.jar=release/modules/ext/opencensus-api-0.19.2.jar file.reference.opencensus-contrib-http-util-0.19.2.jar=release/modules/ext/opencensus-contrib-http-util-0.19.2.jar file.reference.threetenbp-1.3.3.jar=release/modules/ext/threetenbp-1.3.3.jar +file.reference.okhttp-2.7.5-javadoc.jar=release/modules/ext/okhttp-2.7.5-javadoc.jar +file.reference.okhttp-2.7.5-sources.jar=release/modules/ext/okhttp-2.7.5-sources.jar +file.reference.okhttp-2.7.5.jar=release/modules/ext/okhttp-2.7.5.jar +file.reference.okio-1.6.0.jar=release/modules/ext/okio-1.6.0.jar javac.source=1.8 javac.compilerargs=-Xlint -Xlint:-serial license.file=../LICENSE-2.0.txt diff --git a/Core/nbproject/project.xml b/Core/nbproject/project.xml index 1ac3c9d2fd..967399cc20 100644 --- a/Core/nbproject/project.xml +++ b/Core/nbproject/project.xml @@ -482,8 +482,8 @@ release\modules\ext\jmatio-1.5.jar - ext/sleuthkit-postgresql-4.6.6.jar - release/modules/ext/sleuthkit-postgresql-4.6.6.jar + ext/sleuthkit-postgresql-4.6.7.jar + release/modules/ext/sleuthkit-postgresql-4.6.7.jar ext/tika-parsers-1.20.jar @@ -773,6 +773,14 @@ ext/google-api-services-translate-v2-rev20170525-1.27.0.jar release/modules/ext/google-api-services-translate-v2-rev20170525-1.27.0.jar + + ext/okhttp-2.7.5.jar + release/modules/ext/okhttp-2.7.5.jar + + + ext/okio-1.6.0.jar + release/modules/ext/okio-1.6.0.jar + diff --git a/Core/src/org/sleuthkit/autopsy/actions/Bundle.properties b/Core/src/org/sleuthkit/autopsy/actions/Bundle.properties index 8f8b3b4b42..a2feedc54f 100644 --- a/Core/src/org/sleuthkit/autopsy/actions/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/actions/Bundle.properties @@ -36,10 +36,10 @@ GetTagNameDialog.tagNameExistsTskCore.msg=The {0} tag name already exists in the OpenLogFolder.error1=Log File Not Found: {0} OpenLogFolder.CouldNotOpenLogFolder=Could not open log folder CTL_OpenLogFolder=Open Log Folder -CTL_OpenOutputFolder=Open Output Folder -OpenOutputFolder.error1=Output Folder Not Found: {0} -OpenOutputFolder.noCaseOpen=No open case, therefore no current output folder available. -OpenOutputFolder.CouldNotOpenOutputFolder=Could not open output folder +CTL_OpenOutputFolder=Open Case Folder +OpenOutputFolder.error1=Case Folder Not Found: {0} +OpenOutputFolder.noCaseOpen=No open case, therefore no current case folder available. +OpenOutputFolder.CouldNotOpenOutputFolder=Could not open case folder ShowIngestProgressSnapshotAction.actionName.text=Get Ingest Progress Snapshot OpenPythonModulesFolderAction.actionName.text=Python Plugins OpenPythonModulesFolderAction.errorMsg.folderNotFound=Python plugins folder not found: {0} diff --git a/Core/src/org/sleuthkit/autopsy/actions/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/actions/Bundle.properties-MERGED index 506786c42d..a3a13c0cff 100755 --- a/Core/src/org/sleuthkit/autopsy/actions/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/actions/Bundle.properties-MERGED @@ -79,10 +79,10 @@ GetTagNameDialog.tagNameExistsTskCore.msg=The {0} tag name already exists in the OpenLogFolder.error1=Log File Not Found: {0} OpenLogFolder.CouldNotOpenLogFolder=Could not open log folder CTL_OpenLogFolder=Open Log Folder -CTL_OpenOutputFolder=Open Output Folder -OpenOutputFolder.error1=Output Folder Not Found: {0} -OpenOutputFolder.noCaseOpen=No open case, therefore no current output folder available. -OpenOutputFolder.CouldNotOpenOutputFolder=Could not open output folder +CTL_OpenOutputFolder=Open Case Folder +OpenOutputFolder.error1=Case Folder Not Found: {0} +OpenOutputFolder.noCaseOpen=No open case, therefore no current case folder available. +OpenOutputFolder.CouldNotOpenOutputFolder=Could not open case folder # {0} - old tag name # {1} - artifactID ReplaceBlackboardArtifactTagAction.replaceTag.alert=Unable to replace tag {0} for artifact {1}. diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardSelectDspVisual.java b/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardSelectDspVisual.java index eb10fd7c47..acad42a56b 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardSelectDspVisual.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardSelectDspVisual.java @@ -18,7 +18,6 @@ */ package org.sleuthkit.autopsy.casemodule; -import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.GridBagConstraints; @@ -40,8 +39,9 @@ import javax.swing.SwingUtilities; import org.openide.util.Lookup; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; -import org.sleuthkit.autopsy.datasourceprocessors.RawDSProcessor; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.datasourceprocessors.RawDSProcessor; +import org.sleuthkit.autopsy.logicalimager.dsp.LogicalImagerDSProcessor; /** * Panel which displays the available DataSourceProcessors and allows selection diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties index 2683f71d6d..ef41f8c70f 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties @@ -238,13 +238,6 @@ OpenMultiUserCasePanel.cancelButton.text=Cancel OpenMultiUserCasePanel.openSingleUserCaseButton.text=Open Single-User Case... OpenMultiUserCasePanel.openSelectedCaseButton.text=Open Selected Case OpenMultiUserCasePanel.searchLabel.text=Select any case and start typing to search by case name -LogicalImagerPanel.jLabel1.text=Insert external drive -LogicalImagerPanel.scanButton.text=Scan -LogicalImagerPanel.jLabel6.text=Or, pick a Logical Imager folder -LogicalImagerPanel.browseButton.text=Browse -LogicalImagerPanel.topLabel.text=Import Autopsy Imager Results -LogicalImagerPanel.selectDriveLabel.text=Select Drive -LogicalImagerPanel.messageLabel.text=Error/Status message UnpackagePortableCaseDialog.desc2Label.text=Portable Case Report Module. UnpackagePortableCaseDialog.desc1Label.text=Unpackage a portable case so it can be opened in Autopsy. Portable cases are created through the UnpackagePortableCaseDialog.exitButton.text=Exit @@ -259,4 +252,4 @@ UnpackagePortableCaseProgressDialog.cancelButton.text=Cancel UnpackagePortableCaseProgressDialog.okButton.text=OK UnpackagePortableCaseProgressDialog.resultLabel.text=resultLabel UnpackagePortableCaseDialog.extractLabel.text=Folder to extract to: -UnpackagePortableCaseDialog.caseLabel.text=Portable Case: \ No newline at end of file +UnpackagePortableCaseDialog.caseLabel.text=Portable Case: diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED index 5ad9042e9f..48d6c84258 100755 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED @@ -1,19 +1,5 @@ AddImageWizardIngestConfigPanel.name.text=Configure Ingest Modules AddImageWizardSelectDspVisual.multiUserWarning.text=This type of Data Source Processor is not available in multi-user mode -# {0} - file -AddLogicalImageTask.addingToReport=Adding {0} to report -# {0} - src -# {1} - dest -AddLogicalImageTask.copyingImageFromTo=Copying image from {0} to {1} -# {0} - file -AddLogicalImageTask.doneAddingToReport=Done adding {0} to report -AddLogicalImageTask.doneCopying=Done copying -# {0} - file -# {1} - exception message -AddLogicalImageTask.failedToAddReport=Failed to add report {0}. Reason= {1} -# {0} - src -# {1} - dest -AddLogicalImageTask.failedToCopyDirectory=Failed to copy directory {0} to {1} # {0} - exception message Case.closeException.couldNotCloseCase=Error closing case: {0} Case.creationException.couldNotAcquireDirLock=Failed to get lock on case directory @@ -182,27 +168,6 @@ LogicalEvidenceFilePanel.pathValidation.getOpenCase.Error=Warning: Exception whi LogicalEvidenceFilePanel.validatePanel.nonL01Error.text=Only files with the .l01 file extension are supported here. LogicalFilesDspPanel.subTypeComboBox.l01FileOption.text=Logical evidence file (L01) LogicalFilesDspPanel.subTypeComboBox.localFilesOption.text=Local files and folders -LogicalImagerDSProcessor.dataSourceType=Autopsy Imager -# {0} - directory -LogicalImagerDSProcessor.directoryAlreadyExists=Directory {0} already exists -# {0} - directory -LogicalImagerDSProcessor.failToCreateDirectory=Failed to create directory {0} -# {0} - imageDirPath -LogicalImagerDSProcessor.imageDirPathNotFound={0} not found.\nUSB drive has been ejected. -LogicalImagerPanel.imageTable.columnModel.title0=Hostname -LogicalImagerPanel.imageTable.columnModel.title1=Extracted Date -LogicalImagerPanel.messageLabel.clickScanOrBrowse=Click SCAN or BROWSE button to find images -# {0} - sparseImageDirectory -# {1} - image -LogicalImagerPanel.messageLabel.directoryDoesNotContainSparseImage=Directory {0} does not contain {1} -# {0} - invalidFormatDirectory -LogicalImagerPanel.messageLabel.directoryFormatInvalid=Directory {0} does not match format Logical_Imager_HOSTNAME_yyyymmdd_HH_MM_SS -LogicalImagerPanel.messageLabel.driveHasNoImages=Drive has no images -LogicalImagerPanel.messageLabel.noExternalDriveFound=No drive found -LogicalImagerPanel.messageLabel.noImageSelected=No image selected -LogicalImagerPanel.messageLabel.scanningExternalDrives=Scanning external drives for sparse_image.vhd ... -LogicalImagerPanel.messageLabel.selectedImage=Selected folder -LogicalImagerPanel.selectAcquisitionFromDriveLabel.text=Select acquisition from Drive Menu/Case/OpenRecentCase=Open Recent Case CTL_CaseDeleteAction=Delete Case OpenIDE-Module-Name=Case @@ -475,13 +440,6 @@ OpenMultiUserCasePanel.cancelButton.text=Cancel OpenMultiUserCasePanel.openSingleUserCaseButton.text=Open Single-User Case... OpenMultiUserCasePanel.openSelectedCaseButton.text=Open Selected Case OpenMultiUserCasePanel.searchLabel.text=Select any case and start typing to search by case name -LogicalImagerPanel.jLabel1.text=Insert external drive -LogicalImagerPanel.scanButton.text=Scan -LogicalImagerPanel.jLabel6.text=Or, pick a Logical Imager folder -LogicalImagerPanel.browseButton.text=Browse -LogicalImagerPanel.topLabel.text=Import Autopsy Imager Results -LogicalImagerPanel.selectDriveLabel.text=Select Drive -LogicalImagerPanel.messageLabel.text=Error/Status message UnpackagePortableCaseDialog.desc2Label.text=Portable Case Report Module. UnpackagePortableCaseDialog.desc1Label.text=Unpackage a portable case so it can be opened in Autopsy. Portable cases are created through the UnpackagePortableCaseDialog.exitButton.text=Exit diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/LogicalImagerPanel.form b/Core/src/org/sleuthkit/autopsy/casemodule/LogicalImagerPanel.form deleted file mode 100644 index 79e6241551..0000000000 --- a/Core/src/org/sleuthkit/autopsy/casemodule/LogicalImagerPanel.form +++ /dev/null @@ -1,236 +0,0 @@ - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseVisualPanel1.java b/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseVisualPanel1.java index 8b551ce4ec..aedbb3671f 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseVisualPanel1.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/NewCaseVisualPanel1.java @@ -88,7 +88,7 @@ final class NewCaseVisualPanel1 extends JPanel implements DocumentListener { * @return caseName the case name from the case name text field */ String getCaseName() { - return this.caseNameTextField.getText(); + return this.caseNameTextField.getText().trim(); } /** @@ -109,7 +109,7 @@ final class NewCaseVisualPanel1 extends JPanel implements DocumentListener { * @return baseDirectory the base directory from the case dir text field */ String getCaseParentDir() { - String parentDir = this.caseParentDirTextField.getText(); + String parentDir = this.caseParentDirTextField.getText().trim(); if (parentDir.endsWith(File.separator) == false) { parentDir = parentDir + File.separator; } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/OpenRecentCasePanel.form b/Core/src/org/sleuthkit/autopsy/casemodule/OpenRecentCasePanel.form index bb12ba69d9..5e9ec1f38a 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/OpenRecentCasePanel.form +++ b/Core/src/org/sleuthkit/autopsy/casemodule/OpenRecentCasePanel.form @@ -76,6 +76,7 @@ + diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/OpenRecentCasePanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/OpenRecentCasePanel.java index 5f164cd00f..401663056b 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/OpenRecentCasePanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/OpenRecentCasePanel.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"); @@ -24,7 +24,10 @@ import java.io.File; import java.util.logging.Level; import javax.swing.JOptionPane; import javax.swing.JTable; +import javax.swing.ListSelectionModel; import javax.swing.SwingUtilities; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; import javax.swing.table.AbstractTableModel; import org.openide.util.NbBundle; import org.openide.windows.WindowManager; @@ -51,6 +54,16 @@ class OpenRecentCasePanel extends javax.swing.JPanel { */ private OpenRecentCasePanel() { initComponents(); + imagesTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + imagesTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() { + @Override + public void valueChanged(ListSelectionEvent e) { + //enable the ok button when something is selected + if (!e.getValueIsAdjusting()){ + openButton.setEnabled(imagesTable.getSelectedRowCount() > 0); + } + } + }); } /* @@ -90,9 +103,6 @@ class OpenRecentCasePanel extends javax.swing.JPanel { // If there are any images, let's select the first one if (imagesTable.getRowCount() > 0) { imagesTable.setRowSelectionInterval(0, 0); - openButton.setEnabled(true); - } else { - openButton.setEnabled(false); } } @@ -251,6 +261,7 @@ class OpenRecentCasePanel extends javax.swing.JPanel { cancelButton.setText(org.openide.util.NbBundle.getMessage(OpenRecentCasePanel.class, "OpenRecentCasePanel.cancelButton.text")); // NOI18N openButton.setText(org.openide.util.NbBundle.getMessage(OpenRecentCasePanel.class, "OpenRecentCasePanel.openButton.text")); // NOI18N + openButton.setEnabled(false); openButton.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { openButtonActionPerformed(evt); diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/UnpackagePortableCaseDialog.java b/Core/src/org/sleuthkit/autopsy/casemodule/UnpackagePortableCaseDialog.java index 9e347c3809..3b86909953 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/UnpackagePortableCaseDialog.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/UnpackagePortableCaseDialog.java @@ -351,7 +351,11 @@ class UnpackagePortableCaseDialog extends javax.swing.JDialog { private void unpackageButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_unpackageButtonActionPerformed UnpackagePortableCaseProgressDialog dialog = new UnpackagePortableCaseProgressDialog(); dialog.unpackageCase(caseTextField.getText(), outputTextField.getText()); - validatePaths(); // The output folder now exists so we need to disable the unpackage button + if (dialog.isSuccess()) { + dispose(); + } else { + validatePaths(); // The output folder now exists so we need to disable the unpackage button + } }//GEN-LAST:event_unpackageButtonActionPerformed diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/UnpackagePortableCaseProgressDialog.java b/Core/src/org/sleuthkit/autopsy/casemodule/UnpackagePortableCaseProgressDialog.java index 25b7ae084b..5f0f318183 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/UnpackagePortableCaseProgressDialog.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/UnpackagePortableCaseProgressDialog.java @@ -44,11 +44,11 @@ import org.sleuthkit.datamodel.TskCoreException; class UnpackagePortableCaseProgressDialog extends javax.swing.JDialog implements PropertyChangeListener { private UnpackageWorker worker; - + /** * Creates new form UnpackagePortableCaseProgressDialog */ - @NbBundle.Messages({"UnpackagePortableCaseProgressDialog.title.text=Unpackage Portable Case Progress",}) + @NbBundle.Messages({"UnpackagePortableCaseProgressDialog.title.text=Unpackage Portable Case Progress",}) UnpackagePortableCaseProgressDialog() { super((JFrame) WindowManager.getDefault().getMainWindow(), Bundle.UnpackagePortableCaseProgressDialog_title_text(), @@ -56,32 +56,45 @@ class UnpackagePortableCaseProgressDialog extends javax.swing.JDialog implements initComponents(); customizeComponents(); } - + private void customizeComponents() { cancelButton.setEnabled(true); okButton.setEnabled(false); progressBar.setIndeterminate(true); resultLabel.setText(""); // NON-NLS } - + /** * Unpackage the case - * - * @param packagedCase The compressed case - * @param outputFolder The output folder + * + * @param packagedCase The compressed case + * @param outputFolder The output folder */ void unpackageCase(String packagedCase, String outputFolder) { - + worker = new UnpackageWorker(packagedCase, outputFolder); worker.addPropertyChangeListener(this); worker.execute(); setLocationRelativeTo((JFrame) WindowManager.getDefault().getMainWindow()); this.setVisible(true); - + } - - @NbBundle.Messages({"UnpackagePortableCaseProgressDialog.propertyChange.success=Successfully unpacked case",}) + + /** + * Returns whether the unpackaging was completed successfully. + * + * @return True if unpackaging was completed successfully, false otherwise + */ + boolean isSuccess() { + if (worker == null) { + return false; + } else { + return worker.isSuccess(); + } + } + + @NbBundle.Messages({"UnpackagePortableCaseProgressDialog.propertyChange.success=Successfully unpacked case",}) @Override public void propertyChange(PropertyChangeEvent evt) { @@ -92,7 +105,7 @@ class UnpackagePortableCaseProgressDialog extends javax.swing.JDialog implements // Disable cancel button and enable ok cancelButton.setEnabled(false); okButton.setEnabled(true); - + if (worker.isSuccess()) { progressBar.setIndeterminate(false); progressBar.setValue(progressBar.getMaximum()); @@ -106,48 +119,47 @@ class UnpackagePortableCaseProgressDialog extends javax.swing.JDialog implements } } } - + /** * Swing worker to do the decompression. */ private class UnpackageWorker extends SwingWorker { - + private final String packagedCase; private final String outputFolder; - + private final AtomicBoolean success = new AtomicBoolean(); private String lastError = ""; - + UnpackageWorker(String packagedCase, String outputFolder) { this.packagedCase = packagedCase; this.outputFolder = outputFolder; this.success.set(false); } - + @NbBundle.Messages({ - "UnpackageWorker.doInBackground.errorFinding7zip=Could not locate 7-Zip executable", - "UnpackageWorker.doInBackground.errorCompressingCase=Error unpackaging case", - "UnpackageWorker.doInBackground.canceled=Unpackaging canceled by user", - }) + "UnpackageWorker.doInBackground.errorFinding7zip=Could not locate 7-Zip executable", + "UnpackageWorker.doInBackground.errorCompressingCase=Error unpackaging case", + "UnpackageWorker.doInBackground.canceled=Unpackaging canceled by user",}) @Override protected Void doInBackground() throws Exception { - + // Find 7-Zip File sevenZipExe = locate7ZipExecutable(); if (sevenZipExe == null) { setDisplayError(Bundle.UnpackageWorker_doInBackground_errorFinding7zip()); throw new TskCoreException("Error finding 7-Zip executable"); // NON-NLS } - + String outputFolderSwitch = "-o" + outputFolder; // NON-NLS ProcessBuilder procBuilder = new ProcessBuilder(); procBuilder.command( sevenZipExe.getAbsolutePath(), - "x", // Extract + "x", // Extract packagedCase, outputFolderSwitch ); - + try { Process process = procBuilder.start(); @@ -158,7 +170,7 @@ class UnpackagePortableCaseProgressDialog extends javax.swing.JDialog implements } Thread.sleep(200); } - + int exitCode = process.exitValue(); if (exitCode != 0) { // Save any errors so they can be logged @@ -181,63 +193,64 @@ class UnpackagePortableCaseProgressDialog extends javax.swing.JDialog implements success.set(true); return null; } - + @Override synchronized protected void done() { if (this.isCancelled()) { return; } - + try { get(); } catch (Exception ex) { Logger.getLogger(UnpackagePortableCaseProgressDialog.class.getName()).log(Level.SEVERE, "Error unpackaging portable case", ex); // NON-NLS } } - + /** * Save the error that should be displayed to the user - * + * * @param errorStr Error to be displayed in the UI */ private synchronized void setDisplayError(String errorStr) { lastError = errorStr; } - + /** * Gets the error to display to the user + * * @return Error to be displayed in the UI */ private synchronized String getDisplayError() { return lastError; } - - private boolean isSuccess() { + + protected boolean isSuccess() { return success.get(); } - + /** - * Locate the 7-Zip executable from the release folder. - * - * @return 7-Zip executable - */ - private File locate7ZipExecutable() { - if (!PlatformUtil.isWindowsOS()) { - return null; - } + * Locate the 7-Zip executable from the release folder. + * + * @return 7-Zip executable + */ + private File locate7ZipExecutable() { + if (!PlatformUtil.isWindowsOS()) { + return null; + } - String executableToFindName = Paths.get("7-Zip", "7z.exe").toString(); // NON-NLS - File exeFile = InstalledFileLocator.getDefault().locate(executableToFindName, UnpackagePortableCaseProgressDialog.class.getPackage().getName(), false); - if (null == exeFile) { - return null; - } + String executableToFindName = Paths.get("7-Zip", "7z.exe").toString(); // NON-NLS + File exeFile = InstalledFileLocator.getDefault().locate(executableToFindName, UnpackagePortableCaseProgressDialog.class.getPackage().getName(), false); + if (null == exeFile) { + return null; + } - if (!exeFile.canExecute()) { - return null; - } + if (!exeFile.canExecute()) { + return null; + } - return exeFile; - } + return exeFile; + } } /** diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourceSummary/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/casemodule/datasourceSummary/Bundle.properties-MERGED index 508e04b76a..fe998b0e57 100755 --- a/Core/src/org/sleuthkit/autopsy/casemodule/datasourceSummary/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourceSummary/Bundle.properties-MERGED @@ -66,6 +66,7 @@ DataSourceSummaryDialog.window.title=Data Sources Summary DataSourceSummaryNode.column.dataSourceName.header=Data Source Name DataSourceSummaryNode.column.files.header=Files DataSourceSummaryNode.column.results.header=Results +DataSourceSummaryNode.column.status.header=Ingest Status DataSourceSummaryNode.column.tags.header=Tags DataSourceSummaryNode.column.type.header=Type DataSourceSummaryNode.viewDataSourceAction.text=Go to Data Source diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceBrowser.java b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceBrowser.java index e3b9fa0e93..ac09010de1 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceBrowser.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceBrowser.java @@ -37,8 +37,10 @@ import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.casemodule.datasourcesummary.DataSourceSummaryNode.DataSourceSummaryEntryNode; import static javax.swing.SwingConstants.RIGHT; +import javax.swing.SwingUtilities; import javax.swing.table.TableColumn; import org.sleuthkit.datamodel.DataSource; +import org.sleuthkit.datamodel.IngestJobInfo; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TskCoreException; @@ -50,9 +52,10 @@ final class DataSourceBrowser extends javax.swing.JPanel implements ExplorerMana private static final long serialVersionUID = 1L; private static final Logger logger = Logger.getLogger(DataSourceBrowser.class.getName()); - private static final int COUNT_COLUMN_WIDTH = 25; - private static final int USAGE_COLUMN_WIDTH = 120; - private static final int DATA_SOURCE_COLUMN_WIDTH = 325; + private static final int COUNT_COLUMN_WIDTH = 20; + private static final int INGEST_STATUS_WIDTH = 50; + private static final int USAGE_COLUMN_WIDTH = 110; + private static final int DATA_SOURCE_COLUMN_WIDTH = 280; private final Outline outline; private final org.openide.explorer.view.OutlineView outlineView; private final ExplorerManager explorerManager; @@ -69,6 +72,7 @@ final class DataSourceBrowser extends javax.swing.JPanel implements ExplorerMana outlineView = new org.openide.explorer.view.OutlineView(); this.setVisible(true); outlineView.setPropertyColumns( + Bundle.DataSourceSummaryNode_column_status_header(), Bundle.DataSourceSummaryNode_column_status_header(), Bundle.DataSourceSummaryNode_column_type_header(), Bundle.DataSourceSummaryNode_column_type_header(), Bundle.DataSourceSummaryNode_column_files_header(), Bundle.DataSourceSummaryNode_column_files_header(), Bundle.DataSourceSummaryNode_column_results_header(), Bundle.DataSourceSummaryNode_column_results_header(), @@ -90,6 +94,8 @@ final class DataSourceBrowser extends javax.swing.JPanel implements ExplorerMana column.setPreferredWidth(COUNT_COLUMN_WIDTH); } else if (column.getHeaderValue().toString().equals(Bundle.DataSourceSummaryNode_column_type_header())) { column.setPreferredWidth(USAGE_COLUMN_WIDTH); + } else if (column.getHeaderValue().toString().equals(Bundle.DataSourceSummaryNode_column_status_header())) { + column.setPreferredWidth(INGEST_STATUS_WIDTH); } else { column.setPreferredWidth(DATA_SOURCE_COLUMN_WIDTH); } @@ -182,6 +188,47 @@ final class DataSourceBrowser extends javax.swing.JPanel implements ExplorerMana return null; } + /** + * Update the DataSourceBrowser to display up to date status information for + * the data sources. + * + * @param dataSourceId the ID of the data source which should be updated + * @param newStatus the new status which the data source should have + */ + void refresh(long dataSourceId, IngestJobInfo.IngestJobStatusType newStatus) { + + //attempt to update the status of any datasources that had status which was STARTED + for (DataSourceSummary summary : dataSourceSummaryList) { + if (summary.getDataSource().getId() == dataSourceId) { + summary.setIngestStatus(newStatus); + } + } + //figure out which nodes were previously selected + Node[] selectedNodes = explorerManager.getSelectedNodes(); + SwingUtilities.invokeLater(() -> { + explorerManager.setRootContext(new DataSourceSummaryNode(dataSourceSummaryList)); + List nodesToSelect = new ArrayList<>(); + for (Node node : explorerManager.getRootContext().getChildren().getNodes()) { + if (node instanceof DataSourceSummaryEntryNode) { + //there should only be one selected node as multi-select is disabled + for (Node selectedNode : selectedNodes) { + if (((DataSourceSummaryEntryNode) node).getDataSource().equals(((DataSourceSummaryEntryNode) selectedNode).getDataSource())) { + nodesToSelect.add(node); + } + } + } + } + //reselect the previously selected Nodes + try { + explorerManager.setSelectedNodes(nodesToSelect.toArray(new Node[nodesToSelect.size()])); + } catch (PropertyVetoException ex) { + logger.log(Level.WARNING, "Error selecting previously selected nodes", ex); + } + + }); + + } + /** * This method is called from within the constructor to initialize the form. * WARNING: Do NOT modify this code. The content of this method is always diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummary.java b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummary.java index 1aa55d95b5..390dce1afe 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummary.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummary.java @@ -18,15 +18,27 @@ */ package org.sleuthkit.autopsy.casemodule.datasourcesummary; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.datamodel.CaseDbAccessManager; import org.sleuthkit.datamodel.DataSource; +import org.sleuthkit.datamodel.IngestJobInfo.IngestJobStatusType; +import org.sleuthkit.datamodel.TskCoreException; /** * Wrapper object for a DataSource and the information associated with it. * */ class DataSourceSummary { - + + private static final Logger logger = Logger.getLogger(DataSourceSummary.class.getName()); + private static final String INGEST_JOB_STATUS_QUERY = "status_id FROM ingest_jobs WHERE obj_id="; private final DataSource dataSource; + private IngestJobStatusType status = null; private final String type; private final long filesCount; private final long resultsCount; @@ -45,12 +57,26 @@ class DataSourceSummary { */ DataSourceSummary(DataSource dSource, String typeValue, Long numberOfFiles, Long numberOfResults, Long numberOfTags) { dataSource = dSource; + getStatusFromDatabase(); type = typeValue == null ? "" : typeValue; filesCount = numberOfFiles == null ? 0 : numberOfFiles; resultsCount = numberOfResults == null ? 0 : numberOfResults; tagsCount = numberOfTags == null ? 0 : numberOfTags; } + /** + * Get the status of the ingest job from the case database + */ + private void getStatusFromDatabase() { + try { + IngestJobQueryCallback callback = new IngestJobQueryCallback(); + Case.getCurrentCaseThrows().getSleuthkitCase().getCaseDbAccessManager().select(INGEST_JOB_STATUS_QUERY + dataSource.getId(), callback); + status = callback.getStatus(); + } catch (NoCurrentCaseException | TskCoreException ex) { + logger.log(Level.WARNING, "Error getting status for data source from case database", ex); + } + } + /** * Get the DataSource * @@ -60,6 +86,16 @@ class DataSourceSummary { return dataSource; } + /** + * Manually set the ingest job status + * + * @param ingestStatus the status which the ingest job should have + * currently, null to display empty string + */ + void setIngestStatus(IngestJobStatusType ingestStatus) { + status = ingestStatus; + } + /** * Get the type of this DataSource * @@ -87,6 +123,16 @@ class DataSourceSummary { return resultsCount; } + /** + * Get the IngestJobStatusType associated with this data source. + * + * @return the IngestJobStatusType associated with this data source. Can be + * null if the IngestJobStatusType is not STARTED or COMPLETED. + */ + IngestJobStatusType getIngestStatus() { + return status; + } + /** * Get the number of tagged content objects in this DataSource * @@ -95,4 +141,40 @@ class DataSourceSummary { long getTagsCount() { return tagsCount; } + + /** + * Callback to parse result set, getting the status to be associated with + * this data source + */ + class IngestJobQueryCallback implements CaseDbAccessManager.CaseDbAccessQueryCallback { + + private IngestJobStatusType jobStatus = null; + + @Override + public void process(ResultSet rs) { + try { + while (rs.next()) { + IngestJobStatusType currentStatus = IngestJobStatusType.fromID(rs.getInt("status_id")); + if (currentStatus == IngestJobStatusType.COMPLETED) { + jobStatus = currentStatus; + } else if (currentStatus == IngestJobStatusType.STARTED) { + jobStatus = currentStatus; + return; + } + } + } catch (SQLException ex) { + logger.log(Level.WARNING, "Error getting status for ingest job", ex); + } + } + + /** + * Get the status which was determined for this callback + * + * @return the status of the data source which was queried for + */ + IngestJobStatusType getStatus() { + return jobStatus; + } + + } } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDialog.java b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDialog.java index 3fd86c8de8..1535cbefe0 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDialog.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDialog.java @@ -19,14 +19,18 @@ package org.sleuthkit.autopsy.casemodule.datasourcesummary; import java.awt.Frame; +import java.beans.PropertyChangeEvent; import java.util.Map; import java.util.Observable; import java.util.Observer; -import java.util.logging.Logger; import javax.swing.event.ListSelectionEvent; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.casemodule.IngestJobInfoPanel; +import org.sleuthkit.autopsy.ingest.IngestManager; +import org.sleuthkit.autopsy.ingest.events.DataSourceAnalysisCompletedEvent; +import org.sleuthkit.autopsy.ingest.events.DataSourceAnalysisCompletedEvent.Reason; import org.sleuthkit.datamodel.DataSource; +import org.sleuthkit.datamodel.IngestJobInfo; /** * Dialog for displaying the Data Sources Summary information @@ -38,7 +42,6 @@ final class DataSourceSummaryDialog extends javax.swing.JDialog implements Obser private final DataSourceSummaryDetailsPanel detailsPanel; private final DataSourceBrowser dataSourcesPanel; private final IngestJobInfoPanel ingestHistoryPanel; - private static final Logger logger = Logger.getLogger(DataSourceSummaryDialog.class.getName()); /** * Creates new form DataSourceSummaryDialog for displaying a summary of the @@ -73,6 +76,17 @@ final class DataSourceSummaryDialog extends javax.swing.JDialog implements Obser this.repaint(); } }); + //add listener to refresh jobs with Started status when they complete + IngestManager.getInstance().addIngestJobEventListener((PropertyChangeEvent evt) -> { + if (evt instanceof DataSourceAnalysisCompletedEvent) { + DataSourceAnalysisCompletedEvent dsEvent = (DataSourceAnalysisCompletedEvent) evt; + if (dsEvent.getResult() == Reason.ANALYSIS_COMPLETED) { + dataSourcesPanel.refresh(dsEvent.getDataSource().getId(), IngestJobInfo.IngestJobStatusType.COMPLETED); + } else if (dsEvent.getResult() == Reason.ANALYSIS_CANCELLED) { + dataSourcesPanel.refresh(dsEvent.getDataSource().getId(), null); + } + } + }); this.pack(); } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryNode.java b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryNode.java index 8323061a66..0411d94bb2 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryNode.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryNode.java @@ -109,6 +109,7 @@ final class DataSourceSummaryNode extends AbstractNode { static final class DataSourceSummaryEntryNode extends AbstractNode { private final DataSource dataSource; + private final String status; private final String type; private final long filesCount; private final long resultsCount; @@ -124,6 +125,7 @@ final class DataSourceSummaryNode extends AbstractNode { DataSourceSummaryEntryNode(DataSourceSummary dataSourceSummary) { super(Children.LEAF); dataSource = dataSourceSummary.getDataSource(); + status = dataSourceSummary.getIngestStatus() == null ? "" : dataSourceSummary.getIngestStatus().getDisplayName(); type = dataSourceSummary.getType(); filesCount = dataSourceSummary.getFilesCount(); resultsCount = dataSourceSummary.getResultsCount(); @@ -143,6 +145,7 @@ final class DataSourceSummaryNode extends AbstractNode { } @Messages({"DataSourceSummaryNode.column.dataSourceName.header=Data Source Name", + "DataSourceSummaryNode.column.status.header=Ingest Status", "DataSourceSummaryNode.column.type.header=Type", "DataSourceSummaryNode.column.files.header=Files", "DataSourceSummaryNode.column.results.header=Results", @@ -157,6 +160,7 @@ final class DataSourceSummaryNode extends AbstractNode { } sheetSet.put(new NodeProperty<>(Bundle.DataSourceSummaryNode_column_dataSourceName_header(), Bundle.DataSourceSummaryNode_column_dataSourceName_header(), Bundle.DataSourceSummaryNode_column_dataSourceName_header(), dataSource)); + sheetSet.put(new NodeProperty<>(Bundle.DataSourceSummaryNode_column_status_header(), Bundle.DataSourceSummaryNode_column_status_header(), Bundle.DataSourceSummaryNode_column_status_header(), status)); sheetSet.put(new NodeProperty<>(Bundle.DataSourceSummaryNode_column_type_header(), Bundle.DataSourceSummaryNode_column_type_header(), Bundle.DataSourceSummaryNode_column_type_header(), type)); sheetSet.put(new NodeProperty<>(Bundle.DataSourceSummaryNode_column_files_header(), Bundle.DataSourceSummaryNode_column_files_header(), Bundle.DataSourceSummaryNode_column_files_header(), diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/services/FileManager.java b/Core/src/org/sleuthkit/autopsy/casemodule/services/FileManager.java index 5a2521ef8e..31ccf6bac7 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/services/FileManager.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/services/FileManager.java @@ -754,9 +754,8 @@ public class FileManager implements Closeable { * the parent local directory. * @param localFile The local/logical file or directory. * @param progressUpdater notifier to receive progress notifications on - * folders added, or null if not used - * @param progressUpdater Called after each file/directory is added to the - * case database. + * folders added, or null if not used. Called after + * each file/directory is added to the case database. * * @return An AbstractFile representation of the local/logical file. * diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/services/TagsManager.java b/Core/src/org/sleuthkit/autopsy/casemodule/services/TagsManager.java index 896298c2bd..40597e9af3 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/services/TagsManager.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/services/TagsManager.java @@ -21,6 +21,7 @@ package org.sleuthkit.autopsy.casemodule.services; import java.io.Closeable; import java.io.IOException; import java.util.ArrayList; +import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -30,15 +31,18 @@ import java.util.logging.Level; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.casemodule.services.contentviewertags.ContentViewerTagManager; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardArtifactTag; +import org.sleuthkit.datamodel.CaseDbAccessManager; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.ContentTag; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TagName; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData; +import org.sleuthkit.datamodel.TskData.DbType; /** * A per case Autopsy service that manages the addition of content and artifact @@ -48,6 +52,32 @@ public class TagsManager implements Closeable { private static final Logger LOGGER = Logger.getLogger(TagsManager.class.getName()); private final SleuthkitCase caseDb; + + static { + //Create the contentviewer tags table (beta) if the current case does not + //have the table present + Case.addEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), evt -> { + if (evt.getNewValue() != null) { + Case currentCase = (Case) evt.getNewValue(); + try { + CaseDbAccessManager caseDb = currentCase.getSleuthkitCase().getCaseDbAccessManager(); + if (caseDb.tableExists(ContentViewerTagManager.TABLE_NAME)) { + return; + } + + if (currentCase.getSleuthkitCase().getDatabaseType().equals(DbType.SQLITE)) { + caseDb.createTable(ContentViewerTagManager.TABLE_NAME, ContentViewerTagManager.TABLE_SCHEMA_SQLITE); + } else if (currentCase.getSleuthkitCase().getDatabaseType().equals(DbType.POSTGRESQL)) { + caseDb.createTable(ContentViewerTagManager.TABLE_NAME, ContentViewerTagManager.TABLE_SCHEMA_POSTGRESQL); + } + } catch (TskCoreException ex) { + LOGGER.log(Level.SEVERE, + String.format("Unable to create the %s table for image tag storage.", + ContentViewerTagManager.TABLE_NAME), ex); + } + } + }); + } /** * Tests whether or not a given tag display name contains an illegal diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/services/contentviewertags/ContentViewerTagManager.java b/Core/src/org/sleuthkit/autopsy/casemodule/services/contentviewertags/ContentViewerTagManager.java new file mode 100755 index 0000000000..db3424a696 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/services/contentviewertags/ContentViewerTagManager.java @@ -0,0 +1,270 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.casemodule.services.contentviewertags; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.sql.ResultSet; +import java.sql.SQLException; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.datamodel.ContentTag; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * A per case Autopsy service that manages the addition of content viewer tags + * to the case database. This manager is also responsible for serializing and + * deserializing instances of your tag data objects for persistence and + * retrieval. + */ +public class ContentViewerTagManager { + + //Used to convert Java beans into the physical representation that will be stored + //in the database. + private static final ObjectMapper SERIALIZER = new ObjectMapper(); + + public static final String TABLE_NAME = "beta_tag_app_data"; + public static final String TABLE_SCHEMA_SQLITE = "(app_data_id INTEGER PRIMARY KEY, " + + "content_tag_id INTEGER NOT NULL, app_data TEXT NOT NULL, " + + "FOREIGN KEY(content_tag_id) REFERENCES content_tags(tag_id))"; + public static final String TABLE_SCHEMA_POSTGRESQL = "(app_data_id BIGSERIAL PRIMARY KEY, " + + "content_tag_id INTEGER NOT NULL, app_data TEXT NOT NULL, " + + "FOREIGN KEY(content_tag_id) REFERENCES content_tags(tag_id))"; + + private static final String INSERT_TAG_DATA = "(content_tag_id, app_data) VALUES (%d, '%s')"; + private static final String UPDATE_TAG_DATA = "SET content_tag_id = %d, app_data = '%s' WHERE app_data_id = %d"; + private static final String SELECT_TAG_DATA = "* FROM " + TABLE_NAME + " WHERE content_tag_id = %d"; + private static final String DELETE_TAG_DATA = "WHERE app_data_id = %d"; + + /** + * Creates and saves a new ContentViewerTag in the case database. The + * generic tag data instance T will be automatically serialized into a + * storable format. + * + * @param contentTag ContentTag that this ContentViewerTag is associated + * with (1:1). + * @param tagDataBean Data instance that contains the tag information to be + * persisted. + * @return An instance of a ContentViewerTag of type T, which contains all + * the stored information. + * + * @throws SerializationException Thrown if the tag data instance T could + * not be serialized into a storable format. + * @throws TskCoreException Thrown if this operation did not successfully + * persist in the case database. + * @throws NoCurrentCaseException Thrown if invocation of this method occurs + * when no case is open. + */ + public static ContentViewerTag saveTag(ContentTag contentTag, T tagDataBean) + throws SerializationException, TskCoreException, NoCurrentCaseException { + try { + long contentTagId = contentTag.getId(); + String serialAppData = SERIALIZER.writeValueAsString(tagDataBean); + String insertTemplateInstance = String.format(INSERT_TAG_DATA, + contentTagId, serialAppData); + long insertId = Case.getCurrentCaseThrows() + .getSleuthkitCase() + .getCaseDbAccessManager() + .insert(TABLE_NAME, insertTemplateInstance); + return new ContentViewerTag<>(insertId, contentTag, tagDataBean); + } catch (JsonProcessingException ex) { + throw new SerializationException("Unable to convert object instance into a storable format", ex); + } + } + + /** + * Updates the ContentViewerTag instance with the new tag data T and + * persists the changes to the case database. + * + * @param oldTag ContentViewerTag instance to be updated + * @param tagDataBean Data instance that contains the updated information to + * be persisted. + * + * @throws SerializationException Thrown if the tag data instance T could + * not be serialized into a storable format. + * @throws TskCoreException Thrown if this operation did not successfully + * persist in the case database. + * @throws NoCurrentCaseException Thrown if invocation of this method occurs + * when no case is open. + */ + public static ContentViewerTag updateTag(ContentViewerTag oldTag, T tagDataBean) + throws SerializationException, TskCoreException, NoCurrentCaseException { + try { + String serialAppData = SERIALIZER.writeValueAsString(tagDataBean); + String updateTemplateInstance = String.format(UPDATE_TAG_DATA, + oldTag.getContentTag().getId(), serialAppData, oldTag.getId()); + Case.getCurrentCaseThrows() + .getSleuthkitCase() + .getCaseDbAccessManager() + .update(TABLE_NAME, updateTemplateInstance); + return new ContentViewerTag<>(oldTag.getId(), oldTag.getContentTag(), tagDataBean); + } catch (JsonProcessingException ex) { + throw new SerializationException("Unable to convert object instance into a storable format", ex); + } + } + + /** + * Retrieves a ContentViewerTag instance that is associated with the + * specified ContentTag. The Java class T that represents the technical + * details of the tag should be passed so that automatic binding can take + * place. + * + * @param contentTag ContentTag that this ContentViewerTag is associated + * with (1:1) + * @param clazz Generic class that will be instantiated and filled in with + * data. + * @return ContentViewerTag with an instance of T as a member variable or + * null if the content tag does not have an associated ContentViewerTag of + * type T. + * + * @throws TskCoreException Thrown if this operation did not successfully + * persist in the case database. + * @throws NoCurrentCaseException Thrown if invocation of this method occurs + * when no case is open. + */ + public static ContentViewerTag getTag(ContentTag contentTag, Class clazz) throws TskCoreException, NoCurrentCaseException { + String selectTemplateInstance = String.format(SELECT_TAG_DATA, contentTag.getId()); + final ResultWrapper> result = new ResultWrapper<>(); + Case.getCurrentCaseThrows() + .getSleuthkitCase() + .getCaseDbAccessManager() + .select(selectTemplateInstance, (ResultSet rs) -> { + try { + if (rs.next()) { + long tagId = rs.getLong(1); + String appDetails = rs.getString(3); + try { + T instance = SERIALIZER.readValue(appDetails, clazz); + result.setResult(new ContentViewerTag<>(tagId, contentTag, instance)); + } catch (IOException ex) { + //Databind for type T failed. Not a system error + //but rather a logic error on the part of the caller. + result.setResult(null); + } + } + } catch (SQLException ex) { + result.setException(ex); + } + }); + + if (result.hasException()) { + throw new TskCoreException("Unable to select tag from case db", result.getException()); + } + + return result.getResult(); + } + + /** + * Wrapper for holding state in the CaseDbAccessQueryCallback. + * CaseDbAccessQueryCallback has no support for exception handling. + * + * @param + */ + private static class ResultWrapper { + + private T result; + private SQLException ex = null; + + public void setResult(T result) { + this.result = result; + } + + public void setException(SQLException ex) { + this.ex = ex; + } + + public boolean hasException() { + return this.ex != null; + } + + public SQLException getException() { + return ex; + } + + public T getResult() { + return result; + } + } + + /** + * Deletes the content viewer tag with the specified id. + * + * @param contentViewerTag ContentViewerTag to delete + * @throws TskCoreException Thrown if this operation did not successfully + * persist in the case database. + * @throws NoCurrentCaseException Thrown if invocation of this method occurs + * when no case is open. + */ + public static void deleteTag(ContentViewerTag contentViewerTag) throws TskCoreException, NoCurrentCaseException { + String deleteTemplateInstance = String.format(DELETE_TAG_DATA, contentViewerTag.getId()); + Case.getCurrentCaseThrows() + .getSleuthkitCase() + .getCaseDbAccessManager() + .delete(TABLE_NAME, deleteTemplateInstance); + } + + /** + * This class represents a stored tag in the case database. It is a wrapper + * for the tag id, the attached Content tag object, and the Java bean + * instance that describes the technical details for reconstructing the tag. + * + * @param Generic class type that will be instantiated and filled in + * with data. + */ + public static class ContentViewerTag { + + private final long id; + private final ContentTag contentTag; + private final T details; + + private ContentViewerTag(long id, ContentTag contentTag, T details) { + this.id = id; + this.contentTag = contentTag; + this.details = details; + } + + public long getId() { + return id; + } + + public ContentTag getContentTag() { + return contentTag; + } + + public T getDetails() { + return details; + } + } + + /** + * System exception thrown in the event that class instance T could not be + * properly serialized. + */ + public static class SerializationException extends Exception { + + public SerializationException(String message, Exception source) { + super(message, source); + } + } + + //Prevent this class from being instantiated. + private ContentViewerTagManager() { + } +} diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/Bundle.properties b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/Bundle.properties index aa2b4b9297..fca33fe6f4 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/Bundle.properties @@ -1,7 +1,6 @@ -DataContentViewerOtherCases.selectAllMenuItem.text=Select All DataContentViewerOtherCases.showCaseDetailsMenuItem.text=Show Case Details DataContentViewerOtherCases.table.toolTip.text=Click column name to sort. Right-click on the table for more options. -DataContentViewerOtherCases.exportToCSVMenuItem.text=Export Selected Rows to CSV +DataContentViewerOtherCases.exportToCSVMenuItem.text=Export all Other Occurrences to CSV DataContentViewerOtherCases.showCommonalityMenuItem.text=Show Frequency DataContentViewerOtherCases.earliestCaseDate.text=Earliest Case Date DataContentViewerOtherCases.earliestCaseLabel.toolTipText= diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/Bundle.properties-MERGED index 21c7b81c76..b2606170ed 100755 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/Bundle.properties-MERGED @@ -13,12 +13,11 @@ DataContentViewerOtherCases.dataSources.header.text=Data Source Name DataContentViewerOtherCases.earliestCaseNotAvailable=\ Not Enabled. DataContentViewerOtherCases.foundIn.text=Found %d instances in %d cases and %d data sources. DataContentViewerOtherCases.noOpenCase.errMsg=No open case available. -DataContentViewerOtherCases.selectAllMenuItem.text=Select All DataContentViewerOtherCases.showCaseDetailsMenuItem.text=Show Case Details DataContentViewerOtherCases.table.noArtifacts=Item has no attributes with which to search. DataContentViewerOtherCases.table.noResultsFound=No results found. DataContentViewerOtherCases.table.toolTip.text=Click column name to sort. Right-click on the table for more options. -DataContentViewerOtherCases.exportToCSVMenuItem.text=Export Selected Rows to CSV +DataContentViewerOtherCases.exportToCSVMenuItem.text=Export all Other Occurrences to CSV DataContentViewerOtherCases.showCommonalityMenuItem.text=Show Frequency DataContentViewerOtherCases.earliestCaseDate.text=Earliest Case Date DataContentViewerOtherCases.earliestCaseLabel.toolTipText= diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.form b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.form index 03a75cdd5f..fd1b0b81b1 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.form +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.form @@ -11,13 +11,6 @@ - - - - - - - @@ -46,11 +39,11 @@ - + - + @@ -68,7 +61,7 @@ - + @@ -101,7 +94,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.java b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.java index 16c2bbb681..9713a5b47a 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.java @@ -89,11 +89,10 @@ import org.sleuthkit.datamodel.TskData; public class DataContentViewerOtherCases extends JPanel implements DataContentViewer { private static final long serialVersionUID = -1L; - + private static final String UUID_PLACEHOLDER_STRING = "NoCorrelationAttributeInstance"; private static final Logger LOGGER = Logger.getLogger(DataContentViewerOtherCases.class.getName()); private static final CorrelationCaseWrapper NO_ARTIFACTS_CASE = new CorrelationCaseWrapper(Bundle.DataContentViewerOtherCases_table_noArtifacts()); private static final CorrelationCaseWrapper NO_RESULTS_CASE = new CorrelationCaseWrapper(Bundle.DataContentViewerOtherCases_table_noResultsFound()); - private final OtherOccurrencesFilesTableModel filesTableModel; private final OtherOccurrencesCasesTableModel casesTableModel; private final OtherOccurrencesDataSourcesTableModel dataSourcesTableModel; @@ -129,12 +128,20 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi reset(); } + /** + * Get a placeholder string to use in place of case uuid when it isn't + * available + * + * @return UUID_PLACEHOLDER_STRING + */ + static String getPlaceholderUUID() { + return UUID_PLACEHOLDER_STRING; + } + private void customizeComponents() { ActionListener actList = (ActionEvent e) -> { JMenuItem jmi = (JMenuItem) e.getSource(); - if (jmi.equals(selectAllMenuItem)) { - filesTable.selectAll(); - } else if (jmi.equals(showCaseDetailsMenuItem)) { + if (jmi.equals(showCaseDetailsMenuItem)) { showCaseDetails(filesTable.getSelectedRow()); } else if (jmi.equals(exportToCSVMenuItem)) { try { @@ -148,7 +155,6 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi }; exportToCSVMenuItem.addActionListener(actList); - selectAllMenuItem.addActionListener(actList); showCaseDetailsMenuItem.addActionListener(actList); showCommonalityMenuItem.addActionListener(actList); @@ -804,6 +810,13 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi occurrencePanel.getPreferredSize(); detailsPanelScrollPane.setViewportView(occurrencePanel); } else { + String currentCaseName; + try { + currentCaseName = Case.getCurrentCaseThrows().getName(); + } catch (NoCurrentCaseException ex) { + currentCaseName = null; + LOGGER.log(Level.WARNING, "Unable to get current case for other occurrences content viewer", ex); + } for (CorrelationAttributeInstance corAttr : correlationAttributes) { Map correlatedNodeDataMap = new HashMap<>(0); @@ -817,7 +830,7 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi && casesTableModel.getCorrelationCase(casesTable.convertRowIndexToModel(selectedRow)).getCaseUUID().equals(nodeData.getCorrelationAttributeInstance().getCorrelationCase().getCaseUUID())) { dataSourcesTableModel.addNodeData(nodeData); } - } else { + } else if (currentCaseName != null && (casesTableModel.getCorrelationCase(casesTable.convertRowIndexToModel(selectedRow)).getCaseUUID().equals(currentCaseName))) { dataSourcesTableModel.addNodeData(nodeData); } } catch (EamDbException ex) { @@ -837,7 +850,6 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi * selection */ private void updateOnDataSourceSelection() { - int[] selectedCaseIndexes = casesTable.getSelectedRows(); int[] selectedDataSources = dataSourcesTable.getSelectedRows(); filesTableModel.clearTable(); for (CorrelationAttributeInstance corAttr : correlationAttributes) { @@ -846,23 +858,20 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi // get correlation and reference set instances from DB correlatedNodeDataMap.putAll(getCorrelatedInstances(corAttr, dataSourceName, deviceId)); for (OtherOccurrenceNodeInstanceData nodeData : correlatedNodeDataMap.values()) { - for (int selectedCaseRow : selectedCaseIndexes) { - for (int selectedDataSourceRow : selectedDataSources) { - try { - if (nodeData.isCentralRepoNode()) { - if (casesTableModel.getCorrelationCase(casesTable.convertRowIndexToModel(selectedCaseRow)) != null - && casesTableModel.getCorrelationCase(casesTable.convertRowIndexToModel(selectedCaseRow)).getCaseUUID().equals(nodeData.getCorrelationAttributeInstance().getCorrelationCase().getCaseUUID()) - && dataSourcesTableModel.getDeviceIdForRow(dataSourcesTable.convertRowIndexToModel(selectedDataSourceRow)).equals(nodeData.getDeviceID())) { - filesTableModel.addNodeData(nodeData); - } - } else { - if (dataSourcesTableModel.getDeviceIdForRow(dataSourcesTable.convertRowIndexToModel(selectedDataSourceRow)).equals(nodeData.getDeviceID())) { - filesTableModel.addNodeData(nodeData); - } + for (int selectedDataSourceRow : selectedDataSources) { + try { + if (nodeData.isCentralRepoNode()) { + if (dataSourcesTableModel.getCaseUUIDForRow(dataSourcesTable.convertRowIndexToModel(selectedDataSourceRow)).equals(nodeData.getCorrelationAttributeInstance().getCorrelationCase().getCaseUUID()) + && dataSourcesTableModel.getDeviceIdForRow(dataSourcesTable.convertRowIndexToModel(selectedDataSourceRow)).equals(nodeData.getDeviceID())) { + filesTableModel.addNodeData(nodeData); + } + } else { + if (dataSourcesTableModel.getDeviceIdForRow(dataSourcesTable.convertRowIndexToModel(selectedDataSourceRow)).equals(nodeData.getDeviceID())) { + filesTableModel.addNodeData(nodeData); } - } catch (EamDbException ex) { - LOGGER.log(Level.WARNING, "Unable to get correlation attribute instance from OtherOccurrenceNodeInstanceData for case " + nodeData.getCaseName(), ex); } + } catch (EamDbException ex) { + LOGGER.log(Level.WARNING, "Unable to get correlation attribute instance from OtherOccurrenceNodeInstanceData for case " + nodeData.getCaseName(), ex); } } } @@ -928,7 +937,7 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi if (EamDb.isEnabled()) { CorrelationCase partialCase; partialCase = casesTableModel.getCorrelationCase(casesTable.convertRowIndexToModel(caseTableRowIdx)); - if (partialCase == null){ + if (partialCase == null) { return ""; } return EamDb.getInstance().getCaseByUUID(partialCase.getCaseUUID()).getCreationDate(); @@ -951,7 +960,6 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi private void initComponents() { rightClickPopupMenu = new javax.swing.JPopupMenu(); - selectAllMenuItem = new javax.swing.JMenuItem(); exportToCSVMenuItem = new javax.swing.JMenuItem(); showCaseDetailsMenuItem = new javax.swing.JMenuItem(); showCommonalityMenuItem = new javax.swing.JMenuItem(); @@ -981,9 +989,6 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi } }); - org.openide.awt.Mnemonics.setLocalizedText(selectAllMenuItem, org.openide.util.NbBundle.getMessage(DataContentViewerOtherCases.class, "DataContentViewerOtherCases.selectAllMenuItem.text")); // NOI18N - rightClickPopupMenu.add(selectAllMenuItem); - org.openide.awt.Mnemonics.setLocalizedText(exportToCSVMenuItem, org.openide.util.NbBundle.getMessage(DataContentViewerOtherCases.class, "DataContentViewerOtherCases.exportToCSVMenuItem.text")); // NOI18N rightClickPopupMenu.add(exportToCSVMenuItem); @@ -993,9 +998,9 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi org.openide.awt.Mnemonics.setLocalizedText(showCommonalityMenuItem, org.openide.util.NbBundle.getMessage(DataContentViewerOtherCases.class, "DataContentViewerOtherCases.showCommonalityMenuItem.text")); // NOI18N rightClickPopupMenu.add(showCommonalityMenuItem); - setMinimumSize(new java.awt.Dimension(600, 10)); + setMinimumSize(new java.awt.Dimension(1000, 10)); setOpaque(false); - setPreferredSize(new java.awt.Dimension(600, 63)); + setPreferredSize(new java.awt.Dimension(1000, 63)); tableContainerPanel.setPreferredSize(new java.awt.Dimension(600, 63)); tableContainerPanel.setRequestFocusEnabled(false); @@ -1064,7 +1069,7 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi .addComponent(earliestCaseDate, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(foundInLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) - .addComponent(tablesViewerSplitPane, javax.swing.GroupLayout.DEFAULT_SIZE, 765, Short.MAX_VALUE)) + .addComponent(tablesViewerSplitPane, javax.swing.GroupLayout.DEFAULT_SIZE, 990, Short.MAX_VALUE)) .addContainerGap()) ); tableContainerPanelLayout.setVerticalGroup( @@ -1085,7 +1090,7 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi this.setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(tableContainerPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 775, Short.MAX_VALUE) + .addComponent(tableContainerPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 1000, Short.MAX_VALUE) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) @@ -1125,7 +1130,6 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi private javax.swing.JScrollPane filesTableScrollPane; private javax.swing.JLabel foundInLabel; private javax.swing.JPopupMenu rightClickPopupMenu; - private javax.swing.JMenuItem selectAllMenuItem; private javax.swing.JMenuItem showCaseDetailsMenuItem; private javax.swing.JMenuItem showCommonalityMenuItem; private javax.swing.JPanel tableContainerPanel; @@ -1141,6 +1145,7 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi private final String dataSourceID; private final String filePath; private final String type; + private final String caseUUID; UniquePathKey(OtherOccurrenceNodeInstanceData nodeData) { super(); @@ -1151,6 +1156,20 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi filePath = null; } type = nodeData.getType(); + String tempCaseUUID; + try { + tempCaseUUID = nodeData.getCorrelationAttributeInstance().getCorrelationCase().getCaseUUID(); + } catch (EamDbException ignored) { + //non central repo nodeData won't have a correlation case + try { + tempCaseUUID = Case.getCurrentCaseThrows().getName(); + //place holder value will be used since correlation attribute was unavailble + } catch (NoCurrentCaseException ex) { + LOGGER.log(Level.WARNING, "Unable to get current case", ex); + tempCaseUUID = UUID_PLACEHOLDER_STRING; + } + } + caseUUID = tempCaseUUID; } @Override @@ -1159,14 +1178,15 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi UniquePathKey otherKey = (UniquePathKey) (other); return (Objects.equals(otherKey.getDataSourceID(), this.getDataSourceID()) && Objects.equals(otherKey.getFilePath(), this.getFilePath()) - && Objects.equals(otherKey.getType(), this.getType())); + && Objects.equals(otherKey.getType(), this.getType()) + && Objects.equals(otherKey.getCaseUUID(), this.getCaseUUID())); } return false; } @Override public int hashCode() { - return Objects.hash(getDataSourceID(), getFilePath(), getType()); + return Objects.hash(getDataSourceID(), getFilePath(), getType(), getCaseUUID()); } /** @@ -1195,5 +1215,14 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi String getDataSourceID() { return dataSourceID; } + + /** + * Get the case uuid for the UniquePathKey + * + * @return the case UUID + */ + String getCaseUUID() { + return caseUUID; + } } } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OccurrencePanel.java b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OccurrencePanel.java index d8b18d5f82..36877a124d 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OccurrencePanel.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OccurrencePanel.java @@ -175,7 +175,7 @@ final class OccurrencePanel extends javax.swing.JPanel { org.openide.awt.Mnemonics.setLocalizedText(knownStatusLabel, Bundle.OccurrencePanel_commonPropertyKnownStatusLabel_text()); addItemToBag(gridY, 0, 0, 0, knownStatusLabel); javax.swing.JLabel knownStatusValue = new javax.swing.JLabel(); - knownStatusValue.setText(knownStatus.toString()); + knownStatusValue.setText(knownStatus.getName()); if (knownStatus == TskData.FileKnown.BAD) { knownStatusValue.setForeground(Color.RED); } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesDataSourcesTableModel.java b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesDataSourcesTableModel.java index 30b44ea5a6..76871d074d 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesDataSourcesTableModel.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesDataSourcesTableModel.java @@ -21,8 +21,13 @@ package org.sleuthkit.autopsy.centralrepository.contentviewer; import java.util.LinkedHashSet; import java.util.Objects; import java.util.Set; +import java.util.logging.Level; import javax.swing.table.AbstractTableModel; import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.centralrepository.datamodel.EamDbException; +import org.sleuthkit.autopsy.coreutils.Logger; /** * Model for cells in the data sources section of the other occurrences data @@ -31,6 +36,7 @@ import org.openide.util.NbBundle; final class OtherOccurrencesDataSourcesTableModel extends AbstractTableModel { private static final long serialVersionUID = 1L; + private static final Logger logger = Logger.getLogger(OtherOccurrencesDataSourcesTableModel.class.getName()); private final Set dataSourceSet = new LinkedHashSet<>(); /** @@ -86,6 +92,25 @@ final class OtherOccurrencesDataSourcesTableModel extends AbstractTableModel { return ((DataSourceColumnItem) dataSourceSet.toArray()[rowIdx]).getDeviceId(); } + /** + * Get the case uuid of the case the data source shown at the specified row + * index exists in + * + * @param rowIdx the row index of the data source you want the case uuid for + * + * @return the case uuid of the case the specified data source exists in or + * an empty string if a device id could not be retrieved + */ + String getCaseUUIDForRow(int rowIdx) { + //if anything would prevent this from working we will return an empty string + if (dataSourceSet.isEmpty() || rowIdx < 0 + || rowIdx >= dataSourceSet.size() + || !(dataSourceSet.toArray()[rowIdx] instanceof DataSourceColumnItem)) { + return ""; + } + return ((DataSourceColumnItem) dataSourceSet.toArray()[rowIdx]).getCaseUUID(); + } + /** * Get the case name of the data source shown at the specified row index * @@ -115,7 +140,21 @@ final class OtherOccurrencesDataSourcesTableModel extends AbstractTableModel { * @param newNodeData data to add to the table */ void addNodeData(OtherOccurrenceNodeData newNodeData) { - dataSourceSet.add(new DataSourceColumnItem((OtherOccurrenceNodeInstanceData) newNodeData)); + OtherOccurrenceNodeInstanceData nodeData = (OtherOccurrenceNodeInstanceData) newNodeData; + String caseUUID; + try { + caseUUID = nodeData.getCorrelationAttributeInstance().getCorrelationCase().getCaseUUID(); + } catch (EamDbException ignored) { + //non central repo nodeData won't have a correlation case + try { + caseUUID = Case.getCurrentCaseThrows().getName(); + //place holder value will be used since correlation attribute was unavailble + } catch (NoCurrentCaseException ex) { + logger.log(Level.WARNING, "Unable to get current case", ex); + caseUUID = DataContentViewerOtherCases.getPlaceholderUUID(); + } + } + dataSourceSet.add(new DataSourceColumnItem(nodeData.getCaseName(), nodeData.getDeviceID(), nodeData.getDataSourceName(), caseUUID)); fireTableDataChanged(); } @@ -136,30 +175,23 @@ final class OtherOccurrencesDataSourcesTableModel extends AbstractTableModel { private final String caseName; private final String deviceId; private final String dataSourceName; - - /** - * Create a DataSourceColumnItem given an - * OtherOccurrenceNodeInstanceData object - * - * @param nodeData the OtherOccurrenceNodeInstanceData which contains - * the data source information - */ - private DataSourceColumnItem(OtherOccurrenceNodeInstanceData nodeData) { - this(nodeData.getCaseName(), nodeData.getDeviceID(), nodeData.getDataSourceName()); - } + private final String caseUUID; /** * Create a DataSourceColumnItem given a case name, device id, and data * source name * * @param caseName the name of the case the data source exists in - * @param deviceId the name of the device id for the data source + * @param deviceId the device id for the data source * @param dataSourceName the name of the data source + * @param caseUUID the case uuid for the case the data source + * exists in */ - private DataSourceColumnItem(String caseName, String deviceId, String dataSourceName) { + private DataSourceColumnItem(String caseName, String deviceId, String dataSourceName, String caseUUID) { this.caseName = caseName; this.deviceId = deviceId; this.dataSourceName = dataSourceName; + this.caseUUID = caseUUID; } /** @@ -189,17 +221,27 @@ final class OtherOccurrencesDataSourcesTableModel extends AbstractTableModel { return caseName; } + /** + * Get the case uuid of the case the data source exists in + * + * @return the case UUID + */ + private String getCaseUUID() { + return caseUUID; + } + @Override public boolean equals(Object other) { return other instanceof DataSourceColumnItem && caseName.equals(((DataSourceColumnItem) other).getCaseName()) && dataSourceName.equals(((DataSourceColumnItem) other).getDataSourceName()) - && deviceId.equals(((DataSourceColumnItem) other).getDeviceId()); + && deviceId.equals(((DataSourceColumnItem) other).getDeviceId()) + && caseUUID.equals(((DataSourceColumnItem) other).getCaseUUID()); } @Override public int hashCode() { - return Objects.hash(caseName, deviceId, dataSourceName); + return Objects.hash(caseName, deviceId, dataSourceName, caseUUID); } } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesFilesTableModel.java b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesFilesTableModel.java index dd797a19d3..3344951857 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesFilesTableModel.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesFilesTableModel.java @@ -22,9 +22,14 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.logging.Level; import javax.swing.table.AbstractTableModel; import org.openide.util.NbBundle.Messages; import org.apache.commons.io.FilenameUtils; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.centralrepository.datamodel.EamDbException; +import org.sleuthkit.autopsy.coreutils.Logger; /** * Model for cells in the files section of the other occurrences data content @@ -33,6 +38,7 @@ import org.apache.commons.io.FilenameUtils; public class OtherOccurrencesFilesTableModel extends AbstractTableModel { private static final long serialVersionUID = 1L; + private static final Logger logger = Logger.getLogger(OtherOccurrencesFilesTableModel.class.getName()); private final List nodeKeys = new ArrayList<>(); private final Map> nodeMap = new HashMap<>(); @@ -114,7 +120,20 @@ public class OtherOccurrencesFilesTableModel extends AbstractTableModel { } private String createNodeKey(OtherOccurrenceNodeInstanceData nodeData) { - return nodeData.getCaseName() + nodeData.getDataSourceName() + nodeData.getDeviceID() + nodeData.getFilePath(); + String caseUUID; + try { + caseUUID = nodeData.getCorrelationAttributeInstance().getCorrelationCase().getCaseUUID(); + } catch (EamDbException ignored) { + //non central repo nodeData won't have a correlation case + try { + caseUUID = Case.getCurrentCaseThrows().getName(); + //place holder value will be used since correlation attribute was unavailble + } catch (NoCurrentCaseException ex) { + logger.log(Level.WARNING, "Unable to get current case", ex); + caseUUID = DataContentViewerOtherCases.getPlaceholderUUID(); + } + } + return nodeData.getCaseName() + nodeData.getDataSourceName() + nodeData.getDeviceID() + nodeData.getFilePath() + caseUUID; } /** diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeNormalizer.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeNormalizer.java index f6f3b52c6e..08ed088cf1 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeNormalizer.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeNormalizer.java @@ -123,7 +123,7 @@ final public class CorrelationAttributeNormalizer { /** * Verify there are no slashes or invalid domain name characters (such as - * '?' or \: ). Normalize to lower case. + * '?'). Normalize to lower case. */ private static String normalizeDomain(String data) throws CorrelationAttributeNormalizationException { DomainValidator validator = DomainValidator.getInstance(true); diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestSettingsPanel.form b/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestSettingsPanel.form index 82383f135f..6c189d736e 100755 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestSettingsPanel.form +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestSettingsPanel.form @@ -29,7 +29,7 @@ - + @@ -44,7 +44,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestSettingsPanel.java b/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestSettingsPanel.java index 6438b399e7..c9ecbfc146 100755 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestSettingsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/ingestmodule/IngestSettingsPanel.java @@ -88,7 +88,7 @@ final class IngestSettingsPanel extends IngestModuleIngestJobSettingsPanel { .addComponent(flagTaggedNotableItemsCheckbox, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(flagPreviouslySeenDevicesCheckbox, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(createCorrelationPropertiesCheckbox, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)))) - .addContainerGap(65, Short.MAX_VALUE)) + .addContainerGap(47, Short.MAX_VALUE)) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) @@ -101,7 +101,7 @@ final class IngestSettingsPanel extends IngestModuleIngestJobSettingsPanel { .addComponent(flagTaggedNotableItemsCheckbox) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(flagPreviouslySeenDevicesCheckbox) - .addContainerGap(197, Short.MAX_VALUE)) + .addContainerGap(47, Short.MAX_VALUE)) ); }// //GEN-END:initComponents diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/Bundle.properties b/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/Bundle.properties index 635f5a558c..0b30e2584f 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/Bundle.properties @@ -57,7 +57,6 @@ ManageCorrelationPropertiesDialog.cancelButton.text=Cancel ManageCorrelationPropertiesDialog.okButton.text=OK GlobalSettingsPanel.bnManageProperties.text=Manage Correlation Properties EamDbSettingsDialog.lbDatabaseDesc.text=Database File: -EamDbSettingsDialog.lbFullDbPath.text= GlobalSettingsPanel.cbUseCentralRepo.text=Use a Central Repository GlobalSettingsPanel.organizationTextArea.text=Organization information can be tracked in the Central Repository. GlobalSettingsPanel.manageOrganizationButton.text=Manage Organizations @@ -84,4 +83,4 @@ ManageCasesDialog.orgLabel.text=Organization: ManageCasesDialog.closeButton.text=Close ManageCasesDialog.notesLabel.text=Notes: ManageCasesDialog.dataSourcesLabel.text=Data Sources: -ManageCasesDialog.caseInfoLabel.text=Case Info: \ No newline at end of file +ManageCasesDialog.caseInfoLabel.text=Case Info: 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 454cb29e39..dc42aa3b68 100755 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/Bundle.properties-MERGED @@ -115,7 +115,6 @@ ManageCorrelationPropertiesDialog.cancelButton.text=Cancel ManageCorrelationPropertiesDialog.okButton.text=OK GlobalSettingsPanel.bnManageProperties.text=Manage Correlation Properties EamDbSettingsDialog.lbDatabaseDesc.text=Database File: -EamDbSettingsDialog.lbFullDbPath.text= GlobalSettingsPanel.cbUseCentralRepo.text=Use a Central Repository GlobalSettingsPanel.organizationTextArea.text=Organization information can be tracked in the Central Repository. GlobalSettingsPanel.manageOrganizationButton.text=Manage Organizations diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/EamDbSettingsDialog.form b/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/EamDbSettingsDialog.form index 50979ba938..b063773f48 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/EamDbSettingsDialog.form +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/EamDbSettingsDialog.form @@ -9,6 +9,7 @@ + @@ -35,18 +36,18 @@ - + - - - + + + - + @@ -59,9 +60,9 @@ - + - + @@ -85,6 +86,15 @@ + + + + + + + + + @@ -115,43 +125,37 @@ - + - - + + + - + - - - - - - - + + + + - - - - - - - - - - - - - + + + + + + + + + @@ -171,40 +175,42 @@ - + - + - + - + - + - + - - - - + + + + + + + + - - @@ -215,6 +221,9 @@ + + + @@ -225,6 +234,9 @@ + + + @@ -242,42 +254,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -291,6 +338,9 @@ + + + @@ -298,6 +348,15 @@ + + + + + + + + + @@ -305,12 +364,8 @@ - - - - - - + + @@ -324,6 +379,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/EamDbSettingsDialog.java b/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/EamDbSettingsDialog.java index 19b1c4c8e5..5c68b51b92 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/EamDbSettingsDialog.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/EamDbSettingsDialog.java @@ -142,12 +142,17 @@ public class EamDbSettingsDialog extends JDialog { lbSingleUserSqLite = new javax.swing.JLabel(); lbDatabaseType = new javax.swing.JLabel(); lbDatabaseDesc = new javax.swing.JLabel(); - lbFullDbPath = new javax.swing.JLabel(); filler1 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 32767)); + dataBaseFileScrollPane = new javax.swing.JScrollPane(); + dataBaseFileTextArea = new javax.swing.JTextArea(); setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE); + setResizable(false); org.openide.awt.Mnemonics.setLocalizedText(bnCancel, org.openide.util.NbBundle.getMessage(EamDbSettingsDialog.class, "EamDbSettingsDialog.bnCancel.text")); // NOI18N + bnCancel.setMaximumSize(new java.awt.Dimension(79, 23)); + bnCancel.setMinimumSize(new java.awt.Dimension(79, 23)); + bnCancel.setPreferredSize(new java.awt.Dimension(79, 23)); bnCancel.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { bnCancelActionPerformed(evt); @@ -169,25 +174,30 @@ public class EamDbSettingsDialog extends JDialog { .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(bnOk) .addGap(11, 11, 11) - .addComponent(bnCancel) + .addComponent(bnCancel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addContainerGap()) ); + + pnButtonsLayout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {bnCancel, bnOk}); + pnButtonsLayout.setVerticalGroup( pnButtonsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(pnButtonsLayout.createSequentialGroup() .addGap(0, 0, 0) .addGroup(pnButtonsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(bnOk) - .addComponent(bnCancel)) + .addComponent(bnCancel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) .addGap(0, 0, 0)) ); pnSQLiteSettings.setBorder(javax.swing.BorderFactory.createEtchedBorder()); org.openide.awt.Mnemonics.setLocalizedText(lbDatabasePath, org.openide.util.NbBundle.getMessage(EamDbSettingsDialog.class, "EamDbSettingsDialog.lbDatabasePath.text")); // NOI18N + lbDatabasePath.setPreferredSize(new java.awt.Dimension(80, 14)); tfDatabasePath.setText(org.openide.util.NbBundle.getMessage(EamDbSettingsDialog.class, "EamDbSettingsDialog.tfDatabasePath.text")); // NOI18N tfDatabasePath.setToolTipText(org.openide.util.NbBundle.getMessage(EamDbSettingsDialog.class, "EamDbSettingsDialog.tfDatabasePath.toolTipText")); // NOI18N + tfDatabasePath.setPreferredSize(new java.awt.Dimension(420, 23)); org.openide.awt.Mnemonics.setLocalizedText(bnDatabasePathFileOpen, org.openide.util.NbBundle.getMessage(EamDbSettingsDialog.class, "EamDbSettingsDialog.bnDatabasePathFileOpen.text")); // NOI18N bnDatabasePathFileOpen.addActionListener(new java.awt.event.ActionListener() { @@ -197,14 +207,27 @@ public class EamDbSettingsDialog extends JDialog { }); org.openide.awt.Mnemonics.setLocalizedText(lbHostName, org.openide.util.NbBundle.getMessage(EamDbSettingsDialog.class, "EamDbSettingsDialog.lbHostName.text")); // NOI18N + lbHostName.setPreferredSize(new java.awt.Dimension(80, 14)); + + tbDbHostname.setPreferredSize(new java.awt.Dimension(509, 20)); org.openide.awt.Mnemonics.setLocalizedText(lbPort, org.openide.util.NbBundle.getMessage(EamDbSettingsDialog.class, "EamDbSettingsDialog.lbPort.text")); // NOI18N + lbPort.setPreferredSize(new java.awt.Dimension(80, 14)); + + tbDbPort.setPreferredSize(new java.awt.Dimension(509, 20)); org.openide.awt.Mnemonics.setLocalizedText(lbUserName, org.openide.util.NbBundle.getMessage(EamDbSettingsDialog.class, "EamDbSettingsDialog.lbUserName.text")); // NOI18N + lbUserName.setPreferredSize(new java.awt.Dimension(80, 14)); + + tbDbUsername.setPreferredSize(new java.awt.Dimension(509, 20)); org.openide.awt.Mnemonics.setLocalizedText(lbUserPassword, org.openide.util.NbBundle.getMessage(EamDbSettingsDialog.class, "EamDbSettingsDialog.lbUserPassword.text")); // NOI18N + lbUserPassword.setPreferredSize(new java.awt.Dimension(80, 14)); + + jpDbPassword.setPreferredSize(new java.awt.Dimension(509, 20)); cbDatabaseType.setModel(new javax.swing.DefaultComboBoxModel<>(new EamDbPlatformEnum[]{EamDbPlatformEnum.POSTGRESQL, EamDbPlatformEnum.SQLITE})); + cbDatabaseType.setPreferredSize(new java.awt.Dimension(120, 20)); cbDatabaseType.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { cbDatabaseTypeActionPerformed(evt); @@ -212,12 +235,25 @@ public class EamDbSettingsDialog extends JDialog { }); org.openide.awt.Mnemonics.setLocalizedText(lbSingleUserSqLite, org.openide.util.NbBundle.getMessage(EamDbSettingsDialog.class, "EamDbSettingsDialog.lbSingleUserSqLite.text")); // NOI18N + lbSingleUserSqLite.setPreferredSize(new java.awt.Dimension(381, 14)); org.openide.awt.Mnemonics.setLocalizedText(lbDatabaseType, org.openide.util.NbBundle.getMessage(EamDbSettingsDialog.class, "EamDbSettingsDialog.lbDatabaseType.text")); // NOI18N + lbDatabaseType.setMaximumSize(new java.awt.Dimension(80, 14)); + lbDatabaseType.setMinimumSize(new java.awt.Dimension(80, 14)); + lbDatabaseType.setPreferredSize(new java.awt.Dimension(80, 14)); org.openide.awt.Mnemonics.setLocalizedText(lbDatabaseDesc, org.openide.util.NbBundle.getMessage(EamDbSettingsDialog.class, "EamDbSettingsDialog.lbDatabaseDesc.text")); // NOI18N + lbDatabaseDesc.setPreferredSize(new java.awt.Dimension(80, 14)); - org.openide.awt.Mnemonics.setLocalizedText(lbFullDbPath, org.openide.util.NbBundle.getMessage(EamDbSettingsDialog.class, "EamDbSettingsDialog.lbFullDbPath.text")); // NOI18N + dataBaseFileScrollPane.setBorder(null); + + dataBaseFileTextArea.setEditable(false); + dataBaseFileTextArea.setBackground(new java.awt.Color(240, 240, 240)); + dataBaseFileTextArea.setColumns(20); + dataBaseFileTextArea.setFont(new java.awt.Font("Tahoma", 0, 11)); // NOI18N + dataBaseFileTextArea.setLineWrap(true); + dataBaseFileTextArea.setRows(3); + dataBaseFileScrollPane.setViewportView(dataBaseFileTextArea); javax.swing.GroupLayout pnSQLiteSettingsLayout = new javax.swing.GroupLayout(pnSQLiteSettings); pnSQLiteSettings.setLayout(pnSQLiteSettingsLayout); @@ -226,34 +262,30 @@ public class EamDbSettingsDialog extends JDialog { .addGroup(pnSQLiteSettingsLayout.createSequentialGroup() .addContainerGap() .addGroup(pnSQLiteSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(lbHostName) - .addComponent(lbPort) - .addComponent(lbUserName) - .addComponent(lbDatabaseType) + .addComponent(lbHostName, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(lbDatabaseType, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(lbDatabasePath, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(lbUserName, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(lbPort, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addGroup(pnSQLiteSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING, false) - .addComponent(lbDatabasePath, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(lbUserPassword, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) - .addComponent(lbDatabaseDesc)) + .addComponent(lbDatabaseDesc, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(lbUserPassword, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))) .addGap(10, 10, 10) .addGroup(pnSQLiteSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(lbFullDbPath, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(pnSQLiteSettingsLayout.createSequentialGroup() + .addComponent(tfDatabasePath, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(bnDatabasePathFileOpen)) .addGroup(pnSQLiteSettingsLayout.createSequentialGroup() .addComponent(cbDatabaseType, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(lbSingleUserSqLite, javax.swing.GroupLayout.DEFAULT_SIZE, 467, Short.MAX_VALUE) - .addGap(9, 9, 9)) - .addGroup(pnSQLiteSettingsLayout.createSequentialGroup() - .addComponent(tfDatabasePath) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(bnDatabasePathFileOpen) - .addGap(11, 11, 11)) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, pnSQLiteSettingsLayout.createSequentialGroup() - .addGroup(pnSQLiteSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) - .addComponent(tbDbHostname, javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(jpDbPassword, javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(tbDbUsername) - .addComponent(tbDbPort, javax.swing.GroupLayout.Alignment.LEADING)) - .addGap(10, 10, 10)))) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(lbSingleUserSqLite, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(jpDbPassword, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(tbDbUsername, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(tbDbPort, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(tbDbHostname, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(dataBaseFileScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 509, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addContainerGap()) .addGroup(pnSQLiteSettingsLayout.createSequentialGroup() .addGap(55, 55, 55) .addComponent(filler1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) @@ -266,35 +298,36 @@ public class EamDbSettingsDialog extends JDialog { .addGroup(pnSQLiteSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(pnSQLiteSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(cbDatabaseType, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(lbSingleUserSqLite)) - .addComponent(lbDatabaseType, javax.swing.GroupLayout.Alignment.TRAILING)) + .addComponent(lbSingleUserSqLite, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(lbDatabaseType, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(pnSQLiteSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(lbDatabasePath) - .addComponent(tfDatabasePath, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(lbDatabasePath, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(tfDatabasePath, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(bnDatabasePathFileOpen)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(pnSQLiteSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(tbDbHostname, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(lbHostName)) + .addComponent(lbHostName, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(pnSQLiteSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(tbDbPort, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(lbPort)) + .addComponent(lbPort, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(pnSQLiteSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(tbDbUsername, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(lbUserName)) + .addComponent(lbUserName, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(pnSQLiteSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(jpDbPassword, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(lbUserPassword)) + .addComponent(lbUserPassword, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(pnSQLiteSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(lbFullDbPath, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(lbDatabaseDesc, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(filler1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGroup(pnSQLiteSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(pnSQLiteSettingsLayout.createSequentialGroup() + .addComponent(lbDatabaseDesc, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(18, 18, 18) + .addComponent(filler1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(dataBaseFileScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) .addContainerGap()) ); @@ -312,11 +345,11 @@ public class EamDbSettingsDialog extends JDialog { layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() - .addGap(10, 10, 10) - .addComponent(pnSQLiteSettings, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 11, Short.MAX_VALUE) + .addContainerGap() + .addComponent(pnSQLiteSettings, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(pnButtons, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(10, 10, 10)) + .addContainerGap()) ); pack(); @@ -357,6 +390,7 @@ public class EamDbSettingsDialog extends JDialog { } try { tfDatabasePath.setText(databaseFile.getCanonicalPath()); + tfDatabasePath.setCaretPosition(tfDatabasePath.getText().length()); valid(); } catch (IOException ex) { logger.log(Level.SEVERE, "Failed to get path of selected database file", ex); // NON-NLS @@ -584,14 +618,15 @@ public class EamDbSettingsDialog extends JDialog { }//GEN-LAST:event_cbDatabaseTypeActionPerformed private void updateFullDbPath() { - lbFullDbPath.setText(tfDatabasePath.getText() + File.separator + CENTRAL_REPO_DB_NAME + CENTRAL_REPO_SQLITE_EXT); + dataBaseFileTextArea.setText(tfDatabasePath.getText() + File.separator + CENTRAL_REPO_DB_NAME + CENTRAL_REPO_SQLITE_EXT); + dataBaseFileTextArea.setCaretPosition(dataBaseFileTextArea.getText().length()); } private void displayDatabaseSettings(boolean isPostgres) { lbDatabasePath.setVisible(!isPostgres); tfDatabasePath.setVisible(!isPostgres); lbDatabaseDesc.setVisible(!isPostgres); - lbFullDbPath.setVisible(!isPostgres); + dataBaseFileTextArea.setVisible(!isPostgres); lbSingleUserSqLite.setVisible(!isPostgres); bnDatabasePathFileOpen.setVisible(!isPostgres); lbHostName.setVisible(isPostgres); @@ -855,13 +890,14 @@ public class EamDbSettingsDialog extends JDialog { private javax.swing.ButtonGroup bnGrpDatabasePlatforms; private javax.swing.JButton bnOk; private javax.swing.JComboBox cbDatabaseType; + private javax.swing.JScrollPane dataBaseFileScrollPane; + private javax.swing.JTextArea dataBaseFileTextArea; private javax.swing.JFileChooser fcDatabasePath; private javax.swing.Box.Filler filler1; private javax.swing.JPasswordField jpDbPassword; private javax.swing.JLabel lbDatabaseDesc; private javax.swing.JLabel lbDatabasePath; private javax.swing.JLabel lbDatabaseType; - private javax.swing.JLabel lbFullDbPath; private javax.swing.JLabel lbHostName; private javax.swing.JLabel lbPort; private javax.swing.JLabel lbSingleUserSqLite; diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/ManageCorrelationPropertiesDialog.form b/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/ManageCorrelationPropertiesDialog.form index 5124d1d85d..a3ab756f64 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/ManageCorrelationPropertiesDialog.form +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/ManageCorrelationPropertiesDialog.form @@ -7,6 +7,7 @@ + @@ -49,16 +50,16 @@ - - - + + + - + - + diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/ManageCorrelationPropertiesDialog.java b/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/ManageCorrelationPropertiesDialog.java index 550244dffe..883efff72b 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/ManageCorrelationPropertiesDialog.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/ManageCorrelationPropertiesDialog.java @@ -132,6 +132,7 @@ final class ManageCorrelationPropertiesDialog extends javax.swing.JDialog { taInstructions = new javax.swing.JTextArea(); setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE); + setResizable(false); org.openide.awt.Mnemonics.setLocalizedText(okButton, org.openide.util.NbBundle.getMessage(ManageCorrelationPropertiesDialog.class, "ManageCorrelationPropertiesDialog.okButton.text")); // NOI18N okButton.addActionListener(new java.awt.event.ActionListener() { @@ -220,14 +221,14 @@ final class ManageCorrelationPropertiesDialog extends javax.swing.JDialog { .addGap(20, 20, 20) .addComponent(taInstructions, javax.swing.GroupLayout.PREFERRED_SIZE, 34, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 180, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 254, Short.MAX_VALUE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(lbWarningMsg, javax.swing.GroupLayout.PREFERRED_SIZE, 25, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(okButton) .addComponent(cancelButton)) - .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addContainerGap()) ); pack(); diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/ManageOrganizationsDialog.form b/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/ManageOrganizationsDialog.form index 7bccab25f6..33c03ebf9d 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/ManageOrganizationsDialog.form +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/ManageOrganizationsDialog.form @@ -3,7 +3,10 @@
- + + + + @@ -27,7 +30,7 @@ - + @@ -35,7 +38,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/ManageOrganizationsDialog.java b/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/ManageOrganizationsDialog.java index 26a63f03a4..b36b3de413 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/ManageOrganizationsDialog.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/optionspanel/ManageOrganizationsDialog.java @@ -159,7 +159,8 @@ public final class ManageOrganizationsDialog extends JDialog { editButton = new javax.swing.JButton(); orgDetailsLabel = new javax.swing.JLabel(); - setMinimumSize(new java.awt.Dimension(545, 415)); + setMinimumSize(new java.awt.Dimension(600, 450)); + setPreferredSize(new java.awt.Dimension(600, 450)); manageOrganizationsScrollPane.setMinimumSize(null); manageOrganizationsScrollPane.setPreferredSize(new java.awt.Dimension(535, 415)); @@ -331,13 +332,13 @@ public final class ManageOrganizationsDialog extends JDialog { layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() .addGap(0, 0, 0) - .addComponent(manageOrganizationsScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addComponent(manageOrganizationsScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 603, Short.MAX_VALUE)) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() .addGap(0, 0, 0) - .addComponent(manageOrganizationsScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addComponent(manageOrganizationsScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 443, Short.MAX_VALUE)) ); pack(); diff --git a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/InstanceCountNode.java b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/InstanceCountNode.java index d3d1de26b8..5be8f8bae2 100644 --- a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/InstanceCountNode.java +++ b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/InstanceCountNode.java @@ -123,9 +123,9 @@ public final class InstanceCountNode extends DisplayableItemNode { final String NO_DESCR = Bundle.InstanceCountNode_createSheet_noDescription(); sheetSet.put(new NodeProperty<>(NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.nameColLbl"), NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.nameColLbl"), NO_DESCR, "")); - sheetSet.put(new NodeProperty<>(NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.createSheet.score.name"), NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.createSheet.score.name"), NO_DESCR, "")); - sheetSet.put(new NodeProperty<>(NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.createSheet.comment.name"), NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.createSheet.comment.name"), NO_DESCR, "")); - if (UserPreferences.hideCentralRepoCommentsAndOccurrences() == false) { + if (UserPreferences.getHideSCOColumns() == false) { + sheetSet.put(new NodeProperty<>(NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.createSheet.score.name"), NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.createSheet.score.name"), NO_DESCR, "")); + sheetSet.put(new NodeProperty<>(NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.createSheet.comment.name"), NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.createSheet.comment.name"), NO_DESCR, "")); sheetSet.put(new NodeProperty<>(NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.createSheet.count.name"), NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.createSheet.count.name"), NO_DESCR, "")); } sheetSet.put(new NodeProperty<>(Bundle.CommonFilesSearchResultsViewerTable_instancesColLbl(), Bundle.CommonFilesSearchResultsViewerTable_instancesColLbl(), NO_DESCR, this.getInstanceCount())); diff --git a/Core/src/org/sleuthkit/autopsy/communications/AccountDeviceInstanceNode.java b/Core/src/org/sleuthkit/autopsy/communications/AccountDeviceInstanceNode.java index b7359a9088..25d7bf5519 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/AccountDeviceInstanceNode.java +++ b/Core/src/org/sleuthkit/autopsy/communications/AccountDeviceInstanceNode.java @@ -72,7 +72,7 @@ final class AccountDeviceInstanceNode extends AbstractNode { } @Override - @NbBundle.Messages(value = {"AccountNode.device=Device", "AccountNode.accountName=Account", "AccountNode.accountType=Type", "AccountNode.messageCount=Messages"}) + @NbBundle.Messages(value = {"AccountNode.device=Device", "AccountNode.accountName=Account", "AccountNode.accountType=Type", "AccountNode.messageCount=Items"}) protected Sheet createSheet() { Sheet sheet = super.createSheet(); Sheet.Set properties = sheet.get(Sheet.PROPERTIES); diff --git a/Core/src/org/sleuthkit/autopsy/communications/AccountDeviceInstanceNodeFactory.java b/Core/src/org/sleuthkit/autopsy/communications/AccountDeviceInstanceNodeFactory.java index c0043d9ba1..171cc32cb2 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/AccountDeviceInstanceNodeFactory.java +++ b/Core/src/org/sleuthkit/autopsy/communications/AccountDeviceInstanceNodeFactory.java @@ -54,11 +54,9 @@ final class AccountDeviceInstanceNodeFactory extends ChildFactory accountDeviceInstancesWithRelationships = commsManager.getAccountDeviceInstancesWithRelationships(commsFilter); for (AccountDeviceInstance accountDeviceInstance : accountDeviceInstancesWithRelationships) { - //Filter out device accounts, in the table. - if (Account.Type.DEVICE.equals(accountDeviceInstance.getAccount().getAccountType()) ==false) { - long communicationsCount = commsManager.getRelationshipSourcesCount(accountDeviceInstance, commsFilter); - accountDeviceInstanceKeys.add(new AccountDeviceInstanceKey(accountDeviceInstance, commsFilter, communicationsCount)); - } + long communicationsCount = commsManager.getRelationshipSourcesCount(accountDeviceInstance, commsFilter); + accountDeviceInstanceKeys.add(new AccountDeviceInstanceKey(accountDeviceInstance, commsFilter, communicationsCount)); + } } catch (TskCoreException tskCoreException) { logger.log(Level.SEVERE, "Error getting filtered account device instances", tskCoreException); diff --git a/Core/src/org/sleuthkit/autopsy/communications/AccountsBrowser.java b/Core/src/org/sleuthkit/autopsy/communications/AccountsBrowser.java index fcf64182a3..0cad213cd4 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/AccountsBrowser.java +++ b/Core/src/org/sleuthkit/autopsy/communications/AccountsBrowser.java @@ -75,6 +75,10 @@ public final class AccountsBrowser extends JPanel implements ExplorerManager.Pro public AccountsBrowser() { initComponents(); + + jSplitPane1.setResizeWeight(0.5); + jSplitPane1.setDividerLocation(0.75); + outline = outlineView.getOutline(); outlineView.setPropertyColumns( "device", Bundle.AccountNode_device(), @@ -104,7 +108,7 @@ public final class AccountsBrowser extends JPanel implements ExplorerManager.Pro accountDeviceInstances.add(((AccountDeviceInstanceNode) node).getAccountDeviceInstance()); filter = ((AccountDeviceInstanceNode)node).getFilter(); } - relationshipBrowser.setSelectionInfo(new SelectionInfo(accountDeviceInstances, filter)); + relationshipBrowser.setSelectionInfo(new SelectionInfo(accountDeviceInstances, new HashSet<>(), filter)); } }); @@ -118,7 +122,7 @@ public final class AccountsBrowser extends JPanel implements ExplorerManager.Pro final int rows = Math.min(100, outline.getRowCount()); - for (int column = 0; column < outline.getModel().getColumnCount(); column++) { + for (int column = 0; column < outline.getColumnCount(); column++) { int columnWidthLimit = 500; int columnWidth = 0; diff --git a/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties-MERGED index 7a3ef27eec..b314af1cb9 100755 --- a/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties-MERGED @@ -1,7 +1,7 @@ AccountNode.accountName=Account AccountNode.accountType=Type AccountNode.device=Device -AccountNode.messageCount=Messages +AccountNode.messageCount=Items applyText=Apply CTL_OpenCVTAction=Communications CVTTopComponent.name=\ Communications Visualization diff --git a/Core/src/org/sleuthkit/autopsy/communications/CVTTopComponent.form b/Core/src/org/sleuthkit/autopsy/communications/CVTTopComponent.form index 055719c488..bc16bb6da2 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/CVTTopComponent.form +++ b/Core/src/org/sleuthkit/autopsy/communications/CVTTopComponent.form @@ -11,34 +11,10 @@ + - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -51,6 +27,11 @@
+ + + + + @@ -85,11 +66,11 @@ - - - - - + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/communications/CVTTopComponent.java b/Core/src/org/sleuthkit/autopsy/communications/CVTTopComponent.java index 16c3208ff8..989456e157 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/CVTTopComponent.java +++ b/Core/src/org/sleuthkit/autopsy/communications/CVTTopComponent.java @@ -22,12 +22,13 @@ import com.google.common.eventbus.Subscribe; import java.awt.Component; import java.awt.Dimension; import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; import java.util.List; import java.util.stream.Collectors; -import javax.swing.GroupLayout; import javax.swing.ImageIcon; import javax.swing.JTabbedPane; -import javax.swing.LayoutStyle; import org.openide.util.Lookup; import org.openide.util.NbBundle; import org.openide.windows.Mode; @@ -67,7 +68,6 @@ public final class CVTTopComponent extends TopComponent { Lookup lookup = ((Lookup.Provider)selectedComponent).getLookup(); proxyLookup.setNewLookups(lookup); } - filtersPane.setDeviceAccountTypeEnabled(browseVisualizeTabPane.getSelectedIndex() != 0); }); @@ -93,39 +93,39 @@ public final class CVTTopComponent extends TopComponent { */ // //GEN-BEGIN:initComponents private void initComponents() { + GridBagConstraints gridBagConstraints; browseVisualizeTabPane = new JTabbedPane(); accountsBrowser = new AccountsBrowser(); vizPanel = new VisualizationPanel(); filtersPane = new FiltersPanel(); + setLayout(new GridBagLayout()); + browseVisualizeTabPane.setFont(new Font("Tahoma", 0, 18)); // NOI18N browseVisualizeTabPane.addTab(NbBundle.getMessage(CVTTopComponent.class, "CVTTopComponent.accountsBrowser.TabConstraints.tabTitle_1"), new ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/communications/images/table.png")), accountsBrowser); // NOI18N browseVisualizeTabPane.addTab(NbBundle.getMessage(CVTTopComponent.class, "CVTTopComponent.vizPanel.TabConstraints.tabTitle_1"), new ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/communications/images/emblem-web.png")), vizPanel); // NOI18N - filtersPane.setMinimumSize(new Dimension(256, 495)); - - GroupLayout layout = new GroupLayout(this); - this.setLayout(layout); - layout.setHorizontalGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addGap(6, 6, 6) - .addComponent(filtersPane, GroupLayout.PREFERRED_SIZE, 265, GroupLayout.PREFERRED_SIZE) - .addPreferredGap(LayoutStyle.ComponentPlacement.RELATED) - .addComponent(browseVisualizeTabPane, GroupLayout.PREFERRED_SIZE, 786, Short.MAX_VALUE) - .addContainerGap()) - ); - layout.setVerticalGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addGap(6, 6, 6) - .addComponent(filtersPane, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGap(5, 5, 5)) - .addGroup(GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() - .addComponent(browseVisualizeTabPane) - .addContainerGap()) - ); - + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = GridBagConstraints.BOTH; + gridBagConstraints.anchor = GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 0.75; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new Insets(15, 0, 15, 15); + add(browseVisualizeTabPane, gridBagConstraints); browseVisualizeTabPane.getAccessibleContext().setAccessibleName(NbBundle.getMessage(CVTTopComponent.class, "CVTTopComponent.browseVisualizeTabPane.AccessibleContext.accessibleName")); // NOI18N + + gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = GridBagConstraints.BOTH; + gridBagConstraints.anchor = GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 0.25; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new Insets(15, 15, 15, 5); + add(filtersPane, gridBagConstraints); }// //GEN-END:initComponents diff --git a/Core/src/org/sleuthkit/autopsy/communications/FiltersPanel.form b/Core/src/org/sleuthkit/autopsy/communications/FiltersPanel.form index 8bd260ea4b..1aba67e76c 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/FiltersPanel.form +++ b/Core/src/org/sleuthkit/autopsy/communications/FiltersPanel.form @@ -11,493 +11,487 @@ + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - + - + - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + - + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - + + + + + + - - - - + + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/communications/FiltersPanel.java b/Core/src/org/sleuthkit/autopsy/communications/FiltersPanel.java index 22da4a81e9..50bdb30764 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/FiltersPanel.java +++ b/Core/src/org/sleuthkit/autopsy/communications/FiltersPanel.java @@ -53,6 +53,7 @@ import org.sleuthkit.autopsy.core.UserPreferences; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.ingest.IngestManager; +import static org.sleuthkit.autopsy.ingest.IngestManager.IngestJobEvent.COMPLETED; import static org.sleuthkit.autopsy.ingest.IngestManager.IngestModuleEvent.DATA_ADDED; import org.sleuthkit.autopsy.ingest.ModuleDataEvent; import org.sleuthkit.datamodel.Account; @@ -63,6 +64,7 @@ import org.sleuthkit.datamodel.CommunicationsFilter.AccountTypeFilter; import org.sleuthkit.datamodel.CommunicationsFilter.DateRangeFilter; import org.sleuthkit.datamodel.CommunicationsFilter.DeviceFilter; import org.sleuthkit.datamodel.CommunicationsFilter.MostRecentFilter; +import org.sleuthkit.datamodel.CommunicationsManager; import org.sleuthkit.datamodel.DataSource; import static org.sleuthkit.datamodel.Relationship.Type.CALL_LOG; import static org.sleuthkit.datamodel.Relationship.Type.CONTACT; @@ -96,9 +98,10 @@ final public class FiltersPanel extends JPanel { * Listens to ingest events to enable refresh button */ private final PropertyChangeListener ingestListener; + private final PropertyChangeListener ingestJobListener; /** - * Flag that indicates the UI is not up-sto-date with respect to the case DB + * Flag that indicates the UI is not up-to-date with respect to the case DB * and it should be refreshed (by reapplying the filters). */ private boolean needsRefresh; @@ -123,6 +126,11 @@ final public class FiltersPanel extends JPanel { @NbBundle.Messages({"refreshText=Refresh Results", "applyText=Apply"}) public FiltersPanel() { initComponents(); + + CheckBoxIconPanel panel = createAccoutTypeCheckBoxPanel(Account.Type.DEVICE, true); + accountTypeMap.put(Account.Type.DEVICE, panel.getCheckBox()); + accountTypeListPane.add(panel); + deviceRequiredLabel.setVisible(false); accountTypeRequiredLabel.setVisible(false); startDatePicker.setDate(LocalDate.now().minusWeeks(3)); @@ -155,18 +163,30 @@ final public class FiltersPanel extends JPanel { // Indicate that a refresh may be needed, unless the data added is Keyword or Hashset hits ModuleDataEvent eventData = (ModuleDataEvent) pce.getOldValue(); if (null != eventData - && eventData.getBlackboardArtifactType().getTypeID() != BlackboardArtifact.ARTIFACT_TYPE.TSK_KEYWORD_HIT.getTypeID() - && eventData.getBlackboardArtifactType().getTypeID() != BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT.getTypeID()) { - updateFilters(false); + && (eventData.getBlackboardArtifactType().getTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_MESSAGE.getTypeID() + || eventData.getBlackboardArtifactType().getTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_CONTACT.getTypeID() + || eventData.getBlackboardArtifactType().getTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_CALLLOG.getTypeID() + || eventData.getBlackboardArtifactType().getTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_EMAIL_MSG.getTypeID())) + { + updateFilters(true); + needsRefresh = true; + validateFilters(); + } + } + }; + + this.ingestJobListener = pce -> { + String eventType = pce.getPropertyName(); + if (eventType.equals(COMPLETED.toString()) && + updateFilters(true)) { + needsRefresh = true; validateFilters(); - } } }; applyFiltersButton.addActionListener(e -> applyFilters()); refreshButton.addActionListener(e -> applyFilters()); - } /** @@ -219,19 +239,26 @@ final public class FiltersPanel extends JPanel { /** * Updates the filter widgets to reflect he data sources/types in the case. */ - private void updateFilters(boolean initialState) { - updateAccountTypeFilter(); - updateDeviceFilter(initialState); + private boolean updateFilters(boolean initialState) { + boolean newAccountType = updateAccountTypeFilter(initialState); + boolean newDeviceFilter = updateDeviceFilter(initialState); + + // both or either are true, return true; + return newAccountType || newDeviceFilter; } @Override public void addNotify() { super.addNotify(); IngestManager.getInstance().addIngestModuleEventListener(ingestListener); + IngestManager.getInstance().addIngestJobEventListener(ingestJobListener); Case.addEventTypeSubscriber(EnumSet.of(CURRENT_CASE), evt -> { //clear the device filter widget when the case changes. devicesMap.clear(); devicesListPane.removeAll(); + + accountTypeMap.clear(); + accountTypeListPane.removeAll(); }); } @@ -239,69 +266,109 @@ final public class FiltersPanel extends JPanel { public void removeNotify() { super.removeNotify(); IngestManager.getInstance().removeIngestModuleEventListener(ingestListener); + IngestManager.getInstance().removeIngestJobEventListener(ingestJobListener); } /** * Populate the Account Types filter widgets + * + * @param selected the initial value for the account type checkbox + * + * @return True, if a new accountType was found */ - private void updateAccountTypeFilter() { - - //TODO: something like this commented code could be used to show only - //the account types that are found: - //final CommunicationsManager communicationsManager = Case.getCurrentOpenCase().getSleuthkitCase().getCommunicationsManager(); - //List accountTypesInUse = communicationsManager.getAccountTypesInUse(); - //accountTypesInUSe.forEach(...) - Account.Type.PREDEFINED_ACCOUNT_TYPES.forEach(type -> { - if (type.equals(Account.Type.CREDIT_CARD)) { - //don't show a check box for credit cards - } else { - accountTypeMap.computeIfAbsent(type, t -> { - - CheckBoxIconPanel panel = new CheckBoxIconPanel( - type.getDisplayName(), - new ImageIcon(FiltersPanel.class.getResource(Utils.getIconFilePath(type)))); - panel.setSelected(true); - panel.addItemListener(validationListener); + private boolean updateAccountTypeFilter(boolean selected) { + boolean newOneFound = false; + try { + final CommunicationsManager communicationsManager = Case.getCurrentCaseThrows().getSleuthkitCase().getCommunicationsManager(); + List accountTypesInUse = communicationsManager.getAccountTypesInUse(); + + for (Account.Type type : accountTypesInUse) { + + if (!accountTypeMap.containsKey(type) && !type.equals(Account.Type.CREDIT_CARD)) { + CheckBoxIconPanel panel = createAccoutTypeCheckBoxPanel(type, selected); + accountTypeMap.put(type, panel.getCheckBox()); accountTypeListPane.add(panel); - if (t.equals(Account.Type.DEVICE)) { - //Deveice type filter is enabled based on whether we are in table or graph view. - panel.setEnabled(deviceAccountTypeEnabled); - } - return panel.getCheckBox(); - }); - } - }); - } + newOneFound = true; + } + } + + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "Unable to update to update Account Types Filter", ex); + } catch (NoCurrentCaseException ex) { + logger.log(Level.WARNING, "A case is required to update the account types filter.", ex); + } + + if (newOneFound) { + accountTypeListPane.revalidate(); + } + + return newOneFound; + } + + /** + * Helper function to create a new instance of the CheckBoxIconPanel base on + * the Account.Type and initalState (check box state). + * + * @param type Account.Type to display on the panel + * @param initalState initial check box state + * + * @return instance of the CheckBoxIconPanel + */ + private CheckBoxIconPanel createAccoutTypeCheckBoxPanel(Account.Type type, boolean initalState) { + CheckBoxIconPanel panel = new CheckBoxIconPanel( + type.getDisplayName(), + new ImageIcon(FiltersPanel.class.getResource(Utils.getIconFilePath(type)))); + + panel.setSelected(initalState); + panel.addItemListener(validationListener); + return panel; + } + /** * Populate the devices filter widgets + * + * @param selected Sets the initial state of device check box + * + * @return true if a new device was found */ - private void updateDeviceFilter(boolean initialState) { + private boolean updateDeviceFilter(boolean selected) { + boolean newOneFound = false; try { final SleuthkitCase sleuthkitCase = Case.getCurrentCaseThrows().getSleuthkitCase(); for (DataSource dataSource : sleuthkitCase.getDataSources()) { String dsName = sleuthkitCase.getContentById(dataSource.getId()).getName(); - //store the device id in the map, but display a datasource name in the UI. - devicesMap.computeIfAbsent(dataSource.getDeviceId(), ds -> { - final JCheckBox jCheckBox = new JCheckBox(dsName, initialState); - jCheckBox.addItemListener(validationListener); - devicesListPane.add(jCheckBox); - return jCheckBox; - }); + if(devicesMap.containsKey(dataSource.getDeviceId())) { + continue; + } + + final JCheckBox jCheckBox = new JCheckBox(dsName, selected); + jCheckBox.addItemListener(validationListener); + devicesListPane.add(jCheckBox); + devicesMap.put(dataSource.getDeviceId(), jCheckBox); + + newOneFound = true; + } } catch (NoCurrentCaseException ex) { logger.log(Level.INFO, "Filter update cancelled. Case is closed."); } catch (TskCoreException tskCoreException) { logger.log(Level.SEVERE, "There was a error loading the datasources for the case.", tskCoreException); } + + if(newOneFound) { + devicesListPane.revalidate(); + } + + return newOneFound; } /** * Given a list of subFilters, set the states of the panel controls * accordingly. * - * @param subFilters A list of subFilters + * @param commFilter Contains a list of subFilters */ public void setFilters(CommunicationsFilter commFilter) { List subFilters = commFilter.getAndFilters(); @@ -317,7 +384,7 @@ final public class FiltersPanel extends JPanel { } /** - * Sets the state of the device filter checkboxes + * Sets the state of the device filter check boxes * * @param deviceFilter Selected devices */ @@ -364,6 +431,12 @@ final public class FiltersPanel extends JPanel { endDatePicker.setEnabled(state.isEnabled()); } + /** + * Sets the state of the most recent UI controls based on the current values + * in MostRecentFilter. + * + * @param filter The MostRecentFilter state to be set + */ private void setMostRecentFilter(MostRecentFilter filter) { int limit = filter.getLimit(); if(limit > 0) { @@ -392,135 +465,124 @@ final public class FiltersPanel extends JPanel { @SuppressWarnings("unchecked") // //GEN-BEGIN:initComponents private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; - applyFiltersButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/communications/images/tick.png"))); // NOI18N - applyFiltersButton.setText(org.openide.util.NbBundle.getMessage(FiltersPanel.class, "FiltersPanel.applyFiltersButton.text")); // NOI18N - applyFiltersButton.setPreferredSize(null); + setLayout(new java.awt.GridBagLayout()); + + topPane.setLayout(new java.awt.GridBagLayout()); filtersTitleLabel.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/communications/images/funnel.png"))); // NOI18N filtersTitleLabel.setText(org.openide.util.NbBundle.getMessage(FiltersPanel.class, "FiltersPanel.filtersTitleLabel.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + topPane.add(filtersTitleLabel, gridBagConstraints); - unCheckAllAccountTypesButton.setText(org.openide.util.NbBundle.getMessage(FiltersPanel.class, "FiltersPanel.unCheckAllAccountTypesButton.text")); // NOI18N - unCheckAllAccountTypesButton.addActionListener(new java.awt.event.ActionListener() { + refreshButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/communications/images/arrow-circle-double-135.png"))); // NOI18N + refreshButton.setText(org.openide.util.NbBundle.getMessage(FiltersPanel.class, "FiltersPanel.refreshButton.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHEAST; + topPane.add(refreshButton, gridBagConstraints); + + applyFiltersButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/communications/images/tick.png"))); // NOI18N + applyFiltersButton.setText(org.openide.util.NbBundle.getMessage(FiltersPanel.class, "FiltersPanel.applyFiltersButton.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHEAST; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 5); + topPane.add(applyFiltersButton, gridBagConstraints); + + needsRefreshLabel.setText(org.openide.util.NbBundle.getMessage(FiltersPanel.class, "FiltersPanel.needsRefreshLabel.text")); // NOI18N + needsRefreshLabel.setForeground(new java.awt.Color(255, 0, 0)); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.gridwidth = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + topPane.add(needsRefreshLabel, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.FIRST_LINE_END; + gridBagConstraints.weightx = 1.0; + add(topPane, gridBagConstraints); + + scrollPane.setBorder(null); + + mainPanel.setLayout(new java.awt.GridBagLayout()); + + limitPane.setLayout(new java.awt.GridBagLayout()); + + mostRecentLabel.setText(org.openide.util.NbBundle.getMessage(FiltersPanel.class, "FiltersPanel.mostRecentLabel.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(0, 9, 0, 9); + limitPane.add(mostRecentLabel, gridBagConstraints); + + limitComboBox.setEditable(true); + limitComboBox.setModel(new javax.swing.DefaultComboBoxModel<>(new String[] { "All", "10000", "5000", "1000", "500", "100" })); + limitComboBox.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { - unCheckAllAccountTypesButtonActionPerformed(evt); + limitComboBoxActionPerformed(evt); } }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + limitPane.add(limitComboBox, gridBagConstraints); - accountTypesLabel.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/accounts.png"))); // NOI18N - accountTypesLabel.setText(org.openide.util.NbBundle.getMessage(FiltersPanel.class, "FiltersPanel.accountTypesLabel.text")); // NOI18N + limitTitlePanel.setLayout(new java.awt.GridBagLayout()); - checkAllAccountTypesButton.setText(org.openide.util.NbBundle.getMessage(FiltersPanel.class, "FiltersPanel.checkAllAccountTypesButton.text")); // NOI18N - checkAllAccountTypesButton.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - checkAllAccountTypesButtonActionPerformed(evt); - } - }); + limitHeaderLabel.setText(org.openide.util.NbBundle.getMessage(FiltersPanel.class, "FiltersPanel.limitHeaderLabel.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + limitTitlePanel.add(limitHeaderLabel, gridBagConstraints); - accountTypesScrollPane.setPreferredSize(new java.awt.Dimension(2, 200)); + limitErrorMsgLabel.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/error-icon-16.png"))); // NOI18N + limitErrorMsgLabel.setText(org.openide.util.NbBundle.getMessage(FiltersPanel.class, "FiltersPanel.limitErrorMsgLabel.text")); // NOI18N + limitErrorMsgLabel.setForeground(new java.awt.Color(255, 0, 0)); + limitErrorMsgLabel.setHorizontalTextPosition(javax.swing.SwingConstants.LEADING); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHEAST; + limitTitlePanel.add(limitErrorMsgLabel, gridBagConstraints); - accountTypeListPane.setLayout(new javax.swing.BoxLayout(accountTypeListPane, javax.swing.BoxLayout.Y_AXIS)); - accountTypesScrollPane.setViewportView(accountTypeListPane); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 9, 0); + limitPane.add(limitTitlePanel, gridBagConstraints); - accountTypeRequiredLabel.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/error-icon-16.png"))); // NOI18N - accountTypeRequiredLabel.setText(org.openide.util.NbBundle.getMessage(FiltersPanel.class, "FiltersPanel.accountTypeRequiredLabel.text")); // NOI18N - accountTypeRequiredLabel.setForeground(new java.awt.Color(255, 0, 0)); - accountTypeRequiredLabel.setHorizontalTextPosition(javax.swing.SwingConstants.LEFT); - - javax.swing.GroupLayout accountTypesPaneLayout = new javax.swing.GroupLayout(accountTypesPane); - accountTypesPane.setLayout(accountTypesPaneLayout); - accountTypesPaneLayout.setHorizontalGroup( - accountTypesPaneLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, accountTypesPaneLayout.createSequentialGroup() - .addGroup(accountTypesPaneLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) - .addGroup(accountTypesPaneLayout.createSequentialGroup() - .addComponent(accountTypesLabel) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(accountTypeRequiredLabel)) - .addGroup(accountTypesPaneLayout.createSequentialGroup() - .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(unCheckAllAccountTypesButton) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(checkAllAccountTypesButton)) - .addGroup(accountTypesPaneLayout.createSequentialGroup() - .addGap(10, 10, 10) - .addComponent(accountTypesScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))) - .addGap(0, 0, 0)) - ); - accountTypesPaneLayout.setVerticalGroup( - accountTypesPaneLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(accountTypesPaneLayout.createSequentialGroup() - .addGroup(accountTypesPaneLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(accountTypesLabel) - .addComponent(accountTypeRequiredLabel)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(accountTypesScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGroup(accountTypesPaneLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(checkAllAccountTypesButton) - .addComponent(unCheckAllAccountTypesButton))) - ); - - unCheckAllDevicesButton.setText(org.openide.util.NbBundle.getMessage(FiltersPanel.class, "FiltersPanel.unCheckAllDevicesButton.text")); // NOI18N - unCheckAllDevicesButton.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - unCheckAllDevicesButtonActionPerformed(evt); - } - }); - - devicesLabel.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/image.png"))); // NOI18N - devicesLabel.setText(org.openide.util.NbBundle.getMessage(FiltersPanel.class, "FiltersPanel.devicesLabel.text")); // NOI18N - - checkAllDevicesButton.setText(org.openide.util.NbBundle.getMessage(FiltersPanel.class, "FiltersPanel.checkAllDevicesButton.text")); // NOI18N - checkAllDevicesButton.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - checkAllDevicesButtonActionPerformed(evt); - } - }); - - devicesScrollPane.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); - devicesScrollPane.setMinimumSize(new java.awt.Dimension(27, 75)); - - devicesListPane.setMinimumSize(new java.awt.Dimension(4, 100)); - devicesListPane.setLayout(new javax.swing.BoxLayout(devicesListPane, javax.swing.BoxLayout.Y_AXIS)); - devicesScrollPane.setViewportView(devicesListPane); - - deviceRequiredLabel.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/error-icon-16.png"))); // NOI18N - deviceRequiredLabel.setText(org.openide.util.NbBundle.getMessage(FiltersPanel.class, "FiltersPanel.deviceRequiredLabel.text")); // NOI18N - deviceRequiredLabel.setForeground(new java.awt.Color(255, 0, 0)); - deviceRequiredLabel.setHorizontalTextPosition(javax.swing.SwingConstants.LEFT); - - javax.swing.GroupLayout devicesPaneLayout = new javax.swing.GroupLayout(devicesPane); - devicesPane.setLayout(devicesPaneLayout); - devicesPaneLayout.setHorizontalGroup( - devicesPaneLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(devicesPaneLayout.createSequentialGroup() - .addComponent(devicesLabel) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(deviceRequiredLabel)) - .addGroup(devicesPaneLayout.createSequentialGroup() - .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(unCheckAllDevicesButton) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(checkAllDevicesButton)) - .addGroup(devicesPaneLayout.createSequentialGroup() - .addGap(10, 10, 10) - .addComponent(devicesScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) - ); - devicesPaneLayout.setVerticalGroup( - devicesPaneLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(devicesPaneLayout.createSequentialGroup() - .addGroup(devicesPaneLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(devicesLabel) - .addComponent(deviceRequiredLabel)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(devicesScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 94, Short.MAX_VALUE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(devicesPaneLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(checkAllDevicesButton) - .addComponent(unCheckAllDevicesButton)) - .addGap(5, 5, 5)) - ); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(15, 0, 15, 0); + mainPanel.add(limitPane, gridBagConstraints); startDatePicker.setEnabled(false); @@ -580,97 +642,178 @@ final public class FiltersPanel extends JPanel { .addComponent(endCheckBox))) ); - refreshButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/communications/images/arrow-circle-double-135.png"))); // NOI18N - refreshButton.setText(org.openide.util.NbBundle.getMessage(FiltersPanel.class, "FiltersPanel.refreshButton.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(15, 0, 0, 0); + mainPanel.add(dateRangePane, gridBagConstraints); - needsRefreshLabel.setText(org.openide.util.NbBundle.getMessage(FiltersPanel.class, "FiltersPanel.needsRefreshLabel.text")); // NOI18N - needsRefreshLabel.setForeground(new java.awt.Color(255, 0, 0)); + devicesPane.setLayout(new java.awt.GridBagLayout()); - limitHeaderLabel.setText(org.openide.util.NbBundle.getMessage(FiltersPanel.class, "FiltersPanel.limitHeaderLabel.text")); // NOI18N - - mostRecentLabel.setText(org.openide.util.NbBundle.getMessage(FiltersPanel.class, "FiltersPanel.mostRecentLabel.text")); // NOI18N - - limitComboBox.setEditable(true); - limitComboBox.setModel(new javax.swing.DefaultComboBoxModel<>(new String[] { "All", "10000", "5000", "1000", "500", "100" })); - limitComboBox.addActionListener(new java.awt.event.ActionListener() { + unCheckAllDevicesButton.setText(org.openide.util.NbBundle.getMessage(FiltersPanel.class, "FiltersPanel.unCheckAllDevicesButton.text")); // NOI18N + unCheckAllDevicesButton.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { - limitComboBoxActionPerformed(evt); + unCheckAllDevicesButtonActionPerformed(evt); } }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHEAST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(9, 0, 0, 9); + devicesPane.add(unCheckAllDevicesButton, gridBagConstraints); - limitErrorMsgLabel.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/error-icon-16.png"))); // NOI18N - limitErrorMsgLabel.setText(org.openide.util.NbBundle.getMessage(FiltersPanel.class, "FiltersPanel.limitErrorMsgLabel.text")); // NOI18N - limitErrorMsgLabel.setForeground(new java.awt.Color(255, 0, 0)); - limitErrorMsgLabel.setHorizontalTextPosition(javax.swing.SwingConstants.LEADING); + devicesLabel.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/image.png"))); // NOI18N + devicesLabel.setText(org.openide.util.NbBundle.getMessage(FiltersPanel.class, "FiltersPanel.devicesLabel.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 9, 0); + devicesPane.add(devicesLabel, gridBagConstraints); - javax.swing.GroupLayout limitPaneLayout = new javax.swing.GroupLayout(limitPane); - limitPane.setLayout(limitPaneLayout); - limitPaneLayout.setHorizontalGroup( - limitPaneLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(limitPaneLayout.createSequentialGroup() - .addComponent(limitHeaderLabel) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(limitErrorMsgLabel) - .addContainerGap()) - .addGroup(limitPaneLayout.createSequentialGroup() - .addContainerGap() - .addComponent(mostRecentLabel) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(limitComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) - ); - limitPaneLayout.setVerticalGroup( - limitPaneLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(limitPaneLayout.createSequentialGroup() - .addContainerGap() - .addGroup(limitPaneLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(limitHeaderLabel) - .addComponent(limitErrorMsgLabel)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(limitPaneLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(mostRecentLabel) - .addComponent(limitComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addGap(0, 32, Short.MAX_VALUE)) - ); + checkAllDevicesButton.setText(org.openide.util.NbBundle.getMessage(FiltersPanel.class, "FiltersPanel.checkAllDevicesButton.text")); // NOI18N + checkAllDevicesButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + checkAllDevicesButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHEAST; + gridBagConstraints.insets = new java.awt.Insets(9, 0, 0, 0); + devicesPane.add(checkAllDevicesButton, gridBagConstraints); - javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); - this.setLayout(layout); - layout.setHorizontalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(devicesPane, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(accountTypesPane, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGroup(layout.createSequentialGroup() - .addComponent(filtersTitleLabel) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(applyFiltersButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(refreshButton)) - .addComponent(dateRangePane, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGroup(layout.createSequentialGroup() - .addContainerGap() - .addComponent(needsRefreshLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) - .addComponent(limitPane, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - ); - layout.setVerticalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addGap(0, 0, 0) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(filtersTitleLabel) - .addComponent(applyFiltersButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(refreshButton)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(needsRefreshLabel) - .addGap(4, 4, 4) - .addComponent(devicesPane, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(accountTypesPane, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(dateRangePane, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(limitPane, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) - ); + devicesScrollPane.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + devicesScrollPane.setMinimumSize(new java.awt.Dimension(27, 75)); + + devicesListPane.setMinimumSize(new java.awt.Dimension(4, 100)); + devicesListPane.setLayout(new javax.swing.BoxLayout(devicesListPane, javax.swing.BoxLayout.Y_AXIS)); + devicesScrollPane.setViewportView(devicesListPane); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.gridwidth = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + devicesPane.add(devicesScrollPane, gridBagConstraints); + + deviceRequiredLabel.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/error-icon-16.png"))); // NOI18N + deviceRequiredLabel.setText(org.openide.util.NbBundle.getMessage(FiltersPanel.class, "FiltersPanel.deviceRequiredLabel.text")); // NOI18N + deviceRequiredLabel.setForeground(new java.awt.Color(255, 0, 0)); + deviceRequiredLabel.setHorizontalTextPosition(javax.swing.SwingConstants.LEFT); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHEAST; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 9, 0); + devicesPane.add(deviceRequiredLabel, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.ipady = 100; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(15, 0, 0, 0); + mainPanel.add(devicesPane, gridBagConstraints); + + accountTypesPane.setLayout(new java.awt.GridBagLayout()); + + unCheckAllAccountTypesButton.setText(org.openide.util.NbBundle.getMessage(FiltersPanel.class, "FiltersPanel.unCheckAllAccountTypesButton.text")); // NOI18N + unCheckAllAccountTypesButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + unCheckAllAccountTypesButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHEAST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(9, 0, 0, 9); + accountTypesPane.add(unCheckAllAccountTypesButton, gridBagConstraints); + + accountTypesLabel.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/accounts.png"))); // NOI18N + accountTypesLabel.setText(org.openide.util.NbBundle.getMessage(FiltersPanel.class, "FiltersPanel.accountTypesLabel.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + accountTypesPane.add(accountTypesLabel, gridBagConstraints); + + checkAllAccountTypesButton.setText(org.openide.util.NbBundle.getMessage(FiltersPanel.class, "FiltersPanel.checkAllAccountTypesButton.text")); // NOI18N + checkAllAccountTypesButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + checkAllAccountTypesButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHEAST; + gridBagConstraints.insets = new java.awt.Insets(9, 0, 0, 0); + accountTypesPane.add(checkAllAccountTypesButton, gridBagConstraints); + + accountTypesScrollPane.setPreferredSize(new java.awt.Dimension(2, 200)); + + accountTypeListPane.setLayout(new javax.swing.BoxLayout(accountTypeListPane, javax.swing.BoxLayout.PAGE_AXIS)); + accountTypesScrollPane.setViewportView(accountTypeListPane); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.gridwidth = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(9, 0, 0, 0); + accountTypesPane.add(accountTypesScrollPane, gridBagConstraints); + + accountTypeRequiredLabel.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/error-icon-16.png"))); // NOI18N + accountTypeRequiredLabel.setText(org.openide.util.NbBundle.getMessage(FiltersPanel.class, "FiltersPanel.accountTypeRequiredLabel.text")); // NOI18N + accountTypeRequiredLabel.setForeground(new java.awt.Color(255, 0, 0)); + accountTypeRequiredLabel.setHorizontalTextPosition(javax.swing.SwingConstants.LEFT); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHEAST; + accountTypesPane.add(accountTypeRequiredLabel, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(15, 0, 0, 0); + mainPanel.add(accountTypesPane, gridBagConstraints); + + scrollPane.setViewportView(mainPanel); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(9, 0, 0, 0); + add(scrollPane, gridBagConstraints); }// //GEN-END:initComponents /** @@ -738,6 +881,11 @@ final public class FiltersPanel extends JPanel { endCheckBox.isSelected() ? endDatePicker.getDate().atStartOfDay(zone).toEpochSecond() : 0); } + /** + * Get a MostRecentFilter that based on the current state of the ui controls. + * + * @return A new instance of MostRecentFilter + */ private MostRecentFilter getMostRecentFilter() { String value = (String)limitComboBox.getSelectedItem(); if(value.trim().equalsIgnoreCase("all")){ @@ -760,21 +908,6 @@ final public class FiltersPanel extends JPanel { return new DateControlState (endDatePicker.getDate(), endCheckBox.isSelected()); } - /** - * Enable or disable the device account type filter. The filter should be - * disabled for the browse/table mode and enabled for the visualization. - * - * @param enable True to enable the device account type filter, False to - * disable it. - */ - void setDeviceAccountTypeEnabled(boolean enable) { - deviceAccountTypeEnabled = enable; - JCheckBox deviceCheckbox = accountTypeMap.get(Account.Type.DEVICE); - if (deviceCheckbox != null) { - deviceCheckbox.setEnabled(deviceAccountTypeEnabled); - } - } - /** * Set the selection state of all the account type check boxes * @@ -796,11 +929,11 @@ final public class FiltersPanel extends JPanel { } /** - * Helper method that sets all the checkboxes in the given map to the given + * Helper method that sets all the check boxes in the given map to the given * selection state. * * @param map A map from anything to JCheckBoxes. - * @param selected The selection state to set all the checkboxes to. + * @param selected The selection state to set all the check boxes to. */ @ThreadConfined(type = ThreadConfined.ThreadType.AWT) private void setAllSelected(Map map, boolean selected) { @@ -931,11 +1064,15 @@ final public class FiltersPanel extends JPanel { private final javax.swing.JLabel limitErrorMsgLabel = new javax.swing.JLabel(); private final javax.swing.JLabel limitHeaderLabel = new javax.swing.JLabel(); private final javax.swing.JPanel limitPane = new javax.swing.JPanel(); + private final javax.swing.JPanel limitTitlePanel = new javax.swing.JPanel(); + private final javax.swing.JPanel mainPanel = new javax.swing.JPanel(); private final javax.swing.JLabel mostRecentLabel = new javax.swing.JLabel(); private final javax.swing.JLabel needsRefreshLabel = new javax.swing.JLabel(); private final javax.swing.JButton refreshButton = new javax.swing.JButton(); + private final javax.swing.JScrollPane scrollPane = new javax.swing.JScrollPane(); private final javax.swing.JCheckBox startCheckBox = new javax.swing.JCheckBox(); private final com.github.lgooddatepicker.components.DatePicker startDatePicker = new com.github.lgooddatepicker.components.DatePicker(); + private final javax.swing.JPanel topPane = new javax.swing.JPanel(); private final javax.swing.JButton unCheckAllAccountTypesButton = new javax.swing.JButton(); private final javax.swing.JButton unCheckAllDevicesButton = new javax.swing.JButton(); // End of variables declaration//GEN-END:variables diff --git a/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.java b/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.java index 027f2f285a..10732c630f 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.java +++ b/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.java @@ -914,23 +914,24 @@ final public class VisualizationPanel extends JPanel { Object[] selectionCells = graph.getSelectionCells(); if (selectionCells.length > 0) { mxICell[] selectedCells = Arrays.asList(selectionCells).toArray(new mxCell[selectionCells.length]); - HashSet deviceInstances = new HashSet<>(); + HashSet selectedNodes = new HashSet<>(); + HashSet selectedEdges = new HashSet<>(); for (mxICell cell : selectedCells) { if (cell.isEdge()) { mxICell source = (mxICell) graph.getModel().getTerminal(cell, true); mxICell target = (mxICell) graph.getModel().getTerminal(cell, false); - deviceInstances.add(((AccountDeviceInstanceKey) source.getValue()).getAccountDeviceInstance()); - deviceInstances.add(((AccountDeviceInstanceKey) target.getValue()).getAccountDeviceInstance()); + selectedEdges.add(new SelectionInfo.GraphEdge(((AccountDeviceInstanceKey) source.getValue()).getAccountDeviceInstance(), + ((AccountDeviceInstanceKey) target.getValue()).getAccountDeviceInstance())); } else if (cell.isVertex()) { - deviceInstances.add(((AccountDeviceInstanceKey) cell.getValue()).getAccountDeviceInstance()); + selectedNodes.add(((AccountDeviceInstanceKey) cell.getValue()).getAccountDeviceInstance()); } } - relationshipBrowser.setSelectionInfo(new SelectionInfo(deviceInstances, currentFilter)); + relationshipBrowser.setSelectionInfo(new SelectionInfo(selectedNodes, selectedEdges, currentFilter)); } else { - relationshipBrowser.setSelectionInfo(new SelectionInfo(Collections.EMPTY_SET, currentFilter)); + relationshipBrowser.setSelectionInfo(new SelectionInfo(new HashSet<>(), new HashSet<>(), currentFilter)); } } } diff --git a/Core/src/org/sleuthkit/autopsy/communications/images/nail.png b/Core/src/org/sleuthkit/autopsy/communications/images/nail.png new file mode 100755 index 0000000000..f10f365a10 Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/communications/images/nail.png differ diff --git a/Core/src/org/sleuthkit/autopsy/communications/images/screw.png b/Core/src/org/sleuthkit/autopsy/communications/images/screw.png new file mode 100755 index 0000000000..ec8d434e25 Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/communications/images/screw.png differ diff --git a/Core/src/org/sleuthkit/autopsy/communications/images/threaded.png b/Core/src/org/sleuthkit/autopsy/communications/images/threaded.png new file mode 100755 index 0000000000..449f6bad6c Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/communications/images/threaded.png differ diff --git a/Core/src/org/sleuthkit/autopsy/communications/images/unthreaded.png b/Core/src/org/sleuthkit/autopsy/communications/images/unthreaded.png new file mode 100755 index 0000000000..e66695ea88 Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/communications/images/unthreaded.png differ diff --git a/Core/src/org/sleuthkit/autopsy/communications/relationships/Bundle.properties b/Core/src/org/sleuthkit/autopsy/communications/relationships/Bundle.properties index 4970a5958e..701a7b1261 100755 --- a/Core/src/org/sleuthkit/autopsy/communications/relationships/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/communications/relationships/Bundle.properties @@ -1,15 +1,21 @@ ContactDetailsPane.nameLabel.text=Placeholder SummaryViewer.countsPanel.border.title=Counts -SummaryViewer.emailLabel.text=Emails: SummaryViewer.contactsLabel.text=Contacts: -SummaryViewer.attachmentsLabel.text=Attachments: +SummaryViewer.attachmentsLabel.text=Media Attachments: OutlineViewPanel.messageLabel.text= SummaryViewer.messagesDataLabel.text=messages SummaryViewer.callLogsDataLabel.text=callLogs SummaryViewer.contactsDataLabel.text=contacts -SummaryViewer.emailDataLabel.text=emails SummaryViewer.attachmentsDataLabel.text=attachments SummaryViewer.messagesLabel.text=Messages: SummaryViewer.callLogsLabel.text=Call Logs: +ThreadRootMessagePanel.showAllCheckBox.text=Show All Messages +ThreadPane.backButton.text=<--- SummaryViewer.caseReferencesPanel.border.title=Other Occurrences SummaryViewer.fileReferencesPanel.border.title=File References in Current Case +MessageViewer.threadsLabel.text=Select a Thread to View +MessageViewer.threadNameLabel.text= +MessageViewer.showingMessagesLabel.text=Showing Messages for Thread: +MessageViewer.backButton.AccessibleContext.accessibleDescription= +MessageViewer.backButton.text=Threads +MessageViewer.showAllButton.text=All Messages 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 b31598dbc6..f82411bf9d 100755 --- a/Core/src/org/sleuthkit/autopsy/communications/relationships/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/communications/relationships/Bundle.properties-MERGED @@ -11,7 +11,7 @@ ContactsViewer_columnHeader_Name=Name ContactsViewer_columnHeader_Phone=Phone ContactsViewer_noContacts_message= ContactsViewer_tabTitle=Contacts -MediaViewer_Name=Media +MediaViewer_Name=Media Attachments MessageNode_Node_Property_Attms=Attachments MessageNode_Node_Property_Date=Date MessageNode_Node_Property_From=From @@ -20,27 +20,39 @@ MessageNode_Node_Property_To=To MessageNode_Node_Property_Type=Type MessageViewer_columnHeader_Attms=Attachments MessageViewer_columnHeader_Date=Date +MessageViewer_columnHeader_EarlyDate=Earliest Message MessageViewer_columnHeader_From=From MessageViewer_columnHeader_Subject=Subject MessageViewer_columnHeader_To=To MessageViewer_no_messages= MessageViewer_tabTitle=Messages +MessageViewer_viewMessage_all=All +MessageViewer_viewMessage_calllogs=Call Logs +MessageViewer_viewMessage_selected=Selected +MessageViewer_viewMessage_unthreaded=Unthreaded SummaryViewer.countsPanel.border.title=Counts -SummaryViewer.emailLabel.text=Emails: SummaryViewer.contactsLabel.text=Contacts: -SummaryViewer.attachmentsLabel.text=Attachments: +SummaryViewer.attachmentsLabel.text=Media Attachments: OutlineViewPanel.messageLabel.text= SummaryViewer.messagesDataLabel.text=messages SummaryViewer.callLogsDataLabel.text=callLogs SummaryViewer.contactsDataLabel.text=contacts -SummaryViewer.emailDataLabel.text=emails SummaryViewer.attachmentsDataLabel.text=attachments SummaryViewer.messagesLabel.text=Messages: SummaryViewer.callLogsLabel.text=Call Logs: -SummaryViewer.caseReferencesPanel.border.title=Other Occurrences -SummaryViewer.fileReferencesPanel.border.title=File References in Current Case SummaryViewer_CaseRefNameColumn_Title=Case Name SummaryViewer_CentralRepository_Message= SummaryViewer_Creation_Date_Title=Creation Date SummaryViewer_FileRefNameColumn_Title=Path SummaryViewer_TabTitle=Summary +SummeryViewer_FileRef_Message=",}) /** * Creates new form SummaryViewer @@ -70,6 +70,9 @@ public class SummaryViewer extends javax.swing.JPanel implements RelationshipsVi ((DefaultOutlineModel) outline.getOutlineModel()).setNodesColumnLabel(Bundle.SummaryViewer_CaseRefNameColumn_Title()); clearControls(); + + caseReferencesPanel.hideOutlineView(Bundle.SummaryViewer_CentralRepository_Message()); + fileReferencesPanel.hideOutlineView(Bundle.SummeryViewer_FileRef_Message()); } @Override @@ -97,14 +100,16 @@ public class SummaryViewer extends javax.swing.JPanel implements RelationshipsVi setEnabled(false); clearControls(); + fileReferencesPanel.hideOutlineView(Bundle.SummeryViewer_FileRef_Message()); } else { SelectionSummary summaryDetails = info.getSummary(); attachmentsDataLabel.setText(Integer.toString(summaryDetails.getAttachmentCnt())); callLogsDataLabel.setText(Integer.toString(summaryDetails.getCallLogCnt())); contactsDataLabel.setText(Integer.toString(summaryDetails.getContactsCnt())); - emailDataLabel.setText(Integer.toString(summaryDetails.getEmailCnt())); - messagesDataLabel.setText(Integer.toString(summaryDetails.getMessagesCnt())); + messagesDataLabel.setText(Integer.toString(summaryDetails.getMessagesCnt() + summaryDetails.getEmailCnt())); + + fileReferencesPanel.showOutlineView(); fileReferencesPanel.setNode(new AbstractNode(Children.create(new AccountSourceContentChildNodeFactory(info.getAccounts()), true))); caseReferencesPanel.setNode(new AbstractNode(Children.create(new CorrelationCaseChildNodeFactory(info.getAccounts()), true))); @@ -129,7 +134,6 @@ public class SummaryViewer extends javax.swing.JPanel implements RelationshipsVi attachmentsLabel.setEnabled(enabled); callLogsLabel.setEnabled(enabled); contactsLabel.setEnabled(enabled); - emailLabel.setEnabled(enabled); messagesLabel.setEnabled(enabled); caseReferencesPanel.setEnabled(enabled); fileReferencesPanel.setEnabled(enabled); @@ -143,7 +147,6 @@ public class SummaryViewer extends javax.swing.JPanel implements RelationshipsVi attachmentsDataLabel.setText(""); callLogsDataLabel.setText(""); contactsDataLabel.setText(""); - emailDataLabel.setText(""); messagesDataLabel.setText(""); fileReferencesPanel.setNode(new AbstractNode(Children.LEAF)); @@ -178,9 +181,9 @@ public class SummaryViewer extends javax.swing.JPanel implements RelationshipsVi @SuppressWarnings("unchecked") // //GEN-BEGIN:initComponents private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; countsPanel = new javax.swing.JPanel(); - emailLabel = new javax.swing.JLabel(); contactsLabel = new javax.swing.JLabel(); messagesLabel = new javax.swing.JLabel(); callLogsLabel = new javax.swing.JLabel(); @@ -189,13 +192,12 @@ public class SummaryViewer extends javax.swing.JPanel implements RelationshipsVi messagesDataLabel = new javax.swing.JLabel(); callLogsDataLabel = new javax.swing.JLabel(); contactsDataLabel = new javax.swing.JLabel(); - emailDataLabel = new javax.swing.JLabel(); fileReferencesPanel = new org.sleuthkit.autopsy.communications.relationships.OutlineViewPanel(); caseReferencesPanel = new org.sleuthkit.autopsy.communications.relationships.OutlineViewPanel(); - countsPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(org.openide.util.NbBundle.getMessage(SummaryViewer.class, "SummaryViewer.countsPanel.border.title"))); // NOI18N + setLayout(new java.awt.GridBagLayout()); - org.openide.awt.Mnemonics.setLocalizedText(emailLabel, org.openide.util.NbBundle.getMessage(SummaryViewer.class, "SummaryViewer.emailLabel.text")); // NOI18N + countsPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(org.openide.util.NbBundle.getMessage(SummaryViewer.class, "SummaryViewer.countsPanel.border.title"))); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(contactsLabel, org.openide.util.NbBundle.getMessage(SummaryViewer.class, "SummaryViewer.contactsLabel.text")); // NOI18N @@ -213,8 +215,6 @@ public class SummaryViewer extends javax.swing.JPanel implements RelationshipsVi org.openide.awt.Mnemonics.setLocalizedText(contactsDataLabel, org.openide.util.NbBundle.getMessage(SummaryViewer.class, "SummaryViewer.contactsDataLabel.text")); // NOI18N - org.openide.awt.Mnemonics.setLocalizedText(emailDataLabel, org.openide.util.NbBundle.getMessage(SummaryViewer.class, "SummaryViewer.emailDataLabel.text")); // NOI18N - javax.swing.GroupLayout countsPanelLayout = new javax.swing.GroupLayout(countsPanel); countsPanel.setLayout(countsPanelLayout); countsPanelLayout.setHorizontalGroup( @@ -222,28 +222,22 @@ public class SummaryViewer extends javax.swing.JPanel implements RelationshipsVi .addGroup(countsPanelLayout.createSequentialGroup() .addContainerGap() .addGroup(countsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(attachmentsLabel) .addComponent(messagesLabel) .addComponent(callLogsLabel) .addComponent(contactsLabel) - .addComponent(emailLabel)) + .addComponent(attachmentsLabel)) .addGap(18, 18, 18) .addGroup(countsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(emailDataLabel) + .addComponent(attachmentsDataLabel) .addComponent(contactsDataLabel) .addComponent(callLogsDataLabel) - .addComponent(messagesDataLabel) - .addComponent(attachmentsDataLabel)) - .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addComponent(messagesDataLabel)) + .addContainerGap(959, Short.MAX_VALUE)) ); countsPanelLayout.setVerticalGroup( countsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(countsPanelLayout.createSequentialGroup() .addGap(7, 7, 7) - .addGroup(countsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(attachmentsLabel) - .addComponent(attachmentsDataLabel)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(countsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(messagesLabel) .addComponent(messagesDataLabel)) @@ -257,40 +251,38 @@ public class SummaryViewer extends javax.swing.JPanel implements RelationshipsVi .addComponent(contactsDataLabel)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(countsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(emailLabel) - .addComponent(emailDataLabel)) + .addComponent(attachmentsLabel) + .addComponent(attachmentsDataLabel)) .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + add(countsPanel, gridBagConstraints); + fileReferencesPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(org.openide.util.NbBundle.getMessage(SummaryViewer.class, "SummaryViewer.fileReferencesPanel.border.title"))); // NOI18N - fileReferencesPanel.setPreferredSize(new java.awt.Dimension(472, 300)); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + add(fileReferencesPanel, gridBagConstraints); caseReferencesPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(org.openide.util.NbBundle.getMessage(SummaryViewer.class, "SummaryViewer.caseReferencesPanel.border.title"))); // NOI18N - caseReferencesPanel.setPreferredSize(new java.awt.Dimension(472, 300)); - - javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); - this.setLayout(layout); - layout.setHorizontalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addContainerGap() - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(countsPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(fileReferencesPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 485, Short.MAX_VALUE) - .addComponent(caseReferencesPanel, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) - .addContainerGap()) - ); - layout.setVerticalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addContainerGap() - .addComponent(countsPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(fileReferencesPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(caseReferencesPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) - ); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + add(caseReferencesPanel, gridBagConstraints); }// //GEN-END:initComponents @@ -303,8 +295,6 @@ public class SummaryViewer extends javax.swing.JPanel implements RelationshipsVi private javax.swing.JLabel contactsDataLabel; private javax.swing.JLabel contactsLabel; private javax.swing.JPanel countsPanel; - private javax.swing.JLabel emailDataLabel; - private javax.swing.JLabel emailLabel; private org.sleuthkit.autopsy.communications.relationships.OutlineViewPanel fileReferencesPanel; private javax.swing.JLabel messagesDataLabel; private javax.swing.JLabel messagesLabel; diff --git a/Core/src/org/sleuthkit/autopsy/communications/relationships/ThreadChildNodeFactory.java b/Core/src/org/sleuthkit/autopsy/communications/relationships/ThreadChildNodeFactory.java new file mode 100755 index 0000000000..9e0a8fcc26 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/communications/relationships/ThreadChildNodeFactory.java @@ -0,0 +1,347 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.communications.relationships; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import javax.swing.Action; +import org.openide.nodes.AbstractNode; +import org.openide.nodes.ChildFactory; +import org.openide.nodes.Children; +import org.openide.nodes.Node; +import org.openide.nodes.Sheet; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.datamodel.NodeProperty; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * ChildFactory that creates createKeys and nodes from a given selectionInfo for + * only emails, call logs and messages. + * + */ +final class ThreadChildNodeFactory extends ChildFactory { + + private static final Logger logger = Logger.getLogger(ThreadChildNodeFactory.class.getName()); + + private SelectionInfo selectionInfo; + + private final Action preferredAction; + + /** + * Construct a new ThreadChildNodeFactory from the currently selectionInfo + * + * @param preferredAction SelectionInfo object for the currently selected + * accounts + */ + + ThreadChildNodeFactory(Action preferredAction) { + this.preferredAction = preferredAction; + } + + /** + * Updates the current instance of selectionInfo and calls the refresh method. + * + * @param selectionInfo New instance of the currently selected accounts + */ + public void refresh(SelectionInfo selectionInfo) { + this.selectionInfo = selectionInfo; + refresh(true); + } + + /** + * Creates a list of Keys (BlackboardArtifact) for only messages for the + * currently selected accounts. + * + * @param list List of BlackboardArtifact to populate + * + * @return True on success + */ + @Override + protected boolean createKeys(List list) { + if(selectionInfo == null) { + return true; + } + + try { + final Set relationshipSources = selectionInfo.getRelationshipSources(); + createRootMessageKeys(list, relationshipSources) ; + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Failed to load relationship sources.", ex); //NON-NLS + return false; + } + + return true; + } + + /** + * Adds only BlackboardArtifact objects to the list where are the earliest + * message in a message thread (based on threadID). If there are "unthreaded" + * messages (messages that do not have a threadID) one representitive artifact + * will be added to the list and dealt with a node creation time. + * + * @param list List to populate with BlackboardArtifact keys + * @param relationshipSources Set of Content objects + * @return True on success + * @throws TskCoreException + */ + private boolean createRootMessageKeys(List list, Set relationshipSources) throws TskCoreException{ + Map rootMessageMap = new HashMap<>(); + for(Content content: relationshipSources) { + if(!(content instanceof BlackboardArtifact)) { + continue; + } + + BlackboardArtifact bba = (BlackboardArtifact) content; + BlackboardArtifact.ARTIFACT_TYPE fromID = BlackboardArtifact.ARTIFACT_TYPE.fromID(bba.getArtifactTypeID()); + + if (fromID == BlackboardArtifact.ARTIFACT_TYPE.TSK_EMAIL_MSG + || fromID == BlackboardArtifact.ARTIFACT_TYPE.TSK_CALLLOG + || fromID == BlackboardArtifact.ARTIFACT_TYPE.TSK_MESSAGE) { + + // We want email and message artifacts that do not have "threadIDs" to appear as one thread in the UI + // To achive this assign any artifact that does not have a threadID + // the "UNTHREADED_ID" + // All call logs will default to a single call logs thread + String threadID; + if (fromID == BlackboardArtifact.ARTIFACT_TYPE.TSK_CALLLOG) { + threadID = MessageNode.CALL_LOG_ID; + } else { + threadID = MessageNode.UNTHREADED_ID; + } + BlackboardAttribute attribute = bba.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_THREAD_ID)); + + if(attribute != null) { + threadID = attribute.getValueString(); + } + + BlackboardArtifact tableArtifact = rootMessageMap.get(threadID); + if(tableArtifact == null) { + rootMessageMap.put(threadID, bba); + } else { + // Get the date of the message + BlackboardAttribute tableAttribute = null; + switch(fromID) { + case TSK_EMAIL_MSG: + tableAttribute = tableArtifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_SENT)); + attribute = bba.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_SENT)); + // put the earliest message into the table + if(tableAttribute != null + && attribute != null + && tableAttribute.getValueLong() > attribute.getValueLong()) { + rootMessageMap.put(threadID, bba); + } + break; + case TSK_MESSAGE: + tableAttribute = tableArtifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME)); + attribute = bba.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME)); + // put the earliest message into the table + if(tableAttribute != null + && attribute != null + && tableAttribute.getValueLong() < attribute.getValueLong()) { + rootMessageMap.put(threadID, bba); + } + break; + case TSK_CALLLOG: + tableAttribute = tableArtifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_START)); + attribute = bba.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_START)); + // put the earliest message into the table + if(tableAttribute != null + && attribute != null + && tableAttribute.getValueLong() > attribute.getValueLong()) { + rootMessageMap.put(threadID, bba); + } + break; + } + + + } + } + } + + for(BlackboardArtifact bba: rootMessageMap.values()) { + list.add(bba); + } + + list.sort(new ThreadDateComparator()); + + return true; + } + + @Override + protected Node createNodeForKey(BlackboardArtifact bba) { + BlackboardAttribute attribute = null; + try { + attribute = bba.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_THREAD_ID)); + } catch (TskCoreException ex) { + logger.log(Level.WARNING, String.format("Unable to get threadID for artifact: %s", bba.getName()), ex); + } + + if (attribute != null) { + return new ThreadNode(bba, attribute.getValueString(), preferredAction); + } else { + if (bba.getArtifactTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_CALLLOG.getTypeID()) { + return new CallLogNode(); + } else { + // Only one of these should occur. + return new UnthreadedNode(); + } + } + } + + /** + * This node represents the "call log" thread. + */ + final class CallLogNode extends AbstractNode { + /** + * Construct an instance of a CallLogNode. + */ + CallLogNode() { + super(Children.LEAF); + setDisplayName("Call Logs"); + this.setIconBaseWithExtension("org/sleuthkit/autopsy/communications/images/unthreaded.png" ); + } + + @Override + protected Sheet createSheet() { + Sheet sheet = super.createSheet(); + Sheet.Set sheetSet = sheet.get(Sheet.PROPERTIES); + if (sheetSet == null) { + sheetSet = Sheet.createPropertiesSet(); + sheet.put(sheetSet); + } + + // Give this node a threadID of "CALL_LOG_ID" + sheetSet.put(new NodeProperty<>("ThreadID", "ThreadID","",MessageNode.CALL_LOG_ID)); + + return sheet; + } + } + + /** + * This node represents the "unthreaded" thread. + */ + final class UnthreadedNode extends AbstractNode { + /** + * Construct an instance of UnthreadNode. + */ + UnthreadedNode() { + super(Children.LEAF); + setDisplayName("Unthreaded"); + this.setIconBaseWithExtension("org/sleuthkit/autopsy/communications/images/unthreaded.png" ); + } + + @Override + protected Sheet createSheet() { + Sheet sheet = super.createSheet(); + Sheet.Set sheetSet = sheet.get(Sheet.PROPERTIES); + if (sheetSet == null) { + sheetSet = Sheet.createPropertiesSet(); + sheet.put(sheetSet); + } + + // Give this node a threadID of "UNTHEADED_ID" + sheetSet.put(new NodeProperty<>("ThreadID", "ThreadID","",MessageNode.UNTHREADED_ID)); + + return sheet; + } + } + + /** + * A comparator class for comparing BlackboardArtifacts of type + * TSK_EMAIL_MSG, TSK_MESSAGE, and TSK_CALLLOG by their respective creation + * date-time. + * + * Nodes will be sorted newest to oldest. + */ + class ThreadDateComparator implements Comparator { + + @Override + public int compare(BlackboardArtifact bba1, BlackboardArtifact bba2) { + BlackboardAttribute attribute1 = null; + BlackboardAttribute attribute2 = null; + // Inializing to Long.MAX_VALUE so that if a BlackboardArtifact of + // any unexpected type is passed in, it will bubble to the top of + // the list. + long dateTime1 = Long.MAX_VALUE; + long dateTime2 = Long.MAX_VALUE; + + if (bba1 != null) { + BlackboardArtifact.ARTIFACT_TYPE fromID = BlackboardArtifact.ARTIFACT_TYPE.fromID(bba1.getArtifactTypeID()); + if (fromID != null) { + try { + switch (fromID) { + case TSK_EMAIL_MSG: + attribute1 = bba1.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_SENT)); + + break; + case TSK_MESSAGE: + attribute1 = bba1.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME)); + + break; + case TSK_CALLLOG: + attribute1 = bba1.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_START)); + + break; + } + } catch (TskCoreException ex) { + logger.log(Level.WARNING, String.format("Unable to compare attributes for artifact %d", bba1.getArtifactID()), ex); + } + } + } + + if (bba1 != null) { + BlackboardArtifact.ARTIFACT_TYPE fromID = BlackboardArtifact.ARTIFACT_TYPE.fromID(bba2.getArtifactTypeID()); + if (fromID != null) { + try { + switch (fromID) { + case TSK_EMAIL_MSG: + attribute2 = bba2.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_SENT)); + break; + case TSK_MESSAGE: + attribute2 = bba2.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME)); + break; + case TSK_CALLLOG: + attribute2 = bba2.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_START)); + break; + } + } catch (TskCoreException ex) { + logger.log(Level.WARNING, String.format("Unable to compare attributes for artifact %d", bba2.getArtifactID()), ex); + } + } + } + + if (attribute1 != null) { + dateTime1 = attribute1.getValueLong(); + } + + if (attribute2 != null) { + dateTime2 = attribute2.getValueLong(); + } + + return Long.compare(dateTime1, dateTime2) * -1; + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/communications/relationships/ThreadNode.java b/Core/src/org/sleuthkit/autopsy/communications/relationships/ThreadNode.java new file mode 100755 index 0000000000..43e6e82308 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/communications/relationships/ThreadNode.java @@ -0,0 +1,100 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.communications.relationships; + +import java.util.logging.Level; +import javax.swing.Action; +import org.openide.nodes.AbstractNode; +import org.openide.nodes.Children; +import org.openide.nodes.Sheet; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.datamodel.NodeProperty; +import org.sleuthkit.datamodel.BlackboardArtifact; +import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_MESSAGE; +import org.sleuthkit.datamodel.BlackboardAttribute; +import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_TEXT; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * An AbstractNode subclass which wraps a MessageNode object. Doing this allows + * for the reuse of the createSheet and other function from MessageNode, but + * also some customizing of how a ThreadNode is shown. + */ +final class ThreadNode extends AbstractNode{ + + private static final Logger logger = Logger.getLogger(ThreadNode.class.getName()); + + private final static int MAX_SUBJECT_LENGTH = 120; + + final private MessageNode messageNode; + + ThreadNode(BlackboardArtifact artifact, String threadID, Action preferredAction) { + super(Children.LEAF); + messageNode = new MessageNode(artifact, threadID, preferredAction); + this.setIconBaseWithExtension("org/sleuthkit/autopsy/communications/images/threaded.png" ); + } + + @Override + protected Sheet createSheet() { + BlackboardArtifact artifact = messageNode.getArtifact(); + if(artifact == null) { + return messageNode.createSheet() ; + } + + Sheet sheet = messageNode.createSheet(); + BlackboardArtifact.ARTIFACT_TYPE artifactTypeID = BlackboardArtifact.ARTIFACT_TYPE.fromID(artifact.getArtifactTypeID()); + + // If its a text message, replace the subject node which is probably + // an empty string with the firest 120 characters of the text message + if(artifactTypeID != null && artifactTypeID == TSK_MESSAGE) { + try { + BlackboardAttribute attribute = artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.fromID(TSK_TEXT.getTypeID()))); + if(attribute != null) { + Sheet.Set sheetSet = sheet.get(Sheet.PROPERTIES); + sheetSet.remove("Subject"); + + String msg = attribute.getDisplayString(); + if(msg != null && msg.length() > MAX_SUBJECT_LENGTH) { + msg = msg.substring(0, MAX_SUBJECT_LENGTH) + "..."; + } + + sheetSet.put(new NodeProperty<>("Subject", Bundle.MessageNode_Node_Property_Subject(), "", msg)); //NON-NLS + } + } catch (TskCoreException ex) { + logger.log(Level.WARNING, String.format("Unable to get the text message from message artifact %d", artifact.getId()), ex); + } + } + + return sheet; + } + + String getThreadID() { + return messageNode.getThreadID(); + } + + @Override + public Action getPreferredAction() { + return messageNode.getPreferredAction(); + } + + @Override + public String getDisplayName() { + return messageNode.getDisplayName(); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties index 652f8781ba..c799a17d61 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties @@ -83,9 +83,10 @@ MediaViewImagePanel.zoomResetButton.text=Reset MediaViewImagePanel.zoomTextField.text= MediaViewImagePanel.rotationTextField.text= MediaViewImagePanel.rotateLeftButton.toolTipText= -HtmlPanel.showImagesToggleButton.text=Show Images +HtmlPanel.showImagesToggleButton.text=Download Images MediaPlayerPanel.audioSlider.toolTipText= MediaPlayerPanel.VolumeIcon.text=\ \ \ \ \ Volume MediaPlayerPanel.progressLabel.text=00:00:00/00:00:00 MediaPlayerPanel.playButton.text=\u25ba MediaPlayerPanel.infoLabel.text=No Errors +MediaViewImagePanel.tagsMenu.text_1=Tags Menu diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED index 236fddfada..ff3341b60f 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED @@ -22,7 +22,7 @@ GstVideoPanel.cannotProcFile.err=The media player cannot process this file. GstVideoPanel.noOpenCase.errMsg=No open case available. Html_text_display_error=The HTML text cannot be displayed, it may not be correctly formed HTML. HtmlPanel_showImagesToggleButton_hide=Hide Images -HtmlPanel_showImagesToggleButton_show=Show Images +HtmlPanel_showImagesToggleButton_show=Download Images HtmlViewer_file_error=This file is missing or unreadable. MediaFileViewer.initGst.gstException.msg=Error initializing gstreamer for audio/video viewing and frame extraction capabilities. Video and audio viewing will be disabled. GstVideoPanel.setupVideo.infoLabel.text=Playback of deleted videos is not supported, use an external player. @@ -42,9 +42,17 @@ MediaFileViewer.toolTip=Displays supported multimedia files (images, videos, aud MediaPlayerPanel.noSupport=File not supported. MediaPlayerPanel.timeFormat=%02d:%02d:%02d MediaPlayerPanel.unknownTime=Unknown +MediaViewImagePanel.createTagOption=Create +MediaViewImagePanel.deleteTagOption=Delete MediaViewImagePanel.errorLabel.OOMText=Could not load file into Media View: insufficent memory. MediaViewImagePanel.errorLabel.text=Could not load file into Media View. +MediaViewImagePanel.exportSaveText=Save +MediaViewImagePanel.exportTagOption=Export MediaViewImagePanel.externalViewerButton.text=Open in External Viewer Ctrl+E +MediaViewImagePanel.fileChooserTitle=Choose a save location +MediaViewImagePanel.hideTagOption=Hide +MediaViewImagePanel.successfulExport=Tagged image was successfully saved. +MediaViewImagePanel.unsuccessfulExport=Unable to export tagged image to disk. MediaViewVideoPanel.pauseButton.text=\u25ba MediaViewVideoPanel.progressLabel.text=00:00 MediaViewVideoPanel.infoLabel.text=info @@ -145,12 +153,13 @@ MediaViewImagePanel.zoomResetButton.text=Reset MediaViewImagePanel.zoomTextField.text= MediaViewImagePanel.rotationTextField.text= MediaViewImagePanel.rotateLeftButton.toolTipText= -HtmlPanel.showImagesToggleButton.text=Show Images +HtmlPanel.showImagesToggleButton.text=Download Images MediaPlayerPanel.audioSlider.toolTipText= MediaPlayerPanel.VolumeIcon.text=\ \ \ \ \ Volume MediaPlayerPanel.progressLabel.text=00:00:00/00:00:00 MediaPlayerPanel.playButton.text=\u25ba MediaPlayerPanel.infoLabel.text=No Errors +MediaViewImagePanel.tagsMenu.text_1=Tags Menu # {0} - tableName SQLiteViewer.readTable.errorText=Error getting rows for table: {0} # {0} - tableName diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/FileViewer.java b/Core/src/org/sleuthkit/autopsy/contentviewers/FileViewer.java index a32dce1bbf..dddf0f6296 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/FileViewer.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/FileViewer.java @@ -77,12 +77,12 @@ public class FileViewer extends javax.swing.JPanel implements DataContentViewer } /** - * Get the FileTypeViewer for a given mimetype + * Get the FileTypeViewer for a given file * - * @param mimeType + * @param file * * @return FileTypeViewer, null if no known content viewer supports the - * mimetype + * file */ private FileTypeViewer getSupportingViewer(AbstractFile file) { FileTypeViewer viewer = mimeTypeToViewerMap.get(file.getMIMEType()); @@ -143,7 +143,7 @@ public class FileViewer extends javax.swing.JPanel implements DataContentViewer viewer.setFile(file); this.removeAll(); this.add(viewer.getComponent()); - this.repaint(); + this.validate(); } } diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlPanel.form b/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlPanel.form index 97b3c67f72..407f0e6ec7 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlPanel.form +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlPanel.form @@ -18,7 +18,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlPanel.java b/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlPanel.java index f915b8d5a6..b53b1dc258 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlPanel.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlPanel.java @@ -18,6 +18,11 @@ */ package org.sleuthkit.autopsy.contentviewers; +import java.io.IOException; +import java.io.StringReader; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; @@ -25,8 +30,9 @@ import javafx.concurrent.Worker; import javafx.scene.web.WebView; import javafx.embed.swing.JFXPanel; import javafx.scene.Scene; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Node; +import net.htmlparser.jericho.Attribute; +import net.htmlparser.jericho.OutputDocument; +import net.htmlparser.jericho.Source; import org.openide.util.NbBundle.Messages; import org.w3c.dom.Document; import org.w3c.dom.NodeList; @@ -38,6 +44,7 @@ import org.w3c.dom.events.EventTarget; @SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives final class HtmlPanel extends javax.swing.JPanel { + private static final Logger logger = Logger.getLogger(HtmlPanel.class.getName()); private static final long serialVersionUID = 1L; private static final String TEXT_TYPE = "text/plain"; private final JFXPanel jfxPanel = new JFXPanel(); @@ -92,26 +99,76 @@ final class HtmlPanel extends javax.swing.JPanel { } /** - * Cleans out input HTML string + * Cleans out input HTML string so it will not access resources over the internet * * @param htmlInString The HTML string to cleanse * * @return The cleansed HTML String */ private String cleanseHTML(String htmlInString) { - org.jsoup.nodes.Document doc = Jsoup.parse(htmlInString); - // remove all 'img' tags. - doc.select("img").stream().forEach(Node::remove); - // remove all 'span' tags, these are often images which are ads - doc.select("span").stream().forEach(Node::remove); - return doc.html(); + String returnString = ""; + try { + Source source = new Source(new StringReader(htmlInString)); + OutputDocument document = new OutputDocument(source); + //remove background images + source.getAllTags().stream().filter((tag) -> (tag.toString().contains("background-image"))).forEachOrdered((tag) -> { + document.remove(tag); + }); + //remove images + source.getAllElements("img").forEach((element) -> { + document.remove(element.getAllTags()); + }); + //remove frames + source.getAllElements("frame").forEach((element) -> { + document.remove(element.getAllTags()); + }); + //remove iframes + source.getAllElements("iframe").forEach((element) -> { + document.remove(element.getAllTags()); + }); + //remove pictures + source.getAllElements("picture").forEach((element) -> { + document.remove(element.getAllTags()); + }); + //remove svg + source.getAllElements("svg").forEach((element) -> { + document.remove(element.getAllTags()); + }); + //remove audio + source.getAllElements("audio").forEach((element) -> { + document.remove(element.getAllTags()); + }); + //remove video + source.getAllElements("video").forEach((element) -> { + document.remove(element.getAllTags()); + }); + //remove tracks + source.getAllElements("track").forEach((element) -> { + document.remove(element.getAllTags()); + }); + //remove embeded external elements + source.getAllElements("embed").forEach((element) -> { + document.remove(element.getAllTags()); + }); + //remove linked elements + source.getAllElements("link").forEach((element) -> { + document.remove(element.getAllTags()); + }); + //remove other URI elements such as input boxes + List attributesToRemove = source.getURIAttributes(); + document.remove(attributesToRemove); + returnString = document.toString(); + } catch (IOException ex) { + logger.log(Level.WARNING, "Unable to read html for cleaning out URI elements with Jericho", ex); + } + return returnString; } /** * Refresh the panel to reflect the current show/hide images setting. */ @Messages({ - "HtmlPanel_showImagesToggleButton_show=Show Images", + "HtmlPanel_showImagesToggleButton_show=Download Images", "HtmlPanel_showImagesToggleButton_hide=Hide Images", "Html_text_display_error=The HTML text cannot be displayed, it may not be correctly formed HTML.",}) private void refresh() { @@ -164,7 +221,7 @@ final class HtmlPanel extends javax.swing.JPanel { layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() .addComponent(showImagesToggleButton) - .addGap(0, 95, Short.MAX_VALUE)) + .addGap(0, 75, Short.MAX_VALUE)) .addComponent(htmlJPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) ); layout.setVerticalGroup( diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlViewer.form b/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlViewer.form index a08d9e9b31..bb3b76f532 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlViewer.form +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlViewer.form @@ -11,30 +11,8 @@ + - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlViewer.java b/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlViewer.java index a7e3e7fd11..fb88ed9312 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlViewer.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/HtmlViewer.java @@ -37,17 +37,18 @@ final class HtmlViewer extends javax.swing.JPanel implements FileTypeViewer { private static final long serialVersionUID = 1L; private static final Logger logger = Logger.getLogger(HtmlViewer.class.getName()); - private static final String[] SUPPORTED_MIMETYPES = new String[]{ "text/html", "application/xhtml+xml" }; + private final org.sleuthkit.autopsy.contentviewers.HtmlPanel htmlPanel = new org.sleuthkit.autopsy.contentviewers.HtmlPanel(); /** * Creates new form HtmlViewerPanel */ HtmlViewer() { initComponents(); + this.add(htmlPanel); } /** @@ -81,31 +82,12 @@ final class HtmlViewer extends javax.swing.JPanel implements FileTypeViewer { // //GEN-BEGIN:initComponents private void initComponents() { - htmlPanel = new org.sleuthkit.autopsy.contentviewers.HtmlPanel(); - - javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); - this.setLayout(layout); - layout.setHorizontalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addContainerGap() - .addComponent(htmlPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addContainerGap()) - ); - layout.setVerticalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addContainerGap() - .addComponent(htmlPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addContainerGap()) - ); + setLayout(new java.awt.BorderLayout()); }// //GEN-END:initComponents // Variables declaration - do not modify//GEN-BEGIN:variables - private org.sleuthkit.autopsy.contentviewers.HtmlPanel htmlPanel; // End of variables declaration//GEN-END:variables - @Override public List getSupportedMIMETypes() { return Arrays.asList(SUPPORTED_MIMETYPES); diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaViewImagePanel.form b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaViewImagePanel.form index e057337e6f..b157f7a033 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaViewImagePanel.form +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaViewImagePanel.form @@ -19,7 +19,7 @@ - + @@ -200,6 +200,44 @@
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaViewImagePanel.java b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaViewImagePanel.java index 5c16dbed93..0e1b9c9fb1 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaViewImagePanel.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaViewImagePanel.java @@ -20,18 +20,30 @@ package org.sleuthkit.autopsy.contentviewers; import java.awt.EventQueue; import java.awt.event.ActionEvent; +import java.awt.image.BufferedImage; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.List; import static java.util.Objects.nonNull; import java.util.SortedSet; import java.util.concurrent.ExecutionException; +import java.util.logging.Level; import java.util.stream.Collectors; import javafx.application.Platform; +import javafx.collections.ListChangeListener.Change; import javafx.concurrent.Task; import javafx.embed.swing.JFXPanel; import javafx.geometry.Pos; import javafx.geometry.Rectangle2D; import javafx.scene.Cursor; +import javafx.scene.Group; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.Label; @@ -45,19 +57,41 @@ import javafx.scene.transform.Rotate; import javafx.scene.transform.Scale; import javafx.scene.transform.Translate; import javax.imageio.ImageIO; +import javax.swing.JFileChooser; +import javafx.scene.Node; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JSeparator; +import javax.swing.SwingUtilities; +import javax.swing.SwingWorker; +import org.apache.commons.io.FilenameUtils; import org.controlsfx.control.MaskerPane; import org.openide.util.NbBundle; import org.python.google.common.collect.Lists; -import javafx.scene.Group; -import javafx.scene.input.MouseEvent; -import javafx.scene.paint.Color; -import javafx.scene.shape.Rectangle; +import org.sleuthkit.autopsy.actions.GetTagNameAndCommentDialog; +import org.sleuthkit.autopsy.actions.GetTagNameAndCommentDialog.TagNameAndComment; import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.casemodule.services.contentviewertags.ContentViewerTagManager; +import org.sleuthkit.autopsy.casemodule.services.contentviewertags.ContentViewerTagManager.ContentViewerTag; +import org.sleuthkit.autopsy.casemodule.services.contentviewertags.ContentViewerTagManager.SerializationException; +import org.sleuthkit.autopsy.contentviewers.imagetagging.ImageTagsUtil; +import org.sleuthkit.autopsy.contentviewers.imagetagging.ImageTagControls; +import org.sleuthkit.autopsy.contentviewers.imagetagging.ImageTagRegion; +import org.sleuthkit.autopsy.contentviewers.imagetagging.ImageTagCreator; +import org.sleuthkit.autopsy.contentviewers.imagetagging.ImageTag; +import org.sleuthkit.autopsy.contentviewers.imagetagging.ImageTagsGroup; +import org.sleuthkit.autopsy.corelibs.OpenCvLoader; import org.sleuthkit.autopsy.coreutils.ImageUtils; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.PlatformUtil; import org.sleuthkit.autopsy.datamodel.FileNode; import org.sleuthkit.autopsy.directorytree.ExternalViewerAction; import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.ContentTag; +import org.sleuthkit.datamodel.TskCoreException; /** * Image viewer part of the Media View layered pane. Uses JavaFX to display the @@ -70,17 +104,30 @@ import org.sleuthkit.datamodel.AbstractFile; class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPanel { private static final Image EXTERNAL = new Image(MediaViewImagePanel.class.getResource("/org/sleuthkit/autopsy/images/external.png").toExternalForm()); + private final static Logger LOGGER = Logger.getLogger(MediaViewImagePanel.class.getName()); private final boolean fxInited; private JFXPanel fxPanel; - private Group imageGroup; - private ImageTaggingTool tagger; + private AbstractFile file; + private Group masterGroup; + private ImageTagsGroup tagsGroup; + private ImageTagCreator imageTagCreator; private ImageView fxImageView; private ScrollPane scrollPane; private final ProgressBar progressBar = new ProgressBar(); private final MaskerPane maskerPane = new MaskerPane(); + private final JPopupMenu imageTaggingOptions = new JPopupMenu(); + private final JMenuItem createTagMenuItem; + private final JMenuItem deleteTagMenuItem; + private final JMenuItem hideTagsMenuItem; + private final JMenuItem exportTagsMenuItem; + + private final JFileChooser exportChooser; + + private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); + private double zoomRatio; private double rotation; // Can be 0, 90, 180, and 270. @@ -113,36 +160,178 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan /** * Creates new form MediaViewImagePanel */ + @NbBundle.Messages({ + "MediaViewImagePanel.createTagOption=Create", + "MediaViewImagePanel.deleteTagOption=Delete", + "MediaViewImagePanel.hideTagOption=Hide", + "MediaViewImagePanel.exportTagOption=Export" + }) public MediaViewImagePanel() { initComponents(); fxInited = org.sleuthkit.autopsy.core.Installer.isJavaFxInited(); + + exportChooser = new JFileChooser(); + exportChooser.setDialogTitle(Bundle.MediaViewImagePanel_fileChooserTitle()); + + //Build popupMenu when Tags Menu button is pressed. + createTagMenuItem = new JMenuItem(Bundle.MediaViewImagePanel_createTagOption()); + createTagMenuItem.addActionListener((event) -> createTag()); + imageTaggingOptions.add(createTagMenuItem); + + imageTaggingOptions.add(new JSeparator()); + + deleteTagMenuItem = new JMenuItem(Bundle.MediaViewImagePanel_deleteTagOption()); + deleteTagMenuItem.addActionListener((event) -> deleteTag()); + imageTaggingOptions.add(deleteTagMenuItem); + + imageTaggingOptions.add(new JSeparator()); + + hideTagsMenuItem = new JMenuItem(Bundle.MediaViewImagePanel_hideTagOption()); + hideTagsMenuItem.addActionListener((event) -> showOrHideTags()); + imageTaggingOptions.add(hideTagsMenuItem); + + imageTaggingOptions.add(new JSeparator()); + + exportTagsMenuItem = new JMenuItem(Bundle.MediaViewImagePanel_exportTagOption()); + exportTagsMenuItem.addActionListener((event) -> exportTags()); + imageTaggingOptions.add(exportTagsMenuItem); + + imageTaggingOptions.setPopupSize(300, 150); + + //Disable image tagging for non-windows users or upon failure to load OpenCV. + if (!PlatformUtil.isWindowsOS() || !OpenCvLoader.hasOpenCvLoaded()) { + tagsMenu.setEnabled(false); + imageTaggingOptions.setEnabled(false); + } + if (fxInited) { - Platform.runLater(() -> { + Platform.runLater(new Runnable() { + @Override + public void run() { + // build jfx ui (we could do this in FXML?) + fxImageView = new ImageView(); // will hold image + masterGroup = new Group(fxImageView); + tagsGroup = new ImageTagsGroup(fxImageView); + tagsGroup.getChildren().addListener((Change c) -> { + if (c.getList().isEmpty()) { + pcs.firePropertyChange(new PropertyChangeEvent(this, + "state", null, State.EMPTY)); + } + }); - // build jfx ui (we could do this in FXML?) - fxImageView = new ImageView(); // will hold image - imageGroup = new Group(); - imageGroup.getChildren().add(fxImageView); - scrollPane = new ScrollPane(imageGroup); // scrolls and sizes imageview - scrollPane.getStyleClass().add("bg"); //NOI18N - scrollPane.setVbarPolicy(ScrollBarPolicy.AS_NEEDED); - scrollPane.setHbarPolicy(ScrollBarPolicy.AS_NEEDED); + subscribeTagMenuItemsToStateChanges(); - fxPanel = new JFXPanel(); // bridge jfx-swing - Scene scene = new Scene(scrollPane); //root of jfx tree - scene.getStylesheets().add(MediaViewImagePanel.class.getResource("MediaViewImagePanel.css").toExternalForm()); //NOI18N - fxPanel.setScene(scene); + masterGroup.getChildren().add(tagsGroup); - fxImageView.setSmooth(true); - fxImageView.setCache(true); + //Update buttons when users select (or unselect) image tags. + tagsGroup.addFocusChangeListener((event) -> { + if (event.getPropertyName().equals(ImageTagControls.NOT_FOCUSED.getName())) { + if (masterGroup.getChildren().contains(imageTagCreator)) { + return; + } - EventQueue.invokeLater(() -> { - add(fxPanel);//add jfx ui to JPanel - }); + if (tagsGroup.getChildren().isEmpty()) { + pcs.firePropertyChange(new PropertyChangeEvent(this, + "state", null, State.EMPTY)); + } else { + pcs.firePropertyChange(new PropertyChangeEvent(this, + "state", null, State.CREATE)); + } + } else if (event.getPropertyName().equals(ImageTagControls.FOCUSED.getName())) { + pcs.firePropertyChange(new PropertyChangeEvent(this, + "state", null, State.SELECTED)); + } + }); + + scrollPane = new ScrollPane(masterGroup); // scrolls and sizes imageview + scrollPane.getStyleClass().add("bg"); //NOI18N + scrollPane.setVbarPolicy(ScrollBarPolicy.AS_NEEDED); + scrollPane.setHbarPolicy(ScrollBarPolicy.AS_NEEDED); + + fxPanel = new JFXPanel(); // bridge jfx-swing + Scene scene = new Scene(scrollPane); //root of jfx tree + scene.getStylesheets().add(MediaViewImagePanel.class.getResource("MediaViewImagePanel.css").toExternalForm()); //NOI18N + fxPanel.setScene(scene); + + fxImageView.setSmooth(true); + fxImageView.setCache(true); + + EventQueue.invokeLater(() -> { + add(fxPanel);//add jfx ui to JPanel + }); + } }); } } + /** + * Handle tags menu item enabling and disabling given the state of the + * content viewer. For example, when the tags group is empty (no tags on image), + * disable delete menu item, hide menu item, and export menu item. + */ + private void subscribeTagMenuItemsToStateChanges() { + pcs.addPropertyChangeListener((event) -> { + State currentState = (State) event.getNewValue(); + switch (currentState) { + case CREATE: + createTagMenuItem.setEnabled(true); + deleteTagMenuItem.setEnabled(false); + hideTagsMenuItem.setEnabled(true); + exportTagsMenuItem.setEnabled(true); + break; + case SELECTED: + if (masterGroup.getChildren().contains(imageTagCreator)) { + imageTagCreator.disconnect(); + masterGroup.getChildren().remove(imageTagCreator); + } + createTagMenuItem.setEnabled(false); + deleteTagMenuItem.setEnabled(true); + hideTagsMenuItem.setEnabled(true); + exportTagsMenuItem.setEnabled(true); + break; + case HIDDEN: + createTagMenuItem.setEnabled(false); + deleteTagMenuItem.setEnabled(false); + hideTagsMenuItem.setEnabled(true); + hideTagsMenuItem.setText(DisplayOptions.SHOW_TAGS.getName()); + exportTagsMenuItem.setEnabled(false); + break; + case VISIBLE: + createTagMenuItem.setEnabled(true); + deleteTagMenuItem.setEnabled(false); + hideTagsMenuItem.setEnabled(true); + hideTagsMenuItem.setText(DisplayOptions.HIDE_TAGS.getName()); + exportTagsMenuItem.setEnabled(true); + break; + case DEFAULT: + case EMPTY: + if (masterGroup.getChildren().contains(imageTagCreator)) { + imageTagCreator.disconnect(); + } + createTagMenuItem.setEnabled(true); + deleteTagMenuItem.setEnabled(false); + hideTagsMenuItem.setEnabled(false); + hideTagsMenuItem.setText(DisplayOptions.HIDE_TAGS.getName()); + exportTagsMenuItem.setEnabled(false); + break; + case NONEMPTY: + createTagMenuItem.setEnabled(true); + deleteTagMenuItem.setEnabled(false); + hideTagsMenuItem.setEnabled(true); + exportTagsMenuItem.setEnabled(true); + break; + case DISABLE: + createTagMenuItem.setEnabled(false); + deleteTagMenuItem.setEnabled(false); + hideTagsMenuItem.setEnabled(false); + exportTagsMenuItem.setEnabled(false); + break; + default: + break; + } + }); + } + public boolean isInited() { return fxInited; } @@ -154,10 +343,11 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan Platform.runLater(() -> { fxImageView.setViewport(new Rectangle2D(0, 0, 0, 0)); fxImageView.setImage(null); - tagger.defaultSettings(); - + pcs.firePropertyChange(new PropertyChangeEvent(this, + "state", null, State.DEFAULT)); + masterGroup.getChildren().clear(); scrollPane.setContent(null); - scrollPane.setContent(imageGroup); + scrollPane.setContent(masterGroup); }); } @@ -205,14 +395,31 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan try { Image fxImage = readImageTask.get(); + masterGroup.getChildren().clear(); + tagsGroup.getChildren().clear(); + this.file = file; if (nonNull(fxImage)) { // We have a non-null image, so let's show it. fxImageView.setImage(fxImage); - imageGroup.getChildren().remove(tagger); - tagger = new ImageTaggingTool(fxImageView, Color.RED); - imageGroup.getChildren().add(tagger); resetView(); - scrollPane.setContent(imageGroup); + masterGroup.getChildren().add(fxImageView); + masterGroup.getChildren().add(tagsGroup); + + try { + List tags = Case.getCurrentCase().getServices() + .getTagsManager().getContentTagsByContent(file); + + List> contentViewerTags = getContentViewerTags(tags); + //Add all image tags + tagsGroup = buildImageTagsGroup(contentViewerTags); + if (!tagsGroup.getChildren().isEmpty()) { + pcs.firePropertyChange(new PropertyChangeEvent(this, + "state", null, State.NONEMPTY)); + } + } catch (TskCoreException | NoCurrentCaseException ex) { + LOGGER.log(Level.WARNING, "Could not retrieve image tags for file in case db", ex); //NON-NLS + } + scrollPane.setContent(masterGroup); } else { showErrorNode(Bundle.MediaViewImagePanel_errorLabel_text(), file); } @@ -252,6 +459,52 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan }); } + /** + * Finds all ContentViewerTags that are of type 'ImageTagRegion' for the + * current file. + * + * @param contentTags + * @return + * @throws TskCoreException + * @throws NoCurrentCaseException + */ + private List> getContentViewerTags(List contentTags) + throws TskCoreException, NoCurrentCaseException { + List> contentViewerTags = new ArrayList<>(); + for (ContentTag contentTag : contentTags) { + ContentViewerTag contentViewerTag = ContentViewerTagManager + .getTag(contentTag, ImageTagRegion.class); + if (contentViewerTag == null) { + continue; + } + + contentViewerTags.add(contentViewerTag); + } + return contentViewerTags; + } + + /** + * Builds ImageTag instances from stored ContentViewerTags of the + * appropriate type. + * + * @param contentTags + * @return + * @throws TskCoreException + * @throws NoCurrentCaseException + */ + private ImageTagsGroup buildImageTagsGroup(List> contentViewerTags) { + + contentViewerTags.forEach(contentViewerTag -> { + /** + * Build the image tag, add an edit event call back to persist all + * edits made on this image tag instance. + */ + tagsGroup.getChildren().add(buildImageTag(contentViewerTag)); + }); + + return tagsGroup; + } + /** * @return supported mime types */ @@ -303,6 +556,10 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan zoomInButton = new javax.swing.JButton(); jSeparator2 = new javax.swing.JToolBar.Separator(); zoomResetButton = new javax.swing.JButton(); + filler1 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0)); + filler2 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(32767, 0)); + jPanel1 = new javax.swing.JPanel(); + tagsMenu = new javax.swing.JButton(); setBackground(new java.awt.Color(0, 0, 0)); addComponentListener(new java.awt.event.ComponentAdapter() { @@ -406,6 +663,23 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan } }); toolbar.add(zoomResetButton); + toolbar.add(filler1); + toolbar.add(filler2); + toolbar.add(jPanel1); + + org.openide.awt.Mnemonics.setLocalizedText(tagsMenu, org.openide.util.NbBundle.getMessage(MediaViewImagePanel.class, "MediaViewImagePanel.tagsMenu.text_1")); // NOI18N + tagsMenu.setFocusable(false); + tagsMenu.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER); + tagsMenu.setMaximumSize(new java.awt.Dimension(75, 21)); + tagsMenu.setMinimumSize(new java.awt.Dimension(75, 21)); + tagsMenu.setPreferredSize(new java.awt.Dimension(75, 21)); + tagsMenu.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM); + tagsMenu.addMouseListener(new java.awt.event.MouseAdapter() { + public void mousePressed(java.awt.event.MouseEvent evt) { + tagsMenuMousePressed(evt); + } + }); + toolbar.add(tagsMenu); add(toolbar); }// //GEN-END:initComponents @@ -450,12 +724,237 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan updateView(); }//GEN-LAST:event_formComponentResized + /** + * Deletes the selected tag when the Delete button is pressed in the Tag + * Menu. + */ + private void deleteTag() { + Platform.runLater(() -> { + ImageTag tagInFocus = tagsGroup.getFocus(); + if (tagInFocus == null) { + return; + } + + try { + ContentViewerTag contentViewerTag = tagInFocus.getContentViewerTag(); + scrollPane.setCursor(Cursor.WAIT); + ContentViewerTagManager.deleteTag(contentViewerTag); + Case.getCurrentCase().getServices().getTagsManager().deleteContentTag(contentViewerTag.getContentTag()); + tagsGroup.getChildren().remove(tagInFocus); + } catch (TskCoreException | NoCurrentCaseException ex) { + LOGGER.log(Level.WARNING, "Could not delete image tag in case db", ex); //NON-NLS + } + + scrollPane.setCursor(Cursor.DEFAULT); + }); + + pcs.firePropertyChange(new PropertyChangeEvent(this, + "state", null, State.CREATE)); + } + + /** + * Enables create tag logic when the Create button is pressed in the Tags + * Menu. + */ + private void createTag() { + pcs.firePropertyChange(new PropertyChangeEvent(this, + "state", null, State.DISABLE)); + imageTagCreator = new ImageTagCreator(fxImageView); + + PropertyChangeListener newTagListener = (event) -> { + SwingUtilities.invokeLater(() -> { + ImageTagRegion tag = (ImageTagRegion) event.getNewValue(); + //Ask the user for tag name and comment + TagNameAndComment result = GetTagNameAndCommentDialog.doDialog(); + if (result != null) { + //Persist and build image tag + Platform.runLater(() -> { + try { + scrollPane.setCursor(Cursor.WAIT); + ContentViewerTag contentViewerTag = storeImageTag(tag, result); + ImageTag imageTag = buildImageTag(contentViewerTag); + tagsGroup.getChildren().add(imageTag); + } catch (TskCoreException | SerializationException | NoCurrentCaseException ex) { + LOGGER.log(Level.WARNING, "Could not save new image tag in case db", ex); //NON-NLS + } + + scrollPane.setCursor(Cursor.DEFAULT); + }); + } + + pcs.firePropertyChange(new PropertyChangeEvent(this, + "state", null, State.CREATE)); + }); + + //Remove image tag creator from panel + Platform.runLater(() -> { + imageTagCreator.disconnect(); + masterGroup.getChildren().remove(imageTagCreator); + }); + }; + + imageTagCreator.addNewTagListener(newTagListener); + Platform.runLater(() -> masterGroup.getChildren().add(imageTagCreator)); + } + + /** + * Creates an ImageTag instance from the ContentViewerTag. + * + * @param contentViewerTag + * @return + */ + private ImageTag buildImageTag(ContentViewerTag contentViewerTag) { + ImageTag imageTag = new ImageTag(contentViewerTag, fxImageView); + + //Automatically persist edits made by user + imageTag.subscribeToEditEvents((edit) -> { + try { + scrollPane.setCursor(Cursor.WAIT); + ImageTagRegion newRegion = (ImageTagRegion) edit.getNewValue(); + ContentViewerTagManager.updateTag(contentViewerTag, newRegion); + } catch (SerializationException | TskCoreException | NoCurrentCaseException ex) { + LOGGER.log(Level.WARNING, "Could not save edit for image tag in case db", ex); //NON-NLS + } + scrollPane.setCursor(Cursor.DEFAULT); + }); + return imageTag; + } + + /** + * Stores the image tag by creating a ContentTag instance and associating + * the ImageTagRegion data with it in the case database. + * + * @param data + * @param result + */ + private ContentViewerTag storeImageTag(ImageTagRegion data, TagNameAndComment result) + throws TskCoreException, SerializationException, NoCurrentCaseException { + scrollPane.setCursor(Cursor.WAIT); + try { + ContentTag contentTag = Case.getCurrentCaseThrows().getServices().getTagsManager() + .addContentTag(file, result.getTagName(), result.getComment()); + return ContentViewerTagManager.saveTag(contentTag, data); + } finally { + scrollPane.setCursor(Cursor.DEFAULT); + } + } + + /** + * Hides or show tags when the Hide or Show button is pressed in the Tags + * Menu. + */ + private void showOrHideTags() { + Platform.runLater(() -> { + if (DisplayOptions.HIDE_TAGS.getName().equals(hideTagsMenuItem.getText())) { + //Temporarily remove the tags group and update buttons + masterGroup.getChildren().remove(tagsGroup); + hideTagsMenuItem.setText(DisplayOptions.SHOW_TAGS.getName()); + tagsGroup.clearFocus(); + pcs.firePropertyChange(new PropertyChangeEvent(this, + "state", null, State.HIDDEN)); + } else { + //Add tags group back in and update buttons + masterGroup.getChildren().add(tagsGroup); + hideTagsMenuItem.setText(DisplayOptions.HIDE_TAGS.getName()); + pcs.firePropertyChange(new PropertyChangeEvent(this, + "state", null, State.VISIBLE)); + } + }); + } + + @NbBundle.Messages({ + "MediaViewImagePanel.exportSaveText=Save", + "MediaViewImagePanel.successfulExport=Tagged image was successfully saved.", + "MediaViewImagePanel.unsuccessfulExport=Unable to export tagged image to disk.", + "MediaViewImagePanel.fileChooserTitle=Choose a save location" + }) + private void exportTags() { + tagsGroup.clearFocus(); + exportChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + //Always base chooser location to export folder + exportChooser.setCurrentDirectory(new File(Case.getCurrentCase().getExportDirectory())); + int returnVal = exportChooser.showDialog(this, Bundle.MediaViewImagePanel_exportSaveText()); + if (returnVal == JFileChooser.APPROVE_OPTION) { + new SwingWorker() { + @Override + protected Void doInBackground() { + try { + //Retrieve content viewer tags + List tags = Case.getCurrentCase().getServices() + .getTagsManager().getContentTagsByContent(file); + List> contentViewerTags = getContentViewerTags(tags); + + //Pull out image tag regions + Collection regions = contentViewerTags.stream() + .map(cvTag -> cvTag.getDetails()).collect(Collectors.toList()); + + //Apply tags to image and write to file + BufferedImage taggedImage = ImageTagsUtil.getImageWithTags(file, regions); + Path output = Paths.get(exportChooser.getSelectedFile().getPath(), + FilenameUtils.getBaseName(file.getName()) + "-with_tags.png"); //NON-NLS + ImageIO.write(taggedImage, "png", output.toFile()); + + JOptionPane.showMessageDialog(null, Bundle.MediaViewImagePanel_successfulExport()); + } catch (Exception ex) { //Runtime exceptions may spill out of ImageTagsUtil from JavaFX. + //This ensures we (devs and users) have something when it doesn't work. + LOGGER.log(Level.WARNING, "Unable to export tagged image to disk", ex); //NON-NLS + JOptionPane.showMessageDialog(null, Bundle.MediaViewImagePanel_unsuccessfulExport()); + } + return null; + } + }.execute(); + } + } + + private void tagsMenuMousePressed(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_tagsMenuMousePressed + if (imageTaggingOptions.isEnabled()) { + imageTaggingOptions.show(tagsMenu, -300 + tagsMenu.getWidth(), tagsMenu.getHeight() + 3); + } + }//GEN-LAST:event_tagsMenuMousePressed + + /** + * Display states for the show/hide tags button. + */ + enum DisplayOptions { + HIDE_TAGS("Hide"), + SHOW_TAGS("Show"); + + private final String name; + + DisplayOptions(String name) { + this.name = name; + } + + String getName() { + return name; + } + } + + /** + * Different states that the content viewer can be in. These states drive + * which buttons are enabled for tagging. + */ + enum State { + HIDDEN, + VISIBLE, + SELECTED, + CREATE, + EMPTY, + NONEMPTY, + DEFAULT, + DISABLE; + } + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.Box.Filler filler1; + private javax.swing.Box.Filler filler2; + private javax.swing.JPanel jPanel1; private javax.swing.JToolBar.Separator jSeparator1; private javax.swing.JToolBar.Separator jSeparator2; private javax.swing.JButton rotateLeftButton; private javax.swing.JButton rotateRightButton; private javax.swing.JTextField rotationTextField; + private javax.swing.JButton tagsMenu; private javax.swing.JToolBar toolbar; private javax.swing.JButton zoomInButton; private javax.swing.JButton zoomOutButton; @@ -601,8 +1100,8 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan // Add the transforms in reverse order of intended execution. // Note: They MUST be added in this order to ensure translate is // executed last. - imageGroup.getTransforms().clear(); - imageGroup.getTransforms().addAll(translate, rotate, scale); + masterGroup.getTransforms().clear(); + masterGroup.getTransforms().addAll(translate, rotate, scale); // Adjust scroll bar positions for view changes. if (viewportWidth > fxPanel.getWidth()) { @@ -618,107 +1117,4 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan rotationTextField.setText((int) rotation + "°"); zoomTextField.setText((Math.round(zoomRatio * 100.0)) + "%"); } - - /** - * Enables users to 'tag' a region of an image by clicking and dragging a - * rectangle overtop. - */ - class ImageTaggingTool extends Rectangle { - - private final double imageWidth; - private final double imageHeight; - private final double imageOriginX; - private final double imageOriginY; - - //Origin of the drag event. - private double rectangleOriginX; - private double rectangleOriginY; - - //Rectangle lines should be 1.5% of the image. This level of thickness has - //a good balance between visual acuity and loss of selection at the borders - //of the image. - private double lineThicknessAsPercent = 1.5; - - /** - * Adds tagging support to an image, where the 'tag' rectangle will be - * the specified color. - * - * @param image Image to tag - * @param color Color of the 'tag' rectangle - */ - private ImageTaggingTool(ImageView image, Color color) { - defaultSettings(); - - imageWidth = image.getImage().getWidth(); - imageHeight = image.getImage().getHeight(); - imageOriginX = image.getX(); - imageOriginY = image.getY(); - - setStroke(color); - setFill(color.deriveColor(0, 0, 0, 0)); - - //Calculate how many pixels the stroke width should be to guarentee - //a consistent % of image consumed by the rectangle border. - double min = Math.min(imageWidth, imageHeight); - double lineThicknessPixels = min * lineThicknessAsPercent / 100.0; - setStrokeWidth(lineThicknessPixels); - setVisible(false); - - //Create a rectangle by left clicking on the image - image.setOnMousePressed((MouseEvent event) -> { - if (event.isSecondaryButtonDown()) { - return; - } - - //Reset box on new click. - defaultSettings(); - - rectangleOriginX = event.getX(); - rectangleOriginY = event.getY(); - - setX(rectangleOriginX); - setY(rectangleOriginY); - }); - - //Adjust the rectangle by dragging the left mouse button - image.setOnMouseDragged((MouseEvent event) -> { - if (event.isSecondaryButtonDown()) { - return; - } - - /** - * Ensure the rectangle is contained within image boundaries and - * that the line thickness is kept within bounds. - */ - double newX = Math.min(Math.max(event.getX(), imageOriginX) - + lineThicknessPixels / 2, imageWidth - lineThicknessPixels / 2); - double newY = Math.min(Math.max(event.getY(), imageOriginY) - + lineThicknessPixels / 2, imageHeight - lineThicknessPixels / 2); - - setVisible(true); - double offsetX = newX - rectangleOriginX; - if (offsetX < 0) { - setX(newX); - } - setWidth(Math.abs(offsetX)); - - double offsetY = newY - rectangleOriginY; - if (offsetY < 0) { - setY(newY); - } - setHeight(Math.abs(offsetY)); - }); - } - - /** - * Reset the rectangle to default dimensions. - */ - public final void defaultSettings() { - setX(0); - setY(0); - setWidth(0); - setHeight(0); - setVisible(false); - } - } } diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/MessageContentViewer.form b/Core/src/org/sleuthkit/autopsy/contentviewers/MessageContentViewer.form index 1172483699..cf38160cd6 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/MessageContentViewer.form +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/MessageContentViewer.form @@ -271,22 +271,7 @@ - - - - - - - - - - - - - - - - + diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/MessageContentViewer.java b/Core/src/org/sleuthkit/autopsy/contentviewers/MessageContentViewer.java index 48f7e4e82e..0f36c84b9e 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/MessageContentViewer.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/MessageContentViewer.java @@ -87,7 +87,7 @@ public class MessageContentViewer extends javax.swing.JPanel implements DataCont private static final int ATTM_TAB_INDEX = 4; private final List textAreas; - + private final org.sleuthkit.autopsy.contentviewers.HtmlPanel htmlPanel = new org.sleuthkit.autopsy.contentviewers.HtmlPanel(); /** * Artifact currently being displayed */ @@ -101,6 +101,7 @@ public class MessageContentViewer extends javax.swing.JPanel implements DataCont @NbBundle.Messages("MessageContentViewer.AtrachmentsPanel.title=Attachments") public MessageContentViewer() { initComponents(); + htmlPane.add(htmlPanel); envelopePanel.setBackground(new Color(0, 0, 0, 38)); drp = DataResultPanel.createInstanceUninitialized(Bundle.MessageContentViewer_AtrachmentsPanel_title(), "", new TableFilterNode(Node.EMPTY, false), 0, null); attachmentsScrollPane.setViewportView(drp); @@ -153,7 +154,6 @@ public class MessageContentViewer extends javax.swing.JPanel implements DataCont textbodyScrollPane = new javax.swing.JScrollPane(); textbodyTextArea = new javax.swing.JTextArea(); htmlPane = new javax.swing.JPanel(); - htmlPanel = new org.sleuthkit.autopsy.contentviewers.HtmlPanel(); rtfbodyScrollPane = new javax.swing.JScrollPane(); rtfbodyTextPane = new javax.swing.JTextPane(); attachmentsPanel = new javax.swing.JPanel(); @@ -266,17 +266,7 @@ public class MessageContentViewer extends javax.swing.JPanel implements DataCont msgbodyTabbedPane.addTab(org.openide.util.NbBundle.getMessage(MessageContentViewer.class, "MessageContentViewer.textbodyScrollPane.TabConstraints.tabTitle"), textbodyScrollPane); // NOI18N - javax.swing.GroupLayout htmlPaneLayout = new javax.swing.GroupLayout(htmlPane); - htmlPane.setLayout(htmlPaneLayout); - htmlPaneLayout.setHorizontalGroup( - htmlPaneLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(htmlPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 647, Short.MAX_VALUE) - ); - htmlPaneLayout.setVerticalGroup( - htmlPaneLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(htmlPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 362, Short.MAX_VALUE) - ); - + htmlPane.setLayout(new java.awt.BorderLayout()); msgbodyTabbedPane.addTab(org.openide.util.NbBundle.getMessage(MessageContentViewer.class, "MessageContentViewer.htmlPane.TabConstraints.tabTitle"), htmlPane); // NOI18N rtfbodyScrollPane.setVerticalScrollBarPolicy(javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); @@ -357,7 +347,6 @@ public class MessageContentViewer extends javax.swing.JPanel implements DataCont private javax.swing.JScrollPane headersScrollPane; private javax.swing.JTextArea headersTextArea; private javax.swing.JPanel htmlPane; - private org.sleuthkit.autopsy.contentviewers.HtmlPanel htmlPanel; private javax.swing.JTabbedPane msgbodyTabbedPane; private javax.swing.JScrollPane rtfbodyScrollPane; private javax.swing.JTextPane rtfbodyTextPane; @@ -534,7 +523,7 @@ public class MessageContentViewer extends javax.swing.JPanel implements DataCont textComponent.setCaretPosition(0); //make sure we start at the top } } - + final boolean hasText = attributeText.length() > 0; msgbodyTabbedPane.setEnabledAt(index, hasText); @@ -680,18 +669,18 @@ public class MessageContentViewer extends javax.swing.JPanel implements DataCont protected Sheet createSheet() { Sheet sheet = super.createSheet(); Set keepProps = new HashSet<>(Arrays.asList( - NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.nameColLbl"), - NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.createSheet.score.name"), - NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.createSheet.comment.name"), - NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.createSheet.count.name"), - NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.sizeColLbl"), - NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.mimeType"), - NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.knownColLbl"))); - + NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.nameColLbl"), + NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.createSheet.score.name"), + NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.createSheet.comment.name"), + NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.createSheet.count.name"), + NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.sizeColLbl"), + NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.mimeType"), + NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.knownColLbl"))); + //Remove all other props except for the ones above Sheet.Set sheetSet = sheet.get(Sheet.PROPERTIES); - for(Property p : sheetSet.getProperties()) { - if(!keepProps.contains(p.getName())){ + for (Property p : sheetSet.getProperties()) { + if (!keepProps.contains(p.getName())) { sheetSet.remove(p.getName()); } } diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/imagetagging/ImageTag.java b/Core/src/org/sleuthkit/autopsy/contentviewers/imagetagging/ImageTag.java new file mode 100644 index 0000000000..bc249a94ab --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/imagetagging/ImageTag.java @@ -0,0 +1,353 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.contentviewers.imagetagging; + +import com.sun.javafx.event.EventDispatchChainImpl; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import javafx.collections.ListChangeListener; +import javafx.scene.Cursor; +import javafx.scene.Group; +import javafx.scene.Node; +import javafx.scene.control.Tooltip; +import javafx.scene.image.ImageView; +import javafx.scene.input.MouseEvent; +import javafx.scene.paint.Color; +import javafx.scene.shape.Circle; +import javafx.scene.shape.Rectangle; +import org.sleuthkit.autopsy.casemodule.services.contentviewertags.ContentViewerTagManager.ContentViewerTag; + +/** + * A tagged region displayed over an image. This class contains a "physical tag" + * and 8 edit "handles". The physical tag is a plain old rectangle that defines + * the tag boundaries. The edit handles serve two purposes. One is to represent + * selection. All 8 edit handles will become visible overtop the physical tag + * when the user clicks on the rectangle. The other purpose is to allow the user to edit + * and manipulate the physical tag boundaries (hence the name, edit handle). + * This class should be treated as a logical image tag. + */ +public final class ImageTag extends Group { + + // Used to tell the 8 edit handles to hide if this tag is no longer selected + private final EventDispatchChainImpl ALL_CHILDREN; + + private final PhysicalTag physicalTag; + + //Notifies listeners that the user has editted the tag boundaries + private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); + + //The underlying presistent tag details that this image tag originates from + private final ContentViewerTag appTag; + + public ImageTag(ContentViewerTag contentViewerTag, ImageView image) { + ALL_CHILDREN = new EventDispatchChainImpl(); + this.appTag = contentViewerTag; + + this.getChildren().addListener((ListChangeListener) change -> { + change.next(); + change.getAddedSubList().forEach((node) -> ALL_CHILDREN.append(node.getEventDispatcher())); + }); + + ImageTagRegion details = contentViewerTag.getDetails(); + physicalTag = new PhysicalTag(details); + + //Defines the max allowable boundary that a user may drag any given handle. + Boundary dragBoundary = (x, y) -> { + double boundingX = image.getX(); + double boundingY = image.getY(); + double width = image.getImage().getWidth(); + double height = image.getImage().getHeight(); + + return x > boundingX + details.getStrokeThickness() / 2 + && x < boundingX + width - details.getStrokeThickness() / 2 + && y > boundingY + details.getStrokeThickness() / 2 + && y < boundingY + height - details.getStrokeThickness() / 2; + }; + + EditHandle bottomLeft = new EditHandle(physicalTag) + .setPosition(Position.bottom(), Position.left()) + .setDrag(dragBoundary, Draggable.bottom(), Draggable.left()); + + EditHandle bottomRight = new EditHandle(physicalTag) + .setPosition(Position.bottom(), Position.right()) + .setDrag(dragBoundary, Draggable.bottom(), Draggable.right()); + + EditHandle topLeft = new EditHandle(physicalTag) + .setPosition(Position.top(), Position.left()) + .setDrag(dragBoundary, Draggable.top(), Draggable.left()); + + EditHandle topRight = new EditHandle(physicalTag) + .setPosition(Position.top(), Position.right()) + .setDrag(dragBoundary, Draggable.top(), Draggable.right()); + + EditHandle bottomMiddle = new EditHandle(physicalTag) + .setPosition(Position.bottom(), Position.xMiddle()) + .setDrag(dragBoundary, Draggable.bottom()); + + EditHandle topMiddle = new EditHandle(physicalTag) + .setPosition(Position.top(), Position.xMiddle()) + .setDrag(dragBoundary, Draggable.top()); + + EditHandle rightMiddle = new EditHandle(physicalTag) + .setPosition(Position.right(), Position.yMiddle()) + .setDrag(dragBoundary, Draggable.right()); + + EditHandle leftMiddle = new EditHandle(physicalTag) + .setPosition(Position.left(), Position.yMiddle()) + .setDrag(dragBoundary, Draggable.left()); + + //The "logical" tag is the Group + this.getChildren().addAll(physicalTag, bottomLeft, bottomRight, topLeft, + topRight, bottomMiddle, topMiddle, rightMiddle, leftMiddle); + + Tooltip.install(this, new Tooltip(contentViewerTag.getContentTag() + .getName().getDisplayName())); + + this.addEventHandler(ImageTagControls.NOT_FOCUSED, event -> ALL_CHILDREN.dispatchEvent(event)); + this.addEventHandler(ImageTagControls.FOCUSED, event -> ALL_CHILDREN.dispatchEvent(event)); + } + + /** + * Add a new listener for edit events. These events are generated when a + * user drags on one of the edit "knobs" of the tag. + * + * @param listener + */ + public void subscribeToEditEvents(PropertyChangeListener listener) { + pcs.addPropertyChangeListener(listener); + } + + public double getWidth() { + return physicalTag.getWidth(); + } + + public double getHeight() { + return physicalTag.getHeight(); + } + + /** + * Get the content viewer tag that this class represents. + * + * @return + */ + public ContentViewerTag getContentViewerTag() { + return appTag; + } + + /** + * Plain old rectangle that represents an unselected Image Tag + */ + class PhysicalTag extends Rectangle { + + public PhysicalTag(ImageTagRegion details) { + this.setStroke(Color.RED); + this.setFill(Color.RED.deriveColor(0, 0, 0, 0)); + this.setStrokeWidth(details.getStrokeThickness()); + + setX(details.getX()); + setY(details.getY()); + setWidth(details.getWidth()); + setHeight(details.getHeight()); + + this.addEventHandler(ImageTagControls.NOT_FOCUSED, event -> this.setOpacity(1)); + this.addEventHandler(ImageTagControls.FOCUSED, event -> this.setOpacity(0.5)); + } + + /** + * Builds a portable description of the tag region. + * + * @return + */ + public ImageTagRegion getState() { + return new ImageTagRegion() + .setX(this.getX()) + .setY(this.getY()) + .setWidth(this.getWidth()) + .setHeight(this.getHeight()) + .setStrokeThickness(this.getStrokeWidth()); + } + } + + /** + * Draggable "knob" used to manipulate the physical tag boundaries. + */ + class EditHandle extends Circle { + + private final PhysicalTag parent; + + public EditHandle(PhysicalTag parent) { + this.setVisible(false); + + //Hide when the tag is not selected. + this.addEventHandler(ImageTagControls.NOT_FOCUSED, event -> this.setVisible(false)); + this.addEventHandler(ImageTagControls.FOCUSED, event -> this.setVisible(true)); + + this.setRadius(parent.getStrokeWidth()); + this.setFill(parent.getStroke()); + + this.setOnDragDetected(event -> { + this.getParent().setCursor(Cursor.CLOSED_HAND); + }); + + this.setOnMouseReleased(event -> { + this.getParent().setCursor(Cursor.DEFAULT); + pcs.firePropertyChange(new PropertyChangeEvent(this, "Tag Edit", null, parent.getState())); + }); + + this.parent = parent; + } + + /** + * Sets the positioning of this edit handle on the physical tag. + * + * @param vals + * @return + */ + public EditHandle setPosition(Position... vals) { + for (Position pos : vals) { + parent.widthProperty().addListener((obs, oldVal, newVal) -> pos.set(parent, this)); + parent.heightProperty().addListener((obs, oldVal, newVal) -> pos.set(parent, this)); + pos.set(parent, this); + } + return this; + } + + /** + * Sets the drag capabilities for manipulating the physical tag. + * + * @param bounds + * @param vals + * @return + */ + public EditHandle setDrag(Boundary bounds, Draggable... vals) { + this.setOnMouseDragged((event) -> { + for (Draggable drag : vals) { + drag.perform(parent, event, bounds); + } + }); + return this; + } + } + + /** + * Position strategies for "sticking" to a location on the physical tag when + * it is resized. + */ + static interface Position { + + void set(PhysicalTag parent, Circle knob); + + static Position left() { + return (parent, knob) -> knob.centerXProperty().bind(parent.xProperty()); + } + + static Position right() { + return (parent, knob) -> knob.centerXProperty().bind(parent.xProperty().add(parent.getWidth())); + } + + static Position top() { + return (parent, knob) -> knob.centerYProperty().bind(parent.yProperty()); + } + + static Position bottom() { + return (parent, knob) -> knob.centerYProperty().bind(parent.yProperty().add(parent.getHeight())); + } + + static Position xMiddle() { + return (parent, knob) -> knob.centerXProperty().bind(parent.xProperty().add(parent.getWidth() / 2)); + } + + static Position yMiddle() { + return (parent, knob) -> knob.centerYProperty().bind(parent.yProperty().add(parent.getHeight() / 2)); + } + } + + /** + * Defines the bounding box for which dragging is allowable. + */ + @FunctionalInterface + static interface Boundary { + + boolean isPointInBounds(double x, double y); + } + + /** + * Drag strategies for manipulating the physical tag from a given side of + * the rectangle. + */ + static interface Draggable { + + void perform(PhysicalTag parent, MouseEvent event, Boundary b); + + static Draggable bottom() { + return (parent, event, bounds) -> { + if (!bounds.isPointInBounds(event.getX(), event.getY())) { + return; + } + + double deltaY = event.getY() - parent.getY(); + if (deltaY > 0) { + parent.setHeight(deltaY); + } + }; + } + + static Draggable top() { + return (parent, event, bounds) -> { + if (!bounds.isPointInBounds(event.getX(), event.getY())) { + return; + } + + double deltaY = parent.getY() + parent.getHeight() - event.getY(); + if (deltaY < parent.getY() + parent.getHeight() && deltaY > 0) { + parent.setHeight(deltaY); + parent.setY(event.getY()); + } + }; + } + + static Draggable left() { + return (parent, event, bounds) -> { + if (!bounds.isPointInBounds(event.getX(), event.getY())) { + return; + } + + double deltaX = parent.getX() + parent.getWidth() - event.getX(); + if (deltaX < parent.getX() + parent.getWidth() && deltaX > 0) { + parent.setWidth(deltaX); + parent.setX(event.getX()); + } + }; + } + + static Draggable right() { + return (parent, event, bounds) -> { + if (!bounds.isPointInBounds(event.getX(), event.getY())) { + return; + } + + double deltaX = event.getX() - parent.getX(); + if (deltaX > 0) { + parent.setWidth(deltaX); + } + }; + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/imagetagging/ImageTagControls.java b/Core/src/org/sleuthkit/autopsy/contentviewers/imagetagging/ImageTagControls.java new file mode 100644 index 0000000000..d4ee799262 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/imagetagging/ImageTagControls.java @@ -0,0 +1,31 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.contentviewers.imagetagging; + +import javafx.event.Event; +import javafx.event.EventType; + +/** + * Focus events for ImageTags to consume. These events trigger selection behavior + * on ImageTags and are originated from the ImageTagsGroup class. + */ +public class ImageTagControls { + public static final EventType NOT_FOCUSED = new EventType<>("NOT_FOCUSED"); + public static final EventType FOCUSED = new EventType<>("FOCUSED"); +} diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/imagetagging/ImageTagCreator.java b/Core/src/org/sleuthkit/autopsy/contentviewers/imagetagging/ImageTagCreator.java new file mode 100644 index 0000000000..24421d796c --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/imagetagging/ImageTagCreator.java @@ -0,0 +1,180 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.contentviewers.imagetagging; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import javafx.event.EventHandler; +import javafx.scene.image.ImageView; +import javafx.scene.input.MouseEvent; +import javafx.scene.paint.Color; +import javafx.scene.shape.Rectangle; + +/** + * Creates image tags. This class attaches itself to a source image, waiting + * for mouse press, mouse drag, and mouse release events. Upon a mouse release + * event, any listeners are updated with the portable description of the new tag + * boundaries (ImageTagRegion). + */ +public final class ImageTagCreator extends Rectangle { + + //Origin of the drag event. + private double rectangleOriginX, rectangleOriginY; + + //Rectangle lines should be 1.5% of the image. This level of thickness has + //a good balance between visual acuity and loss of selection at the borders + //of the image. + private final static double LINE_THICKNESS_PERCENT = 1.5; + private final double minArea; + + //Used to update listeners of the new tag boundaries + private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); + + private final EventHandler mousePressed; + private final EventHandler mouseDragged; + private final EventHandler mouseReleased; + + //Handles the unregistering this ImageTagCreator from mouse press, mouse drag, + //and mouse release events of the source image. + private final Runnable disconnectRunnable; + + /** + * Adds tagging support to an image, where the 'tag' rectangle will be the + * specified color. + * + * @param image Image to tag + */ + public ImageTagCreator(ImageView image) { + setStroke(Color.RED); + setFill(Color.RED.deriveColor(0, 0, 0, 0)); + + //Calculate how many pixels the stroke width should be to guarentee + //a consistent % of image consumed by the rectangle border. + double min = Math.min(image.getImage().getWidth(), image.getImage().getHeight()); + double lineThicknessPixels = min * LINE_THICKNESS_PERCENT / 100.0; + setStrokeWidth(lineThicknessPixels); + minArea = lineThicknessPixels * lineThicknessPixels; + setVisible(false); + + this.mousePressed = (MouseEvent event) -> { + if (event.isSecondaryButtonDown()) { + return; + } + + //Reset box on new click. + defaultSettings(); + rectangleOriginX = event.getX(); + rectangleOriginY = event.getY(); + + setX(rectangleOriginX); + setY(rectangleOriginY); + }; + + image.addEventHandler(MouseEvent.MOUSE_PRESSED, this.mousePressed); + + this.mouseDragged = (MouseEvent event) -> { + if (event.isSecondaryButtonDown()) { + return; + } + + double currentX = event.getX(), currentY = event.getY(); + + /** + * Ensure the rectangle is contained within image boundaries and + * that the line thickness is kept within bounds. + */ + double newX = Math.min(Math.max(currentX, image.getX()) + + lineThicknessPixels / 2, image.getImage().getWidth() - lineThicknessPixels / 2); + double newY = Math.min(Math.max(currentY, image.getY()) + + lineThicknessPixels / 2, image.getImage().getHeight() - lineThicknessPixels / 2); + + setVisible(true); + double offsetX = newX - rectangleOriginX; + if (offsetX < 0) { + setX(newX); + } + setWidth(Math.abs(offsetX)); + + double offsetY = newY - rectangleOriginY; + if (offsetY < 0) { + setY(newY); + } + setHeight(Math.abs(offsetY)); + }; + + image.addEventHandler(MouseEvent.MOUSE_DRAGGED, this.mouseDragged); + + this.mouseReleased = event -> { + //Reject any drags that are too small to count as a meaningful tag. + //Meaningful is described as having an area that is visible that is + //not consumed by the thickness of the stroke. + if ((this.getWidth() - this.getStrokeWidth()) + * (this.getHeight() - this.getStrokeWidth()) <= minArea) { + defaultSettings(); + return; + } + + this.pcs.firePropertyChange(new PropertyChangeEvent(this, "New Tag", + null, new ImageTagRegion() + .setX(this.getX()) + .setY(this.getY()) + .setWidth(this.getWidth()) + .setHeight(this.getHeight()) + .setStrokeThickness(lineThicknessPixels))); + }; + + image.addEventHandler(MouseEvent.MOUSE_RELEASED, this.mouseReleased); + + //Used to remove itself from mouse events on the source image + disconnectRunnable = () -> { + defaultSettings(); + image.removeEventHandler(MouseEvent.MOUSE_RELEASED, mouseReleased); + image.removeEventHandler(MouseEvent.MOUSE_DRAGGED, mouseDragged); + image.removeEventHandler(MouseEvent.MOUSE_PRESSED, mousePressed); + }; + } + + /** + * Registers a PCL for new tag events. Listeners are updated with a portable + * description (ImageTagRegion) of the new tag, which represent the + * rectangle boundaries. + * + * @param listener + */ + public void addNewTagListener(PropertyChangeListener listener) { + this.pcs.addPropertyChangeListener(listener); + } + + /** + * Removes itself from mouse events on the source image. + */ + public void disconnect() { + this.disconnectRunnable.run(); + } + + /** + * Reset the rectangle to default dimensions. + */ + private void defaultSettings() { + setWidth(0); + setHeight(0); + setVisible(false); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/imagetagging/ImageTagRegion.java b/Core/src/org/sleuthkit/autopsy/contentviewers/imagetagging/ImageTagRegion.java new file mode 100644 index 0000000000..b4ba2035b0 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/imagetagging/ImageTagRegion.java @@ -0,0 +1,82 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.contentviewers.imagetagging; + +/** + * Bean representation of an image tag. This class is used for storage and + * retrieval of ImageTags from the case database. + */ +public class ImageTagRegion { + + /** + * These fields will be serialized and stored in the case database by the + * ContentViewerTagManager. + */ + private double x; + private double y; + private double width; + private double height; + + private double strokeThickness; + + public ImageTagRegion setStrokeThickness(double thickness) { + this.strokeThickness = thickness; + return this; + } + + public ImageTagRegion setX(double x) { + this.x = x; + return this; + } + + public ImageTagRegion setWidth(double width) { + this.width = width; + return this; + } + + public ImageTagRegion setY(double y) { + this.y = y; + return this; + } + + public ImageTagRegion setHeight(double height) { + this.height = height; + return this; + } + + public double getX() { + return x; + } + + public double getWidth() { + return width; + } + + public double getY() { + return y; + } + + public double getHeight() { + return height; + } + + public double getStrokeThickness() { + return strokeThickness; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/imagetagging/ImageTagsGroup.java b/Core/src/org/sleuthkit/autopsy/contentviewers/imagetagging/ImageTagsGroup.java new file mode 100644 index 0000000000..69d2a73c4c --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/imagetagging/ImageTagsGroup.java @@ -0,0 +1,155 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.contentviewers.imagetagging; + +import com.sun.javafx.event.EventDispatchChainImpl; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import javafx.event.Event; +import javafx.geometry.Point2D; +import javafx.scene.Group; +import javafx.scene.Node; +import javafx.scene.input.MouseEvent; + +/** + * Manages the focus and z-ordering of ImageTags. Only one image tag may be + * selected at a time. Image tags show their 8 edit "handles" upon selection + * (see ImageTag class for more details) and get the highest z-ordering to make + * editing easier. This class is responsible for setting and dropping focus as + * the user navigates from tag to tag. The ImageTag is treated as a logical + * unit, however it's underlying representation consists of the physical + * rectangle and the 8 edit handles. JavaFX will report selection on the Node + * level (so either the Rectangle, or a singe edit handle), which makes keeping + * the entire image tag in focus a non-trivial problem. + */ +public final class ImageTagsGroup extends Group { + + private final EventDispatchChainImpl NO_OP_CHAIN = new EventDispatchChainImpl(); + private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); + + private volatile ImageTag currentFocus; + + public ImageTagsGroup(Node backDrop) { + + //Reset focus of current selection if the back drop has focus. + backDrop.setOnMousePressed((mouseEvent) -> { + if (currentFocus != null) { + currentFocus.getEventDispatcher().dispatchEvent( + new Event(ImageTagControls.NOT_FOCUSED), NO_OP_CHAIN); + } + + this.pcs.firePropertyChange(new PropertyChangeEvent(this, + ImageTagControls.NOT_FOCUSED.getName(), currentFocus, null)); + currentFocus = null; + }); + + //Set the focus of selected tag + this.addEventFilter(MouseEvent.MOUSE_PRESSED, (MouseEvent e) -> { + if (!e.isPrimaryButtonDown()) { + return; + } + + ImageTag selection = getTagToSelect(new Point2D(e.getX(), e.getY())); + requestFocus(selection); + }); + } + + /** + * Subscribe to focus change events on Image tags. + * + * @param fcl PCL to be notified which Image tag has been selected. + */ + public void addFocusChangeListener(PropertyChangeListener fcl) { + this.pcs.addPropertyChangeListener(fcl); + } + + /** + * Get the image tag that current has focus. + * + * @return ImageTag instance or null if no tag is in focus. + */ + public ImageTag getFocus() { + return currentFocus; + } + + /** + * Clears the current focus + */ + public void clearFocus() { + if(currentFocus != null) { + resetFocus(currentFocus); + currentFocus = null; + } + } + + /** + * Find which tag to select on a user mouse press. If multiple tags are + * overlapping, pick the smallest one that is determined by the L + W of + * the tag sides. + * + * @param coordinate User mouse press location + * @return The tag to give focus + */ + private ImageTag getTagToSelect(Point2D coordinate) { + ImageTag tagToSelect = null; + double minTagSize = Double.MAX_VALUE; + + //Find all intersecting tags, select the absolute min based on L + W. + for (Node node : this.getChildren()) { + ImageTag tag = (ImageTag) node; + double tagSize = tag.getWidth() + tag.getHeight(); + if (tag.contains(coordinate) && tagSize < minTagSize) { + tagToSelect = tag; + minTagSize = tagSize; + } + } + + return tagToSelect; + } + + /** + * Notifies the logical image tag that it is no longer in focus. + * + * @param n + */ + private void resetFocus(ImageTag n) { + n.getEventDispatcher().dispatchEvent(new Event(ImageTagControls.NOT_FOCUSED), NO_OP_CHAIN); + this.pcs.firePropertyChange(new PropertyChangeEvent(this, ImageTagControls.NOT_FOCUSED.getName(), n, null)); + } + + /** + * Notifies the logical image that it is in focus. + * + * @param n + */ + private void requestFocus(ImageTag n) { + if (n == null || n.equals(currentFocus)) { + return; + } else if (currentFocus != null && !currentFocus.equals(n)) { + resetFocus(currentFocus); + } + + n.getEventDispatcher().dispatchEvent(new Event(ImageTagControls.FOCUSED), NO_OP_CHAIN); + this.pcs.firePropertyChange(new PropertyChangeEvent(this, ImageTagControls.FOCUSED.getName(), currentFocus, n)); + + currentFocus = n; + n.toFront(); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/imagetagging/ImageTagsUtil.java b/Core/src/org/sleuthkit/autopsy/contentviewers/imagetagging/ImageTagsUtil.java new file mode 100755 index 0000000000..53dc304b27 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/imagetagging/ImageTagsUtil.java @@ -0,0 +1,238 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.contentviewers.imagetagging; + +import java.awt.image.BufferedImage; +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Collection; +import java.util.concurrent.ExecutionException; +import javafx.concurrent.Task; +import javafx.embed.swing.SwingFXUtils; +import javafx.scene.image.Image; +import javax.imageio.ImageIO; +import org.opencv.core.Core; +import org.opencv.core.Mat; +import org.opencv.core.MatOfByte; +import org.opencv.core.Point; +import org.opencv.core.Scalar; +import org.opencv.core.Size; +import org.opencv.highgui.Highgui; +import org.opencv.imgproc.Imgproc; +import org.sleuthkit.autopsy.coreutils.ImageUtils; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.ReadContentInputStream; + +/** + * Utility for drawing rectangles on image files. + */ +public final class ImageTagsUtil { + + //String constant for writing PNG in ImageIO + private final static String AWT_PNG = "png"; + + //String constant for encoding PNG in OpenCV + private final static String OPENCV_PNG = ".png"; + + /** + * Creates an image with tags applied. + * + * @param file Source image. + * @param tagRegions Tags to apply. + * @return Tagged image. + * + * @throws IOException + * @throws InterruptedException Calling thread was interrupted + * @throws ExecutionException Error while reading image from AbstractFile + */ + public static BufferedImage getImageWithTags(AbstractFile file, + Collection tagRegions) throws IOException, InterruptedException, ExecutionException { + + //The raw image in OpenCV terms + Mat sourceImage = getImageMatFromFile(file); + //Image with tags in OpenCV terms + MatOfByte taggedMatrix = getTaggedImageMatrix(sourceImage, tagRegions); + + try (ByteArrayInputStream taggedStream = new ByteArrayInputStream(taggedMatrix.toArray())) { + return ImageIO.read(taggedStream); + } finally { + sourceImage.release(); + taggedMatrix.release(); + } + } + + /** + * Get the image from file. + * + * @param file + * @return + * @throws IOException + * @throws InterruptedException + * @throws ExecutionException + */ + private static BufferedImage getImageFromFile(AbstractFile file) throws IOException, InterruptedException, ExecutionException { + if (ImageUtils.isGIF(file)) { + //Grab the first frame. + try (BufferedInputStream bufferedReadContentStream = + new BufferedInputStream(new ReadContentInputStream(file))) { + return ImageIO.read(bufferedReadContentStream); + } + } else { + //Otherwise, read the full image. + Task readImageTask = ImageUtils.newReadImageTask(file); + readImageTask.run(); + Image fxResult = readImageTask.get(); + return SwingFXUtils.fromFXImage(fxResult, null); + } + } + + /** + * Reads the image and converts it into an OpenCV equivalent. + * + * @param file Image to read + * @return raw image bytes + * + * @throws IOException + * @throws InterruptedException Calling thread was interrupted. + * @throws ExecutionException Error while reading image from AbstractFile + */ + private static Mat getImageMatFromFile(AbstractFile file) throws InterruptedException, ExecutionException, IOException { + //Get image from file + BufferedImage buffImage = getImageFromFile(file); + + //Convert it to OpenCV Mat. + try (ByteArrayOutputStream outStream = new ByteArrayOutputStream()) { + ImageIO.write(buffImage, AWT_PNG, outStream); + + byte[] imageBytes = outStream.toByteArray(); + MatOfByte rawSourceBytes = new MatOfByte(imageBytes); + Mat sourceImage = Highgui.imdecode(rawSourceBytes, Highgui.IMREAD_COLOR); + rawSourceBytes.release(); + + return sourceImage; + } + } + + /** + * Adds tags to an image matrix. + * + * @param sourceImage + * @param tagRegions + * @return + */ + private static MatOfByte getTaggedImageMatrix(Mat sourceImage, Collection tagRegions) { + + //Apply all tags to source image + for (ImageTagRegion region : tagRegions) { + Point topLeft = new Point(region.getX(), region.getY()); + Point bottomRight = new Point(topLeft.x + region.getWidth(), + topLeft.y + region.getHeight()); + //Red + Scalar rectangleBorderColor = new Scalar(0, 0, 255); + + int rectangleBorderWidth = (int) Math.rint(region.getStrokeThickness()); + + Core.rectangle(sourceImage, topLeft, bottomRight, + rectangleBorderColor, rectangleBorderWidth); + } + + MatOfByte taggedMatrix = new MatOfByte(); + Highgui.imencode(OPENCV_PNG, sourceImage, taggedMatrix); + + return taggedMatrix; + } + + /** + * Creates a thumbnail with tags applied. + * + * @param file Input file to apply tags & produce thumbnail from + * @param tagRegions Tags to apply + * @param iconSize Size of the output thumbnail + * @return BufferedImage Thumbnail image + * + * @throws InterruptedException Calling thread was interrupted. + * @throws ExecutionException Error while reading image from file. + */ + public static BufferedImage getThumbnailWithTags(AbstractFile file, Collection tagRegions, + IconSize iconSize) throws IOException, InterruptedException, ExecutionException { + + //Raw image + Mat sourceImage = getImageMatFromFile(file); + //Full size image with tags + MatOfByte taggedMatrix = getTaggedImageMatrix(sourceImage, tagRegions); + //Resized to produce thumbnail + MatOfByte thumbnailMatrix = getResizedMatrix(taggedMatrix, iconSize); + + try (ByteArrayInputStream thumbnailStream = new ByteArrayInputStream(thumbnailMatrix.toArray())) { + return ImageIO.read(thumbnailStream); + } finally { + sourceImage.release(); + taggedMatrix.release(); + thumbnailMatrix.release(); + } + } + + /** + * Resizes the image matrix. + * + * @param taggedMatrix Image to resize. + * @param size Size of thumbnail. + * + * @return A new resized image matrix. + */ + private static MatOfByte getResizedMatrix(MatOfByte taggedMatrix, IconSize size) { + Size resizeDimensions = new Size(size.getSize(), size.getSize()); + Mat taggedImage = Highgui.imdecode(taggedMatrix, Highgui.IMREAD_COLOR); + + Mat thumbnailImage = new Mat(); + Imgproc.resize(taggedImage, thumbnailImage, resizeDimensions); + + MatOfByte thumbnailMatrix = new MatOfByte(); + Highgui.imencode(OPENCV_PNG, thumbnailImage, thumbnailMatrix); + + thumbnailImage.release(); + taggedImage.release(); + + return thumbnailMatrix; + } + + private ImageTagsUtil() { + } + + /** + * Sizes for thumbnails + */ + public enum IconSize { + SMALL(50), + MEDIUM(100), + LARGE(200); + + private final int SIZE; + + IconSize(int size) { + this.SIZE = size; + } + + public int getSize() { + return SIZE; + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/textcontentviewer/TextContentViewer.java b/Core/src/org/sleuthkit/autopsy/contentviewers/textcontentviewer/TextContentViewer.java index 81384acbaa..d7f853caab 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/textcontentviewer/TextContentViewer.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/textcontentviewer/TextContentViewer.java @@ -23,6 +23,7 @@ import org.openide.nodes.Node; import org.openide.util.NbBundle.Messages; import org.openide.util.lookup.ServiceProvider; import org.sleuthkit.autopsy.corecomponentinterfaces.DataContentViewer; +import org.sleuthkit.datamodel.AbstractFile; /** * A DataContentViewer that displays text with the TextViewers available. @@ -90,6 +91,17 @@ public class TextContentViewer implements DataContentViewer { if (node == null) { return false; } + // get the node's File, if it has one + AbstractFile file = node.getLookup().lookup(AbstractFile.class); + if (file == null) { + return false; + } + + // disable the text content viewer for directories and empty files + if (file.isDir() || file.getSize() == 0) { + return false; + } + return panel.isSupported(node); } diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/textcontentviewer/TextContentViewerPanel.java b/Core/src/org/sleuthkit/autopsy/contentviewers/textcontentviewer/TextContentViewerPanel.java index e1af060cfc..7c80d606fd 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/textcontentviewer/TextContentViewerPanel.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/textcontentviewer/TextContentViewerPanel.java @@ -79,7 +79,7 @@ public class TextContentViewerPanel extends javax.swing.JPanel implements DataCo } /** - * Deterime wether the content viewer which displays this panel isSupported. + * Determine whether the content viewer which displays this panel isSupported. * This panel is supported if any of the TextViewer's displayed in it are * supported. * diff --git a/Core/src/org/sleuthkit/autopsy/core/Installer.java b/Core/src/org/sleuthkit/autopsy/core/Installer.java index a9a4131ea4..329fe3dfc9 100644 --- a/Core/src/org/sleuthkit/autopsy/core/Installer.java +++ b/Core/src/org/sleuthkit/autopsy/core/Installer.java @@ -46,6 +46,7 @@ import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.coreutils.ModuleSettings; import org.sleuthkit.autopsy.coreutils.PlatformUtil; +import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector; /** * Wrapper over Installers in packages in Core module. This is the main @@ -226,6 +227,21 @@ public class Installer extends ModuleInstall { packageInstallers.add(org.sleuthkit.autopsy.ingest.Installer.getDefault()); packageInstallers.add(org.sleuthkit.autopsy.centralrepository.eventlisteners.Installer.getDefault()); packageInstallers.add(org.sleuthkit.autopsy.healthmonitor.Installer.getDefault()); + + /** + * This is a temporary workaround for the following bug in Tika that + * results in a null pointer exception when used from the Image Gallery. + * The current hypothesis is that the Image Gallery is cancelling the + * thumbnail task that Tika initialization is happening on. Once the + * Tika issue has been fixed we should no longer need this workaround. + * + * https://issues.apache.org/jira/browse/TIKA-2896 + */ + try { + FileTypeDetector fileTypeDetector = new FileTypeDetector(); + } catch (FileTypeDetector.FileTypeDetectorInitException ex) { + logger.log(Level.SEVERE, "Failed to load file type detector.", ex); + } } /** diff --git a/Core/src/org/sleuthkit/autopsy/core/UserPreferences.java b/Core/src/org/sleuthkit/autopsy/core/UserPreferences.java index d6f68086c4..0d2bfa90d3 100644 --- a/Core/src/org/sleuthkit/autopsy/core/UserPreferences.java +++ b/Core/src/org/sleuthkit/autopsy/core/UserPreferences.java @@ -73,12 +73,12 @@ public final class UserPreferences { private static final int LOG_FILE_NUM_INT = 10; public static final String GROUP_ITEMS_IN_TREE_BY_DATASOURCE = "GroupItemsInTreeByDataSource"; //NON-NLS public static final String SHOW_ONLY_CURRENT_USER_TAGS = "ShowOnlyCurrentUserTags"; - public static final String HIDE_CENTRAL_REPO_COMMENTS_AND_OCCURRENCES = "HideCentralRepoCommentsAndOccurrences"; + public static final String HIDE_SCO_COLUMNS = "HideCentralRepoCommentsAndOccurrences"; //The key for this setting pre-dates the settings current functionality //NON-NLS public static final String DISPLAY_TRANSLATED_NAMES = "DisplayTranslatedNames"; public static final String EXTERNAL_HEX_EDITOR_PATH = "ExternalHexEditorPath"; public static final String SOLR_MAX_JVM_SIZE = "SolrMaxJVMSize"; public static final String RESULTS_TABLE_PAGE_SIZE = "ResultsTablePageSize"; - + // Prevent instantiation. private UserPreferences() { } @@ -187,11 +187,11 @@ public final class UserPreferences { public static void setDisplayTimesInLocalTime(boolean value) { preferences.putBoolean(DISPLAY_TIMES_IN_LOCAL_TIME, value); } - + public static String getTimeZoneForDisplays() { return preferences.get(TIME_ZONE_FOR_DISPLAYS, TimeZone.GMT_ZONE.getID()); } - + public static void setTimeZoneForDisplays(String timeZone) { preferences.put(TIME_ZONE_FOR_DISPLAYS, timeZone); } @@ -224,11 +224,10 @@ public final class UserPreferences { return preferences.getBoolean(SHOW_ONLY_CURRENT_USER_TAGS, false); } - /** * Set the user preference which identifies whether tags should be shown for * only the current user or all users. - * + * * @param value - true for just the current user, false for all users */ public static void setShowOnlyCurrentUserTags(boolean value) { @@ -236,33 +235,31 @@ public final class UserPreferences { } /** - * Get the user preference which identifies whether the Central Repository - * should be called to get comments and occurrences for the (C)omments and - * (O)ccurrences columns in the result view. - * - * @return True if hiding Central Repository data for comments and - * occurrences; otherwise false. + * Get the user preference which identifies whether the (S)core, (C)omments, + * and (O)ccurrences columns should be populated and displayed in the result + * view. + * + * @return True if hiding SCO columns; otherwise false. */ - public static boolean hideCentralRepoCommentsAndOccurrences() { - return preferences.getBoolean(HIDE_CENTRAL_REPO_COMMENTS_AND_OCCURRENCES, false); + public static boolean getHideSCOColumns() { + return preferences.getBoolean(HIDE_SCO_COLUMNS, false); } - /** - * Set the user preference which identifies whether the Central Repository - * should be called to get comments and occurrences for the (C)omments and - * (O)ccurrences columns in the result view. - * + * Set the user preference which identifies whether the (S)core, (C)omments, + * and (O)ccurrences columns should be populated and displayed in the result + * view. + * * @param value The value of which to assign to the user preference. */ - public static void setHideCentralRepoCommentsAndOccurrences(boolean value) { - preferences.putBoolean(HIDE_CENTRAL_REPO_COMMENTS_AND_OCCURRENCES, value); + public static void setHideSCOColumns(boolean value) { + preferences.putBoolean(HIDE_SCO_COLUMNS, value); } - + public static void setDisplayTranslatedFileNames(boolean value) { preferences.putBoolean(DISPLAY_TRANSLATED_NAMES, value); } - + public static boolean displayTranslatedFileNames() { return preferences.getBoolean(DISPLAY_TRANSLATED_NAMES, false); } @@ -336,12 +333,12 @@ public final class UserPreferences { public static void setIndexingServerPort(int port) { preferences.putInt(INDEXING_SERVER_PORT, port); } - - public static void setTextTranslatorName(String textTranslatorName){ + + public static void setTextTranslatorName(String textTranslatorName) { preferences.put(TEXT_TRANSLATOR_NAME, textTranslatorName); } - - public static String getTextTranslatorName(){ + + public static String getTextTranslatorName() { return preferences.get(TEXT_TRANSLATOR_NAME, null); } @@ -482,7 +479,7 @@ public final class UserPreferences { public static void setLogFileCount(int count) { preferences.putInt(MAX_NUM_OF_LOG_FILE, count); } - + /** * Get the maximum JVM heap size (in MB) for the embedded Solr server. * @@ -504,10 +501,10 @@ public final class UserPreferences { /** * Get the maximum number of results to display in a result table. * - * @return Saved value or default (0) which indicates no max. + * @return Saved value or default (10,000). */ public static int getResultsTablePageSize() { - return preferences.getInt(RESULTS_TABLE_PAGE_SIZE, 0); + return preferences.getInt(RESULTS_TABLE_PAGE_SIZE, 10_000); } /** @@ -521,17 +518,17 @@ public final class UserPreferences { /** * Set the HdX path. - * + * * @param executablePath User-inputted path to HxD executable */ public static void setExternalHexEditorPath(String executablePath) { preferences.put(EXTERNAL_HEX_EDITOR_PATH, executablePath); } - + /** * Retrieves the HdXEditor path set by the User. If not found, the default * will be the default install location of HxD. - * + * * @return Path to HdX */ public static String getExternalHexEditorPath() { diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/AutoWrappingJTextPane.java b/Core/src/org/sleuthkit/autopsy/corecomponents/AutoWrappingJTextPane.java new file mode 100755 index 0000000000..8b739b8dcf --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/AutoWrappingJTextPane.java @@ -0,0 +1,103 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.corecomponents; + +import javax.swing.JTextPane; +import javax.swing.SizeRequirements; +import javax.swing.text.Element; +import javax.swing.text.View; +import static javax.swing.text.View.GoodBreakWeight; +import javax.swing.text.ViewFactory; +import javax.swing.text.html.HTMLEditorKit; +import javax.swing.text.html.InlineView; +import javax.swing.text.html.ParagraphView; + +/** + * JTextPane extension that auto wraps input text using an HTMLEditorKit trick. + */ +public class AutoWrappingJTextPane extends JTextPane { + + public AutoWrappingJTextPane() { + /* + * This appears to be an attempt to modify the wrapping behavior of the + * text pane. Taken form this website: http://java-sl.com/tip_html_letter_wrap.html. + */ + HTMLEditorKit editorKit = new HTMLEditorKit() { + private static final long serialVersionUID = 1L; + + @Override + public ViewFactory getViewFactory() { + + return new HTMLEditorKit.HTMLFactory() { + @Override + public View create(Element e) { + View v = super.create(e); + if (v instanceof InlineView) { + return new InlineView(e) { + @Override + public int getBreakWeight(int axis, float pos, float len) { + return GoodBreakWeight; + } + + @Override + public View breakView(int axis, int p0, float pos, float len) { + if (axis == View.X_AXIS) { + checkPainter(); + int p1 = getGlyphPainter().getBoundedPosition(this, p0, pos, len); + if (p0 == getStartOffset() && p1 == getEndOffset()) { + return this; + } + return createFragment(p0, p1); + } + return this; + } + }; + } else if (v instanceof ParagraphView) { + return new ParagraphView(e) { + @Override + protected SizeRequirements calculateMinorAxisRequirements(int axis, SizeRequirements r) { + SizeRequirements requirements = r; + if (requirements == null) { + requirements = new SizeRequirements(); + } + float pref = layoutPool.getPreferredSpan(axis); + float min = layoutPool.getMinimumSpan(axis); + // Don't include insets, Box.getXXXSpan will include them. + requirements.minimum = (int) min; + requirements.preferred = Math.max(requirements.minimum, (int) pref); + requirements.maximum = Integer.MAX_VALUE; + requirements.alignment = 0.5f; + return requirements; + } + }; + } + return v; + } + }; + } + }; + + this.setEditorKit(editorKit); + } + + @Override + public void setText(String text) { + super.setText("
" + text + "
"); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties index ac5bccebc8..15ee654347 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties @@ -153,10 +153,7 @@ ViewPreferencesPanel.currentSessionSettingsPanel.border.title=Current Session Se ViewPreferencesPanel.hideRejectedResultsCheckbox.text=Hide rejected results ViewPreferencesPanel.selectFileLabel.text=When selecting a file: ViewPreferencesPanel.globalSettingsPanel.border.title=Global Settings -ViewPreferencesPanel.commentsOccurencesColumnWrapAroundText.text=to reduce loading times ViewPreferencesPanel.translateTextLabel.text=Translate text: -ViewPreferencesPanel.commentsOccurencesColumnsCheckbox.text=C(omments) and O(ccurences) columns -ViewPreferencesPanel.centralRepoLabel.text=Do not use Central Repository for: ViewPreferencesPanel.hideOtherUsersTagsLabel.text=Hide other users' tags in the: ViewPreferencesPanel.hideOtherUsersTagsCheckbox.text=Tags area in the tree ViewPreferencesPanel.useAnotherTimeRadioButton.text=Use another time zone @@ -216,4 +213,8 @@ DataResultViewerTable.pagesLabel.text=Pages: DataResultViewerTable.pageNumLabel.text= DataResultViewerTable.pageLabel.text=Page: ViewPreferencesPanel.maxResultsLabel.text=Maximum number of Results to show in table: -ViewPreferencesPanel.maxResultsLabel.toolTipText=\nAll results are shown in the results table if this value is 0 (default).\n
You may want to consider setting this value if you have large numbers of results that are taking a long time to display.\n +ViewPreferencesPanel.maxResultsLabel.toolTipText=\nSetting this value to 0 will display all results in the results table.\n
Note that setting this value to 0 may result in poor UI responsiveness when there are large numbers of results.\n +DataResultViewerTable.exportCSVButton.text=Save Table as CSV +ViewPreferencesPanel.scoColumnsCheckbox.text=S(core), C(omments), and O(ccurences) +ViewPreferencesPanel.scoColumnsWrapAroundText.text=to reduce loading times +ViewPreferencesPanel.scoColumnsLabel.text=Do not add columns for: diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties-MERGED index 2c0af06f50..9f363b7723 100755 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties-MERGED @@ -32,6 +32,7 @@ DataResultViewerTable.commentRenderer.noComment.toolTip=No comments found DataResultViewerTable.commentRenderer.tagComment.toolTip=Comment exists on associated tag(s) DataResultViewerTable.countRender.name=O DataResultViewerTable.countRender.toolTip=O(ccurrences) indicates the number of data sources containing the item in the Central Repository +DataResultViewerTable.exportCSVButtonActionPerformed.empty=No data to export DataResultViewerTable.firstColLbl=Name DataResultViewerTable.goToPageTextField.err=Invalid page number # {0} - totalPages @@ -206,10 +207,7 @@ ViewPreferencesPanel.currentSessionSettingsPanel.border.title=Current Session Se ViewPreferencesPanel.hideRejectedResultsCheckbox.text=Hide rejected results ViewPreferencesPanel.selectFileLabel.text=When selecting a file: ViewPreferencesPanel.globalSettingsPanel.border.title=Global Settings -ViewPreferencesPanel.commentsOccurencesColumnWrapAroundText.text=to reduce loading times ViewPreferencesPanel.translateTextLabel.text=Translate text: -ViewPreferencesPanel.commentsOccurencesColumnsCheckbox.text=C(omments) and O(ccurences) columns -ViewPreferencesPanel.centralRepoLabel.text=Do not use Central Repository for: ViewPreferencesPanel.hideOtherUsersTagsLabel.text=Hide other users' tags in the: ViewPreferencesPanel.hideOtherUsersTagsCheckbox.text=Tags area in the tree ViewPreferencesPanel.useAnotherTimeRadioButton.text=Use another time zone @@ -269,4 +267,8 @@ DataResultViewerTable.pagesLabel.text=Pages: DataResultViewerTable.pageNumLabel.text= DataResultViewerTable.pageLabel.text=Page: ViewPreferencesPanel.maxResultsLabel.text=Maximum number of Results to show in table: -ViewPreferencesPanel.maxResultsLabel.toolTipText=\nAll results are shown in the results table if this value is 0 (default).\n
You may want to consider setting this value if you have large numbers of results that are taking a long time to display.\n +ViewPreferencesPanel.maxResultsLabel.toolTipText=\nSetting this value to 0 will display all results in the results table.\n
Note that setting this value to 0 may result in poor UI responsiveness when there are large numbers of results.\n +DataResultViewerTable.exportCSVButton.text=Save Table as CSV +ViewPreferencesPanel.scoColumnsCheckbox.text=S(core), C(omments), and O(ccurences) +ViewPreferencesPanel.scoColumnsWrapAroundText.text=to reduce loading times +ViewPreferencesPanel.scoColumnsLabel.text=Do not add columns for: diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.form b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.form index 6b17cfd6dd..6aeda07298 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.form +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.form @@ -16,13 +16,13 @@ - - - + + + - + - + @@ -32,14 +32,15 @@ - + + - + @@ -48,9 +49,10 @@ + - - + + @@ -164,5 +166,15 @@ + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java index a5bbc461b8..b99a94af96 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java @@ -33,8 +33,10 @@ import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Queue; import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; @@ -77,6 +79,7 @@ import org.openide.util.lookup.ServiceProvider; import org.sleuthkit.autopsy.core.UserPreferences; import org.sleuthkit.autopsy.corecomponentinterfaces.DataResultViewer; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.datamodel.NodeProperty; import org.sleuthkit.autopsy.datamodel.NodeSelectionInfo; @@ -148,7 +151,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer { * OutlineView to the actions global context. * * @param explorerManager The explorer manager of the ancestor top - * component. + * component. */ public DataResultViewerTable(ExplorerManager explorerManager) { this(explorerManager, Bundle.DataResultViewerTable_title()); @@ -161,8 +164,8 @@ public class DataResultViewerTable extends AbstractDataResultViewer { * in the OutlineView to the actions global context. * * @param explorerManager The explorer manager of the ancestor top - * component. - * @param title The title. + * component. + * @param title The title. */ public DataResultViewerTable(ExplorerManager explorerManager, String title) { super(explorerManager); @@ -177,6 +180,13 @@ public class DataResultViewerTable extends AbstractDataResultViewer { initializePagingSupport(); + /* + * Disable the CSV export button for the common properties results + */ + if (this instanceof org.sleuthkit.autopsy.commonpropertiessearch.CommonAttributesSearchResultsViewerTable) { + exportCSVButton.setEnabled(false); + } + /* * Configure the child OutlineView (explorer view) component. */ @@ -293,18 +303,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer { this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); try { - /* - * If the given node is not null and has children, set it as the - * root context of the child OutlineView, otherwise make an - * "empty"node the root context. - * - * IMPORTANT NOTE: This is the first of many times where a - * getChildren call on the current root node causes all of the - * children of the root node to be created and defeats lazy child - * node creation, if it is enabled. It also likely leads to many - * case database round trips. - */ - if (rootNode != null && rootNode.getChildren().getNodesCount() > 0) { + if (rootNode != null) { this.rootNode = rootNode; /** @@ -355,7 +354,20 @@ public class DataResultViewerTable extends AbstractDataResultViewer { // No-op } }); + } + /* + * If the given node is not null and has children, set it as the + * root context of the child OutlineView, otherwise make an + * "empty"node the root context. + * + * IMPORTANT NOTE: This is the first of many times where a + * getChildren call on the current root node causes all of the + * children of the root node to be created and defeats lazy child + * node creation, if it is enabled. It also likely leads to many + * case database round trips. + */ + if (rootNode != null && rootNode.getChildren().getNodesCount() > 0) { this.getExplorerManager().setRootContext(this.rootNode); setupTable(); } else { @@ -690,7 +702,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer { * order. * * @return a List> of the properties in the persisted - * order. + * order. */ private synchronized List> loadColumnOrder() { @@ -711,7 +723,6 @@ public class DataResultViewerTable extends AbstractDataResultViewer { * property at the end. */ int offset = props.size(); - boolean noPreviousSettings = true; final Preferences preferences = NbPreferences.forModule(DataResultViewerTable.class); @@ -719,23 +730,51 @@ public class DataResultViewerTable extends AbstractDataResultViewer { Integer value = preferences.getInt(ResultViewerPersistence.getColumnPositionKey(tfn, prop.getName()), -1); if (value >= 0 && value < offset && !propertiesMap.containsKey(value)) { propertiesMap.put(value, prop); - noPreviousSettings = false; } else { propertiesMap.put(offset, prop); offset++; } } - // If none of the properties had previous settings, we should decrement - // each value by the number of properties to make the values 0-indexed. - if (noPreviousSettings) { - ArrayList keys = new ArrayList<>(propertiesMap.keySet()); - for (int key : keys) { - propertiesMap.put(key - props.size(), propertiesMap.remove(key)); + /* + NOTE: it is possible to have "discontinuities" in the keys (i.e. column numbers) + of the map. This happens when some of the columns had a previous setting, and + other columns did not. We need to make the keys 0-indexed and continuous. + */ + compactPropertiesMap(); + + return new ArrayList<>(propertiesMap.values()); + } + + /** + * Makes properties map 0-indexed and re-arranges elements to make sure the + * indexes are continuous. + */ + private void compactPropertiesMap() { + + // check if there are discontinuities in the map keys. + int size = propertiesMap.size(); + Queue availablePositions = new LinkedList<>(); + for (int i = 0; i < size; i++) { + if (!propertiesMap.containsKey(i)) { + availablePositions.add(i); } } - return new ArrayList<>(propertiesMap.values()); + // if there are no discontinuities, we are done + if (availablePositions.isEmpty()) { + return; + } + + // otherwise, move map elements into the available positions. + // we don't want to just move down all elements, as we want to preserve the order + // of the ones that had previous setting (i.e. ones that have key < size) + ArrayList keys = new ArrayList<>(propertiesMap.keySet()); + for (int key : keys) { + if (key >= size) { + propertiesMap.put(availablePositions.remove(), propertiesMap.remove(key)); + } + } } /** @@ -847,7 +886,6 @@ public class DataResultViewerTable extends AbstractDataResultViewer { void postPageSizeChangeEvent() { // Reset page variables when page size changes currentPage = 1; - totalPages = 0; if (this == pagingSupport) { updateControls(); @@ -873,7 +911,10 @@ public class DataResultViewerTable extends AbstractDataResultViewer { togglePageControls(true); } - updateControls(); + // Only update UI controls if this event is for the node currently being viewed. + if (nodeName.equals(rootNode.getName())) { + updateControls(); + } } } @@ -1291,6 +1332,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer { outlineView = new OutlineView(DataResultViewerTable.FIRST_COLUMN_LABEL); gotoPageLabel = new javax.swing.JLabel(); gotoPageTextField = new javax.swing.JTextField(); + exportCSVButton = new javax.swing.JButton(); pageLabel.setText(org.openide.util.NbBundle.getMessage(DataResultViewerTable.class, "DataResultViewerTable.pageLabel.text")); // NOI18N @@ -1338,17 +1380,24 @@ public class DataResultViewerTable extends AbstractDataResultViewer { } }); + exportCSVButton.setText(org.openide.util.NbBundle.getMessage(DataResultViewerTable.class, "DataResultViewerTable.exportCSVButton.text")); // NOI18N + exportCSVButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + exportCSVButtonActionPerformed(evt); + } + }); + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(outlineView, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() - .addContainerGap(608, Short.MAX_VALUE) + .addComponent(outlineView, javax.swing.GroupLayout.DEFAULT_SIZE, 904, Short.MAX_VALUE) + .addGroup(layout.createSequentialGroup() + .addContainerGap() .addComponent(pageLabel) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(pageNumLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 53, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addGap(14, 14, 14) .addComponent(pagesLabel) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) .addComponent(pagePrevButton, javax.swing.GroupLayout.PREFERRED_SIZE, 16, javax.swing.GroupLayout.PREFERRED_SIZE) @@ -1358,7 +1407,8 @@ public class DataResultViewerTable extends AbstractDataResultViewer { .addComponent(gotoPageLabel) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(gotoPageTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 33, javax.swing.GroupLayout.PREFERRED_SIZE) - .addContainerGap()) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(exportCSVButton)) ); layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {pageNextButton, pagePrevButton}); @@ -1366,7 +1416,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer { layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() - .addContainerGap() + .addGap(3, 3, 3) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.CENTER) .addComponent(pageLabel) .addComponent(pageNumLabel) @@ -1374,9 +1424,10 @@ public class DataResultViewerTable extends AbstractDataResultViewer { .addComponent(pagePrevButton, javax.swing.GroupLayout.PREFERRED_SIZE, 14, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(pageNextButton, javax.swing.GroupLayout.PREFERRED_SIZE, 15, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(gotoPageLabel) - .addComponent(gotoPageTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(outlineView, javax.swing.GroupLayout.DEFAULT_SIZE, 324, Short.MAX_VALUE) + .addComponent(gotoPageTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(exportCSVButton)) + .addGap(3, 3, 3) + .addComponent(outlineView, javax.swing.GroupLayout.DEFAULT_SIZE, 321, Short.MAX_VALUE) .addContainerGap()) ); @@ -1397,7 +1448,19 @@ public class DataResultViewerTable extends AbstractDataResultViewer { pagingSupport.gotoPage(); }//GEN-LAST:event_gotoPageTextFieldActionPerformed + @NbBundle.Messages({"DataResultViewerTable.exportCSVButtonActionPerformed.empty=No data to export" + }) + private void exportCSVButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exportCSVButtonActionPerformed + Node currentRoot = this.getExplorerManager().getRootContext(); + if (currentRoot != null && currentRoot.getChildren().getNodesCount() > 0) { + org.sleuthkit.autopsy.directorytree.ExportCSVAction.saveNodesToCSV(java.util.Arrays.asList(currentRoot.getChildren().getNodes()), this); + } else { + MessageNotifyUtil.Message.info(Bundle.DataResultViewerTable_exportCSVButtonActionPerformed_empty()); + } + }//GEN-LAST:event_exportCSVButtonActionPerformed + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton exportCSVButton; private javax.swing.JLabel gotoPageLabel; private javax.swing.JTextField gotoPageTextField; private org.openide.explorer.view.OutlineView outlineView; diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerThumbnail.form b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerThumbnail.form index a042fd7de6..798b0b51bd 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerThumbnail.form +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerThumbnail.form @@ -11,217 +11,278 @@ + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - - - - - - - - - - - - - + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerThumbnail.java b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerThumbnail.java index 4f4e41d04e..3199dbd7e8 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerThumbnail.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerThumbnail.java @@ -122,6 +122,11 @@ public final class DataResultViewerThumbnail extends AbstractDataResultViewer { currentPage = -1; totalPages = 0; currentPageImages = 0; + + // The GUI builder is using FlowLayout therefore this change so have no + // impact on the initally designed layout. This change will just effect + // how the components are laid out as size of the window changes. + buttonBarPanel.setLayout(new WrapLayout(java.awt.FlowLayout.LEFT)); } /** @@ -132,33 +137,84 @@ public final class DataResultViewerThumbnail extends AbstractDataResultViewer { @SuppressWarnings("unchecked") // //GEN-BEGIN:initComponents private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + buttonBarPanel = new javax.swing.JPanel(); + pagesPanel = new javax.swing.JPanel(); + pageNumberPane = new javax.swing.JPanel(); pageLabel = new javax.swing.JLabel(); + pageNumLabel = new javax.swing.JLabel(); + pageButtonPanel = new javax.swing.JPanel(); pagesLabel = new javax.swing.JLabel(); pagePrevButton = new javax.swing.JButton(); pageNextButton = new javax.swing.JButton(); - imagesLabel = new javax.swing.JLabel(); - imagesRangeLabel = new javax.swing.JLabel(); - pageNumLabel = new javax.swing.JLabel(); - filePathLabel = new javax.swing.JLabel(); + pageGotoPane = new javax.swing.JPanel(); goToPageLabel = new javax.swing.JLabel(); goToPageField = new javax.swing.JTextField(); + imagePane = new javax.swing.JPanel(); + imagesLabel = new javax.swing.JLabel(); + imagesRangeLabel = new javax.swing.JLabel(); thumbnailSizeComboBox = new javax.swing.JComboBox<>(); - iconView = new org.openide.explorer.view.IconView(); - sortButton = new javax.swing.JButton(); + sortPane = new javax.swing.JPanel(); sortLabel = new javax.swing.JLabel(); + sortButton = new javax.swing.JButton(); + filePathLabel = new javax.swing.JLabel(); + iconView = new org.openide.explorer.view.IconView(); + + setLayout(new java.awt.BorderLayout()); + + buttonBarPanel.setLayout(new java.awt.FlowLayout(java.awt.FlowLayout.LEFT)); + + pagesPanel.setLayout(new java.awt.GridBagLayout()); + + pageNumberPane.setLayout(new java.awt.GridBagLayout()); pageLabel.setText(org.openide.util.NbBundle.getMessage(DataResultViewerThumbnail.class, "DataResultViewerThumbnail.pageLabel.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.VERTICAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.FIRST_LINE_START; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 9); + pageNumberPane.add(pageLabel, gridBagConstraints); + + pageNumLabel.setText(org.openide.util.NbBundle.getMessage(DataResultViewerThumbnail.class, "DataResultViewerThumbnail.pageNumLabel.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.FIRST_LINE_START; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 15); + pageNumberPane.add(pageNumLabel, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.fill = java.awt.GridBagConstraints.VERTICAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.FIRST_LINE_START; + pagesPanel.add(pageNumberPane, gridBagConstraints); + + buttonBarPanel.add(pagesPanel); + + pageButtonPanel.setLayout(new java.awt.GridBagLayout()); pagesLabel.setText(org.openide.util.NbBundle.getMessage(DataResultViewerThumbnail.class, "DataResultViewerThumbnail.pagesLabel.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.VERTICAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 9); + pageButtonPanel.add(pagesLabel, gridBagConstraints); pagePrevButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/corecomponents/btn_step_back.png"))); // NOI18N pagePrevButton.setText(org.openide.util.NbBundle.getMessage(DataResultViewerThumbnail.class, "DataResultViewerThumbnail.pagePrevButton.text")); // NOI18N + pagePrevButton.setBorder(null); pagePrevButton.setDisabledIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/corecomponents/btn_step_back_disabled.png"))); // NOI18N pagePrevButton.setFocusable(false); pagePrevButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER); - pagePrevButton.setMargin(new java.awt.Insets(2, 0, 2, 0)); - pagePrevButton.setPreferredSize(new java.awt.Dimension(55, 23)); + pagePrevButton.setMargin(new java.awt.Insets(0, 0, 0, 0)); pagePrevButton.setRolloverIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/corecomponents/btn_step_back_hover.png"))); // NOI18N pagePrevButton.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM); pagePrevButton.addActionListener(new java.awt.event.ActionListener() { @@ -166,13 +222,20 @@ public final class DataResultViewerThumbnail extends AbstractDataResultViewer { pagePrevButtonActionPerformed(evt); } }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.gridheight = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.FIRST_LINE_START; + pageButtonPanel.add(pagePrevButton, gridBagConstraints); pageNextButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/corecomponents/btn_step_forward.png"))); // NOI18N pageNextButton.setText(org.openide.util.NbBundle.getMessage(DataResultViewerThumbnail.class, "DataResultViewerThumbnail.pageNextButton.text")); // NOI18N + pageNextButton.setBorder(null); pageNextButton.setDisabledIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/corecomponents/btn_step_forward_disabled.png"))); // NOI18N pageNextButton.setFocusable(false); pageNextButton.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER); - pageNextButton.setMargin(new java.awt.Insets(2, 0, 2, 0)); + pageNextButton.setMargin(new java.awt.Insets(0, 0, 0, 0)); pageNextButton.setMaximumSize(new java.awt.Dimension(27, 23)); pageNextButton.setMinimumSize(new java.awt.Dimension(27, 23)); pageNextButton.setRolloverIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/corecomponents/btn_step_forward_hover.png"))); // NOI18N @@ -182,16 +245,27 @@ public final class DataResultViewerThumbnail extends AbstractDataResultViewer { pageNextButtonActionPerformed(evt); } }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 0; + gridBagConstraints.gridheight = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.FIRST_LINE_START; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 15); + pageButtonPanel.add(pageNextButton, gridBagConstraints); - imagesLabel.setText(org.openide.util.NbBundle.getMessage(DataResultViewerThumbnail.class, "DataResultViewerThumbnail.imagesLabel.text")); // NOI18N + buttonBarPanel.add(pageButtonPanel); - imagesRangeLabel.setText(org.openide.util.NbBundle.getMessage(DataResultViewerThumbnail.class, "DataResultViewerThumbnail.imagesRangeLabel.text")); // NOI18N - - pageNumLabel.setText(org.openide.util.NbBundle.getMessage(DataResultViewerThumbnail.class, "DataResultViewerThumbnail.pageNumLabel.text")); // NOI18N - - filePathLabel.setText(org.openide.util.NbBundle.getMessage(DataResultViewerThumbnail.class, "DataResultViewerThumbnail.filePathLabel.text")); // NOI18N + pageGotoPane.setLayout(new java.awt.GridBagLayout()); goToPageLabel.setText(org.openide.util.NbBundle.getMessage(DataResultViewerThumbnail.class, "DataResultViewerThumbnail.goToPageLabel.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.VERTICAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 9); + pageGotoPane.add(goToPageLabel, gridBagConstraints); goToPageField.setText(org.openide.util.NbBundle.getMessage(DataResultViewerThumbnail.class, "DataResultViewerThumbnail.goToPageField.text")); // NOI18N goToPageField.addActionListener(new java.awt.event.ActionListener() { @@ -199,12 +273,49 @@ public final class DataResultViewerThumbnail extends AbstractDataResultViewer { goToPageFieldActionPerformed(evt); } }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.gridheight = 2; + gridBagConstraints.ipadx = 75; + gridBagConstraints.anchor = java.awt.GridBagConstraints.FIRST_LINE_START; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 15); + pageGotoPane.add(goToPageField, gridBagConstraints); + + buttonBarPanel.add(pageGotoPane); + + imagePane.setLayout(new java.awt.GridBagLayout()); + + imagesLabel.setText(org.openide.util.NbBundle.getMessage(DataResultViewerThumbnail.class, "DataResultViewerThumbnail.imagesLabel.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 9); + imagePane.add(imagesLabel, gridBagConstraints); + + imagesRangeLabel.setText(org.openide.util.NbBundle.getMessage(DataResultViewerThumbnail.class, "DataResultViewerThumbnail.imagesRangeLabel.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 15); + imagePane.add(imagesRangeLabel, gridBagConstraints); + + buttonBarPanel.add(imagePane); thumbnailSizeComboBox.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { thumbnailSizeComboBoxActionPerformed(evt); } }); + buttonBarPanel.add(thumbnailSizeComboBox); + + sortPane.setLayout(new java.awt.GridBagLayout()); + + sortLabel.setText(org.openide.util.NbBundle.getMessage(DataResultViewerThumbnail.class, "DataResultViewerThumbnail.sortLabel.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.VERTICAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.FIRST_LINE_START; + gridBagConstraints.weighty = 1.0; + sortPane.add(sortLabel, gridBagConstraints); sortButton.setText(org.openide.util.NbBundle.getMessage(DataResultViewerThumbnail.class, "DataResultViewerThumbnail.sortButton.text")); // NOI18N sortButton.addActionListener(new java.awt.event.ActionListener() { @@ -212,65 +323,20 @@ public final class DataResultViewerThumbnail extends AbstractDataResultViewer { sortButtonActionPerformed(evt); } }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.FIRST_LINE_START; + gridBagConstraints.insets = new java.awt.Insets(0, 15, 0, 9); + sortPane.add(sortButton, gridBagConstraints); - sortLabel.setText(org.openide.util.NbBundle.getMessage(DataResultViewerThumbnail.class, "DataResultViewerThumbnail.sortLabel.text")); // NOI18N + buttonBarPanel.add(sortPane); - javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); - this.setLayout(layout); - layout.setHorizontalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() - .addContainerGap() - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) - .addComponent(iconView, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup() - .addComponent(pageLabel) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(pageNumLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 95, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addComponent(pagesLabel) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addComponent(pagePrevButton, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(0, 0, 0) - .addComponent(pageNextButton, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addComponent(goToPageLabel) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(goToPageField, javax.swing.GroupLayout.PREFERRED_SIZE, 54, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(12, 12, 12) - .addComponent(imagesLabel) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(imagesRangeLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 91, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(thumbnailSizeComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(30, 30, 30) - .addComponent(sortButton) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addComponent(sortLabel)) - .addComponent(filePathLabel, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) - .addContainerGap()) - ); - layout.setVerticalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.CENTER) - .addComponent(pageLabel) - .addComponent(pageNumLabel) - .addComponent(pagesLabel) - .addComponent(pagePrevButton, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(pageNextButton, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(goToPageLabel) - .addComponent(goToPageField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(imagesLabel) - .addComponent(imagesRangeLabel) - .addComponent(thumbnailSizeComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(sortButton) - .addComponent(sortLabel)) - .addGap(13, 13, 13) - .addComponent(iconView, javax.swing.GroupLayout.DEFAULT_SIZE, 322, Short.MAX_VALUE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(filePathLabel)) - ); + add(buttonBarPanel, java.awt.BorderLayout.NORTH); + + filePathLabel.setText(org.openide.util.NbBundle.getMessage(DataResultViewerThumbnail.class, "DataResultViewerThumbnail.filePathLabel.text")); // NOI18N + add(filePathLabel, java.awt.BorderLayout.SOUTH); + add(iconView, java.awt.BorderLayout.CENTER); }// //GEN-END:initComponents private void pagePrevButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_pagePrevButtonActionPerformed @@ -355,19 +421,26 @@ public final class DataResultViewerThumbnail extends AbstractDataResultViewer { // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JPanel buttonBarPanel; private javax.swing.JLabel filePathLabel; private javax.swing.JTextField goToPageField; private javax.swing.JLabel goToPageLabel; private org.openide.explorer.view.IconView iconView; + private javax.swing.JPanel imagePane; private javax.swing.JLabel imagesLabel; private javax.swing.JLabel imagesRangeLabel; + private javax.swing.JPanel pageButtonPanel; + private javax.swing.JPanel pageGotoPane; private javax.swing.JLabel pageLabel; private javax.swing.JButton pageNextButton; private javax.swing.JLabel pageNumLabel; + private javax.swing.JPanel pageNumberPane; private javax.swing.JButton pagePrevButton; private javax.swing.JLabel pagesLabel; + private javax.swing.JPanel pagesPanel; private javax.swing.JButton sortButton; private javax.swing.JLabel sortLabel; + private javax.swing.JPanel sortPane; private javax.swing.JComboBox thumbnailSizeComboBox; // End of variables declaration//GEN-END:variables @@ -679,5 +752,5 @@ public final class DataResultViewerThumbnail extends AbstractDataResultViewer { } } } - } + } } diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/MultiUserSettingsPanel.java b/Core/src/org/sleuthkit/autopsy/corecomponents/MultiUserSettingsPanel.java index 97a645da3c..8747d49795 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/MultiUserSettingsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/MultiUserSettingsPanel.java @@ -507,7 +507,9 @@ public final class MultiUserSettingsPanel extends javax.swing.JPanel { private void bnTestDatabaseActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnTestDatabaseActionPerformed lbTestDatabase.setIcon(null); + lbTestDatabase.paintImmediately(lbTestDatabase.getVisibleRect()); lbTestDbWarning.setText(""); + lbTestDbWarning.paintImmediately(lbTestDbWarning.getVisibleRect()); setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); try { CaseDbConnectionInfo info = new CaseDbConnectionInfo( @@ -530,7 +532,9 @@ public final class MultiUserSettingsPanel extends javax.swing.JPanel { private void bnTestMessageServiceActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnTestMessageServiceActionPerformed lbTestMessageService.setIcon(null); + lbTestMessageService.paintImmediately(lbTestMessageService.getVisibleRect()); lbTestMessageWarning.setText(""); + lbTestMessageWarning.paintImmediately(lbTestMessageWarning.getVisibleRect()); setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); int port; @@ -561,7 +565,9 @@ public final class MultiUserSettingsPanel extends javax.swing.JPanel { private void bnTestSolrActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnTestSolrActionPerformed lbTestSolr.setIcon(null); + lbTestSolr.paintImmediately(lbTestSolr.getVisibleRect()); lbTestSolrWarning.setText(""); + lbTestSolrWarning.paintImmediately(lbTestSolrWarning.getVisibleRect()); setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); KeywordSearchService kwsService = Lookup.getDefault().lookup(KeywordSearchService.class); diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/TextPrompt.java b/Core/src/org/sleuthkit/autopsy/corecomponents/TextPrompt.java index 1a34609627..05699059e6 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/TextPrompt.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/TextPrompt.java @@ -35,10 +35,18 @@ public final class TextPrompt extends JLabel private int focusLost; public TextPrompt(String text, JTextComponent component) { - this(text, component, Show.ALWAYS); + this(text, component, Show.ALWAYS, null); } public TextPrompt(String text, JTextComponent component, Show show) { + this(text, component, show, null); + } + + public TextPrompt(String text, JTextComponent component, String layoutConstraint) { + this(text, component, Show.ALWAYS, layoutConstraint); + } + + public TextPrompt(String text, JTextComponent component, Show show, String layoutConstraint) { this.component = component; component.removeAll(); setShow(show); @@ -49,15 +57,19 @@ public final class TextPrompt extends JLabel setForeground(component.getForeground()); setBorder(new EmptyBorder(component.getInsets())); setHorizontalAlignment(JLabel.LEADING); - + component.addFocusListener(this); document.addDocumentListener(this); component.setLayout(new BorderLayout()); - component.add(this); + if (layoutConstraint == null) { + component.add(this); + } else { + component.add(this, layoutConstraint); + } checkForPrompt(); } - + /** * Convenience method to change the alpha value of the current foreground * Color to the specifice value. diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/ViewPreferencesPanel.form b/Core/src/org/sleuthkit/autopsy/corecomponents/ViewPreferencesPanel.form index 1e3c9cbf5e..f604a6257e 100755 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/ViewPreferencesPanel.form +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/ViewPreferencesPanel.form @@ -100,7 +100,7 @@ - + @@ -129,11 +129,11 @@ - + - + @@ -185,11 +185,11 @@ - + - + - + @@ -353,22 +353,22 @@
- + - + - + - + - + @@ -400,10 +400,10 @@
- + - + diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/ViewPreferencesPanel.java b/Core/src/org/sleuthkit/autopsy/corecomponents/ViewPreferencesPanel.java index f23caa3c61..93aadeed65 100755 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/ViewPreferencesPanel.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/ViewPreferencesPanel.java @@ -23,6 +23,7 @@ import java.util.EnumSet; import java.util.Objects; import java.util.TimeZone; import javax.swing.JPanel; +import javax.swing.JSpinner; import org.netbeans.spi.options.OptionsPanelController; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.CasePreferences; @@ -55,6 +56,9 @@ public class ViewPreferencesPanel extends JPanel implements OptionsPanel { groupByDataSourceCheckbox.setEnabled(evt.getNewValue() != null); }); this.timeZoneList.setListData(TimeZoneUtils.createTimeZoneList().stream().toArray(String[]::new)); + + // Disable manual editing of max results spinner + ((JSpinner.DefaultEditor)maxResultsSpinner.getEditor()).getTextField().setEditable(false); } @Override @@ -76,9 +80,7 @@ public class ViewPreferencesPanel extends JPanel implements OptionsPanel { dataSourcesHideSlackCheckbox.setSelected(UserPreferences.hideSlackFilesInDataSourcesTree()); viewsHideSlackCheckbox.setSelected(UserPreferences.hideSlackFilesInViewsTree()); - commentsOccurencesColumnsCheckbox.setEnabled(EamDb.isEnabled()); - commentsOccurencesColumnWrapAroundText.setEnabled(EamDb.isEnabled()); - commentsOccurencesColumnsCheckbox.setSelected(UserPreferences.hideCentralRepoCommentsAndOccurrences()); + scoColumnsCheckbox.setSelected(UserPreferences.getHideSCOColumns()); hideOtherUsersTagsCheckbox.setSelected(UserPreferences.showOnlyCurrentUserTags()); fileNameTranslationColumnCheckbox.setSelected(UserPreferences.displayTranslatedFileNames()); @@ -115,7 +117,7 @@ public class ViewPreferencesPanel extends JPanel implements OptionsPanel { UserPreferences.setHideSlackFilesInDataSourcesTree(dataSourcesHideSlackCheckbox.isSelected()); UserPreferences.setHideSlackFilesInViewsTree(viewsHideSlackCheckbox.isSelected()); UserPreferences.setShowOnlyCurrentUserTags(hideOtherUsersTagsCheckbox.isSelected()); - UserPreferences.setHideCentralRepoCommentsAndOccurrences(commentsOccurencesColumnsCheckbox.isSelected()); + UserPreferences.setHideSCOColumns(scoColumnsCheckbox.isSelected()); UserPreferences.setDisplayTranslatedFileNames(fileNameTranslationColumnCheckbox.isSelected()); UserPreferences.setResultsTablePageSize((int)maxResultsSpinner.getValue()); @@ -164,12 +166,12 @@ public class ViewPreferencesPanel extends JPanel implements OptionsPanel { useAnotherTimeRadioButton = new javax.swing.JRadioButton(); hideOtherUsersTagsCheckbox = new javax.swing.JCheckBox(); hideOtherUsersTagsLabel = new javax.swing.JLabel(); - centralRepoLabel = new javax.swing.JLabel(); - commentsOccurencesColumnsCheckbox = new javax.swing.JCheckBox(); + scoColumnsLabel = new javax.swing.JLabel(); + scoColumnsCheckbox = new javax.swing.JCheckBox(); jScrollPane1 = new javax.swing.JScrollPane(); timeZoneList = new javax.swing.JList<>(); translateTextLabel = new javax.swing.JLabel(); - commentsOccurencesColumnWrapAroundText = new javax.swing.JLabel(); + scoColumnsWrapAroundText = new javax.swing.JLabel(); fileNameTranslationColumnCheckbox = new javax.swing.JCheckBox(); maxResultsLabel = new javax.swing.JLabel(); maxResultsSpinner = new javax.swing.JSpinner(); @@ -262,13 +264,13 @@ public class ViewPreferencesPanel extends JPanel implements OptionsPanel { org.openide.awt.Mnemonics.setLocalizedText(hideOtherUsersTagsLabel, org.openide.util.NbBundle.getMessage(ViewPreferencesPanel.class, "ViewPreferencesPanel.hideOtherUsersTagsLabel.text")); // NOI18N - org.openide.awt.Mnemonics.setLocalizedText(centralRepoLabel, org.openide.util.NbBundle.getMessage(ViewPreferencesPanel.class, "ViewPreferencesPanel.centralRepoLabel.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(scoColumnsLabel, org.openide.util.NbBundle.getMessage(ViewPreferencesPanel.class, "ViewPreferencesPanel.scoColumnsLabel.text")); // NOI18N - org.openide.awt.Mnemonics.setLocalizedText(commentsOccurencesColumnsCheckbox, org.openide.util.NbBundle.getMessage(ViewPreferencesPanel.class, "ViewPreferencesPanel.commentsOccurencesColumnsCheckbox.text")); // NOI18N - commentsOccurencesColumnsCheckbox.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING); - commentsOccurencesColumnsCheckbox.addActionListener(new java.awt.event.ActionListener() { + org.openide.awt.Mnemonics.setLocalizedText(scoColumnsCheckbox, org.openide.util.NbBundle.getMessage(ViewPreferencesPanel.class, "ViewPreferencesPanel.scoColumnsCheckbox.text")); // NOI18N + scoColumnsCheckbox.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING); + scoColumnsCheckbox.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { - commentsOccurencesColumnsCheckboxActionPerformed(evt); + scoColumnsCheckboxActionPerformed(evt); } }); @@ -281,7 +283,7 @@ public class ViewPreferencesPanel extends JPanel implements OptionsPanel { org.openide.awt.Mnemonics.setLocalizedText(translateTextLabel, org.openide.util.NbBundle.getMessage(ViewPreferencesPanel.class, "ViewPreferencesPanel.translateTextLabel.text")); // NOI18N - org.openide.awt.Mnemonics.setLocalizedText(commentsOccurencesColumnWrapAroundText, org.openide.util.NbBundle.getMessage(ViewPreferencesPanel.class, "ViewPreferencesPanel.commentsOccurencesColumnWrapAroundText.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(scoColumnsWrapAroundText, org.openide.util.NbBundle.getMessage(ViewPreferencesPanel.class, "ViewPreferencesPanel.scoColumnsWrapAroundText.text")); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(fileNameTranslationColumnCheckbox, org.openide.util.NbBundle.getMessage(ViewPreferencesPanel.class, "ViewPreferencesPanel.fileNameTranslationColumnCheckbox.text")); // NOI18N fileNameTranslationColumnCheckbox.addActionListener(new java.awt.event.ActionListener() { @@ -311,7 +313,7 @@ public class ViewPreferencesPanel extends JPanel implements OptionsPanel { .addGap(10, 10, 10) .addComponent(hideOtherUsersTagsCheckbox)) .addGroup(globalSettingsPanelLayout.createSequentialGroup() - .addComponent(centralRepoLabel) + .addComponent(scoColumnsLabel) .addGap(135, 135, 135) .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 272, javax.swing.GroupLayout.PREFERRED_SIZE)) .addComponent(hideOtherUsersTagsLabel) @@ -333,10 +335,10 @@ public class ViewPreferencesPanel extends JPanel implements OptionsPanel { .addComponent(viewsHideKnownCheckbox)))) .addGroup(globalSettingsPanelLayout.createSequentialGroup() .addGap(10, 10, 10) - .addComponent(commentsOccurencesColumnsCheckbox)) + .addComponent(scoColumnsCheckbox)) .addGroup(globalSettingsPanelLayout.createSequentialGroup() .addGap(32, 32, 32) - .addComponent(commentsOccurencesColumnWrapAroundText))) + .addComponent(scoColumnsWrapAroundText))) .addGap(18, 18, 18) .addGroup(globalSettingsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(displayTimeLabel) @@ -378,11 +380,11 @@ public class ViewPreferencesPanel extends JPanel implements OptionsPanel { .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(hideOtherUsersTagsCheckbox) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addComponent(centralRepoLabel) + .addComponent(scoColumnsLabel) .addGap(3, 3, 3) - .addComponent(commentsOccurencesColumnsCheckbox, javax.swing.GroupLayout.PREFERRED_SIZE, 18, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(scoColumnsCheckbox, javax.swing.GroupLayout.PREFERRED_SIZE, 18, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(commentsOccurencesColumnWrapAroundText)) + .addComponent(scoColumnsWrapAroundText)) .addGroup(globalSettingsPanelLayout.createSequentialGroup() .addComponent(selectFileLabel) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) @@ -519,13 +521,13 @@ public class ViewPreferencesPanel extends JPanel implements OptionsPanel { } }//GEN-LAST:event_timeZoneListValueChanged - private void commentsOccurencesColumnsCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_commentsOccurencesColumnsCheckboxActionPerformed + private void scoColumnsCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_scoColumnsCheckboxActionPerformed if (immediateUpdates) { - UserPreferences.setHideCentralRepoCommentsAndOccurrences(commentsOccurencesColumnsCheckbox.isSelected()); + UserPreferences.setHideSCOColumns(scoColumnsCheckbox.isSelected()); } else { firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null); } - }//GEN-LAST:event_commentsOccurencesColumnsCheckboxActionPerformed + }//GEN-LAST:event_scoColumnsCheckboxActionPerformed private void hideOtherUsersTagsCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_hideOtherUsersTagsCheckboxActionPerformed if (immediateUpdates) { @@ -627,9 +629,6 @@ public class ViewPreferencesPanel extends JPanel implements OptionsPanel { // Variables declaration - do not modify//GEN-BEGIN:variables - private javax.swing.JLabel centralRepoLabel; - private javax.swing.JLabel commentsOccurencesColumnWrapAroundText; - private javax.swing.JCheckBox commentsOccurencesColumnsCheckbox; private javax.swing.JPanel currentCaseSettingsPanel; private javax.swing.JPanel currentSessionSettingsPanel; private javax.swing.JCheckBox dataSourcesHideKnownCheckbox; @@ -647,6 +646,9 @@ public class ViewPreferencesPanel extends JPanel implements OptionsPanel { private javax.swing.JRadioButton keepCurrentViewerRadioButton; private javax.swing.JLabel maxResultsLabel; private javax.swing.JSpinner maxResultsSpinner; + private javax.swing.JCheckBox scoColumnsCheckbox; + private javax.swing.JLabel scoColumnsLabel; + private javax.swing.JLabel scoColumnsWrapAroundText; private javax.swing.JLabel selectFileLabel; private javax.swing.JList timeZoneList; private javax.swing.JLabel translateTextLabel; diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/WrapLayout.java b/Core/src/org/sleuthkit/autopsy/corecomponents/WrapLayout.java new file mode 100644 index 0000000000..be35ea0581 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/WrapLayout.java @@ -0,0 +1,203 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.corecomponents; + +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.Insets; +import javax.swing.JScrollPane; +import javax.swing.SwingUtilities; + +/** +* FlowLayout subclass that fully supports wrapping of components. +* +* Originally written by Rob Camick +* https://tips4java.wordpress.com/2008/11/06/wrap-layout/ +*/ +class WrapLayout extends FlowLayout { + + /** + * Constructs a new WrapLayout with a left alignment and a + * default 5-unit horizontal and vertical gap. + */ + public WrapLayout() { + super(); + } + + /** + * Constructs a new FlowLayout with the specified alignment + * and a default 5-unit horizontal and vertical gap. The value of the + * alignment argument must be one of WrapLayout, + * WrapLayout, or WrapLayout. + * + * @param align the alignment value + */ + public WrapLayout(int align) { + super(align); + } + + /** + * Creates a new flow layout manager with the indicated alignment and + * the indicated horizontal and vertical gaps. + *

+ * The value of the alignment argument must be one of + * WrapLayout, WrapLayout, or + * WrapLayout. + * + * @param align the alignment value + * @param hgap the horizontal gap between components + * @param vgap the vertical gap between components + */ + public WrapLayout(int align, int hgap, int vgap) { + super(align, hgap, vgap); + } + + /** + * Returns the preferred dimensions for this layout given the + * visible components in the specified target container. + * + * @param target the component which needs to be laid out + * + * @return the preferred dimensions to lay out the subcomponents of the + * specified container + */ + @Override + public Dimension preferredLayoutSize(Container target) { + return layoutSize(target, true); + } + + /** + * Returns the minimum dimensions needed to layout the visible + * components contained in the specified target container. + * + * @param target the component which needs to be laid out + * + * @return the minimum dimensions to lay out the subcomponents of the + * specified container + */ + @Override + public Dimension minimumLayoutSize(Container target) { + Dimension minimum = layoutSize(target, false); + minimum.width -= (getHgap() + 1); + return minimum; + } + + /** + * Returns the minimum or preferred dimension needed to layout the + * target container. + * + * @param target target to get layout size for + * @param preferred should preferred size be calculated + * + * @return the dimension to layout the target container + */ + private Dimension layoutSize(Container target, boolean preferred) { + synchronized (target.getTreeLock()) { + // Each row must fit with the width allocated to the containter. + // When the container width = 0, the preferred width of the container + // has not yet been calculated so lets ask for the maximum. + + int targetWidth = target.getSize().width; + Container container = target; + + while (container.getSize().width == 0 && container.getParent() != null) { + container = container.getParent(); + } + + targetWidth = container.getSize().width; + + if (targetWidth == 0) { + targetWidth = Integer.MAX_VALUE; + } + + int hgap = getHgap(); + int vgap = getVgap(); + Insets insets = target.getInsets(); + int horizontalInsetsAndGap = insets.left + insets.right + (hgap * 2); + int maxWidth = targetWidth - horizontalInsetsAndGap; + + // Fit components into the allowed width + Dimension dim = new Dimension(0, 0); + int rowWidth = 0; + int rowHeight = 0; + + int nmembers = target.getComponentCount(); + + for (int i = 0; i < nmembers; i++) { + Component m = target.getComponent(i); + + if (m.isVisible()) { + Dimension d = preferred ? m.getPreferredSize() : m.getMinimumSize(); + + // Can't add the component to current row. Start a new row. + if (rowWidth + d.width > maxWidth) { + addRow(dim, rowWidth, rowHeight); + rowWidth = 0; + rowHeight = 0; + } + + // Add a horizontal gap for all components after the first + if (rowWidth != 0) { + rowWidth += hgap; + } + + rowWidth += d.width; + rowHeight = Math.max(rowHeight, d.height); + } + } + + addRow(dim, rowWidth, rowHeight); + + dim.width += horizontalInsetsAndGap; + dim.height += insets.top + insets.bottom + vgap * 2; + + // When using a scroll pane or the DecoratedLookAndFeel we need to + // make sure the preferred size is less than the size of the + // target containter so shrinking the container size works + // correctly. Removing the horizontal gap is an easy way to do this. + Container scrollPane = SwingUtilities.getAncestorOfClass(JScrollPane.class, target); + + if (scrollPane != null && target.isValid()) { + dim.width -= (hgap + 1); + } + + return dim; + } + } + + /* + * A new row has been completed. Use the dimensions of this row to + * update the preferred size for the container. + * + * @param dim update the width and height when appropriate @param + * rowWidth the width of the row to add @param rowHeight the height of + * the row to add + */ + private void addRow(Dimension dim, int rowWidth, int rowHeight) { + dim.width = Math.max(dim.width, rowWidth); + + if (dim.height > 0) { + dim.height += getVgap(); + } + + dim.height += rowHeight; + } + } \ No newline at end of file diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java b/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java index 97922760c5..c2ed64987e 100644 --- a/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java +++ b/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java @@ -22,7 +22,6 @@ package org.sleuthkit.autopsy.coreutils; import com.google.common.collect.ImmutableSortedSet; -import com.google.common.io.Files; import java.awt.Image; import java.awt.image.BufferedImage; import java.io.BufferedInputStream; @@ -122,7 +121,7 @@ public class ImageUtils { } DEFAULT_THUMBNAIL = tempImage; boolean tempFfmpegLoaded = false; - if (OpenCvLoader.isOpenCvLoaded()) { + if (OpenCvLoader.hasOpenCvLoaded()) { try { if (System.getProperty("os.arch").equals("amd64") || System.getProperty("os.arch").equals("x86_64")) { //NON-NLS System.loadLibrary("opencv_ffmpeg248_64"); //NON-NLS diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/StringExtract.java b/Core/src/org/sleuthkit/autopsy/coreutils/StringExtract.java index aded120ac7..0f89eb2291 100644 --- a/Core/src/org/sleuthkit/autopsy/coreutils/StringExtract.java +++ b/Core/src/org/sleuthkit/autopsy/coreutils/StringExtract.java @@ -221,7 +221,8 @@ public class StringExtract { StringExtractResult resWin = null; if (enableUTF8 && resUTF16 != null) { resWin = runUTF16 && resUTF16.numChars > resUTF8.numChars ? resUTF16 : resUTF8; - } else if (enableUTF16) { + } else if (runUTF16) { + //Only let resUTF16 "win" if it was actually run. resWin = resUTF16; } else if (enableUTF8) { resWin = resUTF8; diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java index fa4529e44b..eeacf491bf 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.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"); @@ -18,7 +18,6 @@ */ package org.sleuthkit.autopsy.datamodel; -import com.google.common.util.concurrent.ThreadFactoryBuilder; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.lang.ref.WeakReference; @@ -27,8 +26,6 @@ import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.logging.Level; import java.util.stream.Collectors; import org.apache.commons.io.FilenameUtils; @@ -65,6 +62,7 @@ import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.ContentTag; +import org.sleuthkit.datamodel.Tag; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData; @@ -76,15 +74,10 @@ import org.sleuthkit.datamodel.TskData; public abstract class AbstractAbstractFileNode extends AbstractContentNode { private static final Logger logger = Logger.getLogger(AbstractAbstractFileNode.class.getName()); - @NbBundle.Messages("AbstractAbstractFileNode.addFileProperty.desc=no description") - private static final String NO_DESCR = AbstractAbstractFileNode_addFileProperty_desc(); private static final Set CASE_EVENTS_OF_INTEREST = EnumSet.of(Case.Events.CURRENT_CASE, Case.Events.CONTENT_TAG_ADDED, Case.Events.CONTENT_TAG_DELETED, Case.Events.CR_COMMENT_CHANGED); - private static final ExecutorService translationPool; - private static final Integer MAX_POOL_SIZE = 10; - /** * @param abstractFile file to wrap */ @@ -101,7 +94,7 @@ public abstract class AbstractAbstractFileNode extends A } if (UserPreferences.displayTranslatedFileNames()) { - AbstractAbstractFileNode.translationPool.submit(new TranslationTask( + backgroundTasksPool.submit(new TranslationTask( new WeakReference<>(this), weakPcl)); } @@ -110,13 +103,6 @@ public abstract class AbstractAbstractFileNode extends A Case.addEventTypeSubscriber(CASE_EVENTS_OF_INTEREST, weakPcl); } - static { - //Initialize this pool only once! This will be used by every instance of AAFN - //to do their heavy duty SCO column and translation updates. - translationPool = Executors.newFixedThreadPool(MAX_POOL_SIZE, - new ThreadFactoryBuilder().setNameFormat("translation-task-thread-%d").build()); - } - /** * The finalizer removes event listeners as the BlackboardArtifactNode is * being garbage collected. Yes, we know that finalizers are considered to @@ -137,16 +123,6 @@ public abstract class AbstractAbstractFileNode extends A Case.removeEventTypeSubscriber(CASE_EVENTS_OF_INTEREST, weakPcl); } - /** - * Event signals to indicate the background tasks have completed processing. - * Currently, we have one property task in the background: - * - * 1) Retreiving the translation of the file name - */ - enum NodeSpecificEvents { - TRANSLATION_AVAILABLE, - } - private final PropertyChangeListener pcl = (PropertyChangeEvent evt) -> { String eventType = evt.getPropertyName(); @@ -190,7 +166,7 @@ public abstract class AbstractAbstractFileNode extends A } else if (eventType.equals(Case.Events.CONTENT_TAG_ADDED.toString())) { ContentTagAddedEvent event = (ContentTagAddedEvent) evt; if (event.getAddedTag().getContent().equals(content)) { - List tags = getContentTagsFromDatabase(); + List tags = this.getAllTagsFromDatabase(); Pair scorePropAndDescr = getScorePropertyAndDescription(tags); Score value = scorePropAndDescr.getLeft(); String descr = scorePropAndDescr.getRight(); @@ -202,7 +178,7 @@ public abstract class AbstractAbstractFileNode extends A } else if (eventType.equals(Case.Events.CONTENT_TAG_DELETED.toString())) { ContentTagDeletedEvent event = (ContentTagDeletedEvent) evt; if (event.getDeletedTagInfo().getContentID() == content.getId()) { - List tags = getContentTagsFromDatabase(); + List tags = getAllTagsFromDatabase(); Pair scorePropAndDescr = getScorePropertyAndDescription(tags); Score value = scorePropAndDescr.getLeft(); String descr = scorePropAndDescr.getRight(); @@ -214,7 +190,7 @@ public abstract class AbstractAbstractFileNode extends A } else if (eventType.equals(Case.Events.CR_COMMENT_CHANGED.toString())) { CommentChangedEvent event = (CommentChangedEvent) evt; if (event.getContentID() == content.getId()) { - List tags = getContentTagsFromDatabase(); + List tags = getAllTagsFromDatabase(); CorrelationAttributeInstance attribute = getCorrelationAttributeInstance(); updateSheet(new NodeProperty<>(COMMENT.toString(), COMMENT.toString(), NO_DESCR, getCommentProperty(tags, attribute))); } @@ -223,6 +199,17 @@ public abstract class AbstractAbstractFileNode extends A //Set the tooltip this.setShortDescription(content.getName()); updateSheet(new NodeProperty<>(ORIGINAL_NAME.toString(), ORIGINAL_NAME.toString(), NO_DESCR, content.getName())); + } else if (eventType.equals(NodeSpecificEvents.SCO_AVAILABLE.toString()) && !UserPreferences.getHideSCOColumns()) { + SCOData scoData = (SCOData) evt.getNewValue(); + if (scoData.getScoreAndDescription() != null) { + updateSheet(new NodeProperty<>(SCORE.toString(), SCORE.toString(), scoData.getScoreAndDescription().getRight(), scoData.getScoreAndDescription().getLeft())); + } + if (scoData.getComment() != null) { + updateSheet(new NodeProperty<>(COMMENT.toString(), COMMENT.toString(), NO_DESCR, scoData.getComment())); + } + if (scoData.getCountAndDescription() != null) { + updateSheet(new NodeProperty<>(OCCURRENCES.toString(), OCCURRENCES.toString(), scoData.getCountAndDescription().getRight(), scoData.getCountAndDescription().getLeft())); + } } }; /** @@ -235,38 +222,6 @@ public abstract class AbstractAbstractFileNode extends A */ private final PropertyChangeListener weakPcl = WeakListeners.propertyChange(pcl, null); - /** - * Updates the values of the properties in the current property sheet with - * the new properties being passed in. Only if that property exists in the - * current sheet will it be applied. That way, we allow for subclasses to - * add their own (or omit some!) properties and we will not accidentally - * disrupt their UI. - * - * Race condition if not synchronized. Only one update should be applied at - * a time. - * - * @param newProps New file property instances to be updated in the current - * sheet. - */ - private synchronized void updateSheet(NodeProperty... newProps) { - //Refresh ONLY those properties in the sheet currently. Subclasses may have - //only added a subset of our properties or their own props.s - Sheet visibleSheet = this.getSheet(); - Sheet.Set visibleSheetSet = visibleSheet.get(Sheet.PROPERTIES); - Property[] visibleProps = visibleSheetSet.getProperties(); - for (NodeProperty newProp : newProps) { - for (int i = 0; i < visibleProps.length; i++) { - if (visibleProps[i].getName().equals(newProp.getName())) { - visibleProps[i] = newProp; - } - } - } - visibleSheetSet.put(visibleProps); - visibleSheet.put(visibleSheetSet); - //setSheet() will notify Netbeans to update this node in the UI. - this.setSheet(visibleSheet); - } - /* * This is called when the node is first initialized. Any new updates or * changes happen by directly manipulating the sheet. That means we can fire @@ -368,18 +323,19 @@ public abstract class AbstractAbstractFileNode extends A properties.add(new NodeProperty<>(ORIGINAL_NAME.toString(), ORIGINAL_NAME.toString(), NO_DESCR, "")); } - //SCO column prereq info.. - List tags = getContentTagsFromDatabase(); - CorrelationAttributeInstance attribute = getCorrelationAttributeInstance(); - - Pair scoreAndDescription = getScorePropertyAndDescription(tags); - properties.add(new NodeProperty<>(SCORE.toString(), SCORE.toString(), scoreAndDescription.getRight(), scoreAndDescription.getLeft())); - DataResultViewerTable.HasCommentStatus comment = getCommentProperty(tags, attribute); - properties.add(new NodeProperty<>(COMMENT.toString(), COMMENT.toString(), NO_DESCR, comment)); - if (!UserPreferences.hideCentralRepoCommentsAndOccurrences()) { - Pair countAndDescription = getCountPropertyAndDescription(attribute); - properties.add(new NodeProperty<>(OCCURRENCES.toString(), OCCURRENCES.toString(), countAndDescription.getRight(), countAndDescription.getLeft())); + // Create place holders for S C O + if (!UserPreferences.getHideSCOColumns()) { + properties.add(new NodeProperty<>(SCORE.toString(), SCORE.toString(), VALUE_LOADING, "")); + properties.add(new NodeProperty<>(COMMENT.toString(), COMMENT.toString(), VALUE_LOADING, "")); + if (EamDb.isEnabled()) { + properties.add(new NodeProperty<>(OCCURRENCES.toString(), OCCURRENCES.toString(), VALUE_LOADING, "")); + } } + + // Get the SCO columns data in a background task + backgroundTasksPool.submit(new GetSCOTask( + new WeakReference<>(this), weakPcl)); + properties.add(new NodeProperty<>(LOCATION.toString(), LOCATION.toString(), NO_DESCR, getContentPath(content))); properties.add(new NodeProperty<>(MOD_TIME.toString(), MOD_TIME.toString(), NO_DESCR, ContentUtils.getStringTime(content.getMtime(), content))); properties.add(new NodeProperty<>(CHANGED_TIME.toString(), CHANGED_TIME.toString(), NO_DESCR, ContentUtils.getStringTime(content.getCtime(), content))); @@ -437,19 +393,20 @@ public abstract class AbstractAbstractFileNode extends A @NbBundle.Messages({ "AbstractAbstractFileNode.createSheet.count.displayName=O", - "AbstractAbstractFileNode.createSheet.count.noCentralRepo.description=Central repository was not enabled when this column was populated", "AbstractAbstractFileNode.createSheet.count.hashLookupNotRun.description=Hash lookup had not been run on this file when the column was populated", - "# {0} - occuranceCount", - "AbstractAbstractFileNode.createSheet.count.description=There were {0} datasource(s) found with occurances of the correlation value"}) - Pair getCountPropertyAndDescription(CorrelationAttributeInstance attribute) { + "# {0} - occurrenceCount", + "AbstractAbstractFileNode.createSheet.count.description=There were {0} datasource(s) found with occurrences of the MD5 correlation value"}) + @Override + protected Pair getCountPropertyAndDescription(CorrelationAttributeInstance.Type attributeType, String attributeValue, + String defaultDescription) { Long count = -1L; //The column renderer will not display negative values, negative value used when count unavailble to preserve sorting - String description = Bundle.AbstractAbstractFileNode_createSheet_count_noCentralRepo_description(); + String description = defaultDescription; try { //don't perform the query if there is no correlation value - if (attribute != null && StringUtils.isNotBlank(attribute.getCorrelationValue())) { - count = EamDb.getInstance().getCountUniqueCaseDataSourceTuplesHavingTypeValue(attribute.getCorrelationType(), attribute.getCorrelationValue()); + if (attributeType != null && StringUtils.isNotBlank(attributeValue)) { + count = EamDb.getInstance().getCountUniqueCaseDataSourceTuplesHavingTypeValue(attributeType, attributeValue); description = Bundle.AbstractAbstractFileNode_createSheet_count_description(count); - } else if (attribute != null) { + } else if (attributeType != null) { description = Bundle.AbstractAbstractFileNode_createSheet_count_hashLookupNotRun_description(); } } catch (EamDbException ex) { @@ -457,7 +414,6 @@ public abstract class AbstractAbstractFileNode extends A } catch (CorrelationAttributeNormalizationException ex) { logger.log(Level.WARNING, "Unable to normalize data to get count of datasources with correlation attribute", ex); } - return Pair.of(count, description); } @@ -468,7 +424,8 @@ public abstract class AbstractAbstractFileNode extends A "AbstractAbstractFileNode.createSheet.taggedFile.description=File has been tagged.", "AbstractAbstractFileNode.createSheet.notableTaggedFile.description=File tagged with notable tag.", "AbstractAbstractFileNode.createSheet.noScore.description=No score"}) - Pair getScorePropertyAndDescription(List tags) { + @Override + protected Pair getScorePropertyAndDescription(List tags) { DataResultViewerTable.Score score = DataResultViewerTable.Score.NO_SCORE; String description = Bundle.AbstractAbstractFileNode_createSheet_noScore_description(); if (content.getKnown() == TskData.FileKnown.BAD) { @@ -486,7 +443,7 @@ public abstract class AbstractAbstractFileNode extends A if (!tags.isEmpty() && (score == DataResultViewerTable.Score.NO_SCORE || score == DataResultViewerTable.Score.INTERESTING_SCORE)) { score = DataResultViewerTable.Score.INTERESTING_SCORE; description = Bundle.AbstractAbstractFileNode_createSheet_taggedFile_description(); - for (ContentTag tag : tags) { + for (Tag tag : tags) { if (tag.getName().getKnownStatus() == TskData.FileKnown.BAD) { score = DataResultViewerTable.Score.NOTABLE_SCORE; description = Bundle.AbstractAbstractFileNode_createSheet_notableTaggedFile_description(); @@ -499,11 +456,12 @@ public abstract class AbstractAbstractFileNode extends A @NbBundle.Messages({ "AbstractAbstractFileNode.createSheet.comment.displayName=C"}) - HasCommentStatus getCommentProperty(List tags, CorrelationAttributeInstance attribute) { + @Override + protected HasCommentStatus getCommentProperty(List tags, CorrelationAttributeInstance attribute) { DataResultViewerTable.HasCommentStatus status = !tags.isEmpty() ? DataResultViewerTable.HasCommentStatus.TAG_NO_COMMENT : DataResultViewerTable.HasCommentStatus.NO_COMMENT; - for (ContentTag tag : tags) { + for (Tag tag : tags) { if (!StringUtils.isBlank(tag.getComment())) { //if the tag is null or empty or contains just white space it will indicate there is not a comment status = DataResultViewerTable.HasCommentStatus.TAG_COMMENT; @@ -571,9 +529,15 @@ public abstract class AbstractAbstractFileNode extends A return tags; } - CorrelationAttributeInstance getCorrelationAttributeInstance() { + @Override + protected List getAllTagsFromDatabase() { + return new ArrayList<>(getContentTagsFromDatabase()); + } + + @Override + protected CorrelationAttributeInstance getCorrelationAttributeInstance() { CorrelationAttributeInstance attribute = null; - if (EamDb.isEnabled() && !UserPreferences.hideCentralRepoCommentsAndOccurrences()) { + if (EamDb.isEnabled() && !UserPreferences.getHideSCOColumns()) { attribute = EamArtifactUtil.getInstanceFromContent(content); } return attribute; diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractContentNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractContentNode.java index 4f6f2e5f47..8b1d22d349 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractContentNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractContentNode.java @@ -18,20 +18,30 @@ */ package org.sleuthkit.autopsy.datamodel; +import com.google.common.util.concurrent.ThreadFactoryBuilder; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.logging.Level; +import org.apache.commons.lang3.tuple.Pair; import org.openide.nodes.Children; +import org.openide.nodes.Sheet; import org.openide.util.lookup.Lookups; import org.openide.util.Lookup; +import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance.Type; +import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.Tag; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData; import org.sleuthkit.datamodel.TskException; @@ -51,27 +61,60 @@ public abstract class AbstractContentNode extends ContentNode private static final Logger logger = Logger.getLogger(AbstractContentNode.class.getName()); /** - * Handles aspects that depend on the Content object - * - * @param content Underlying Content instances + * A pool of background tasks to run any long computation needed to populate + * this node. */ - AbstractContentNode(T content) { - this(content, Lookups.singleton(content) ); + static final ExecutorService backgroundTasksPool; + private static final Integer MAX_POOL_SIZE = 10; + + /** + * Default no description string + */ + @NbBundle.Messages({"AbstractContentNode.nodescription=no description", + "AbstractContentNode.valueLoading=value loading"}) + protected static final String NO_DESCR = Bundle.AbstractContentNode_nodescription(); + protected static final String VALUE_LOADING = Bundle.AbstractContentNode_valueLoading(); + + /** + * Event signals to indicate the background tasks have completed processing. + * Currently, we have one property task in the background: + * + * 1) Retrieving the translation of the file name + */ + enum NodeSpecificEvents { + TRANSLATION_AVAILABLE, + SCO_AVAILABLE + } + + static { + //Initialize this pool only once! This will be used by every instance of AAFN + //to do their heavy duty SCO column and translation updates. + backgroundTasksPool = Executors.newFixedThreadPool(MAX_POOL_SIZE, + new ThreadFactoryBuilder().setNameFormat("content-node-background-task-%d").build()); } /** * Handles aspects that depend on the Content object * * @param content Underlying Content instances - * @param lookup The Lookup object for the node. + */ + AbstractContentNode(T content) { + this(content, Lookups.singleton(content)); + } + + /** + * Handles aspects that depend on the Content object + * + * @param content Underlying Content instances + * @param lookup The Lookup object for the node. */ AbstractContentNode(T content, Lookup lookup) { - super(Children.create(new ContentChildren(content), true), lookup); + super(Children.create(new ContentChildren(content), false), lookup); this.content = content; //super.setName(ContentUtils.getSystemName(content)); super.setName("content_" + Long.toString(content.getId())); //NON-NLS } - + /** * Return the content data associated with this node * @@ -100,40 +143,40 @@ public abstract class AbstractContentNode extends ContentNode public boolean hasVisibleContentChildren() { return contentHasVisibleContentChildren(content); } - + /** * Return true if the given content object has children. Useful for lazy * loading. - * + * * @param c The content object to look for children on + * * @return true if has children */ - public static boolean contentHasVisibleContentChildren(Content c){ + public static boolean contentHasVisibleContentChildren(Content c) { if (c != null) { - + try { - if( ! c.hasChildren()) { + if (!c.hasChildren()) { return false; } } catch (TskCoreException ex) { - + logger.log(Level.SEVERE, "Error checking if the node has children, for content: " + c, ex); //NON-NLS return false; } - + String query = "SELECT COUNT(obj_id) AS count FROM " - + " ( SELECT obj_id FROM tsk_objects WHERE par_obj_id = " + c.getId() + " AND type = " - + TskData.ObjectType.ARTIFACT.getObjectType() - + " INTERSECT SELECT artifact_obj_id FROM blackboard_artifacts WHERE obj_id = " + c.getId() - + " AND (artifact_type_id = " + ARTIFACT_TYPE.TSK_EMAIL_MSG.getTypeID() - + " OR artifact_type_id = " + ARTIFACT_TYPE.TSK_MESSAGE.getTypeID() + ") " - + " UNION SELECT obj_id FROM tsk_objects WHERE par_obj_id = " + c.getId() - + " AND type = " + TskData.ObjectType.ABSTRACTFILE.getObjectType() + ") AS OBJECT_IDS"; //NON-NLS; - - + + " ( SELECT obj_id FROM tsk_objects WHERE par_obj_id = " + c.getId() + " AND type = " + + TskData.ObjectType.ARTIFACT.getObjectType() + + " INTERSECT SELECT artifact_obj_id FROM blackboard_artifacts WHERE obj_id = " + c.getId() + + " AND (artifact_type_id = " + ARTIFACT_TYPE.TSK_EMAIL_MSG.getTypeID() + + " OR artifact_type_id = " + ARTIFACT_TYPE.TSK_MESSAGE.getTypeID() + ") " + + " UNION SELECT obj_id FROM tsk_objects WHERE par_obj_id = " + c.getId() + + " AND type = " + TskData.ObjectType.ABSTRACTFILE.getObjectType() + ") AS OBJECT_IDS"; //NON-NLS; + try (SleuthkitCase.CaseDbQuery dbQuery = Case.getCurrentCaseThrows().getSleuthkitCase().executeQuery(query)) { ResultSet resultSet = dbQuery.getResultSet(); - if(resultSet.next()){ + if (resultSet.next()) { return (0 < resultSet.getInt("count")); } } catch (TskCoreException | SQLException | NoCurrentCaseException ex) { @@ -142,7 +185,7 @@ public abstract class AbstractContentNode extends ContentNode } return false; } - + /** * Return true if the underlying content object has children Useful for lazy * loading. @@ -162,7 +205,7 @@ public abstract class AbstractContentNode extends ContentNode return hasChildren; } - + /** * Return ids of children of the underlying content. The ids can be treated * as keys - useful for lazy loading. @@ -240,4 +283,83 @@ public abstract class AbstractContentNode extends ContentNode public int read(byte[] buf, long offset, long len) throws TskException { return content.read(buf, offset, len); } + + /** + * Updates the values of the properties in the current property sheet with + * the new properties being passed in. Only if that property exists in the + * current sheet will it be applied. That way, we allow for subclasses to + * add their own (or omit some!) properties and we will not accidentally + * disrupt their UI. + * + * Race condition if not synchronized. Only one update should be applied at + * a time. + * + * @param newProps New file property instances to be updated in the current + * sheet. + */ + protected synchronized void updateSheet(NodeProperty... newProps) { + //Refresh ONLY those properties in the sheet currently. Subclasses may have + //only added a subset of our properties or their own props.s + Sheet visibleSheet = this.getSheet(); + Sheet.Set visibleSheetSet = visibleSheet.get(Sheet.PROPERTIES); + Property[] visibleProps = visibleSheetSet.getProperties(); + for (NodeProperty newProp : newProps) { + for (int i = 0; i < visibleProps.length; i++) { + if (visibleProps[i].getName().equals(newProp.getName())) { + visibleProps[i] = newProp; + } + } + } + visibleSheetSet.put(visibleProps); + visibleSheet.put(visibleSheetSet); + //setSheet() will notify Netbeans to update this node in the UI. + this.setSheet(visibleSheet); + } + + /** + * Reads and returns a list of all tags associated with this content node. + * + * @return list of tags associated with the node. + */ + abstract protected List getAllTagsFromDatabase(); + + /** + * Returns correlation attribute instance for the underlying content of the + * node. + * + * @return correlation attribute instance for the underlying content of the + * node. + */ + abstract protected CorrelationAttributeInstance getCorrelationAttributeInstance(); + + /** + * Returns Score property for the node. + * + * @param tags list of tags. + * + * @return Score property for the underlying content of the node. + */ + abstract protected Pair getScorePropertyAndDescription(List tags); + + /** + * Returns comment property for the node. + * + * @param tags list of tags + * @param attribute correlation attribute instance + * + * @return Comment property for the underlying content of the node. + */ + abstract protected DataResultViewerTable.HasCommentStatus getCommentProperty(List tags, CorrelationAttributeInstance attribute); + + /** + * Returns occurrences/count property for the node. + * + * @param attributeType the type of the attribute to count + * @param attributeValue the value of the attribute to count + * @param defaultDescription a description to use when none is determined by + * the getCountPropertyAndDescription method + * + * @return count property for the underlying content of the node. + */ + abstract protected Pair getCountPropertyAndDescription(Type attributeType, String attributeValue, String defaultDescription); } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/BaseChildFactory.java b/Core/src/org/sleuthkit/autopsy/datamodel/BaseChildFactory.java index 4a1147494a..9727d83e39 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/BaseChildFactory.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/BaseChildFactory.java @@ -286,7 +286,12 @@ public abstract class BaseChildFactory extends ChildFactory.D * If pageSize is set split keys into pages, otherwise create a * single page containing all keys. */ - pages = Lists.partition(keys, pageSize > 0 ? pageSize : keys.size()); + if (keys.isEmpty()) { + pages.clear(); + } else { + pages = Lists.partition(keys, pageSize > 0 ? pageSize : keys.size()); + } + if (pages.size() != oldPageCount) { try { // Number of pages has changed so we need to send out a notification. diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java index 55e81acd17..0ea8b04660 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.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"); @@ -22,6 +22,7 @@ import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; +import java.lang.ref.WeakReference; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; @@ -37,6 +38,7 @@ import java.util.logging.Level; import java.util.stream.Collectors; import javax.swing.Action; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; import org.openide.nodes.Sheet; import org.openide.util.Lookup; import org.openide.util.NbBundle; @@ -50,16 +52,19 @@ import org.sleuthkit.autopsy.casemodule.events.CommentChangedEvent; import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance.Type; import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeNormalizationException; import org.sleuthkit.autopsy.centralrepository.datamodel.EamArtifactUtil; import org.sleuthkit.autopsy.centralrepository.datamodel.EamDb; import org.sleuthkit.autopsy.centralrepository.datamodel.EamDbException; import org.sleuthkit.autopsy.core.UserPreferences; +import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable; import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable.Score; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import static org.sleuthkit.autopsy.datamodel.DisplayableItemNode.findLinked; import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable.HasCommentStatus; +import static org.sleuthkit.autopsy.datamodel.AbstractContentNode.backgroundTasksPool; import org.sleuthkit.autopsy.modules.hashdatabase.HashDbManager; import org.sleuthkit.autopsy.timeline.actions.ViewArtifactInTimelineAction; import org.sleuthkit.autopsy.timeline.actions.ViewFileInTimelineAction; @@ -96,9 +101,6 @@ public class BlackboardArtifactNode extends AbstractContentNode> customProperties; - private final static String NO_DESCR = NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.noDesc.text"); - - /* * Artifact types which should have the full unique path of the associated * content as a property. @@ -150,6 +152,17 @@ public class BlackboardArtifactNode extends AbstractContentNode(Bundle.BlackboardArtifactNode_createSheet_score_name(), Bundle.BlackboardArtifactNode_createSheet_score_displayName(), scoData.getScoreAndDescription().getRight(), scoData.getScoreAndDescription().getLeft())); + } + if (scoData.getComment() != null) { + updateSheet(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_comment_name(), Bundle.BlackboardArtifactNode_createSheet_comment_displayName(), NO_DESCR, scoData.getComment())); + } + if (scoData.getCountAndDescription() != null) { + updateSheet(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_count_name(), Bundle.BlackboardArtifactNode_createSheet_count_displayName(), scoData.getCountAndDescription().getRight(), scoData.getCountAndDescription().getLeft())); + } } } }; @@ -319,7 +332,7 @@ public class BlackboardArtifactNode extends AbstractContentNode tags = getAllTagsFromDatabase(); - Sheet.Set sheetSet = sheet.get(Sheet.PROPERTIES); if (sheetSet == null) { sheetSet = Sheet.createPropertiesSet(); @@ -351,17 +362,19 @@ public class BlackboardArtifactNode extends AbstractContentNode(Bundle.BlackboardArtifactNode_createSheet_score_name(), Bundle.BlackboardArtifactNode_createSheet_score_displayName(), VALUE_LOADING, "")); + sheetSet.put(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_comment_name(), Bundle.BlackboardArtifactNode_createSheet_comment_displayName(), VALUE_LOADING, "")); + if (EamDb.isEnabled()) { + sheetSet.put(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_count_name(), Bundle.BlackboardArtifactNode_createSheet_count_displayName(), VALUE_LOADING, "")); + } } - addCommentProperty(sheetSet, tags, correlationAttribute); - if (UserPreferences.hideCentralRepoCommentsAndOccurrences() == false) { - addCountProperty(sheetSet, correlationAttribute); - } + // Get the SCO columns data in a background task + backgroundTasksPool.submit(new GetSCOTask( + new WeakReference<>(this), weakPcl)); + if (artifact.getArtifactTypeID() == ARTIFACT_TYPE.TSK_INTERESTING_ARTIFACT_HIT.getTypeID()) { try { BlackboardAttribute attribute = artifact.getAttribute(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT)); @@ -520,6 +533,7 @@ public class BlackboardArtifactNode extends AbstractContentNode getAllTagsFromDatabase() { List tags = new ArrayList<>(); try { @@ -569,6 +583,13 @@ public class BlackboardArtifactNode extends AbstractContentNode t.getName().getDisplayName()).collect(Collectors.joining(", ")))); } + /** + * Gets the correlation attribute for the associated file + * + * @return the correlation attribute for the file associated with this + * BlackboardArtifactNode + */ + @Override protected final CorrelationAttributeInstance getCorrelationAttributeInstance() { CorrelationAttributeInstance correlationAttribute = null; if (EamDb.isEnabled()) { @@ -581,16 +602,37 @@ public class BlackboardArtifactNode extends AbstractContentNode tags, CorrelationAttributeInstance attribute) { + HasCommentStatus status = getCommentProperty(tags, attribute); + sheetSet.put(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_comment_name(), Bundle.BlackboardArtifactNode_createSheet_comment_displayName(), NO_DESCR, + status)); + } + + /** + * Gets the comment property for the node + * + * @param tags the list of tags associated with the file + * @param attribute the correlation attribute associated with this + * artifact's associated file, null if central repo is not + * enabled + * + * @return comment property + */ + @Override + protected DataResultViewerTable.HasCommentStatus getCommentProperty(List tags, CorrelationAttributeInstance attribute) { + HasCommentStatus status = tags.size() > 0 ? HasCommentStatus.TAG_NO_COMMENT : HasCommentStatus.NO_COMMENT; for (Tag tag : tags) { if (!StringUtils.isBlank(tag.getComment())) { @@ -609,17 +651,18 @@ public class BlackboardArtifactNode extends AbstractContentNode(Bundle.BlackboardArtifactNode_createSheet_comment_name(), Bundle.BlackboardArtifactNode_createSheet_comment_displayName(), NO_DESCR, - status)); + return status; } /** * Used by (subclasses of) BlackboardArtifactNode to add the Score property * to their sheets. * - * @param sheetSet the modifiable Sheet.Set returned by - * Sheet.get(Sheet.PROPERTIES) + * @param sheetSet the modifiable Sheet.Set to add the property to * @param tags the list of tags associated with the file + * + * @deprecated Use the GetSCOTask to get this data on a background + * thread..., and then update the property sheet asynchronously */ @NbBundle.Messages({"BlackboardArtifactNode.createSheet.score.name=S", "BlackboardArtifactNode.createSheet.score.displayName=S", @@ -628,7 +671,21 @@ public class BlackboardArtifactNode extends AbstractContentNode tags) { + @Deprecated + protected final void addScorePropertyAndDescription(Sheet.Set sheetSet, List tags) { + Pair scoreAndDescription = getScorePropertyAndDescription(tags); + sheetSet.put(new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_score_name(), Bundle.BlackboardArtifactNode_createSheet_score_displayName(), scoreAndDescription.getRight(), scoreAndDescription.getLeft())); + } + + /** + * Get the score property for the node. + * + * @param tags the list of tags associated with the file + * + * @return score property and description + */ + @Override + protected Pair getScorePropertyAndDescription(List tags) { Score score = Score.NO_SCORE; String description = Bundle.BlackboardArtifactNode_createSheet_noScore_description(); if (associated instanceof AbstractFile) { @@ -673,34 +730,63 @@ public class BlackboardArtifactNode extends AbstractContentNode(Bundle.BlackboardArtifactNode_createSheet_score_name(), Bundle.BlackboardArtifactNode_createSheet_score_displayName(), description, score)); + + return Pair.of(score, description); } + /** + * Used by (subclasses of) BlackboardArtifactNode to add the Occurrences + * property to their sheets. + * + * @param sheetSet the modifiable Sheet.Set to add the property to + * @param attribute correlation attribute instance + * + * @deprecated Use the GetSCOTask to get this data on a background + * thread..., and then update the property sheet asynchronously + */ @NbBundle.Messages({"BlackboardArtifactNode.createSheet.count.name=O", "BlackboardArtifactNode.createSheet.count.displayName=O", - "BlackboardArtifactNode.createSheet.count.noCentralRepo.description=Central repository was not enabled when this column was populated", - "BlackboardArtifactNode.createSheet.count.hashLookupNotRun.description=Hash lookup had not been run on this artifact's associated file when the column was populated", - "# {0} - occuranceCount", - "BlackboardArtifactNode.createSheet.count.description=There were {0} datasource(s) found with occurances of the correlation value"}) - + "BlackboardArtifactNode.createSheet.count.noCorrelationAttributes.description=No correlation properties found", + "BlackboardArtifactNode.createSheet.count.noCorrelationValues.description=Unable to find other occurrences because no value exists for the available correlation property", + "# {0} - occurrenceCount", + "# {1} - attributeType", + "BlackboardArtifactNode.createSheet.count.description=There were {0} datasource(s) found with occurrences of the correlation value of type {1}"}) + @Deprecated protected final void addCountProperty(Sheet.Set sheetSet, CorrelationAttributeInstance attribute) { - Long count = -1L; //The column renderer will not display negative values, negative value used when count unavailble to preserve sorting - String description = Bundle.BlackboardArtifactNode_createSheet_count_noCentralRepo_description(); + Pair countAndDescription = getCountPropertyAndDescription(attribute.getCorrelationType(), attribute.getCorrelationValue(), Bundle.BlackboardArtifactNode_createSheet_count_noCorrelationAttributes_description()); + sheetSet.put( + new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_count_name(), Bundle.BlackboardArtifactNode_createSheet_count_displayName(), countAndDescription.getRight(), countAndDescription.getLeft())); + } + + /** + * Gets the Occurrences property for the node. + * + * @param attributeType the type of the attribute to count + * @param attributeValue the value of the attribute to count + * @param defaultDescription a description to use when none is determined by + * the getCountPropertyAndDescription method + * + * @return count and description + * + */ + @Override + protected Pair getCountPropertyAndDescription(Type attributeType, String attributeValue, String defaultDescription) { + Long count = -1L; + String description = defaultDescription; try { //don't perform the query if there is no correlation value - if (attribute != null && StringUtils.isNotBlank(attribute.getCorrelationValue())) { - count = EamDb.getInstance().getCountUniqueCaseDataSourceTuplesHavingTypeValue(attribute.getCorrelationType(), attribute.getCorrelationValue()); - description = Bundle.BlackboardArtifactNode_createSheet_count_description(count); - } else if (attribute != null) { - description = Bundle.BlackboardArtifactNode_createSheet_count_hashLookupNotRun_description(); + if (attributeType != null && StringUtils.isNotBlank(attributeValue)) { + count = EamDb.getInstance().getCountUniqueCaseDataSourceTuplesHavingTypeValue(attributeType, attributeValue); + description = Bundle.BlackboardArtifactNode_createSheet_count_description(count, attributeType.getDisplayName()); + } else if (attributeType != null) { + description = Bundle.BlackboardArtifactNode_createSheet_count_noCorrelationValues_description(); } } catch (EamDbException ex) { logger.log(Level.WARNING, "Error getting count of datasources with correlation attribute", ex); } catch (CorrelationAttributeNormalizationException ex) { logger.log(Level.WARNING, "Unable to normalize data to get count of datasources with correlation attribute", ex); } - sheetSet.put( - new NodeProperty<>(Bundle.BlackboardArtifactNode_createSheet_count_name(), Bundle.BlackboardArtifactNode_createSheet_count_displayName(), description, count)); + return Pair.of(count, description); } private void updateSheet() { diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties-MERGED index 1d8fb7eb14..57cdd1bf48 100755 --- a/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties-MERGED @@ -1,16 +1,14 @@ AbstractAbstractFileNode.accessTimeColLbl=Access Time -AbstractAbstractFileNode.addFileProperty.desc=no description AbstractAbstractFileNode.attrAddrColLbl=Attr. Addr. AbstractAbstractFileNode.changeTimeColLbl=Change Time AbstractAbstractFileNode.createdTimeColLbl=Created Time AbstractAbstractFileNode.createSheet.comment.displayName=C AbstractAbstractFileNode.createSheet.comment.name=C -# {0} - occuranceCount -AbstractAbstractFileNode.createSheet.count.description=There were {0} datasource(s) found with occurances of the correlation value +# {0} - occurrenceCount +AbstractAbstractFileNode.createSheet.count.description=There were {0} datasource(s) found with occurrences of the MD5 correlation value AbstractAbstractFileNode.createSheet.count.displayName=O AbstractAbstractFileNode.createSheet.count.hashLookupNotRun.description=Hash lookup had not been run on this file when the column was populated AbstractAbstractFileNode.createSheet.count.name=O -AbstractAbstractFileNode.createSheet.count.noCentralRepo.description=Central repository was not enabled when this column was populated AbstractAbstractFileNode.createSheet.interestingResult.description=File has interesting result associated with it. AbstractAbstractFileNode.createSheet.noScore.description=No score AbstractAbstractFileNode.createSheet.notableFile.description=File recognized as notable. @@ -37,6 +35,8 @@ AbstractAbstractFileNode.tagsProperty.displayName=Tags AbstractAbstractFileNode.typeDirColLbl=Type(Dir) AbstractAbstractFileNode.typeMetaColLbl=Type(Meta) AbstractAbstractFileNode.useridColLbl=UserID +AbstractContentNode.nodescription=no description +AbstractContentNode.valueLoading=value loading AbstractFsContentNode.noDesc.text=no description ArtifactStringContent.attrsTableHeader.sources=Source(s) ArtifactStringContent.attrsTableHeader.type=Type @@ -53,12 +53,13 @@ BlackboardArtifactNode.createSheet.artifactType.displayName=Result Type BlackboardArtifactNode.createSheet.artifactType.name=Result Type BlackboardArtifactNode.createSheet.comment.displayName=C BlackboardArtifactNode.createSheet.comment.name=C -# {0} - occuranceCount -BlackboardArtifactNode.createSheet.count.description=There were {0} datasource(s) found with occurances of the correlation value +# {0} - occurrenceCount +# {1} - attributeType +BlackboardArtifactNode.createSheet.count.description=There were {0} datasource(s) found with occurrences of the correlation value of type {1} BlackboardArtifactNode.createSheet.count.displayName=O -BlackboardArtifactNode.createSheet.count.hashLookupNotRun.description=Hash lookup had not been run on this artifact's associated file when the column was populated BlackboardArtifactNode.createSheet.count.name=O -BlackboardArtifactNode.createSheet.count.noCentralRepo.description=Central repository was not enabled when this column was populated +BlackboardArtifactNode.createSheet.count.noCorrelationAttributes.description=No correlation properties found +BlackboardArtifactNode.createSheet.count.noCorrelationValues.description=Unable to find other occurrences because no value exists for the available correlation property BlackboardArtifactNode.createSheet.fileSize.displayName=Size BlackboardArtifactNode.createSheet.fileSize.name=Size BlackboardArtifactNode.createSheet.interestingResult.description=Result has an interesting result associated with it. @@ -114,6 +115,8 @@ FileTypesByMimeTypeNode.createSheet.mediaSubtype.name=Subtype FileTypesByMimeTypeNode.createSheet.mediaType.desc=no description FileTypesByMimeTypeNode.createSheet.mediaType.displayName=Type FileTypesByMimeTypeNode.createSheet.mediaType.name=Type +GetSCOTask.occurrences.defaultDescription=No correlation properties found +GetSCOTask.occurrences.multipleProperties=Multiple different correlation properties exist for this result ImageNode.action.runIngestMods.text=Run Ingest Modules ImageNode.createSheet.deviceId.desc=Device ID of the image ImageNode.createSheet.deviceId.displayName=Device ID diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/DataModelActionsFactory.java b/Core/src/org/sleuthkit/autopsy/datamodel/DataModelActionsFactory.java index 13a0753ca1..bdb4a394a8 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/DataModelActionsFactory.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/DataModelActionsFactory.java @@ -36,6 +36,7 @@ import org.sleuthkit.autopsy.actions.ReplaceBlackboardArtifactTagAction; import org.sleuthkit.autopsy.actions.ReplaceContentTagAction; import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; import org.sleuthkit.autopsy.datamodel.Reports.ReportNode; +import org.sleuthkit.autopsy.directorytree.ExportCSVAction; import org.sleuthkit.autopsy.directorytree.ExternalViewerAction; import org.sleuthkit.autopsy.directorytree.ExternalViewerShortcutAction; import org.sleuthkit.autopsy.directorytree.ExtractAction; @@ -92,6 +93,7 @@ public class DataModelActionsFactory { } actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); if (isArtifactSource) { @@ -119,6 +121,7 @@ public class DataModelActionsFactory { actionsList.add(new NewWindowViewAction(VIEW_IN_NEW_WINDOW, slackFileNode)); actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); if (isArtifactSource) { @@ -155,6 +158,7 @@ public class DataModelActionsFactory { } actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance());// + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); if (isArtifactSource) { @@ -189,6 +193,7 @@ public class DataModelActionsFactory { } actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); if (isArtifactSource) { @@ -223,6 +228,7 @@ public class DataModelActionsFactory { } actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); if (isArtifactSource) { @@ -257,6 +263,7 @@ public class DataModelActionsFactory { } actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); if (isArtifactSource) { @@ -291,6 +298,7 @@ public class DataModelActionsFactory { } actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); if (isArtifactSource) { @@ -325,6 +333,7 @@ public class DataModelActionsFactory { } actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); if (isArtifactSource) { @@ -379,6 +388,7 @@ public class DataModelActionsFactory { } actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); if (isArtifactSource) { @@ -415,6 +425,7 @@ public class DataModelActionsFactory { } actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); if (isArtifactSource) { diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/DataSourcesNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/DataSourcesNode.java index 1265db5658..5ab29a5376 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/DataSourcesNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/DataSourcesNode.java @@ -57,7 +57,7 @@ public class DataSourcesNode extends DisplayableItemNode { } public DataSourcesNode(long dsObjId) { - super(Children.create(new DataSourcesNodeChildren(dsObjId), true), Lookups.singleton(NAME)); + super(Children.create(new DataSourcesNodeChildren(dsObjId), false), Lookups.singleton(NAME)); displayName = (dsObjId > 0) ? NbBundle.getMessage(DataSourcesNode.class, "DataSourcesNode.group_by_datasource.name") : NAME; init(); } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/DeletedContent.java b/Core/src/org/sleuthkit/autopsy/datamodel/DeletedContent.java index 25069d9564..adc5a34fa7 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/DeletedContent.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/DeletedContent.java @@ -24,7 +24,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; import java.util.List; -import java.util.Objects; import java.util.Observable; import java.util.Observer; import java.util.Set; @@ -37,7 +36,6 @@ import org.openide.nodes.Sheet; import org.openide.util.NbBundle; import org.openide.util.lookup.Lookups; import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.casemodule.CasePreferences; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.ingest.IngestManager; @@ -59,7 +57,7 @@ import org.sleuthkit.datamodel.VirtualDirectory; public class DeletedContent implements AutopsyVisitableItem { private SleuthkitCase skCase; - private final long datasourceObjId; + private final long filteringDSObjId; // 0 if not filtering/grouping by data source @NbBundle.Messages({"DeletedContent.fsDelFilter.text=File System", "DeletedContent.allDelFilter.text=All"}) @@ -105,11 +103,11 @@ public class DeletedContent implements AutopsyVisitableItem { public DeletedContent(SleuthkitCase skCase, long dsObjId) { this.skCase = skCase; - this.datasourceObjId = dsObjId; + this.filteringDSObjId = dsObjId; } long filteringDataSourceObjId() { - return this.datasourceObjId; + return this.filteringDSObjId; } @Override @@ -439,7 +437,7 @@ public class DeletedContent implements AutopsyVisitableItem { } - if (Objects.equals(CasePreferences.getGroupItemsInTreeByDataSource(), true)) { + if (filteringDSObjId > 0) { query += " AND data_source_obj_id = " + filteringDSObjId; } return query; diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/DirectoryNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/DirectoryNode.java index 97b0fd8216..5a3476f896 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/DirectoryNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/DirectoryNode.java @@ -28,6 +28,7 @@ import org.openide.util.Utilities; import org.sleuthkit.autopsy.actions.AddContentTagAction; import org.sleuthkit.autopsy.actions.DeleteFileContentTagAction; import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; +import org.sleuthkit.autopsy.directorytree.ExportCSVAction; import org.sleuthkit.autopsy.directorytree.ExtractAction; import org.sleuthkit.autopsy.directorytree.NewWindowViewAction; import org.sleuthkit.autopsy.directorytree.ViewContextAction; @@ -89,6 +90,7 @@ public class DirectoryNode extends AbstractFsContentNode { actionsList.add(ViewFileInTimelineAction.createViewFileAction(content)); actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(new RunIngestModulesAction(content)); actionsList.add(null); // creates a menu separator diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/EmailExtracted.java b/Core/src/org/sleuthkit/autopsy/datamodel/EmailExtracted.java index 83fcee1b65..7285b2cb8d 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/EmailExtracted.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/EmailExtracted.java @@ -28,7 +28,6 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Observable; import java.util.Observer; import java.util.Set; @@ -40,7 +39,6 @@ import org.openide.nodes.Sheet; import org.openide.util.NbBundle; import org.openide.util.lookup.Lookups; import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.casemodule.CasePreferences; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.ingest.IngestManager; @@ -88,7 +86,7 @@ public class EmailExtracted implements AutopsyVisitableItem { } private SleuthkitCase skCase; private final EmailResults emailResults; - private final long datasourceObjId; + private final long filteringDSObjId; // 0 if not filtering/grouping by data source @@ -110,7 +108,7 @@ public class EmailExtracted implements AutopsyVisitableItem { */ public EmailExtracted(SleuthkitCase skCase, long objId) { this.skCase = skCase; - this.datasourceObjId = objId; + this.filteringDSObjId = objId; emailResults = new EmailResults(); } @@ -162,8 +160,8 @@ public class EmailExtracted implements AutopsyVisitableItem { + "attribute_type_id=" + pathAttrId //NON-NLS + " AND blackboard_attributes.artifact_id=blackboard_artifacts.artifact_id" //NON-NLS + " AND blackboard_artifacts.artifact_type_id=" + artId; //NON-NLS - if (Objects.equals(CasePreferences.getGroupItemsInTreeByDataSource(), true)) { - query += " AND blackboard_artifacts.data_source_obj_id = " + datasourceObjId; + if (filteringDSObjId > 0) { + query += " AND blackboard_artifacts.data_source_obj_id = " + filteringDSObjId; } try (CaseDbQuery dbQuery = skCase.executeQuery(query)) { diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/ExtractedContent.java b/Core/src/org/sleuthkit/autopsy/datamodel/ExtractedContent.java index a21afff190..a51e36eb87 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/ExtractedContent.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/ExtractedContent.java @@ -26,7 +26,6 @@ import java.util.Comparator; import java.util.EnumSet; import java.util.HashMap; import java.util.List; -import java.util.Objects; import java.util.logging.Level; import org.openide.nodes.ChildFactory; import org.openide.nodes.Children; @@ -35,7 +34,6 @@ import org.openide.nodes.Sheet; import org.openide.util.NbBundle; import org.openide.util.lookup.Lookups; import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.casemodule.CasePreferences; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.ingest.IngestManager; @@ -64,7 +62,7 @@ public class ExtractedContent implements AutopsyVisitableItem { private SleuthkitCase skCase; // set to null after case has been closed private Blackboard blackboard; public static final String NAME = NbBundle.getMessage(RootNode.class, "ExtractedContentNode.name.text"); - private final long datasourceObjId; + private final long filteringDSObjId; // 0 if not filtering/grouping by data source /** * Constructs extracted content object @@ -83,7 +81,7 @@ public class ExtractedContent implements AutopsyVisitableItem { */ public ExtractedContent(SleuthkitCase skCase, long objId) { this.skCase = skCase; - this.datasourceObjId = objId; + this.filteringDSObjId = objId; this.blackboard = skCase.getBlackboard(); } @@ -307,8 +305,8 @@ public class ExtractedContent implements AutopsyVisitableItem { protected boolean createKeys(List list) { if (skCase != null) { try { - List types = Objects.equals(CasePreferences.getGroupItemsInTreeByDataSource(), true) ? - blackboard.getArtifactTypesInUse(datasourceObjId) : + List types = (filteringDSObjId > 0) ? + blackboard.getArtifactTypesInUse(filteringDSObjId) : skCase.getArtifactTypesInUse() ; types.removeAll(doNotShow); @@ -372,8 +370,8 @@ public class ExtractedContent implements AutopsyVisitableItem { // a performance increase might be had by adding a // "getBlackboardArtifactCount()" method to skCase try { - this.childCount = Objects.equals(CasePreferences.getGroupItemsInTreeByDataSource(), true) ? - blackboard.getArtifactsCount(type.getTypeID(), datasourceObjId) : + this.childCount = (filteringDSObjId > 0) ? + blackboard.getArtifactsCount(type.getTypeID(), filteringDSObjId) : skCase.getBlackboardArtifactsTypeCount(type.getTypeID()); } catch (TskException ex) { Logger.getLogger(TypeNode.class.getName()) @@ -501,8 +499,8 @@ public class ExtractedContent implements AutopsyVisitableItem { protected List makeKeys() { if (skCase != null) { try { - return Objects.equals(CasePreferences.getGroupItemsInTreeByDataSource(), true) - ? blackboard.getArtifacts(type.getTypeID(), datasourceObjId) + return (filteringDSObjId > 0) + ? blackboard.getArtifacts(type.getTypeID(), filteringDSObjId) : skCase.getBlackboardArtifacts(type.getTypeID()); } catch (TskException ex) { Logger.getLogger(ArtifactFactory.class.getName()).log(Level.SEVERE, "Couldn't get blackboard artifacts from database", ex); //NON-NLS diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/FileNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/FileNode.java index 1695b253b0..7b3a55c20b 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/FileNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/FileNode.java @@ -32,6 +32,7 @@ import org.sleuthkit.autopsy.actions.AddContentTagAction; import org.sleuthkit.autopsy.actions.DeleteFileContentTagAction; import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.directorytree.ExportCSVAction; import org.sleuthkit.autopsy.directorytree.ExternalViewerAction; import org.sleuthkit.autopsy.directorytree.ExternalViewerShortcutAction; import org.sleuthkit.autopsy.directorytree.ExtractAction; @@ -173,6 +174,7 @@ public class FileNode extends AbstractFsContentNode { actionsList.add(null); // Creates an item separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // Creates an item separator actionsList.add(AddContentTagAction.getInstance()); diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/FileSize.java b/Core/src/org/sleuthkit/autopsy/datamodel/FileSize.java index e9c49c596e..9cceeb5a29 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/FileSize.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/FileSize.java @@ -24,7 +24,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; import java.util.List; -import java.util.Objects; import java.util.Observable; import java.util.Observer; import java.util.Set; @@ -37,9 +36,7 @@ import org.openide.nodes.Sheet; import org.openide.util.NbBundle; import org.openide.util.lookup.Lookups; import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.casemodule.CasePreferences; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; -import org.sleuthkit.autopsy.core.UserPreferences; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.datamodel.AbstractFile; @@ -63,7 +60,7 @@ import org.sleuthkit.datamodel.VirtualDirectory; public class FileSize implements AutopsyVisitableItem { private SleuthkitCase skCase; - private final long datasourceObjId; + private final long filteringDSObjId; // 0 if not filtering/grouping by data source public enum FileSizeFilter implements AutopsyVisitableItem { @@ -105,7 +102,7 @@ public class FileSize implements AutopsyVisitableItem { public FileSize(SleuthkitCase skCase, long dsObjId) { this.skCase = skCase; - this.datasourceObjId = dsObjId; + this.filteringDSObjId = dsObjId; } @Override @@ -118,7 +115,7 @@ public class FileSize implements AutopsyVisitableItem { } long filteringDataSourceObjId() { - return this.datasourceObjId; + return this.filteringDSObjId; } /* * Root node. Children are nodes for specific sizes. @@ -437,7 +434,7 @@ public class FileSize implements AutopsyVisitableItem { query = query + " AND (type != " + TskData.TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS.getFileType() + ")"; //NON-NLS // filter by datasource if indicated in case preferences - if (Objects.equals(CasePreferences.getGroupItemsInTreeByDataSource(), true)) { + if (filteringDSObjId > 0) { query += " AND data_source_obj_id = " + filteringDSObjId; } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByExtension.java b/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByExtension.java index 152e8a6bac..34ec74280a 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByExtension.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByExtension.java @@ -24,7 +24,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; import java.util.List; -import java.util.Objects; import java.util.Observable; import java.util.Observer; import java.util.Set; @@ -39,7 +38,6 @@ import org.openide.util.NbBundle; import org.openide.util.NbBundle.Messages; import org.openide.util.lookup.Lookups; import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.casemodule.CasePreferences; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.core.UserPreferences; import org.sleuthkit.autopsy.coreutils.Logger; @@ -366,7 +364,7 @@ public final class FileTypesByExtension implements AutopsyVisitableItem { + (UserPreferences.hideKnownFilesInViewsTree() ? " AND (known IS NULL OR known != " + TskData.FileKnown.KNOWN.getFileKnownValue() + ")" : " ") - + (Objects.equals(CasePreferences.getGroupItemsInTreeByDataSource(), true) + + (filteringDataSourceObjId() > 0 ? " AND data_source_obj_id = " + filteringDataSourceObjId() : " ") + " AND (extension IN (" + filter.getFilter().stream() @@ -390,6 +388,7 @@ public final class FileTypesByExtension implements AutopsyVisitableItem { * @param skCase * @param o Observable that will notify when there could be new * data to display + * @param nodeName */ private FileExtensionNodeChildren(FileTypesByExtension.SearchFilterInterface filter, SleuthkitCase skCase, Observable o, String nodeName) { super(nodeName, new ViewsKnownAndSlackFilter<>()); diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByMimeType.java b/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByMimeType.java index 1e4f61fc87..86cc42aa8c 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByMimeType.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByMimeType.java @@ -28,7 +28,6 @@ import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Observable; import java.util.Observer; import java.util.Set; @@ -42,7 +41,6 @@ import org.openide.nodes.Sheet; import org.openide.util.NbBundle; import org.openide.util.lookup.Lookups; import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.casemodule.CasePreferences; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import static org.sleuthkit.autopsy.core.UserPreferences.hideKnownFilesInViewsTree; import static org.sleuthkit.autopsy.core.UserPreferences.hideSlackFilesInViewsTree; @@ -103,7 +101,7 @@ public final class FileTypesByMimeType extends Observable implements AutopsyVisi + TskData.TSK_DB_FILES_TYPE_ENUM.LOCAL.ordinal() + (hideSlackFilesInViewsTree() ? "" : ("," + TskData.TSK_DB_FILES_TYPE_ENUM.SLACK.ordinal())) + "))" - + ( Objects.equals(CasePreferences.getGroupItemsInTreeByDataSource(), true) ? " AND data_source_obj_id = " + this.filteringDataSourceObjId() : " ") + + ( (filteringDataSourceObjId() > 0) ? " AND data_source_obj_id = " + this.filteringDataSourceObjId() : " ") + (hideKnownFilesInViewsTree() ? (" AND (known IS NULL OR known != " + TskData.FileKnown.KNOWN.getFileKnownValue() + ")") : ""); } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/GetSCOTask.java b/Core/src/org/sleuthkit/autopsy/datamodel/GetSCOTask.java new file mode 100644 index 0000000000..c96dd582d5 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datamodel/GetSCOTask.java @@ -0,0 +1,130 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datamodel; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.lang.ref.WeakReference; +import java.util.List; +import java.util.logging.Level; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance.Type; +import org.sleuthkit.autopsy.centralrepository.datamodel.EamArtifactUtil; +import org.sleuthkit.autopsy.centralrepository.datamodel.EamDb; +import org.sleuthkit.autopsy.centralrepository.datamodel.EamDbException; +import org.sleuthkit.autopsy.core.UserPreferences; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.events.AutopsyEvent; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.Tag; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Background task to get Score, Comment and Occurrences values for an Abstract + * content node. + * + */ +class GetSCOTask implements Runnable { + + private final WeakReference> weakNodeRef; + private final PropertyChangeListener listener; + private static final Logger logger = Logger.getLogger(GetSCOTask.class.getName()); + + GetSCOTask(WeakReference> weakContentRef, PropertyChangeListener listener) { + this.weakNodeRef = weakContentRef; + this.listener = listener; + } + + @Messages({"GetSCOTask.occurrences.defaultDescription=No correlation properties found", + "GetSCOTask.occurrences.multipleProperties=Multiple different correlation properties exist for this result"}) + @Override + public void run() { + AbstractContentNode contentNode = weakNodeRef.get(); + + //Check for stale reference + if (contentNode == null) { + return; + } + + // get the SCO column values + List tags = contentNode.getAllTagsFromDatabase(); + CorrelationAttributeInstance fileAttribute = contentNode.getCorrelationAttributeInstance(); + + SCOData scoData = new SCOData(); + scoData.setScoreAndDescription(contentNode.getScorePropertyAndDescription(tags)); + scoData.setComment(contentNode.getCommentProperty(tags, fileAttribute)); + + if (EamDb.isEnabled() && !UserPreferences.getHideSCOColumns()) { + Type type = null; + String value = null; + String description = Bundle.GetSCOTask_occurrences_defaultDescription(); + if (contentNode instanceof BlackboardArtifactNode) { + BlackboardArtifact bbArtifact = ((BlackboardArtifactNode) contentNode).getArtifact(); + //for specific artifact types we still want to display information for the file instance correlation attribute + if (bbArtifact.getArtifactTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_ENCRYPTION_DETECTED.getTypeID() + || bbArtifact.getArtifactTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_ENCRYPTION_SUSPECTED.getTypeID() + || bbArtifact.getArtifactTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT.getTypeID() + || bbArtifact.getArtifactTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_METADATA_EXIF.getTypeID() + || bbArtifact.getArtifactTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_KEYWORD_HIT.getTypeID() + || bbArtifact.getArtifactTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_OBJECT_DETECTED.getTypeID() + || bbArtifact.getArtifactTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_EXT_MISMATCH_DETECTED.getTypeID() + || bbArtifact.getArtifactTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT.getTypeID()) { + try { + if (bbArtifact.getParent() instanceof AbstractFile) { + type = CorrelationAttributeInstance.getDefaultCorrelationTypes().get(CorrelationAttributeInstance.FILES_TYPE_ID); + value = ((AbstractFile) bbArtifact.getParent()).getMd5Hash(); + } + } catch (TskCoreException | EamDbException ex) { + logger.log(Level.WARNING, "Unable to get correlation type or value to determine value for O column for artifact", ex); + } + } else { + List listOfPossibleAttributes = EamArtifactUtil.makeInstancesFromBlackboardArtifact(bbArtifact, false); + if (listOfPossibleAttributes.size() > 1) { + //Don't display anything if there is more than 1 correlation property for an artifact but let the user know + description = Bundle.GetSCOTask_occurrences_multipleProperties(); + } else if (!listOfPossibleAttributes.isEmpty()) { + //there should only be one item in the list + type = listOfPossibleAttributes.get(0).getCorrelationType(); + value = listOfPossibleAttributes.get(0).getCorrelationValue(); + } + } + } else if (contentNode.getContent() instanceof AbstractFile) { + //use the file instance correlation attribute if the node is not a BlackboardArtifactNode + try { + type = CorrelationAttributeInstance.getDefaultCorrelationTypes().get(CorrelationAttributeInstance.FILES_TYPE_ID); + value = ((AbstractFile) contentNode.getContent()).getMd5Hash(); + } catch (EamDbException ex) { + logger.log(Level.WARNING, "Unable to get correlation type to determine value for O column for file", ex); + } + } + scoData.setCountAndDescription(contentNode.getCountPropertyAndDescription(type, value, description)); + } + + // signal SCO data is available. + if (listener + != null) { + listener.propertyChange(new PropertyChangeEvent( + AutopsyEvent.SourceType.LOCAL.toString(), + AbstractAbstractFileNode.NodeSpecificEvents.SCO_AVAILABLE.toString(), + null, scoData)); + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/HashsetHits.java b/Core/src/org/sleuthkit/autopsy/datamodel/HashsetHits.java index 9d6ed53431..03c72d2d2e 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/HashsetHits.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/HashsetHits.java @@ -30,7 +30,6 @@ import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Observable; import java.util.Observer; import java.util.Set; @@ -42,7 +41,6 @@ import org.openide.nodes.Sheet; import org.openide.util.NbBundle; import org.openide.util.lookup.Lookups; import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.casemodule.CasePreferences; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.ingest.IngestManager; @@ -65,7 +63,7 @@ public class HashsetHits implements AutopsyVisitableItem { private static final Logger logger = Logger.getLogger(HashsetHits.class.getName()); private SleuthkitCase skCase; private final HashsetResults hashsetResults; - private final long datasourceObjId; + private final long filteringDSObjId; // 0 if not filtering/grouping by data source /** @@ -87,7 +85,7 @@ public class HashsetHits implements AutopsyVisitableItem { */ public HashsetHits(SleuthkitCase skCase, long objId) { this.skCase = skCase; - this.datasourceObjId = objId; + this.filteringDSObjId = objId; hashsetResults = new HashsetResults(); } @@ -142,8 +140,8 @@ public class HashsetHits implements AutopsyVisitableItem { + "attribute_type_id=" + setNameId //NON-NLS + " AND blackboard_attributes.artifact_id=blackboard_artifacts.artifact_id" //NON-NLS + " AND blackboard_artifacts.artifact_type_id=" + artId; //NON-NLS - if (Objects.equals(CasePreferences.getGroupItemsInTreeByDataSource(), true)) { - query += " AND blackboard_artifacts.data_source_obj_id = " + datasourceObjId; + if (filteringDSObjId > 0) { + query += " AND blackboard_artifacts.data_source_obj_id = " + filteringDSObjId; } try (CaseDbQuery dbQuery = skCase.executeQuery(query)) { diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/ImageNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/ImageNode.java index 584a405fcc..da82c315b5 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/ImageNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/ImageNode.java @@ -28,12 +28,15 @@ import java.util.EnumSet; import java.util.List; import java.util.logging.Level; import javax.swing.Action; +import org.apache.commons.lang3.tuple.Pair; import org.openide.nodes.Sheet; import org.openide.util.NbBundle; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.casemodule.datasourcesummary.ViewSummaryInformationAction; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; +import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.directorytree.ExplorerNodeActionVisitor; import org.sleuthkit.autopsy.directorytree.FileSearchAction; @@ -47,6 +50,7 @@ import org.sleuthkit.datamodel.SleuthkitCase.CaseDbQuery; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.VirtualDirectory; import org.sleuthkit.autopsy.datamodel.BaseChildFactory.NoSuchEventBusException; +import org.sleuthkit.datamodel.Tag; /** * This class is used to represent the "Node" for the image. The children of @@ -126,7 +130,7 @@ public class ImageNode extends AbstractContentNode { "ImageNode.createSheet.type.text=Image", "ImageNode.createSheet.sectorSize.name=Sector Size (Bytes)", "ImageNode.createSheet.sectorSize.displayName=Sector Size (Bytes)", - "ImageNode.createSheet.sectorSize.desc=Sector size of the image in bytes.", + "ImageNode.createSheet.sectorSize.desc=Sector size of the image in bytes.", "ImageNode.createSheet.timezone.name=Timezone", "ImageNode.createSheet.timezone.displayName=Timezone", "ImageNode.createSheet.timezone.desc=Timezone of the image", @@ -250,4 +254,75 @@ public class ImageNode extends AbstractContentNode { } }; + /** + * Reads and returns a list of all tags associated with this content node. + * + * Null implementation of an abstract method. + * + * @return list of tags associated with the node. + */ + @Override + protected List getAllTagsFromDatabase() { + return new ArrayList<>(); + } + + /** + * Returns correlation attribute instance for the underlying content of the + * node. + * + * Null implementation of an abstract method. + * + * @return correlation attribute instance for the underlying content of the + * node. + */ + @Override + protected CorrelationAttributeInstance getCorrelationAttributeInstance() { + return null; + } + + /** + * Returns Score property for the node. + * + * Null implementation of an abstract method. + * + * @param tags list of tags. + * + * @return Score property for the underlying content of the node. + */ + @Override + protected Pair getScorePropertyAndDescription(List tags) { + return Pair.of(DataResultViewerTable.Score.NO_SCORE, NO_DESCR); + } + + /** + * Returns comment property for the node. + * + * Null implementation of an abstract method. + * + * @param tags list of tags + * @param attribute correlation attribute instance + * + * @return Comment property for the underlying content of the node. + */ + @Override + protected DataResultViewerTable.HasCommentStatus getCommentProperty(List tags, CorrelationAttributeInstance attribute) { + return DataResultViewerTable.HasCommentStatus.NO_COMMENT; + } + + /** + * Returns occurrences/count property for the node. + * + * Null implementation of an abstract method. + * + * @param attributeType the type of the attribute to count + * @param attributeValue the value of the attribute to coun + * @param defaultDescription a description to use when none is determined by + * the getCountPropertyAndDescription method + * + * @return count property for the underlying content of the node. + */ + @Override + protected Pair getCountPropertyAndDescription(CorrelationAttributeInstance.Type attributeType, String attributeValue, String defaultDescription) { + return Pair.of(-1L, NO_DESCR); + } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/InterestingHits.java b/Core/src/org/sleuthkit/autopsy/datamodel/InterestingHits.java index c36833992c..482ebf1558 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/InterestingHits.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/InterestingHits.java @@ -30,7 +30,6 @@ import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Observable; import java.util.Observer; import java.util.Set; @@ -42,7 +41,6 @@ import org.openide.nodes.Sheet; import org.openide.util.NbBundle; import org.openide.util.lookup.Lookups; import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.casemodule.CasePreferences; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.ingest.IngestManager; @@ -61,7 +59,7 @@ public class InterestingHits implements AutopsyVisitableItem { private static final Logger logger = Logger.getLogger(InterestingHits.class.getName()); private SleuthkitCase skCase; private final InterestingResults interestingResults = new InterestingResults(); - private final long datasourceObjId; + private final long filteringDSObjId; // 0 if not filtering/grouping by data source /** * Constructor @@ -82,7 +80,7 @@ public class InterestingHits implements AutopsyVisitableItem { */ public InterestingHits(SleuthkitCase skCase, long objId) { this.skCase = skCase; - this.datasourceObjId = objId; + this.filteringDSObjId = objId; interestingResults.update(); } @@ -133,8 +131,8 @@ public class InterestingHits implements AutopsyVisitableItem { + "attribute_type_id=" + setNameId //NON-NLS + " AND blackboard_attributes.artifact_id=blackboard_artifacts.artifact_id" //NON-NLS + " AND blackboard_artifacts.artifact_type_id=" + artId; //NON-NLS - if (Objects.equals(CasePreferences.getGroupItemsInTreeByDataSource(), true)) { - query += " AND blackboard_artifacts.data_source_obj_id = " + datasourceObjId; + if (filteringDSObjId > 0) { + query += " AND blackboard_artifacts.data_source_obj_id = " + filteringDSObjId; } try (CaseDbQuery dbQuery = skCase.executeQuery(query)) { diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/KeywordHits.java b/Core/src/org/sleuthkit/autopsy/datamodel/KeywordHits.java index cb24744b0c..655f0c1973 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/KeywordHits.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/KeywordHits.java @@ -31,7 +31,6 @@ import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Observable; import java.util.Observer; import java.util.Set; @@ -45,7 +44,6 @@ import org.openide.util.Lookup; import org.openide.util.NbBundle; import org.openide.util.lookup.Lookups; import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.casemodule.CasePreferences; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.coreutils.Logger; import static org.sleuthkit.autopsy.datamodel.Bundle.*; @@ -76,7 +74,7 @@ public class KeywordHits implements AutopsyVisitableItem { private SleuthkitCase skCase; private final KeywordResults keywordResults; - private final long datasourceObjId; + private final long filteringDSObjId; // 0 if not filtering/grouping by data source /** * String used in the instance MAP so that exact matches and substring can @@ -123,7 +121,7 @@ public class KeywordHits implements AutopsyVisitableItem { */ public KeywordHits(SleuthkitCase skCase, long objId) { this.skCase = skCase; - this.datasourceObjId = objId; + this.filteringDSObjId = objId; keywordResults = new KeywordResults(); } @@ -344,8 +342,8 @@ public class KeywordHits implements AutopsyVisitableItem { } String queryStr = KEYWORD_HIT_ATTRIBUTES_QUERY; - if (Objects.equals(CasePreferences.getGroupItemsInTreeByDataSource(), true)) { - queryStr += " AND blackboard_artifacts.data_source_obj_id = " + datasourceObjId; + if (filteringDSObjId > 0) { + queryStr += " AND blackboard_artifacts.data_source_obj_id = " + filteringDSObjId; } try (CaseDbQuery dbQuery = skCase.executeQuery(queryStr)) { diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/LayoutFileNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/LayoutFileNode.java index bfd8f7bd91..8f7f753db2 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/LayoutFileNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/LayoutFileNode.java @@ -23,24 +23,34 @@ import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.logging.Level; import javax.swing.Action; import org.openide.util.NbBundle; import org.openide.util.Utilities; import org.sleuthkit.autopsy.actions.AddContentTagAction; import org.sleuthkit.autopsy.actions.DeleteFileContentTagAction; import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.directorytree.ExportCSVAction; import org.sleuthkit.autopsy.directorytree.ExternalViewerAction; import org.sleuthkit.autopsy.directorytree.ExternalViewerShortcutAction; import org.sleuthkit.autopsy.directorytree.ExtractAction; import org.sleuthkit.autopsy.directorytree.NewWindowViewAction; +import org.sleuthkit.autopsy.directorytree.ViewContextAction; +import org.sleuthkit.autopsy.modules.embeddedfileextractor.ExtractArchiveWithPasswordAction; +import org.sleuthkit.autopsy.timeline.actions.ViewFileInTimelineAction; import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.LayoutFile; +import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData; /** * Node for layout file */ public class LayoutFileNode extends AbstractAbstractFileNode { + + private static final Logger logger = Logger.getLogger(LayoutFileNode.class.getName()); @Deprecated public static enum LayoutContentPropertyType { @@ -90,9 +100,14 @@ public class LayoutFileNode extends AbstractAbstractFileNode { } @Override + @NbBundle.Messages({ + "LayoutFileNode.getActions.viewFileInDir.text=View File in Directory"}) public Action[] getActions(boolean context) { List actionsList = new ArrayList<>(); actionsList.addAll(Arrays.asList(super.getActions(true))); + actionsList.add(new ViewContextAction(Bundle.LayoutFileNode_getActions_viewFileInDir_text(), this)); + actionsList.add(null); // Creates an item separator + actionsList.add(new NewWindowViewAction( NbBundle.getMessage(this.getClass(), "LayoutFileNode.getActions.viewInNewWin.text"), this)); final Collection selectedFilesList @@ -103,8 +118,10 @@ public class LayoutFileNode extends AbstractAbstractFileNode { } else { actionsList.add(ExternalViewerShortcutAction.getInstance()); } + actionsList.add(ViewFileInTimelineAction.createViewFileAction(getContent())); actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); @@ -113,6 +130,15 @@ public class LayoutFileNode extends AbstractAbstractFileNode { } actionsList.addAll(ContextMenuExtensionPoint.getActions()); + if (FileTypeExtensions.getArchiveExtensions().contains("." + this.content.getNameExtension().toLowerCase())) { + try { + if (this.content.getArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_ENCRYPTION_DETECTED).size() > 0) { + actionsList.add(new ExtractArchiveWithPasswordAction(this.getContent())); + } + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "Unable to add unzip with password action to context menus", ex); + } + } return actionsList.toArray(new Action[actionsList.size()]); } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/LocalFileNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/LocalFileNode.java index 6d64aeb621..4a62cd9e4f 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/LocalFileNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/LocalFileNode.java @@ -31,6 +31,7 @@ import org.sleuthkit.autopsy.actions.AddContentTagAction; import org.sleuthkit.autopsy.actions.DeleteFileContentTagAction; import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.directorytree.ExportCSVAction; import org.sleuthkit.autopsy.directorytree.ExternalViewerAction; import org.sleuthkit.autopsy.directorytree.ExternalViewerShortcutAction; import org.sleuthkit.autopsy.directorytree.ExtractAction; @@ -82,6 +83,7 @@ public class LocalFileNode extends AbstractAbstractFileNode { actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/RootContentChildren.java b/Core/src/org/sleuthkit/autopsy/datamodel/RootContentChildren.java index 66291955a8..c5d77692c8 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/RootContentChildren.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/RootContentChildren.java @@ -25,6 +25,7 @@ import org.openide.nodes.Children; import org.openide.nodes.Node; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.datamodel.accounts.Accounts; +import org.sleuthkit.datamodel.SleuthkitVisitableItem; /** * Children implementation for the root node of a ContentNode tree. Accepts a @@ -34,6 +35,7 @@ public class RootContentChildren extends Children.Keys { private final Collection contentKeys; private final CreateAutopsyNodeVisitor createAutopsyNodeVisitor = new CreateAutopsyNodeVisitor(); + private final CreateSleuthkitNodeVisitor createSleuthkitNodeVisitor = new CreateSleuthkitNodeVisitor(); /** * @param contentKeys root Content objects for the Node tree @@ -68,7 +70,7 @@ public class RootContentChildren extends Children.Keys { if (key instanceof AutopsyVisitableItem) { return new Node[] {((AutopsyVisitableItem)key).accept(createAutopsyNodeVisitor)}; } else { - return null; + return new Node[] {((SleuthkitVisitableItem)key).accept(createSleuthkitNodeVisitor)}; } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/SCOData.java b/Core/src/org/sleuthkit/autopsy/datamodel/SCOData.java new file mode 100644 index 0000000000..a9dd99369d --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datamodel/SCOData.java @@ -0,0 +1,56 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datamodel; + +import org.apache.commons.lang3.tuple.Pair; +import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable; + +/** + * Container to bag the S C & O data for an abstract file node. + * + */ +class SCOData { + + private Pair scoreAndDescription = null; + private DataResultViewerTable.HasCommentStatus comment = null; + private Pair countAndDescription = null; + + Pair getScoreAndDescription() { + return scoreAndDescription; + } + + DataResultViewerTable.HasCommentStatus getComment() { + return comment; + } + + Pair getCountAndDescription() { + return countAndDescription; + } + + void setScoreAndDescription(Pair scoreAndDescription) { + this.scoreAndDescription = scoreAndDescription; + } + void setComment(DataResultViewerTable.HasCommentStatus comment) { + this.comment = comment; + } + void setCountAndDescription(Pair countAndDescription) { + this.countAndDescription = countAndDescription; + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/SlackFileNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/SlackFileNode.java index 49d1b9da54..0e7bce56cc 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/SlackFileNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/SlackFileNode.java @@ -28,6 +28,7 @@ import org.openide.util.Utilities; import org.sleuthkit.autopsy.actions.AddContentTagAction; import org.sleuthkit.autopsy.actions.DeleteFileContentTagAction; import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; +import org.sleuthkit.autopsy.directorytree.ExportCSVAction; import org.sleuthkit.autopsy.directorytree.ExtractAction; import org.sleuthkit.autopsy.directorytree.NewWindowViewAction; import org.sleuthkit.autopsy.directorytree.ViewContextAction; @@ -85,6 +86,7 @@ public class SlackFileNode extends AbstractFsContentNode { NbBundle.getMessage(this.getClass(), "SlackFileNode.getActions.viewInNewWin.text"), this)); actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/SpecialDirectoryNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/SpecialDirectoryNode.java index 47f616de12..ce5948ce68 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/SpecialDirectoryNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/SpecialDirectoryNode.java @@ -25,6 +25,7 @@ import javax.swing.Action; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.datasourcesummary.ViewSummaryInformationAction; import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; +import org.sleuthkit.autopsy.directorytree.ExportCSVAction; import org.sleuthkit.autopsy.directorytree.ExtractAction; import org.sleuthkit.autopsy.directorytree.FileSearchAction; import org.sleuthkit.autopsy.directorytree.NewWindowViewAction; @@ -61,6 +62,7 @@ public abstract class SpecialDirectoryNode extends AbstractAbstractFileNode implements Observer { - private final long datasourceObjId; + private final long filteringDSObjId; // 0 if not filtering/grouping by data source private final Set CASE_EVENTS_OF_INTEREST = EnumSet.of(Case.Events.BLACKBOARD_ARTIFACT_TAG_ADDED, Case.Events.BLACKBOARD_ARTIFACT_TAG_DELETED, @@ -219,7 +217,7 @@ public class Tags implements AutopsyVisitableItem { * @param objId data source object id */ TagNameNodeFactory(long objId) { - this.datasourceObjId = objId; + this.filteringDSObjId = objId; } @@ -246,12 +244,12 @@ public class Tags implements AutopsyVisitableItem { List tagNamesInUse; if (UserPreferences.showOnlyCurrentUserTags()) { String userName = System.getProperty(USER_NAME_PROPERTY); - tagNamesInUse = Objects.equals(CasePreferences.getGroupItemsInTreeByDataSource(), true) - ? Case.getCurrentCaseThrows().getServices().getTagsManager().getTagNamesInUseForUser(datasourceObjId, userName) + tagNamesInUse = (filteringDSObjId > 0) + ? Case.getCurrentCaseThrows().getServices().getTagsManager().getTagNamesInUseForUser(filteringDSObjId, userName) : Case.getCurrentCaseThrows().getServices().getTagsManager().getTagNamesInUseForUser(userName); } else { - tagNamesInUse = Objects.equals(CasePreferences.getGroupItemsInTreeByDataSource(), true) - ? Case.getCurrentCaseThrows().getServices().getTagsManager().getTagNamesInUse(datasourceObjId) + tagNamesInUse = (filteringDSObjId > 0) + ? Case.getCurrentCaseThrows().getServices().getTagsManager().getTagNamesInUse(filteringDSObjId) : Case.getCurrentCaseThrows().getServices().getTagsManager().getTagNamesInUse(); } Collections.sort(tagNamesInUse); @@ -303,17 +301,17 @@ public class Tags implements AutopsyVisitableItem { TagsManager tm = Case.getCurrentCaseThrows().getServices().getTagsManager(); if (UserPreferences.showOnlyCurrentUserTags()) { String userName = System.getProperty(USER_NAME_PROPERTY); - if (Objects.equals(CasePreferences.getGroupItemsInTreeByDataSource(), true)) { - tagsCount = tm.getContentTagsCountByTagNameForUser(tagName, datasourceObjId, userName); - tagsCount += tm.getBlackboardArtifactTagsCountByTagNameForUser(tagName, datasourceObjId, userName); + if (filteringDSObjId > 0) { + tagsCount = tm.getContentTagsCountByTagNameForUser(tagName, filteringDSObjId, userName); + tagsCount += tm.getBlackboardArtifactTagsCountByTagNameForUser(tagName, filteringDSObjId, userName); } else { tagsCount = tm.getContentTagsCountByTagNameForUser(tagName, userName); tagsCount += tm.getBlackboardArtifactTagsCountByTagNameForUser(tagName, userName); } } else { - if (Objects.equals(CasePreferences.getGroupItemsInTreeByDataSource(), true)) { - tagsCount = tm.getContentTagsCountByTagName(tagName, datasourceObjId); - tagsCount += tm.getBlackboardArtifactTagsCountByTagName(tagName, datasourceObjId); + if (filteringDSObjId > 0) { + tagsCount = tm.getContentTagsCountByTagName(tagName, filteringDSObjId); + tagsCount += tm.getBlackboardArtifactTagsCountByTagName(tagName, filteringDSObjId); } else { tagsCount = tm.getContentTagsCountByTagName(tagName); tagsCount += tm.getBlackboardArtifactTagsCountByTagName(tagName); @@ -424,12 +422,12 @@ public class Tags implements AutopsyVisitableItem { if (UserPreferences.showOnlyCurrentUserTags()) { String userName = System.getProperty(USER_NAME_PROPERTY); - tagsCount = Objects.equals(CasePreferences.getGroupItemsInTreeByDataSource(), true) - ? Case.getCurrentCaseThrows().getServices().getTagsManager().getContentTagsCountByTagNameForUser(tagName, datasourceObjId, userName) + tagsCount = (filteringDSObjId > 0) + ? Case.getCurrentCaseThrows().getServices().getTagsManager().getContentTagsCountByTagNameForUser(tagName, filteringDSObjId, userName) : Case.getCurrentCaseThrows().getServices().getTagsManager().getContentTagsCountByTagNameForUser(tagName, userName); } else { - tagsCount = Objects.equals(CasePreferences.getGroupItemsInTreeByDataSource(), true) - ? Case.getCurrentCaseThrows().getServices().getTagsManager().getContentTagsCountByTagName(tagName, datasourceObjId) + tagsCount = (filteringDSObjId > 0) + ? Case.getCurrentCaseThrows().getServices().getTagsManager().getContentTagsCountByTagName(tagName, filteringDSObjId) : Case.getCurrentCaseThrows().getServices().getTagsManager().getContentTagsCountByTagName(tagName); } } catch (TskCoreException | NoCurrentCaseException ex) { @@ -486,8 +484,8 @@ public class Tags implements AutopsyVisitableItem { protected boolean createKeys(List keys) { // Use the content tags bearing the specified tag name as the keys. try { - List contentTags = Objects.equals(CasePreferences.getGroupItemsInTreeByDataSource(), true) - ? Case.getCurrentCaseThrows().getServices().getTagsManager().getContentTagsByTagName(tagName, datasourceObjId) + List contentTags = (filteringDSObjId > 0) + ? Case.getCurrentCaseThrows().getServices().getTagsManager().getContentTagsByTagName(tagName, filteringDSObjId) : Case.getCurrentCaseThrows().getServices().getTagsManager().getContentTagsByTagName(tagName); if (UserPreferences.showOnlyCurrentUserTags()) { String userName = System.getProperty(USER_NAME_PROPERTY); @@ -544,12 +542,12 @@ public class Tags implements AutopsyVisitableItem { try { if (UserPreferences.showOnlyCurrentUserTags()) { String userName = System.getProperty(USER_NAME_PROPERTY); - tagsCount = Objects.equals(CasePreferences.getGroupItemsInTreeByDataSource(), true) - ? Case.getCurrentCaseThrows().getServices().getTagsManager().getBlackboardArtifactTagsCountByTagNameForUser(tagName, datasourceObjId, userName) + tagsCount = (filteringDSObjId > 0) + ? Case.getCurrentCaseThrows().getServices().getTagsManager().getBlackboardArtifactTagsCountByTagNameForUser(tagName, filteringDSObjId, userName) : Case.getCurrentCaseThrows().getServices().getTagsManager().getBlackboardArtifactTagsCountByTagNameForUser(tagName, userName); } else { - tagsCount = Objects.equals(CasePreferences.getGroupItemsInTreeByDataSource(), true) - ? Case.getCurrentCaseThrows().getServices().getTagsManager().getBlackboardArtifactTagsCountByTagName(tagName, datasourceObjId) + tagsCount = (filteringDSObjId > 0) + ? Case.getCurrentCaseThrows().getServices().getTagsManager().getBlackboardArtifactTagsCountByTagName(tagName, filteringDSObjId) : Case.getCurrentCaseThrows().getServices().getTagsManager().getBlackboardArtifactTagsCountByTagName(tagName); } } catch (TskCoreException | NoCurrentCaseException ex) { @@ -606,8 +604,8 @@ public class Tags implements AutopsyVisitableItem { protected boolean createKeys(List keys) { try { // Use the blackboard artifact tags bearing the specified tag name as the keys. - List artifactTags = Objects.equals(CasePreferences.getGroupItemsInTreeByDataSource(), true) - ? Case.getCurrentCaseThrows().getServices().getTagsManager().getBlackboardArtifactTagsByTagName(tagName, datasourceObjId) + List artifactTags = (filteringDSObjId > 0) + ? Case.getCurrentCaseThrows().getServices().getTagsManager().getBlackboardArtifactTagsByTagName(tagName, filteringDSObjId) : Case.getCurrentCaseThrows().getServices().getTagsManager().getBlackboardArtifactTagsByTagName(tagName); if (UserPreferences.showOnlyCurrentUserTags()) { String userName = System.getProperty(USER_NAME_PROPERTY); diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/VolumeNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/VolumeNode.java index d25b50d59f..307013136f 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/VolumeNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/VolumeNode.java @@ -25,10 +25,14 @@ import java.util.EnumSet; import java.util.List; import java.util.logging.Level; import javax.swing.Action; +import org.apache.commons.lang3.tuple.Pair; import org.openide.nodes.Sheet; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; +import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable; import org.sleuthkit.autopsy.coreutils.Logger; +import static org.sleuthkit.autopsy.datamodel.AbstractContentNode.NO_DESCR; import org.sleuthkit.autopsy.datamodel.BaseChildFactory.NoSuchEventBusException; import org.sleuthkit.autopsy.directorytree.ExplorerNodeActionVisitor; import org.sleuthkit.autopsy.directorytree.NewWindowViewAction; @@ -39,12 +43,14 @@ import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.VirtualDirectory; import org.sleuthkit.datamodel.Volume; import org.sleuthkit.autopsy.directorytree.FileSystemDetailsAction; +import org.sleuthkit.datamodel.Tag; /** * This class is used to represent the "Node" for the volume. Its child is the * root directory of a file system */ public class VolumeNode extends AbstractContentNode { + private static final Logger logger = Logger.getLogger(VolumeNode.class.getName()); /** @@ -212,4 +218,76 @@ public class VolumeNode extends AbstractContentNode { public String getItemType() { return DisplayableItemNode.FILE_PARENT_NODE_KEY; } + + /** + * Reads and returns a list of all tags associated with this content node. + * + * Null implementation of an abstract method. + * + * @return list of tags associated with the node. + */ + @Override + protected List getAllTagsFromDatabase() { + return new ArrayList<>(); + } + + /** + * Returns correlation attribute instance for the underlying content of the + * node. + * + * Null implementation of an abstract method. + * + * @return correlation attribute instance for the underlying content of the + * node. + */ + @Override + protected CorrelationAttributeInstance getCorrelationAttributeInstance() { + return null; + } + + /** + * Returns Score property for the node. + * + * Null implementation of an abstract method. + * + * @param tags list of tags. + * + * @return Score property for the underlying content of the node. + */ + @Override + protected Pair getScorePropertyAndDescription(List tags) { + return Pair.of(DataResultViewerTable.Score.NO_SCORE, NO_DESCR); + } + + /** + * Returns comment property for the node. + * + * Null implementation of an abstract method. + * + * @param tags list of tags + * @param attribute correlation attribute instance + * + * @return Comment property for the underlying content of the node. + */ + @Override + protected DataResultViewerTable.HasCommentStatus getCommentProperty(List tags, CorrelationAttributeInstance attribute) { + return DataResultViewerTable.HasCommentStatus.NO_COMMENT; + } + + /** + * Returns occurrences/count property for the node. + * + * Null implementation of an abstract method. + * + * @param attributeType the type of the attribute to count + * @param attributeValue the value of the attribute to coun + * @param defaultDescription a description to use when none is determined by + * the getCountPropertyAndDescription method + * + * @return count property for the underlying content of the node. + */ + @Override + protected Pair getCountPropertyAndDescription(CorrelationAttributeInstance.Type attributeType, String attributeValue, String defaultDescription) { + return Pair.of(-1L, NO_DESCR); + } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/accounts/Accounts.java b/Core/src/org/sleuthkit/autopsy/datamodel/accounts/Accounts.java index d8e40d8a3e..a75bca2972 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/accounts/Accounts.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/accounts/Accounts.java @@ -57,9 +57,7 @@ import org.openide.util.NbBundle; import org.openide.util.Utilities; import org.openide.util.lookup.Lookups; import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.casemodule.CasePreferences; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; -import org.sleuthkit.autopsy.core.UserPreferences; import org.sleuthkit.autopsy.corecomponents.DataResultTopComponent; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.datamodel.AutopsyItemVisitor; @@ -96,7 +94,7 @@ final public class Accounts implements AutopsyVisitableItem { final public static String NAME = Bundle.AccountsRootNode_name(); private SleuthkitCase skCase; - private final long datasourceObjId; + private final long filteringDSObjId; // 0 if not filtering/grouping by data source private final EventBus reviewStatusBus = new EventBus("ReviewStatusBus"); @@ -123,7 +121,7 @@ final public class Accounts implements AutopsyVisitableItem { */ public Accounts(SleuthkitCase skCase, long objId) { this.skCase = skCase; - this.datasourceObjId = objId; + this.filteringDSObjId = objId; this.rejectActionInstance = new RejectAccounts(); this.approveActionInstance = new ApproveAccounts(); @@ -153,8 +151,8 @@ final public class Accounts implements AutopsyVisitableItem { * based on the CasePreferences groupItemsInTreeByDataSource setting */ private String getFilterByDataSourceClause() { - if (Objects.equals(CasePreferences.getGroupItemsInTreeByDataSource(), true)) { - return " AND blackboard_artifacts.data_source_obj_id = " + datasourceObjId + " "; + if (filteringDSObjId > 0) { + return " AND blackboard_artifacts.data_source_obj_id = " + filteringDSObjId + " "; } return " "; @@ -1847,7 +1845,8 @@ final public class Accounts implements AutopsyVisitableItem { return ICON_BASE_PATH + "WhatsApp.png"; } else { //there could be a default icon instead... - throw new IllegalArgumentException("Unknown Account.Type: " + type.getTypeName()); + return ICON_BASE_PATH + "face.png"; +// throw new IllegalArgumentException("Unknown Account.Type: " + type.getTypeName()); } } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/utils/FileTypeUtils.java b/Core/src/org/sleuthkit/autopsy/datamodel/utils/FileTypeUtils.java index b4887cf259..c255becf34 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/utils/FileTypeUtils.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/utils/FileTypeUtils.java @@ -89,6 +89,7 @@ public final class FileTypeUtils { "application/json", //NON-NLS "application/javascript", //NON-NLS "application/xml", //NON-NLS + "application/xhtml+xml", //NON-NLS "application/x-msoffice", //NON-NLS "application/x-ooxml", //NON-NLS "application/msword", //NON-NLS diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/AddRawImageTask.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/AddRawImageTask.java index f8ff559afd..e85f1ac3d5 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/AddRawImageTask.java +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/AddRawImageTask.java @@ -25,7 +25,9 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; +import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgressMonitor; import org.sleuthkit.autopsy.coreutils.Logger; @@ -34,8 +36,6 @@ import org.sleuthkit.datamodel.Image; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskFileRange; -import org.openide.util.NbBundle.Messages; -import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; /* * A runnable that adds a raw data source to a case database. diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/directorytree/Bundle.properties-MERGED index 0d04cc1c46..f70a374bc5 100755 --- a/Core/src/org/sleuthkit/autopsy/directorytree/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/directorytree/Bundle.properties-MERGED @@ -1,3 +1,8 @@ +CSVWriter.done.notifyMsg.error=Error exporting to CSV file +# {0} - Output file +CSVWriter.done.notifyMsg.success=Wrote to {0} +CSVWriter.progress.cancelling=Cancelling +CSVWriter.progress.extracting=Exporting to CSV file CTL_DirectoryTreeTopComponent=Directory Tree DataResultFilterNode.viewSourceArtifact.text=View Source Result # {0} - dataSourceCount @@ -5,6 +10,11 @@ DirectoryTreeTopComponent.componentOpened.groupDataSources.text=This case contai DirectoryTreeTopComponent.componentOpened.groupDataSources.title=Group by data source? DirectoryTreeTopComponent.emptyMimeNode.text=Data not available. Run file type identification module. DirectoryTreeTopComponent.resultsView.title=Listing +ExportCSV.saveNodesToCSV.empty=No data to export +# {0} - Output file +ExportCSV.saveNodesToCSV.fileExists=File {0} already exists +ExportCSV.saveNodesToCSV.noCurrentCase=No open case available +ExportCSV.title.text=Export selected rows to CSV ExternalViewerAction.actionPerformed.failure.exe.message=The file is an executable and will not be opened. ExternalViewerAction.actionPerformed.failure.IO.message=There is no associated editor for files of this type or the associated application failed to launch. ExternalViewerAction.actionPerformed.failure.missingFile.message=The file no longer exists. diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java b/Core/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java index 0b3a6f698f..3c206049d6 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java @@ -372,6 +372,7 @@ public class DataResultFilterNode extends FilterNode { } actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); actionsList.add(AddBlackboardArtifactTagAction.getInstance()); @@ -489,6 +490,12 @@ public class DataResultFilterNode extends FilterNode { @Override public AbstractAction visit(BlackboardArtifactNode ban) { + + Action preferredAction = ban.getPreferredAction(); + if(preferredAction instanceof AbstractAction) { + return (AbstractAction) preferredAction; + } + BlackboardArtifact artifact = ban.getArtifact(); try { if ((artifact.getArtifactTypeID() == ARTIFACT_TYPE.TSK_EMAIL_MSG.getTypeID()) diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/DirectoryTreeFilterChildren.java b/Core/src/org/sleuthkit/autopsy/directorytree/DirectoryTreeFilterChildren.java index 959b2c6653..e84dd16da6 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/DirectoryTreeFilterChildren.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/DirectoryTreeFilterChildren.java @@ -44,7 +44,6 @@ import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.Directory; import org.sleuthkit.datamodel.LayoutFile; import org.sleuthkit.datamodel.TskException; -import org.sleuthkit.datamodel.VirtualDirectory; import org.sleuthkit.datamodel.Volume; /** @@ -140,8 +139,7 @@ class DirectoryTreeFilterChildren extends FilterNode.Children { try { for (Content c : vol.getChildren()) { - if (!(c instanceof LayoutFile - || c instanceof VirtualDirectory)) { + if (!(c instanceof LayoutFile)) { ret = false; break; } diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/DirectoryTreeTopComponent.java b/Core/src/org/sleuthkit/autopsy/directorytree/DirectoryTreeTopComponent.java index 4d2ea876a0..280ccd3c3a 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/DirectoryTreeTopComponent.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/DirectoryTreeTopComponent.java @@ -219,7 +219,7 @@ public final class DirectoryTreeTopComponent extends TopComponent implements Dat case UserPreferences.TIME_ZONE_FOR_DISPLAYS: case UserPreferences.HIDE_KNOWN_FILES_IN_DATA_SRCS_TREE: case UserPreferences.HIDE_SLACK_FILES_IN_DATA_SRCS_TREE: - case UserPreferences.HIDE_CENTRAL_REPO_COMMENTS_AND_OCCURRENCES: + case UserPreferences.HIDE_SCO_COLUMNS: case UserPreferences.DISPLAY_TRANSLATED_NAMES: case UserPreferences.KEEP_PREFERRED_VIEWER: refreshContentTreeSafe(); diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/ExplorerNodeActionVisitor.java b/Core/src/org/sleuthkit/autopsy/directorytree/ExplorerNodeActionVisitor.java index 99692ea633..674eb6bf2e 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/ExplorerNodeActionVisitor.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/ExplorerNodeActionVisitor.java @@ -125,6 +125,7 @@ public class ExplorerNodeActionVisitor extends ContentVisitor.Default visit(final DerivedFile d) { List actionsList = new ArrayList<>(); actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(AddContentTagAction.getInstance()); final Collection selectedFilesList = @@ -166,6 +169,7 @@ public class ExplorerNodeActionVisitor extends ContentVisitor.Default visit(final LocalFile d) { List actionsList = new ArrayList<>(); actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(AddContentTagAction.getInstance()); final Collection selectedFilesList = @@ -182,6 +186,7 @@ public class ExplorerNodeActionVisitor extends ContentVisitor.Default visit(final org.sleuthkit.datamodel.File d) { List actionsList = new ArrayList<>(); actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); actionsList.add(AddContentTagAction.getInstance()); final Collection selectedFilesList = diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/ExportCSVAction.java b/Core/src/org/sleuthkit/autopsy/directorytree/ExportCSVAction.java new file mode 100644 index 0000000000..7761e82231 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/directorytree/ExportCSVAction.java @@ -0,0 +1,360 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this content except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.directorytree; + +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.io.File; +import java.io.BufferedWriter; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.lang.reflect.InvocationTargetException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import javax.swing.AbstractAction; +import javax.swing.JFileChooser; +import javax.swing.JOptionPane; +import javax.swing.SwingWorker; +import javax.swing.filechooser.FileNameExtensionFilter; +import org.netbeans.api.progress.ProgressHandle; +import org.openide.util.Cancellable; +import org.openide.util.NbBundle; +import org.openide.util.Utilities; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import org.sleuthkit.autopsy.datamodel.AbstractAbstractFileNode.AbstractFilePropertyType; +import org.openide.nodes.Node; +import org.openide.nodes.Node.PropertySet; +import org.openide.nodes.Node.Property; + +/** + * Exports CSV version of result nodes to a location selected by the user. + */ +public final class ExportCSVAction extends AbstractAction { + + private static final Logger logger = Logger.getLogger(ExportCSVAction.class.getName()); + private final static String DEFAULT_FILENAME = "Results"; + private final static List columnsToSkip = Arrays.asList(AbstractFilePropertyType.SCORE.toString(), + AbstractFilePropertyType.COMMENT.toString(), AbstractFilePropertyType.OCCURRENCES.toString()); + + private static String userDefinedExportPath; + + // This class is a singleton to support multi-selection of nodes, since + // org.openide.nodes.NodeOp.findActions(Node[] nodes) will only pick up an Action if every + // node in the array returns a reference to the same action object from Node.getActions(boolean). + private static ExportCSVAction instance; + + /** + * Get an instance of the Action. See above for why + * the class is a singleton. + * + * @return the instance + */ + public static synchronized ExportCSVAction getInstance() { + if (null == instance) { + instance = new ExportCSVAction(); + } + return instance; + } + + /** + * Private constructor for the action. + */ + @NbBundle.Messages({"ExportCSV.title.text=Export selected rows to CSV"}) + private ExportCSVAction() { + super(Bundle.ExportCSV_title_text()); + } + + /** + * Asks user to choose destination, then extracts content to destination + * (recursing on directories). + * + * @param e The action event. + */ + + @Override + public void actionPerformed(ActionEvent e) { + Collection selectedNodes = Utilities.actionsGlobalContext().lookupAll(Node.class); + saveNodesToCSV(selectedNodes, (Component)e.getSource()); + } + + /** + * Save the selected nodes to a CSV file + * + * @param nodesToExport the nodes to save + * @param component + */ + @NbBundle.Messages({ + "# {0} - Output file", + "ExportCSV.saveNodesToCSV.fileExists=File {0} already exists", + "ExportCSV.saveNodesToCSV.noCurrentCase=No open case available", + "ExportCSV.saveNodesToCSV.empty=No data to export"}) + public static void saveNodesToCSV(Collection nodesToExport, Component component) { + + if (nodesToExport.isEmpty()) { + MessageNotifyUtil.Message.info(Bundle.ExportCSV_saveNodesToCSV_empty()); + return; + } + + try { + // Set up the file chooser with a default name and either the Export + // folder or the last used folder. + String fileName = getDefaultOutputFileName(nodesToExport.iterator().next().getParentNode()); + JFileChooser fileChooser = new JFileChooser(); + fileChooser.setCurrentDirectory(new File(getExportDirectory(Case.getCurrentCaseThrows()))); + fileChooser.setSelectedFile(new File(fileName)); + fileChooser.setFileFilter(new FileNameExtensionFilter("csv file", "csv")); + + int returnVal = fileChooser.showSaveDialog(component); + if (returnVal == JFileChooser.APPROVE_OPTION) { + + // Get the file name, appending .csv if necessary + File selectedFile = fileChooser.getSelectedFile(); + if (!selectedFile.getName().endsWith(".csv")) { // NON-NLS + selectedFile = new File(selectedFile.toString() + ".csv"); // NON-NLS + } + + // Save the directory used for next time + updateExportDirectory(selectedFile.getParent(), Case.getCurrentCaseThrows()); + + if (selectedFile.exists()) { + logger.log(Level.SEVERE, "File {0} already exists", selectedFile.getAbsolutePath()); //NON-NLS + MessageNotifyUtil.Message.info(Bundle.ExportCSV_saveNodesToCSV_fileExists(selectedFile)); + return; + } + + CSVWriter writer = new CSVWriter(nodesToExport, selectedFile); + writer.execute(); + } + } catch (NoCurrentCaseException ex) { + JOptionPane.showMessageDialog(component, Bundle.ExportCSV_saveNodesToCSV_noCurrentCase()); + logger.log(Level.INFO, "Exception while getting open case.", ex); //NON-NLS + } + } + + /** + * Create a default name for the CSV output. + * + * @param parent The parent node for the selected nodes + * + * @return the default name + */ + private static String getDefaultOutputFileName(Node parent) { + String dateStr = String.format("%1$tY%1$tm%1$te%1$tI%1$tM%1$tS", Calendar.getInstance()); + + if (parent != null) { + // The first value in the property set is generally a reasonable name + for (PropertySet set : parent.getPropertySets()) { + for (Property prop : set.getProperties()) { + try { + String parentName = prop.getValue().toString(); + + // Strip off the count (if present) + parentName = parentName.replaceAll("\\([0-9]+\\)$", ""); + + // Strip out any invalid characters + parentName = parentName.replaceAll("[\\\\/:*?\"<>|]", "_"); + + return parentName + " " + dateStr; + } catch (IllegalAccessException | InvocationTargetException ex) { + logger.log(Level.WARNING, "Failed to get property set value as string", ex); + } + } + } + } + return DEFAULT_FILENAME + " " + dateStr; + } + + /** + * Get the export directory path. + * + * @param openCase The current case. + * + * @return The export directory path. + */ + private static String getExportDirectory(Case openCase) { + String caseExportPath = openCase.getExportDirectory(); + + if (userDefinedExportPath == null) { + return caseExportPath; + } + + File file = new File(userDefinedExportPath); + if (file.exists() == false || file.isDirectory() == false) { + return caseExportPath; + } + + return userDefinedExportPath; + } + + /** + * Update the default export directory. If the directory path matches the + * case export directory, then the directory used will always match the + * export directory of any given case. Otherwise, the path last used will be + * saved. + * + * @param exportPath The export path. + * @param openCase The current case. + */ + private static void updateExportDirectory(String exportPath, Case openCase) { + if (exportPath.equalsIgnoreCase(openCase.getExportDirectory())) { + userDefinedExportPath = null; + } else { + userDefinedExportPath = exportPath; + } + } + + + /** + * Thread that does the actual extraction work + */ + private static class CSVWriter extends SwingWorker { + + private static final Logger logger = Logger.getLogger(CSVWriter.class.getName()); + private ProgressHandle progress; + + private final Collection nodesToExport; + private final File outputFile; + + /** + * Create an instance of the CSVWriter. + * + * @param extractionTasks List of file extraction tasks. + */ + CSVWriter(Collection nodesToExport, File outputFile) { + this.nodesToExport = nodesToExport; + this.outputFile = outputFile; + } + + @NbBundle.Messages({"CSVWriter.progress.extracting=Exporting to CSV file", + "CSVWriter.progress.cancelling=Cancelling"}) + @Override + protected Object doInBackground() throws Exception { + if (nodesToExport.isEmpty()) { + return null; + } + + // Set up progress bar. + final String displayName = Bundle.CSVWriter_progress_extracting(); + progress = ProgressHandle.createHandle(displayName, new Cancellable() { + @Override + public boolean cancel() { + if (progress != null) { + progress.setDisplayName(Bundle.CSVWriter_progress_cancelling()); + } + return ExportCSVAction.CSVWriter.this.cancel(true); + } + }); + progress.start(); + progress.switchToIndeterminate(); + + try (BufferedWriter br = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outputFile), StandardCharsets.UTF_8))) { + // Write BOM + br.write('\ufeff'); + + // Write the header + List headers = new ArrayList<>(); + PropertySet[] sets = nodesToExport.iterator().next().getPropertySets(); + for(PropertySet set : sets) { + for (Property prop : set.getProperties()) { + if ( ! columnsToSkip.contains(prop.getDisplayName())) { + headers.add(prop.getDisplayName()); + } + } + } + br.write(listToCSV(headers)); + + // Write each line + Iterator nodeIterator = nodesToExport.iterator(); + while (nodeIterator.hasNext()) { + if (this.isCancelled()) { + break; + } + + Node node = (Node)nodeIterator.next(); + List values = new ArrayList<>(); + sets = node.getPropertySets(); + for(PropertySet set : sets) { + for (Property prop : set.getProperties()) { + if ( ! columnsToSkip.contains(prop.getDisplayName())) { + values.add(escapeQuotes(prop.getValue().toString())); + } + } + } + br.write(listToCSV(values)); + } + } + + return null; + } + + /** + * Escape any quotes in the string + * + * @param original + * + * @return the string with quotes escaped + */ + private String escapeQuotes(String original) { + return original.replaceAll("\"", "\\\\\""); + } + + /** + * Convert list of values to a comma separated string. + * + * @param values Values to convert + * + * @return values as CSV + */ + private String listToCSV(List values) { + return "\"" + String.join("\",\"", values) + "\"\n"; + } + + @NbBundle.Messages({"CSVWriter.done.notifyMsg.error=Error exporting to CSV file", + "# {0} - Output file", + "CSVWriter.done.notifyMsg.success=Wrote to {0}"}) + @Override + protected void done() { + boolean msgDisplayed = false; + try { + super.get(); + } catch (InterruptedException | ExecutionException ex) { + logger.log(Level.SEVERE, "Fatal error during file extraction", ex); //NON-NLS + MessageNotifyUtil.Message.info(Bundle.CSVWriter_done_notifyMsg_error()); + msgDisplayed = true; + } catch (java.util.concurrent.CancellationException ex) { + // catch and ignore if we were cancelled + } finally { + progress.finish(); + if (!this.isCancelled() && !msgDisplayed) { + MessageNotifyUtil.Message.info(Bundle.CSVWriter_done_notifyMsg_success(outputFile)); + } + } + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/ViewContextAction.java b/Core/src/org/sleuthkit/autopsy/directorytree/ViewContextAction.java index 62c57f7118..ef8adfa319 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/ViewContextAction.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/ViewContextAction.java @@ -39,6 +39,7 @@ import org.sleuthkit.autopsy.casemodule.CasePreferences; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.core.UserPreferences; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import org.sleuthkit.autopsy.datamodel.AbstractAbstractFileNode; import org.sleuthkit.autopsy.datamodel.AbstractFsContentNode; import org.sleuthkit.autopsy.datamodel.BlackboardArtifactNode; import org.sleuthkit.autopsy.datamodel.ContentNodeSelectionInfo; @@ -75,7 +76,7 @@ public class ViewContextAction extends AbstractAction { * parent of the content, selecting the parent in the tree view, then * selecting the content in the results view. * - * @param displayName The display name for the action. + * @param displayName The display name for the action. * @param artifactNode The artifact node for the artifact. */ public ViewContextAction(String displayName, BlackboardArtifactNode artifactNode) { @@ -97,15 +98,30 @@ public class ViewContextAction extends AbstractAction { * parent of the content, selecting the parent in the tree view, then * selecting the content in the results view. * - * @param displayName The display name for the action. + * @param displayName The display name for the action. * @param fileSystemContentNode The file system content node for the - * content. + * content. */ public ViewContextAction(String displayName, AbstractFsContentNode fileSystemContentNode) { super(displayName); this.content = fileSystemContentNode.getLookup().lookup(Content.class); } + /** + * An action that displays the context for abstract file by expanding the + * data sources branch of the tree view to the level of the parent of the + * content, selecting the parent in the tree view, then selecting the + * content in the results view. + * + * @param displayName The display name for the action. + * @param abstractAbstractFileNode The AbstractAbstractFileNode node for the + * content. + */ + public ViewContextAction(String displayName, AbstractAbstractFileNode abstractAbstractFileNode) { + super(displayName); + this.content = abstractAbstractFileNode.getLookup().lookup(Content.class); + } + /** * An action that displays the context for some content by expanding the * data sources branch of the tree view to the level of the parent of the @@ -113,7 +129,7 @@ public class ViewContextAction extends AbstractAction { * content in the results view. * * @param displayName The display name for the action. - * @param content The content. + * @param content The content. */ public ViewContextAction(String displayName, Content content) { super(displayName); @@ -136,12 +152,29 @@ public class ViewContextAction extends AbstractAction { }) public void actionPerformed(ActionEvent event) { EventQueue.invokeLater(() -> { + + /* + * Get the parent content for the content to be selected in the + * results view. If the parent content is null, then the specified + * content is a data source, and the parent tree view node is the + * "Data Sources" node. Otherwise, the tree view needs to be + * searched to find the parent treeview node. + */ + Content parentContent = null; + try { + parentContent = content.getParent(); + } catch (TskCoreException ex) { + MessageNotifyUtil.Message.error(Bundle.ViewContextAction_errorMessage_cannotFindDirectory()); + logger.log(Level.SEVERE, String.format("Could not get parent of Content object: %s", content), ex); //NON-NLS + return; + } + /* * Get the "Data Sources" node from the tree view. */ DirectoryTreeTopComponent treeViewTopComponent = DirectoryTreeTopComponent.findInstance(); ExplorerManager treeViewExplorerMgr = treeViewTopComponent.getExplorerManager(); - Node parentTreeViewNode; + Node parentTreeViewNode = null; if (Objects.equals(CasePreferences.getGroupItemsInTreeByDataSource(), true)) { // 'Group by Data Source' view SleuthkitCase skCase; @@ -152,13 +185,44 @@ public class ViewContextAction extends AbstractAction { long contentDSObjid = content.getDataSource().getId(); DataSource datasource = skCase.getDataSource(contentDSObjid); dsname = datasource.getName(); - Children rootChildren = treeViewExplorerMgr.getRootContext().getChildren(); - Node datasourceGroupingNode = rootChildren.findChild(dsname); - if (!Objects.isNull(datasourceGroupingNode)) { - Children dsChildren = datasourceGroupingNode.getChildren(); - parentTreeViewNode = dsChildren.findChild(DataSourcesNode.NAME); + + if (null != parentContent) { + // the tree view needs to be searched to find the parent treeview node. + /* NOTE: we can't do a lookup by data source name here, becase if there + are multiple data sources with the same name, then "getChildren().findChild(dsname)" + simply returns the first one that it finds. Instead we have to loop over all + data sources with that name, and make sure we find the correct one. + */ + for (int i = 0; i < rootChildren.getNodesCount(); i++) { + // in the root, look for a data source node with the name of interest + Node treeNode = rootChildren.getNodeAt(i); + if (!(treeNode.getName().equals(dsname))) { + continue; + } + + // for this data source, get the "Data Sources" child node + Node datasourceGroupingNode = treeNode.getChildren().findChild(DataSourcesNode.NAME); + + // check whether this is the data source we are looking for + parentTreeViewNode = findParentNodeInTree(parentContent, datasourceGroupingNode); + if (parentTreeViewNode != null) { + // found the data source node + break; + } + } } else { + /* If the parent content is null, then the specified + * content is a data source, and the parent tree view node is the + * "Data Sources" node. */ + Node datasourceGroupingNode = rootChildren.findChild(dsname); + if (!Objects.isNull(datasourceGroupingNode)) { + Children dsChildren = datasourceGroupingNode.getChildren(); + parentTreeViewNode = dsChildren.findChild(DataSourcesNode.NAME); + } + } + + if (parentTreeViewNode == null) { MessageNotifyUtil.Message.error(Bundle.ViewContextAction_errorMessage_cannotFindNode()); logger.log(Level.SEVERE, "Failed to locate data source node in tree."); //NON-NLS return; @@ -171,65 +235,12 @@ public class ViewContextAction extends AbstractAction { } else { // Classic view // Start the search at the DataSourcesNode parentTreeViewNode = treeViewExplorerMgr.getRootContext().getChildren().findChild(DataSourcesNode.NAME); - } - /* - * Get the parent content for the content to be selected in the - * results view. If the parent content is null, then the specified - * content is a data source, and the parent tree view node is the - * "Data Sources" node. Otherwise, the tree view needs to be - * searched to find the parent treeview node. - */ - Content parentContent = null; - - try { - parentContent = content.getParent(); - } catch (TskCoreException ex) { - MessageNotifyUtil.Message.error(Bundle.ViewContextAction_errorMessage_cannotFindDirectory()); - logger.log(Level.SEVERE, String.format("Could not get parent of Content object: %s", content), ex); //NON-NLS - return; - } - if (null != parentContent) { - /* - * Get an ordered list of the ancestors of the specified - * content, starting with its data source. - * - */ - AncestorVisitor ancestorVisitor = new AncestorVisitor(); - List contentBranch = parentContent.accept(ancestorVisitor); - Collections.reverse(contentBranch); - - /** - * Convert the list of ancestors into a list of tree nodes. - * - * IMPORTANT: The "dummy" root node used to create this single - * layer of children needs to be wrapped in a - * DirectoryTreeFilterNode so that its child nodes will also be - * wrapped in DirectoryTreeFilterNodes, via - * DirectoryTreeFilterNodeChildren. Otherwise, the display names - * of the nodes in the branch will not have child node counts - * and will not match the display names of the corresponding - * nodes in the actual tree view. - */ - Node dummyRootNode = new DirectoryTreeFilterNode(new AbstractNode(new RootContentChildren(contentBranch)), true); - Children ancestorChildren = dummyRootNode.getChildren(); - - /* - * Search the tree for the parent node. Note that this algorithm - * simply discards "extra" ancestor nodes not shown in the tree, - * such as the root directory of the file system for file system - * content. - */ - Children treeNodeChildren = parentTreeViewNode.getChildren(); - for (int i = 0; i < ancestorChildren.getNodesCount(); i++) { - Node ancestorNode = ancestorChildren.getNodeAt(i); - for (int j = 0; j < treeNodeChildren.getNodesCount(); j++) { - Node treeNode = treeNodeChildren.getNodeAt(j); - if (ancestorNode.getName().equals(treeNode.getName())) { - parentTreeViewNode = treeNode; - treeNodeChildren = treeNode.getChildren(); - break; - } + if (null != parentContent) { + // the tree view needs to be searched to find the parent treeview node. + Node potentialParentTreeViewNode = findParentNodeInTree(parentContent, parentTreeViewNode); + if (potentialParentTreeViewNode != null) { + parentTreeViewNode = potentialParentTreeViewNode; } } } @@ -275,6 +286,60 @@ public class ViewContextAction extends AbstractAction { }); } + /** + * Searches tree for parent node by getting an ordered list of the ancestors + * of the specified content. + * + * @param parentContent parent content for the content to be searched for + * @param node Node tree to search + * @return Node object of the matching parent, NULL if not found + */ + private Node findParentNodeInTree(Content parentContent, Node node) { + /* + * Get an ordered list of the ancestors of the specified + * content, starting with its data source. + * + */ + AncestorVisitor ancestorVisitor = new AncestorVisitor(); + List contentBranch = parentContent.accept(ancestorVisitor); + Collections.reverse(contentBranch); + + /** + * Convert the list of ancestors into a list of tree nodes. + * + * IMPORTANT: The "dummy" root node used to create this single layer of + * children needs to be wrapped in a DirectoryTreeFilterNode so that its + * child nodes will also be wrapped in DirectoryTreeFilterNodes, via + * DirectoryTreeFilterNodeChildren. Otherwise, the display names of the + * nodes in the branch will not have child node counts and will not + * match the display names of the corresponding nodes in the actual tree + * view. + */ + Node dummyRootNode = new DirectoryTreeFilterNode(new AbstractNode(new RootContentChildren(contentBranch)), true); + Children ancestorChildren = dummyRootNode.getChildren(); + + /* + * Search the tree for the parent node. Note that this algorithm + * simply discards "extra" ancestor nodes not shown in the tree, + * such as the root directory of the file system for file system + * content. + */ + Children treeNodeChildren = node.getChildren(); + Node parentTreeViewNode = null; + for (int i = 0; i < ancestorChildren.getNodesCount(); i++) { + Node ancestorNode = ancestorChildren.getNodeAt(i); + for (int j = 0; j < treeNodeChildren.getNodesCount(); j++) { + Node treeNode = treeNodeChildren.getNodeAt(j); + if (ancestorNode.getName().equals(treeNode.getName())) { + parentTreeViewNode = treeNode; + treeNodeChildren = treeNode.getChildren(); + break; + } + } + } + return parentTreeViewNode; + } + /** * A ContentVisitor that returns a list of content objects by starting with * a given content and following its chain of ancestors to the root content diff --git a/Core/src/org/sleuthkit/autopsy/examples/SampleIngestModuleIngestJobSettingsPanel.form b/Core/src/org/sleuthkit/autopsy/examples/SampleIngestModuleIngestJobSettingsPanel.form index 4ea5377716..5629803cd2 100644 --- a/Core/src/org/sleuthkit/autopsy/examples/SampleIngestModuleIngestJobSettingsPanel.form +++ b/Core/src/org/sleuthkit/autopsy/examples/SampleIngestModuleIngestJobSettingsPanel.form @@ -19,7 +19,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/examples/SampleIngestModuleIngestJobSettingsPanel.java b/Core/src/org/sleuthkit/autopsy/examples/SampleIngestModuleIngestJobSettingsPanel.java index 22a7cb3e2f..0d54172cf7 100644 --- a/Core/src/org/sleuthkit/autopsy/examples/SampleIngestModuleIngestJobSettingsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/examples/SampleIngestModuleIngestJobSettingsPanel.java @@ -81,7 +81,7 @@ public class SampleIngestModuleIngestJobSettingsPanel extends IngestModuleIngest .addGroup(layout.createSequentialGroup() .addContainerGap() .addComponent(skipKnownFilesCheckBox) - .addContainerGap(255, Short.MAX_VALUE)) + .addContainerGap(155, Short.MAX_VALUE)) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) diff --git a/Core/src/org/sleuthkit/autopsy/ingest/DataSourceIngestJob.java b/Core/src/org/sleuthkit/autopsy/ingest/DataSourceIngestJob.java index 6563dd3540..22745a3c54 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/DataSourceIngestJob.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/DataSourceIngestJob.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2014-2018 Basis Technology Corp. + * Copyright 2014-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -286,7 +286,7 @@ public final class DataSourceIngestJob { this.addIngestModules(fileIngestModuleTemplates, IngestModuleType.FILE_LEVEL, skCase); this.addIngestModules(secondStageDataSourceModuleTemplates, IngestModuleType.DATA_SOURCE_LEVEL, skCase); } catch (TskCoreException | NoCurrentCaseException ex) { - logger.log(Level.SEVERE, "Failed to add ingest modules to database.", ex); + logErrorMessage(Level.WARNING, "Failed to add ingest modules listing to case database", ex); } } @@ -421,13 +421,13 @@ public final class DataSourceIngestJob { try { this.ingestJob = Case.getCurrentCaseThrows().getSleuthkitCase().addIngestJob(dataSource, NetworkUtils.getLocalHostName(), ingestModules, new Date(this.createTime), new Date(0), IngestJobStatusType.STARTED, ""); } catch (TskCoreException | NoCurrentCaseException ex) { - logger.log(Level.SEVERE, "Failed to add ingest job to database.", ex); + logErrorMessage(Level.WARNING, "Failed to add ingest job info to case database", ex); //NON-NLS } if (this.hasFirstStageDataSourceIngestPipeline() || this.hasFileIngestPipeline()) { - logger.log(Level.INFO, "Starting first stage analysis for {0} (jobId={1})", new Object[]{dataSource.getName(), this.id}); //NON-NLS + logInfoMessage("Starting first stage analysis"); //NON-NLS this.startFirstStage(); } else if (this.hasSecondStageDataSourceIngestPipeline()) { - logger.log(Level.INFO, "Starting second stage analysis for {0} (jobId={1}), no first stage configured", new Object[]{dataSource.getName(), this.id}); //NON-NLS + logInfoMessage("Starting second stage analysis"); //NON-NLS this.startSecondStage(); } } @@ -521,13 +521,13 @@ public final class DataSourceIngestJob { * Schedule the first stage tasks. */ if (this.hasFirstStageDataSourceIngestPipeline() && this.hasFileIngestPipeline()) { - logger.log(Level.INFO, "Scheduling first stage data source and file level analysis tasks for {0} (jobId={1})", new Object[]{dataSource.getName(), this.id}); //NON-NLS + logInfoMessage("Scheduling first stage data source and file level analysis tasks"); //NON-NLS DataSourceIngestJob.taskScheduler.scheduleIngestTasks(this); } else if (this.hasFirstStageDataSourceIngestPipeline()) { - logger.log(Level.INFO, "Scheduling first stage data source level analysis tasks for {0} (jobId={1}), no file level analysis configured", new Object[]{dataSource.getName(), this.id}); //NON-NLS + logInfoMessage("Scheduling first stage data source level analysis tasks"); //NON-NLS DataSourceIngestJob.taskScheduler.scheduleDataSourceIngestTask(this); } else { - logger.log(Level.INFO, "Scheduling file level analysis tasks for {0} (jobId={1}), no first stage data source level analysis configured", new Object[]{dataSource.getName(), this.id}); //NON-NLS + logInfoMessage("Scheduling file level analysis tasks, no first stage data source level analysis configured"); //NON-NLS DataSourceIngestJob.taskScheduler.scheduleFileIngestTasks(this, this.files); /** @@ -546,7 +546,7 @@ public final class DataSourceIngestJob { * Starts the second stage of this ingest job. */ private void startSecondStage() { - logger.log(Level.INFO, "Starting second stage analysis for {0} (jobId={1})", new Object[]{dataSource.getName(), this.id}); //NON-NLS + logInfoMessage("Starting second stage analysis"); //NON-NLS this.stage = DataSourceIngestJob.Stages.SECOND; if (this.doUI) { this.startDataSourceIngestProgressBar(); @@ -554,7 +554,7 @@ public final class DataSourceIngestJob { synchronized (this.dataSourceIngestPipelineLock) { this.currentDataSourceIngestPipeline = this.secondStageDataSourceIngestPipeline; } - logger.log(Level.INFO, "Scheduling second stage data source level analysis tasks for {0} (jobId={1})", new Object[]{dataSource.getName(), this.id}); //NON-NLS + logInfoMessage("Scheduling second stage data source level analysis tasks"); //NON-NLS DataSourceIngestJob.taskScheduler.scheduleDataSourceIngestTask(this); } @@ -643,7 +643,7 @@ public final class DataSourceIngestJob { * job and starts the second stage, if appropriate. */ private void finishFirstStage() { - logger.log(Level.INFO, "Finished first stage analysis for {0} (jobId={1})", new Object[]{dataSource.getName(), this.id}); //NON-NLS + logInfoMessage("Finished first stage analysis"); //NON-NLS // Shut down the file ingest pipelines. Note that no shut down is // required for the data source ingest pipeline because data source @@ -693,7 +693,7 @@ public final class DataSourceIngestJob { * Shuts down the ingest pipelines and progress bars for this job. */ private void finish() { - logger.log(Level.INFO, "Finished analysis for {0} (jobId={1})", new Object[]{dataSource.getName(), this.id}); //NON-NLS + logInfoMessage("Finished analysis"); //NON-NLS this.stage = DataSourceIngestJob.Stages.FINALIZATION; if (this.doUI) { @@ -711,19 +711,19 @@ public final class DataSourceIngestJob { try { ingestJob.setIngestJobStatus(IngestJobStatusType.CANCELLED); } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Failed to set ingest status for ingest job in database.", ex); + logErrorMessage(Level.WARNING, "Failed to update ingest job status in case database", ex); } } else { try { ingestJob.setIngestJobStatus(IngestJobStatusType.COMPLETED); } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Failed to set ingest status for ingest job in database.", ex); + logErrorMessage(Level.WARNING, "Failed to update ingest job status in case database", ex); } } try { this.ingestJob.setEndDateTime(new Date()); } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Failed to set end date for ingest job in database.", ex); + logErrorMessage(Level.WARNING, "Failed to set job end date in case database", ex); } } this.parentJob.dataSourceJobFinished(this); @@ -842,7 +842,7 @@ public final class DataSourceIngestJob { if (DataSourceIngestJob.Stages.FIRST == this.stage) { DataSourceIngestJob.taskScheduler.fastTrackFileIngestTasks(this, files); } else { - DataSourceIngestJob.logger.log(Level.SEVERE, "Adding files during second stage not supported"); //NON-NLS + logErrorMessage(Level.SEVERE, "Adding files to job during second stage analysis not supported"); } /** @@ -1066,6 +1066,39 @@ public final class DataSourceIngestJob { return this.cancellationReason; } + /** + * Writes an info message to the application log that includes the data + * source name, data source object id, and the job id. + * + * @param message The message. + */ + private void logInfoMessage(String message) { + logger.log(Level.INFO, String.format("%s (data source = %s, objId = %d, jobId = %d)", message, dataSource.getName(), dataSource.getId(), id)); //NON-NLS + } + + /** + * Writes an error message to the application log that includes the data + * source name, data source object id, and the job id. + * + * @param level The logging level for the message. + * @param message The message. + * @param throwable The throwable associated with the error. + */ + private void logErrorMessage(Level level, String message, Throwable throwable) { + logger.log(level, String.format("%s (data source = %s, objId = %d, jobId = %d)", message, dataSource.getName(), dataSource.getId(), id), throwable); //NON-NLS + } + + /** + * Writes an error message to the application log that includes the data + * source name, data source object id, and the job id. + * + * @param level The logging level for the message. + * @param message The message. + */ + private void logErrorMessage(Level level, String message) { + logger.log(level, String.format("%s (data source = %s, objId = %d, jobId = %d)", message, dataSource.getName(), dataSource.getId(), id)); //NON-NLS + } + /** * Write ingest module errors to the log. * @@ -1073,7 +1106,7 @@ public final class DataSourceIngestJob { */ private void logIngestModuleErrors(List errors) { for (IngestModuleError error : errors) { - DataSourceIngestJob.logger.log(Level.SEVERE, String.format("%s experienced an error analyzing %s (jobId=%d)", error.getModuleDisplayName(), dataSource.getName(), this.id), error.getThrowable()); //NON-NLS + logErrorMessage(Level.SEVERE, String.format("%s experienced an error during analysis", error.getModuleDisplayName()), error.getThrowable()); //NON-NLS } } diff --git a/Core/src/org/sleuthkit/autopsy/ingest/IngestManager.java b/Core/src/org/sleuthkit/autopsy/ingest/IngestManager.java index d5a2240ca5..b59e0a6ccb 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/IngestManager.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/IngestManager.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2012-2018 Basis Technology Corp. + * Copyright 2012-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -403,10 +403,10 @@ public class IngestManager implements IngestProgressSnapshotProvider { synchronized (ingestJobsById) { ingestJobsById.put(job.getId(), job); } + IngestManager.logger.log(Level.INFO, "Starting ingest job {0}", job.getId()); //NON-NLS errors = job.start(); if (errors.isEmpty()) { this.fireIngestJobStarted(job.getId()); - IngestManager.logger.log(Level.INFO, "Ingest job {0} started", job.getId()); //NON-NLS } else { synchronized (ingestJobsById) { this.ingestJobsById.remove(job.getId()); diff --git a/Core/src/org/sleuthkit/autopsy/ingest/IngestModuleIngestJobSettingsPanel.java b/Core/src/org/sleuthkit/autopsy/ingest/IngestModuleIngestJobSettingsPanel.java index cb321f35cb..f356b4d04f 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/IngestModuleIngestJobSettingsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/IngestModuleIngestJobSettingsPanel.java @@ -22,7 +22,8 @@ import javax.swing.JPanel; /** * Abstract base class for panels that allow users to specify per ingest job - * settings for ingest modules. + * settings for ingest modules. The max recommended dimensions for these panels + * is 300 width by 300 height. */ public abstract class IngestModuleIngestJobSettingsPanel extends JPanel { diff --git a/Core/src/org/sleuthkit/autopsy/ingest/IngestServices.java b/Core/src/org/sleuthkit/autopsy/ingest/IngestServices.java index 386326df10..8108bba1b2 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/IngestServices.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/IngestServices.java @@ -170,8 +170,6 @@ public final class IngestServices { * Sets all of the global configuration settings for an ingest module. * * @param moduleName A unique identifier for the module. - * - * @param moduleName moduleName identifier unique to that module * @param settings A mapping of setting names to setting values. * */ diff --git a/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/Bundle.properties b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/Bundle.properties new file mode 100644 index 0000000000..7eb2002f9e --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/Bundle.properties @@ -0,0 +1,122 @@ +ConfigureLogicalImagerDialog.loadButton.text=Load +ConfigureLogicalImagerDialog.newButton.text=New +ConfigureLogicalImagerDialog.title=Configure Logical Imager +ConfigureLogicalImagerDialog.saveButton.text=Save +ConfigureLogicalImagerDialog.configFile.text= +ConfigureLogicalImagerDialog.configFile.toolTipText= +ConfigureLogicalImagerDialog.jLabel1.text=Rule Set: +ConfigureLogicalImagerDialog.finalizeImageWriter.text=Finalize Image Writer +ConfigureLogicalImagerDialog.newRuleButton.text=New Rule +ConfigureLogicalImagerDialog.editRuleButton.text=Edit Rule +ConfigureLogicalImagerDialog.deleteRuleButton.text=Delete Rule +ConfigureLogicalImagerDialog.jLabel2.text=Rule Details +ConfigureLogicalImagerDialog.shouldSaveCheckBox.text=Should Save +ConfigureLogicalImagerDialog.shouldAlertCheckBox.text=Should Alert +ConfigureLogicalImagerDialog.jLabel3.text=Extensions: +ConfigureLogicalImagerDialog.extensionsTextField.text= +ConfigureLogicalImagerDialog.filenamesLabel.text=File names: +ConfigureLogicalImagerDialog.folderNamesLabel.text=Folder names: +ConfigureLogicalImagerDialog.jLabel4.text=File size: +ConfigureLogicalImagerDialog.daysIncludedLabel.text=day(s) +ConfigureLogicalImagerDialog.daysIncludedTextField.text= +ConfigureLogicalImagerDialog.modifiedDateLabel.text=Modified Within: +ConfigureLogicalImagerDialog.fullPathsLabel.text=Full paths: +ConfigureLogicalImagerDialog.flagEncryptionProgramsCheckBox.text=Flag encryption programs +EditRulePanel.shouldAlertCheckBox.text=Alert in imager console if rule matches +EditRulePanel.shouldSaveCheckBox.text=Extract file if it matches a rule +EditRulePanel.fullPathsLabel.text=Full paths: +EditRulePanel.daysIncludedLabel.text=day(s) +EditRulePanel.daysIncludedTextField.text= +EditRulePanel.modifiedDateLabel.text=Modified Within: +EditRulePanel.folderNamesLabel.text=Folder names: +EditRulePanel.filenamesLabel.text=File names: +EditRulePanel.extensionsTextField.text= +ConfigureLogicalImagerDialog.jLabel5.text=Rule Name: +ConfigureLogicalImagerDialog.jLabel6.text=Description: +ConfigureLogicalImagerDialog.ruleNameEditTextField.text= +ConfigureLogicalImagerDialog.descriptionEditTextField.text= +ConfigureLogicalImagerDialog.fullPathsTable.columnModel.title0= +ConfigVisualPanel1.jTextField1.text= +ConfigVisualPanel1.jLabel1.text=Configuration file +ConfigVisualPanel2.jCheckBox1.text=jCheckBox1 +ConfigVisualPanel2.jCheckBox2.text=jCheckBox2 +ConfigVisualPanel2.jTextField1.text=jTextField1 +ConfigVisualPanel1.jRadioButton1.text=Create new configuration +ConfigVisualPanel1.jRadioButton2.text=Open existing configuration +ConfigVisualPanel2.modifiedDateLabel.text=Modified Within: +ConfigVisualPanel2.folderNamesLabel.text=Folder names: +ConfigVisualPanel2.filenamesLabel.text=File names: +ConfigVisualPanel2.shouldAlertCheckBox.text=Alert in imager console +ConfigVisualPanel2.shouldSaveCheckBox.text=Extract file +ConfigVisualPanel2.deleteRuleButton.text=Delete Rule +ConfigVisualPanel2.editRuleButton.text=Edit Rule +ConfigVisualPanel2.newRuleButton.text=New Rule +ConfigVisualPanel2.fullPathsLabel.text=Full paths: +ConfigVisualPanel2.daysIncludedLabel.text=day(s) +ConfigVisualPanel2.filenamesTable.columnModel.title0= +ConfigVisualPanel2.fileSizeLabel.text=File size in bytes: +ConfigVisualPanel2.extensionsLabel.text=Extensions: +ConfigVisualPanel2.descriptionLabel.text=Description: +ConfigVisualPanel2.ruleNameLabel.text=Rule name: +ConfigVisualPanel2.ruleSetFileLabel.text=Configuration rule file: +EditRulePanel.ruleNameLabel.text=Rule Set: +EditRulePanel.descriptionTextField.text= +EditRulePanel.extensionsLabel.text=Extensions: +EditRulePanel.ruleNameTextField.text= +EditRulePanel.extensionsCheckBox.text= +EditRulePanel.filenamesCheckBox.text= +EditRulePanel.folderNamesCheckBox.text= +EditRulePanel.fullPathsCheckBox.text= +EditRulePanel.fileSizeCheckBox.text= +EditRulePanel.minDaysCheckBox.text= +EditRulePanel.fileSizeLabel.text=File size: +EditRulePanel.descriptionLabel.text=Description: +EditRulePanel.jTable1.columnModel.title3=Title 4 +EditRulePanel.jTable1.columnModel.title2=Title 3 +EditRulePanel.jTable1.columnModel.title1=Title 2 +EditRulePanel.shouldAlertCheckBox.actionCommand= +EditFullPathsRulePanel.ruleNameLabel.text=Rule name: +EditFullPathsRulePanel.shouldAlertCheckBox.actionCommand= +EditFullPathsRulePanel.shouldAlertCheckBox.text=Alert in imager console +EditFullPathsRulePanel.shouldSaveCheckBox.text=Extract file +EditFullPathsRulePanel.fullPathsLabel.text=Full paths: +EditFullPathsRulePanel.fullPathsLabel.toolTipText= +EditNonFullPathsRulePanel.ruleNameLabel.text=Rule name: +EditNonFullPathsRulePanel.shouldSaveCheckBox.text=Extract file +EditNonFullPathsRulePanel.daysIncludedLabel.text=day(s) +EditNonFullPathsRulePanel.shouldAlertCheckBox.actionCommand= +EditNonFullPathsRulePanel.shouldAlertCheckBox.text=Alert in imager console +ConfigVisualPanel1.browseButton.text=Browse +ConfigVisualPanel2.fullPathsTable.columnModel.title0= +ConfigVisualPanel2.folderNamesTable.columnModel.title0= +ConfigVisualPanel2.shouldSaveCheckBox.toolTipText= +ConfigVisualPanel2.maxSizeLabel.text=Maximum: +ConfigVisualPanel2.minSizeLabel.text=Minimum: +EditNonFullPathsRulePanel.minDaysTextField.text=jFormattedTextField1 +ConfigVisualPanel1.browseButton.toolTipText= +EditNonFullPathsRulePanel.userFolderNote.text=Starting a folder name with the token [USER_FOLDER] will allow matches of all user folders in the file system. +EditNonFullPathsRulePanel.modifiedWithinCheckbox.text=Modified within: +EditNonFullPathsRulePanel.folderNamesCheckbox.text=Folder names: +EditNonFullPathsRulePanel.fileNamesCheckbox.text=File names: +EditNonFullPathsRulePanel.extensionsCheckbox.text=Extensions: +EditNonFullPathsRulePanel.filenamesScrollPane.toolTipText= +EditNonFullPathsRulePanel.maxSizeCheckbox.text=Maximum size: +EditNonFullPathsRulePanel.minSizeCheckbox.text=Minimum size: +NewRulePanel.chooseLabel.text=Choose the type of rule +ConfigVisualPanel1.configureDriveRadioButton.text_1=Configure selected external drive: +ConfigVisualPanel1.configureFolderRadioButton.text_1=Configure in a folder: +ConfigVisualPanel1.descriptionTextArea.text=Select a location for the Logical Imager. This location will contain the imaging program and a configuration file. If that location already contains a configuration file, it will be loaded to edit. Imaging results will be saved to this location, so ensure it has enough free space. +ConfigVisualPanel1.refreshButton.text=Refresh +ConfigVisualPanel3.saveButton.text=Save +ConfigVisualPanel3.configLabel.text=Logical Imager config file save status: +ConfigVisualPanel3.executableLabel.text=Logical Imager executable save status: +ConfigVisualPanel3.executableStatusLabel.text= +EditFullPathsRulePanel.jLabel1.text=Description (Optional): +EditNonFullPathsRulePanel.jLabel2.text=Folder name matches are case insensitive and occur anywhere in a path. +EditNonFullPathsRulePanel.descriptionLabel.text=Description (Optional): +EditNonFullPathsRulePanel.jLabel1.text=If file is found: +EditFullPathsRulePanel.jLabel2.text=If file is found: +ConfigVisualPanel2.flagEncryptionProgramsCheckBox.text=Alert if encryption programs are found +ConfigVisualPanel2.finalizeImageWriter.text=Continue imaging after searches are performed +EditNonFullPathsRulePanel.fileNamesInfoLabel.text=File names are case insensitive. +EditNonFullPathsRulePanel.extensionsInfoLabel.text=Extensions are case insensitive. diff --git a/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/Bundle.properties-MERGED new file mode 100644 index 0000000000..5846a4ad34 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/Bundle.properties-MERGED @@ -0,0 +1,195 @@ +ConfigureLogicalImagerDialog.loadButton.text=Load +ConfigureLogicalImagerDialog.newButton.text=New +ConfigureLogicalImagerDialog.title=Configure Logical Imager +ConfigureLogicalImagerDialog.saveButton.text=Save +ConfigureLogicalImagerDialog.configFile.text= +ConfigureLogicalImagerDialog.configFile.toolTipText= +ConfigureLogicalImagerDialog.jLabel1.text=Rule Set: +ConfigureLogicalImagerDialog.finalizeImageWriter.text=Finalize Image Writer +ConfigureLogicalImagerDialog.newRuleButton.text=New Rule +ConfigureLogicalImagerDialog.editRuleButton.text=Edit Rule +ConfigureLogicalImagerDialog.deleteRuleButton.text=Delete Rule +ConfigureLogicalImagerDialog.jLabel2.text=Rule Details +ConfigureLogicalImagerDialog.shouldSaveCheckBox.text=Should Save +ConfigureLogicalImagerDialog.shouldAlertCheckBox.text=Should Alert +ConfigureLogicalImagerDialog.jLabel3.text=Extensions: +ConfigureLogicalImagerDialog.extensionsTextField.text= +ConfigureLogicalImagerDialog.filenamesLabel.text=File names: +ConfigureLogicalImagerDialog.folderNamesLabel.text=Folder names: +ConfigureLogicalImagerDialog.jLabel4.text=File size: +ConfigureLogicalImagerDialog.daysIncludedLabel.text=day(s) +ConfigureLogicalImagerDialog.daysIncludedTextField.text= +ConfigureLogicalImagerDialog.modifiedDateLabel.text=Modified Within: +ConfigureLogicalImagerDialog.fullPathsLabel.text=Full paths: +ConfigureLogicalImagerDialog.flagEncryptionProgramsCheckBox.text=Flag encryption programs +ConfigVisualPanel1.chooseFileTitle=Select a Logical Imager configuration +# {0} - filename +ConfigVisualPanel1.configFileIsEmpty=Configuration file {0} is empty +ConfigVisualPanel1.configurationError=Configuration error +ConfigVisualPanel1.fileNameExtensionFilter=Configuration JSON File +ConfigVisualPanel1.invalidConfigJson=Invalid config JSON: +ConfigVisualPanel1.messageLabel.noExternalDriveFound=No drive found +ConfigVisualPanel1.selectConfigurationFile=Select location +ConfigVisualPanel2.cancel=Cancel +ConfigVisualPanel2.deleteRuleSet=Delete rule +ConfigVisualPanel2.deleteRuleSetConfirmation=Delete rule confirmation +ConfigVisualPanel2.editConfiguration=Configure imager +ConfigVisualPanel2.editRuleError=Edit rule error +ConfigVisualPanel2.editRuleSet=Edit Rule +ConfigVisualPanel2.newRule.name=New Rule +# {0} - ruleName +ConfigVisualPanel2.newRuleError.duplicateName=A rule with name "{0}" already exists. Please enter a different rule name +ConfigVisualPanel2.newRuleError.title=New rule error +ConfigVisualPanel2.ok=OK +ConfigVisualPanel2.rulesTable.columnModel.title0=Rule Name +ConfigVisualPanel2.rulesTable.columnModel.title1=Description +ConfigVisualPanel3.copyStatus.error=Unable to save file +ConfigVisualPanel3.copyStatus.notSaved=File has not been saved +ConfigVisualPanel3.copyStatus.saved=Saved +ConfigVisualPanel3.copyStatus.savingInProgress=Saving file, please wait +# {0} - configurationLocation +ConfigVisualPanel3.description.text=Press Save to write the imaging tool and configuration file to the destination.\nDestination: {0} +ConfigVisualPanel3.errorMsg.cannotFindLogicalImager=Cannot locate logical imager, cannot copy to destination +# {0} - configFilename +ConfigVisualPanel3.failedToSaveConfigMsg=Failed to save configuration file: {0} +ConfigVisualPanel3.failedToSaveExeMsg=Failed to save tsk_logical_imager.exe file +# {0} - reason +ConfigVisualPanel3.reason=\nReason: {0} +ConfigVisualPanel3.saveConfigurationFile=Save imager +CreateLogicalImagerAction.title=Create Logical Imager +CTL_CreateLogicalImagerAction=Create Logical Imager +EditFullPathsRulePanel.example=Example: +EditFullPathsRulePanel.fullPaths=Full paths +EditNonFullPathsRulePanel.emptyExtensionException=Extensions cannot have an empty entry +EditNonFullPathsRulePanel.example=Example: +EditNonFullPathsRulePanel.fileNames=File names +EditNonFullPathsRulePanel.folderNames=Folder names +# {0} - message +EditNonFullPathsRulePanel.maxFileSizeMustBeNumberException=Maximum file size must be a number: {0} +EditNonFullPathsRulePanel.maxFileSizeNotPositiveException=Maximum file size must be a positive +# {0} - maxFileSize +# {1} - minFileSize +EditNonFullPathsRulePanel.maxFileSizeSmallerThanMinException=Maximum file size: {0} bytes must be bigger than minimum file size: {1} bytes +# {0} - message +EditNonFullPathsRulePanel.minFileSizeMustBeNumberException=Minimum file size must be a number: {0} +EditNonFullPathsRulePanel.minFileSizeNotPositiveException=Minimum file size must be a positive +# {0} - message +EditNonFullPathsRulePanel.modifiedDaysMustBeNumberException=Modified days must be a number: {0} +EditNonFullPathsRulePanel.modifiedDaysNotPositiveException=Modified days must be a positive +EditNonFullPathsRulePanel.units.bytes=Bytes +EditNonFullPathsRulePanel.units.gigabytes=Gigabytes +EditNonFullPathsRulePanel.units.kilobytes=Kilobytes +EditNonFullPathsRulePanel.units.megabytes=Megabytes +# {0} - fieldName +EditRulePanel.blankLineException={0} cannot have a blank line +EditRulePanel.emptyRuleName.message=Rule name cannot be empty +# {0} - ruleName +EditRulePanel.reservedRuleName.message=Rule name "{0}" is reserved for use with a predefined rule +EditRulePanel.shouldAlertCheckBox.text=Alert in imager console if rule matches +EditRulePanel.shouldSaveCheckBox.text=Extract file if it matches a rule +EditRulePanel.fullPathsLabel.text=Full paths: +EditRulePanel.daysIncludedLabel.text=day(s) +EditRulePanel.daysIncludedTextField.text= +EditRulePanel.modifiedDateLabel.text=Modified Within: +EditRulePanel.folderNamesLabel.text=Folder names: +EditRulePanel.filenamesLabel.text=File names: +EditRulePanel.extensionsTextField.text= +ConfigureLogicalImagerDialog.jLabel5.text=Rule Name: +ConfigureLogicalImagerDialog.jLabel6.text=Description: +ConfigureLogicalImagerDialog.ruleNameEditTextField.text= +ConfigureLogicalImagerDialog.descriptionEditTextField.text= +ConfigureLogicalImagerDialog.fullPathsTable.columnModel.title0= +ConfigVisualPanel1.jTextField1.text= +ConfigVisualPanel1.jLabel1.text=Configuration file +ConfigVisualPanel2.jCheckBox1.text=jCheckBox1 +ConfigVisualPanel2.jCheckBox2.text=jCheckBox2 +ConfigVisualPanel2.jTextField1.text=jTextField1 +ConfigVisualPanel1.jRadioButton1.text=Create new configuration +ConfigVisualPanel1.jRadioButton2.text=Open existing configuration +ConfigVisualPanel2.modifiedDateLabel.text=Modified Within: +ConfigVisualPanel2.folderNamesLabel.text=Folder names: +ConfigVisualPanel2.filenamesLabel.text=File names: +ConfigVisualPanel2.shouldAlertCheckBox.text=Alert in imager console +ConfigVisualPanel2.shouldSaveCheckBox.text=Extract file +ConfigVisualPanel2.deleteRuleButton.text=Delete Rule +ConfigVisualPanel2.editRuleButton.text=Edit Rule +ConfigVisualPanel2.newRuleButton.text=New Rule +ConfigVisualPanel2.fullPathsLabel.text=Full paths: +ConfigVisualPanel2.daysIncludedLabel.text=day(s) +ConfigVisualPanel2.filenamesTable.columnModel.title0= +ConfigVisualPanel2.fileSizeLabel.text=File size in bytes: +ConfigVisualPanel2.extensionsLabel.text=Extensions: +ConfigVisualPanel2.descriptionLabel.text=Description: +ConfigVisualPanel2.ruleNameLabel.text=Rule name: +ConfigVisualPanel2.ruleSetFileLabel.text=Configuration rule file: +EditRulePanel.ruleNameLabel.text=Rule Set: +EditRulePanel.descriptionTextField.text= +EditRulePanel.extensionsLabel.text=Extensions: +EditRulePanel.ruleNameTextField.text= +EditRulePanel.extensionsCheckBox.text= +EditRulePanel.filenamesCheckBox.text= +EditRulePanel.folderNamesCheckBox.text= +EditRulePanel.fullPathsCheckBox.text= +EditRulePanel.fileSizeCheckBox.text= +EditRulePanel.minDaysCheckBox.text= +EditRulePanel.fileSizeLabel.text=File size: +EditRulePanel.descriptionLabel.text=Description: +EditRulePanel.jTable1.columnModel.title3=Title 4 +EditRulePanel.jTable1.columnModel.title2=Title 3 +EditRulePanel.jTable1.columnModel.title1=Title 2 +EditRulePanel.shouldAlertCheckBox.actionCommand= +EditFullPathsRulePanel.ruleNameLabel.text=Rule name: +EditFullPathsRulePanel.shouldAlertCheckBox.actionCommand= +EditFullPathsRulePanel.shouldAlertCheckBox.text=Alert in imager console +EditFullPathsRulePanel.shouldSaveCheckBox.text=Extract file +EditFullPathsRulePanel.fullPathsLabel.text=Full paths: +EditFullPathsRulePanel.fullPathsLabel.toolTipText= +EditNonFullPathsRulePanel.ruleNameLabel.text=Rule name: +EditNonFullPathsRulePanel.shouldSaveCheckBox.text=Extract file +EditNonFullPathsRulePanel.daysIncludedLabel.text=day(s) +EditNonFullPathsRulePanel.shouldAlertCheckBox.actionCommand= +EditNonFullPathsRulePanel.shouldAlertCheckBox.text=Alert in imager console +ConfigVisualPanel1.browseButton.text=Browse +ConfigVisualPanel2.fullPathsTable.columnModel.title0= +ConfigVisualPanel2.folderNamesTable.columnModel.title0= +ConfigVisualPanel2.shouldSaveCheckBox.toolTipText= +ConfigVisualPanel2.maxSizeLabel.text=Maximum: +ConfigVisualPanel2.minSizeLabel.text=Minimum: +EditNonFullPathsRulePanel.minDaysTextField.text=jFormattedTextField1 +ConfigVisualPanel1.browseButton.toolTipText= +EditNonFullPathsRulePanel.userFolderNote.text=Starting a folder name with the token [USER_FOLDER] will allow matches of all user folders in the file system. +EditNonFullPathsRulePanel.modifiedWithinCheckbox.text=Modified within: +EditNonFullPathsRulePanel.folderNamesCheckbox.text=Folder names: +EditNonFullPathsRulePanel.fileNamesCheckbox.text=File names: +EditNonFullPathsRulePanel.extensionsCheckbox.text=Extensions: +EditNonFullPathsRulePanel.filenamesScrollPane.toolTipText= +EditNonFullPathsRulePanel.maxSizeCheckbox.text=Maximum size: +EditNonFullPathsRulePanel.minSizeCheckbox.text=Minimum size: +EncryptionProgramsRule.encryptionProgramsRuleDescription=Find encryption programs +EncryptionProgramsRule.encryptionProgramsRuleName=Encryption Programs +LogicalImagerConfigDeserializer.fullPathsException=A rule with full-paths cannot have other rule definitions +LogicalImagerConfigDeserializer.missingRuleSetException=Missing rule-set +# {0} - key +LogicalImagerConfigDeserializer.unsupportedKeyException=Unsupported key: {0} +NewRulePanel.chooseLabel.text=Choose the type of rule +ConfigVisualPanel1.configureDriveRadioButton.text_1=Configure selected external drive: +ConfigVisualPanel1.configureFolderRadioButton.text_1=Configure in a folder: +ConfigVisualPanel1.descriptionTextArea.text=Select a location for the Logical Imager. This location will contain the imaging program and a configuration file. If that location already contains a configuration file, it will be loaded to edit. Imaging results will be saved to this location, so ensure it has enough free space. +ConfigVisualPanel1.refreshButton.text=Refresh +ConfigVisualPanel3.saveButton.text=Save +ConfigVisualPanel3.configLabel.text=Logical Imager config file save status: +ConfigVisualPanel3.executableLabel.text=Logical Imager executable save status: +ConfigVisualPanel3.executableStatusLabel.text= +EditFullPathsRulePanel.jLabel1.text=Description (Optional): +EditNonFullPathsRulePanel.jLabel2.text=Folder name matches are case insensitive and occur anywhere in a path. +EditNonFullPathsRulePanel.descriptionLabel.text=Description (Optional): +EditNonFullPathsRulePanel.jLabel1.text=If file is found: +EditFullPathsRulePanel.jLabel2.text=If file is found: +ConfigVisualPanel2.flagEncryptionProgramsCheckBox.text=Alert if encryption programs are found +ConfigVisualPanel2.finalizeImageWriter.text=Continue imaging after searches are performed +EditNonFullPathsRulePanel.fileNamesInfoLabel.text=File names are case insensitive. +EditNonFullPathsRulePanel.extensionsInfoLabel.text=Extensions are case insensitive. +NewRuleSetPanel.attributeRule.description=Search for files based on one or more attributes or metadata fields. +NewRuleSetPanel.attributeRule.name=Attribute +NewRuleSetPanel.fullPathRule.description=Search for files based on full exact match path. +NewRuleSetPanel.fullPathRule.name=Full Path diff --git a/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/ConfigVisualPanel1.form b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/ConfigVisualPanel1.form new file mode 100644 index 0000000000..9cdd08be3f --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/ConfigVisualPanel1.form @@ -0,0 +1,201 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/ConfigVisualPanel1.java b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/ConfigVisualPanel1.java new file mode 100644 index 0000000000..2765bb1856 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/ConfigVisualPanel1.java @@ -0,0 +1,481 @@ +/* + * Autopsy + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.logicalimager.configuration; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonIOException; +import com.google.gson.JsonSyntaxException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileStore; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import javax.swing.JFileChooser; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.filechooser.FileFilter; +import javax.swing.filechooser.FileNameExtensionFilter; +import javax.swing.filechooser.FileSystemView; +import org.apache.commons.lang.StringUtils; +import org.openide.util.NbBundle; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.logicalimager.dsp.DriveListUtils; + +/** + * Configuration Visual Panel 1 + */ +@SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives +final class ConfigVisualPanel1 extends JPanel { + + private static final Logger logger = Logger.getLogger(ConfigVisualPanel1.class.getName()); + private static final long serialVersionUID = 1L; + private static final String DEFAULT_CONFIG_FILE_NAME = "logical-imager-config.json"; + private static final String UPDATE_UI_EVENT_NAME = "UPDATE_UI"; + private String configFilename; + + /** + * Creates new form ConfigVisualPanel1 + */ + ConfigVisualPanel1() { + initComponents(); + configFileTextField.getDocument().addDocumentListener(new MyDocumentListener(this)); + refreshDriveList(); + SwingUtilities.invokeLater(() -> { + updateControls(); + }); + } + + @NbBundle.Messages({ + "ConfigVisualPanel1.selectConfigurationFile=Select location" + }) + @Override + public String getName() { + return Bundle.ConfigVisualPanel1_selectConfigurationFile(); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + + configurationLocationButtonGroup = new javax.swing.ButtonGroup(); + configFileTextField = new javax.swing.JTextField(); + browseButton = new javax.swing.JButton(); + descriptionScrollPane = new javax.swing.JScrollPane(); + descriptionTextArea = new javax.swing.JTextArea(); + configureDriveRadioButton = new javax.swing.JRadioButton(); + configureFolderRadioButton = new javax.swing.JRadioButton(); + driveListScrollPane = new javax.swing.JScrollPane(); + driveList = new javax.swing.JList<>(); + refreshButton = new javax.swing.JButton(); + warningLabel = new javax.swing.JLabel(); + + configFileTextField.setEditable(false); + configFileTextField.setEnabled(false); + + org.openide.awt.Mnemonics.setLocalizedText(browseButton, org.openide.util.NbBundle.getMessage(ConfigVisualPanel1.class, "ConfigVisualPanel1.browseButton.text")); // NOI18N + browseButton.setToolTipText(org.openide.util.NbBundle.getMessage(ConfigVisualPanel1.class, "ConfigVisualPanel1.browseButton.toolTipText")); // NOI18N + browseButton.setEnabled(false); + browseButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + browseButtonActionPerformed(evt); + } + }); + + descriptionTextArea.setEditable(false); + descriptionTextArea.setBackground(new java.awt.Color(240, 240, 240)); + descriptionTextArea.setColumns(20); + descriptionTextArea.setFont(new java.awt.Font("Tahoma", 0, 11)); // NOI18N + descriptionTextArea.setLineWrap(true); + descriptionTextArea.setRows(4); + descriptionTextArea.setText(org.openide.util.NbBundle.getMessage(ConfigVisualPanel1.class, "ConfigVisualPanel1.descriptionTextArea.text")); // NOI18N + descriptionTextArea.setWrapStyleWord(true); + descriptionTextArea.setEnabled(false); + descriptionScrollPane.setViewportView(descriptionTextArea); + + configurationLocationButtonGroup.add(configureDriveRadioButton); + configureDriveRadioButton.setSelected(true); + org.openide.awt.Mnemonics.setLocalizedText(configureDriveRadioButton, org.openide.util.NbBundle.getMessage(ConfigVisualPanel1.class, "ConfigVisualPanel1.configureDriveRadioButton.text_1")); // NOI18N + configureDriveRadioButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + configureDriveRadioButtonActionPerformed(evt); + } + }); + + configurationLocationButtonGroup.add(configureFolderRadioButton); + org.openide.awt.Mnemonics.setLocalizedText(configureFolderRadioButton, org.openide.util.NbBundle.getMessage(ConfigVisualPanel1.class, "ConfigVisualPanel1.configureFolderRadioButton.text_1")); // NOI18N + configureFolderRadioButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + configureFolderRadioButtonActionPerformed(evt); + } + }); + + driveList.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); + driveList.setEnabled(false); + driveList.addMouseListener(new java.awt.event.MouseAdapter() { + public void mouseReleased(java.awt.event.MouseEvent evt) { + driveListMouseReleasedSelection(evt); + } + }); + driveList.addKeyListener(new java.awt.event.KeyAdapter() { + public void keyReleased(java.awt.event.KeyEvent evt) { + driveListKeyReleasedSelection(evt); + } + }); + driveListScrollPane.setViewportView(driveList); + + org.openide.awt.Mnemonics.setLocalizedText(refreshButton, org.openide.util.NbBundle.getMessage(ConfigVisualPanel1.class, "ConfigVisualPanel1.refreshButton.text")); // NOI18N + refreshButton.setEnabled(false); + refreshButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + refreshButtonActionPerformed(evt); + } + }); + + warningLabel.setForeground(new java.awt.Color(255, 0, 0)); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(warningLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(descriptionScrollPane) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGap(21, 21, 21) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(driveListScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 437, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(configFileTextField, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.PREFERRED_SIZE, 437, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(refreshButton, javax.swing.GroupLayout.PREFERRED_SIZE, 87, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(browseButton, javax.swing.GroupLayout.PREFERRED_SIZE, 87, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addComponent(configureDriveRadioButton) + .addComponent(configureFolderRadioButton)) + .addGap(0, 0, Short.MAX_VALUE))) + .addContainerGap()) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(descriptionScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(13, 13, 13) + .addComponent(configureDriveRadioButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(driveListScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE) + .addGroup(layout.createSequentialGroup() + .addComponent(refreshButton) + .addGap(0, 95, Short.MAX_VALUE))) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(configureFolderRadioButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(configFileTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(browseButton)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(warningLabel) + .addContainerGap(60, Short.MAX_VALUE)) + ); + }// //GEN-END:initComponents + + @NbBundle.Messages({ + "ConfigVisualPanel1.chooseFileTitle=Select a Logical Imager configuration" + }) + private void browseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_browseButtonActionPerformed + chooseFile(Bundle.ConfigVisualPanel1_chooseFileTitle()); + }//GEN-LAST:event_browseButtonActionPerformed + + private void configureFolderRadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_configureFolderRadioButtonActionPerformed + updateControls(); + }//GEN-LAST:event_configureFolderRadioButtonActionPerformed + + private void configureDriveRadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_configureDriveRadioButtonActionPerformed + updateControls(); + }//GEN-LAST:event_configureDriveRadioButtonActionPerformed + + private void refreshButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_refreshButtonActionPerformed + refreshDriveList(); + }//GEN-LAST:event_refreshButtonActionPerformed + + private void driveListKeyReleasedSelection(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_driveListKeyReleasedSelection + firePropertyChange(UPDATE_UI_EVENT_NAME, false, true); // NON-NLS + }//GEN-LAST:event_driveListKeyReleasedSelection + + private void driveListMouseReleasedSelection(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_driveListMouseReleasedSelection + firePropertyChange(UPDATE_UI_EVENT_NAME, false, true); // NON-NLS + }//GEN-LAST:event_driveListMouseReleasedSelection + + /** + * Refresh the list of local drives on the current machine + */ + @Messages({"ConfigVisualPanel1.messageLabel.noExternalDriveFound=No drive found"}) + private void refreshDriveList() { + List listData = new ArrayList<>(); + File[] roots = File.listRoots(); + int firstRemovableDrive = -1; + int i = 0; + for (File root : roots) { + if (DriveListUtils.isNetworkDrive(root.toString().replace(":\\", ""))) { + continue; + } + String description = FileSystemView.getFileSystemView().getSystemTypeDescription(root); + long spaceInBytes = root.getTotalSpace(); + String sizeWithUnit = DriveListUtils.humanReadableByteCount(spaceInBytes, false); + listData.add(root + " (" + description + ") (" + sizeWithUnit + ")"); + if (firstRemovableDrive == -1) { + try { + FileStore fileStore = Files.getFileStore(root.toPath()); + if ((boolean) fileStore.getAttribute("volume:isRemovable")) { //NON-NLS + firstRemovableDrive = i; + } + } catch (IOException ignored) { + //unable to get this removable drive for default selection will try and select next removable drive by default + logger.log(Level.INFO, "Unable to select first removable drive found", ignored); + } + } + i++; + } + driveList.setListData(listData.toArray(new String[listData.size()])); + if (!listData.isEmpty()) { + // auto-select the first external drive, if any + driveList.setSelectedIndex(firstRemovableDrive == -1 ? 0 : firstRemovableDrive); + firePropertyChange(UPDATE_UI_EVENT_NAME, false, true); // NON-NLS + driveList.requestFocusInWindow(); + warningLabel.setText(""); + } else { + warningLabel.setText(Bundle.ConfigVisualPanel1_messageLabel_noExternalDriveFound()); + } + } + + /** + * Update which controls are enabled to reflect the current radio button + * selection. + */ + private void updateControls() { + browseButton.setEnabled(configureFolderRadioButton.isSelected()); + refreshButton.setEnabled(configureDriveRadioButton.isSelected()); + driveList.setEnabled(configureDriveRadioButton.isSelected()); + driveListScrollPane.setEnabled(configureDriveRadioButton.isSelected()); + firePropertyChange(UPDATE_UI_EVENT_NAME, false, true); // NON-NLS + } + + /** + * Open a file chooser to allow users to choose a json configuration file in + * a folder. + * + * @param title the dialog title + */ + @NbBundle.Messages({ + "ConfigVisualPanel1.fileNameExtensionFilter=Configuration JSON File", + "ConfigVisualPanel1.invalidConfigJson=Invalid config JSON: ", + "ConfigVisualPanel1.configurationError=Configuration error",}) + private void chooseFile(String title) { + final String jsonExt = ".json"; // NON-NLS + JFileChooser fileChooser = new JFileChooser(); + fileChooser.setDialogTitle(title); + fileChooser.setDragEnabled(false); + fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); + FileFilter filter = new FileNameExtensionFilter(Bundle.ConfigVisualPanel1_fileNameExtensionFilter(), new String[]{"json"}); // NON-NLS + fileChooser.setFileFilter(filter); + fileChooser.setSelectedFile(new File(DEFAULT_CONFIG_FILE_NAME)); // default + fileChooser.setMultiSelectionEnabled(false); + if (fileChooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) { + String path = fileChooser.getSelectedFile().getPath(); + if (new File(path).exists()) { + try { + loadConfigFile(path); + configFilename = path; + configFileTextField.setText(path); + } catch (JsonIOException | JsonSyntaxException | IOException ex) { + JOptionPane.showMessageDialog(this, + Bundle.ConfigVisualPanel1_invalidConfigJson() + ex.getMessage(), + Bundle.ConfigVisualPanel1_configurationError(), + JOptionPane.ERROR_MESSAGE); + } + } else { + if (!path.endsWith(jsonExt)) { + path += jsonExt; + } + configFilename = path; + configFileTextField.setText(path); + } + } + } + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton browseButton; + private javax.swing.JTextField configFileTextField; + private javax.swing.ButtonGroup configurationLocationButtonGroup; + private javax.swing.JRadioButton configureDriveRadioButton; + private javax.swing.JRadioButton configureFolderRadioButton; + private javax.swing.JScrollPane descriptionScrollPane; + private javax.swing.JTextArea descriptionTextArea; + private javax.swing.JList driveList; + private javax.swing.JScrollPane driveListScrollPane; + private javax.swing.JButton refreshButton; + private javax.swing.JLabel warningLabel; + // End of variables declaration//GEN-END:variables + + /** + * Load a json config file specified by the path argument. + * + * + * @param path the path of the json config to load + * + * @return the LogicalImagerConfig which contains the rules from the loaded + * config. + * + * @throws FileNotFoundException + * @throws JsonIOException + * @throws JsonSyntaxException + * @throws IOException + */ + @NbBundle.Messages({ + "# {0} - filename", + "ConfigVisualPanel1.configFileIsEmpty=Configuration file {0} is empty",}) + private LogicalImagerConfig loadConfigFile(String path) throws FileNotFoundException, JsonIOException, JsonSyntaxException, IOException { + try (FileInputStream is = new FileInputStream(path)) { + InputStreamReader reader = new InputStreamReader(is, StandardCharsets.UTF_8); + GsonBuilder gsonBuilder = new GsonBuilder().setPrettyPrinting(); + gsonBuilder.registerTypeAdapter(LogicalImagerConfig.class, new LogicalImagerConfigDeserializer()); + Gson gson = gsonBuilder.create(); + LogicalImagerConfig loadedConfig = gson.fromJson(reader, LogicalImagerConfig.class); + if (loadedConfig == null) { + // This happens if the file is empty. Gson doesn't call the deserializer and doesn't throw any exception. + throw new IOException(Bundle.ConfigVisualPanel1_configFileIsEmpty(path)); + } + return loadedConfig; + } + } + + /** + * Get the LogicalImagerConfig for the currently selected config file. + * + * @return the LogicalImagerConfig which contains the rules from the loaded + * config. + */ + LogicalImagerConfig getConfig() { + String configFileName = getConfigPath(); + if (configFileName != null && new File(configFileName).exists()) { + try { + return loadConfigFile(configFileName); + } catch (JsonIOException | JsonSyntaxException | IOException ex) { + return new LogicalImagerConfig(); + } + } else { + return new LogicalImagerConfig(); + } + } + + /** + * Get the path of the currently selected json config file. + * + * @return the path of the currently selected config file or null if invalid + * settings are selected + */ + String getConfigPath() { + if (configureFolderRadioButton.isSelected()) { + return configFilename; + } else { + String selectedStr = driveList.getSelectedValue(); + if (selectedStr == null) { + return null; + } + return selectedStr.substring(0, 3) + DEFAULT_CONFIG_FILE_NAME; + } + } + + /** + * The name of the event which signifies an update to the settings reflected + * by ConfigVisualPanel1 + * + * @return UPDATE_UI_EVENT_NAME + */ + static String getUpdateEventName() { + return UPDATE_UI_EVENT_NAME; + } + + void setConfigFilename(String filename + ) { + configFileTextField.setText(filename); + } + + /** + * Checks if the current panel has valid settings selected. + * + * @return true if panel has valid settings selected, false otherwise + */ + boolean isPanelValid() { + return !StringUtils.isBlank(getConfigPath()) && ((configureDriveRadioButton.isSelected() && !StringUtils.isBlank(driveList.getSelectedValue())) + || (configureFolderRadioButton.isSelected() && (!configFileTextField.getText().isEmpty()))); + + } + + /** + * Document Listener for textfield + */ + private static class MyDocumentListener implements DocumentListener { + + private final ConfigVisualPanel1 panel; + + MyDocumentListener(ConfigVisualPanel1 aThis) { + this.panel = aThis; + } + + @Override + public void insertUpdate(DocumentEvent e) { + fireChange(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + fireChange(); + } + + @Override + public void changedUpdate(DocumentEvent e) { + fireChange(); + } + + private void fireChange() { + panel.firePropertyChange(UPDATE_UI_EVENT_NAME, false, true); // NON-NLS + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/ConfigVisualPanel2.form b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/ConfigVisualPanel2.form new file mode 100644 index 0000000000..9fd42600c8 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/ConfigVisualPanel2.form @@ -0,0 +1,567 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <ResourceString bundle="org/sleuthkit/autopsy/logicalimager/configuration/Bundle.properties" key="ConfigVisualPanel2.fullPathsTable.columnModel.title0" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <ResourceString bundle="org/sleuthkit/autopsy/logicalimager/configuration/Bundle.properties" key="ConfigVisualPanel2.filenamesTable.columnModel.title0" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <ResourceString bundle="org/sleuthkit/autopsy/logicalimager/configuration/Bundle.properties" key="ConfigVisualPanel2.folderNamesTable.columnModel.title0" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + <ResourceString bundle="org/sleuthkit/autopsy/logicalimager/configuration/Bundle.properties" key="ConfigVisualPanel2.rulesTable.columnModel.title0" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> + + + + + + + <ResourceString bundle="org/sleuthkit/autopsy/logicalimager/configuration/Bundle.properties" key="ConfigVisualPanel2.rulesTable.columnModel.title1" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/ConfigVisualPanel2.java b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/ConfigVisualPanel2.java new file mode 100644 index 0000000000..967ed1ff75 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/ConfigVisualPanel2.java @@ -0,0 +1,971 @@ +/* + * Autopsy + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.logicalimager.configuration; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import javax.swing.JButton; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTable; +import javax.swing.table.AbstractTableModel; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.openide.util.NbBundle; +import org.openide.util.NbBundle.Messages; + +/** + * Configuration Visual Panel 2 + */ +@NbBundle.Messages({ + "ConfigVisualPanel2.ok=OK", + "ConfigVisualPanel2.cancel=Cancel" +}) +@SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives +final class ConfigVisualPanel2 extends JPanel { + + private static final long serialVersionUID = 1L; + private String configFilename; + private LogicalImagerConfig config = null; + private final JButton okButton = new JButton(Bundle.ConfigVisualPanel2_ok()); + private final JButton cancelButton = new JButton(Bundle.ConfigVisualPanel2_cancel()); + private boolean flagEncryptionPrograms = false; + + /** + * Creates new form ConfigVisualPanel2 + */ + ConfigVisualPanel2() { + initComponents(); + if (config != null) { + updatePanel(configFilename, config); + } + } + + @NbBundle.Messages({ + "ConfigVisualPanel2.editConfiguration=Configure imager" + }) + @Override + public String getName() { + return Bundle.ConfigVisualPanel2_editConfiguration(); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + + modifiedDateLabel = new javax.swing.JLabel(); + modifiedWithinTextField = new javax.swing.JTextField(); + daysIncludedLabel = new javax.swing.JLabel(); + fullPathsLabel = new javax.swing.JLabel(); + flagEncryptionProgramsCheckBox = new javax.swing.JCheckBox(); + ruleNameLabel = new javax.swing.JLabel(); + ruleNameEditTextField = new javax.swing.JTextField(); + newRuleButton = new javax.swing.JButton(); + descriptionLabel = new javax.swing.JLabel(); + editRuleButton = new javax.swing.JButton(); + descriptionEditTextField = new javax.swing.JTextField(); + deleteRuleButton = new javax.swing.JButton(); + fullPathsScrollPane = new javax.swing.JScrollPane(); + fullPathsTable = new javax.swing.JTable(); + filenamesScrollPane = new javax.swing.JScrollPane(); + filenamesTable = new javax.swing.JTable(); + shouldSaveCheckBox = new javax.swing.JCheckBox(); + shouldAlertCheckBox = new javax.swing.JCheckBox(); + folderNamesScrollPane = new javax.swing.JScrollPane(); + folderNamesTable = new javax.swing.JTable(); + extensionsLabel = new javax.swing.JLabel(); + extensionsTextField = new javax.swing.JTextField(); + filenamesLabel = new javax.swing.JLabel(); + configFileTextField = new javax.swing.JTextField(); + ruleSetFileLabel = new javax.swing.JLabel(); + finalizeImageWriter = new javax.swing.JCheckBox(); + rulesScrollPane = new javax.swing.JScrollPane(); + rulesTable = new javax.swing.JTable(); + folderNamesLabel = new javax.swing.JLabel(); + fileSizeLabel = new javax.swing.JLabel(); + jSeparator1 = new javax.swing.JSeparator(); + minSizeLabel = new javax.swing.JLabel(); + minSizeTextField = new javax.swing.JFormattedTextField(); + maxSizeLabel = new javax.swing.JLabel(); + maxSizeTextField = new javax.swing.JFormattedTextField(); + + org.openide.awt.Mnemonics.setLocalizedText(modifiedDateLabel, org.openide.util.NbBundle.getMessage(ConfigVisualPanel2.class, "ConfigVisualPanel2.modifiedDateLabel.text")); // NOI18N + + modifiedWithinTextField.setEditable(false); + modifiedWithinTextField.setHorizontalAlignment(javax.swing.JTextField.TRAILING); + modifiedWithinTextField.setEnabled(false); + modifiedWithinTextField.setMinimumSize(new java.awt.Dimension(60, 20)); + modifiedWithinTextField.setPreferredSize(new java.awt.Dimension(60, 20)); + + org.openide.awt.Mnemonics.setLocalizedText(daysIncludedLabel, org.openide.util.NbBundle.getMessage(ConfigVisualPanel2.class, "ConfigVisualPanel2.daysIncludedLabel.text")); // NOI18N + daysIncludedLabel.setEnabled(false); + + org.openide.awt.Mnemonics.setLocalizedText(fullPathsLabel, org.openide.util.NbBundle.getMessage(ConfigVisualPanel2.class, "ConfigVisualPanel2.fullPathsLabel.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(flagEncryptionProgramsCheckBox, org.openide.util.NbBundle.getMessage(ConfigVisualPanel2.class, "ConfigVisualPanel2.flagEncryptionProgramsCheckBox.text")); // NOI18N + flagEncryptionProgramsCheckBox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + flagEncryptionProgramsCheckBoxActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(ruleNameLabel, org.openide.util.NbBundle.getMessage(ConfigVisualPanel2.class, "ConfigVisualPanel2.ruleNameLabel.text")); // NOI18N + + ruleNameEditTextField.setEnabled(false); + + newRuleButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/add16.png"))); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(newRuleButton, org.openide.util.NbBundle.getMessage(ConfigVisualPanel2.class, "ConfigVisualPanel2.newRuleButton.text")); // NOI18N + newRuleButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + newRuleButtonActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(descriptionLabel, org.openide.util.NbBundle.getMessage(ConfigVisualPanel2.class, "ConfigVisualPanel2.descriptionLabel.text")); // NOI18N + + editRuleButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/edit16.png"))); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(editRuleButton, org.openide.util.NbBundle.getMessage(ConfigVisualPanel2.class, "ConfigVisualPanel2.editRuleButton.text")); // NOI18N + editRuleButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + editRuleButtonActionPerformed(evt); + } + }); + + descriptionEditTextField.setEnabled(false); + + deleteRuleButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/delete16.png"))); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(deleteRuleButton, org.openide.util.NbBundle.getMessage(ConfigVisualPanel2.class, "ConfigVisualPanel2.deleteRuleButton.text")); // NOI18N + deleteRuleButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + deleteRuleButtonActionPerformed(evt); + } + }); + + fullPathsTable.setColumnSelectionAllowed(true); + fullPathsTable.setEnabled(false); + fullPathsTable.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); + fullPathsTable.setShowHorizontalLines(false); + fullPathsTable.setShowVerticalLines(false); + fullPathsTable.getTableHeader().setReorderingAllowed(false); + fullPathsScrollPane.setViewportView(fullPathsTable); + fullPathsTable.getColumnModel().getSelectionModel().setSelectionMode(javax.swing.ListSelectionModel.SINGLE_INTERVAL_SELECTION); + if (fullPathsTable.getColumnModel().getColumnCount() > 0) { + fullPathsTable.getColumnModel().getColumn(0).setHeaderValue(org.openide.util.NbBundle.getMessage(ConfigVisualPanel2.class, "ConfigVisualPanel2.fullPathsTable.columnModel.title0")); // NOI18N + } + + filenamesTable.setEnabled(false); + filenamesTable.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); + filenamesTable.setShowHorizontalLines(false); + filenamesTable.setShowVerticalLines(false); + filenamesTable.getTableHeader().setReorderingAllowed(false); + filenamesScrollPane.setViewportView(filenamesTable); + if (filenamesTable.getColumnModel().getColumnCount() > 0) { + filenamesTable.getColumnModel().getColumn(0).setHeaderValue(org.openide.util.NbBundle.getMessage(ConfigVisualPanel2.class, "ConfigVisualPanel2.filenamesTable.columnModel.title0")); // NOI18N + } + + org.openide.awt.Mnemonics.setLocalizedText(shouldSaveCheckBox, org.openide.util.NbBundle.getMessage(ConfigVisualPanel2.class, "ConfigVisualPanel2.shouldSaveCheckBox.text")); // NOI18N + shouldSaveCheckBox.setToolTipText(org.openide.util.NbBundle.getMessage(ConfigVisualPanel2.class, "ConfigVisualPanel2.shouldSaveCheckBox.toolTipText")); // NOI18N + shouldSaveCheckBox.setEnabled(false); + + org.openide.awt.Mnemonics.setLocalizedText(shouldAlertCheckBox, org.openide.util.NbBundle.getMessage(ConfigVisualPanel2.class, "ConfigVisualPanel2.shouldAlertCheckBox.text")); // NOI18N + shouldAlertCheckBox.setEnabled(false); + + folderNamesTable.setEnabled(false); + folderNamesTable.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); + folderNamesTable.setShowHorizontalLines(false); + folderNamesTable.setShowVerticalLines(false); + folderNamesTable.getTableHeader().setReorderingAllowed(false); + folderNamesScrollPane.setViewportView(folderNamesTable); + if (folderNamesTable.getColumnModel().getColumnCount() > 0) { + folderNamesTable.getColumnModel().getColumn(0).setHeaderValue(org.openide.util.NbBundle.getMessage(ConfigVisualPanel2.class, "ConfigVisualPanel2.folderNamesTable.columnModel.title0")); // NOI18N + } + + org.openide.awt.Mnemonics.setLocalizedText(extensionsLabel, org.openide.util.NbBundle.getMessage(ConfigVisualPanel2.class, "ConfigVisualPanel2.extensionsLabel.text")); // NOI18N + + extensionsTextField.setEnabled(false); + + org.openide.awt.Mnemonics.setLocalizedText(filenamesLabel, org.openide.util.NbBundle.getMessage(ConfigVisualPanel2.class, "ConfigVisualPanel2.filenamesLabel.text")); // NOI18N + + configFileTextField.setToolTipText(""); + configFileTextField.setEnabled(false); + + org.openide.awt.Mnemonics.setLocalizedText(ruleSetFileLabel, org.openide.util.NbBundle.getMessage(ConfigVisualPanel2.class, "ConfigVisualPanel2.ruleSetFileLabel.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(finalizeImageWriter, org.openide.util.NbBundle.getMessage(ConfigVisualPanel2.class, "ConfigVisualPanel2.finalizeImageWriter.text")); // NOI18N + finalizeImageWriter.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + finalizeImageWriterActionPerformed(evt); + } + }); + + rulesTable.setAutoResizeMode(javax.swing.JTable.AUTO_RESIZE_ALL_COLUMNS); + rulesTable.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); + rulesTable.setShowHorizontalLines(false); + rulesTable.setShowVerticalLines(false); + rulesTable.getTableHeader().setReorderingAllowed(false); + rulesTable.addMouseListener(new java.awt.event.MouseAdapter() { + public void mouseReleased(java.awt.event.MouseEvent evt) { + rulesTableMouseReleased(evt); + } + }); + rulesTable.addKeyListener(new java.awt.event.KeyAdapter() { + public void keyReleased(java.awt.event.KeyEvent evt) { + rulesTableKeyReleased(evt); + } + }); + rulesScrollPane.setViewportView(rulesTable); + if (rulesTable.getColumnModel().getColumnCount() > 0) { + rulesTable.getColumnModel().getColumn(0).setHeaderValue(org.openide.util.NbBundle.getMessage(ConfigVisualPanel2.class, "ConfigVisualPanel2.rulesTable.columnModel.title0")); // NOI18N + rulesTable.getColumnModel().getColumn(1).setHeaderValue(org.openide.util.NbBundle.getMessage(ConfigVisualPanel2.class, "ConfigVisualPanel2.rulesTable.columnModel.title1")); // NOI18N + } + + org.openide.awt.Mnemonics.setLocalizedText(folderNamesLabel, org.openide.util.NbBundle.getMessage(ConfigVisualPanel2.class, "ConfigVisualPanel2.folderNamesLabel.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(fileSizeLabel, org.openide.util.NbBundle.getMessage(ConfigVisualPanel2.class, "ConfigVisualPanel2.fileSizeLabel.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(minSizeLabel, org.openide.util.NbBundle.getMessage(ConfigVisualPanel2.class, "ConfigVisualPanel2.minSizeLabel.text")); // NOI18N + + minSizeTextField.setFormatterFactory(new javax.swing.text.DefaultFormatterFactory(new javax.swing.text.NumberFormatter(new java.text.DecimalFormat("#,###; ")))); + minSizeTextField.setEnabled(false); + + org.openide.awt.Mnemonics.setLocalizedText(maxSizeLabel, org.openide.util.NbBundle.getMessage(ConfigVisualPanel2.class, "ConfigVisualPanel2.maxSizeLabel.text")); // NOI18N + + maxSizeTextField.setFormatterFactory(new javax.swing.text.DefaultFormatterFactory(new javax.swing.text.NumberFormatter(new java.text.DecimalFormat("#,###; ")))); + maxSizeTextField.setEnabled(false); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGap(17, 17, 17) + .addComponent(ruleSetFileLabel) + .addGap(18, 18, 18) + .addComponent(configFileTextField) + .addContainerGap()) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(newRuleButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(editRuleButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(deleteRuleButton)) + .addComponent(rulesScrollPane, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.PREFERRED_SIZE, 341, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGap(10, 10, 10) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(flagEncryptionProgramsCheckBox) + .addComponent(finalizeImageWriter) + .addComponent(shouldSaveCheckBox) + .addComponent(shouldAlertCheckBox) + .addComponent(extensionsLabel) + .addComponent(filenamesLabel) + .addComponent(descriptionLabel) + .addComponent(ruleNameLabel) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(modifiedDateLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 79, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(fileSizeLabel) + .addComponent(fullPathsLabel) + .addComponent(folderNamesLabel)) + .addGap(4, 4, 4) + .addComponent(minSizeLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(minSizeTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 63, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(18, 18, 18) + .addComponent(maxSizeLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(maxSizeTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 63, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addGap(107, 107, 107)) + .addGroup(layout.createSequentialGroup() + .addGap(129, 129, 129) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(modifiedWithinTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 54, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(daysIncludedLabel)) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(ruleNameEditTextField, javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(descriptionEditTextField, javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(extensionsTextField, javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(fullPathsScrollPane, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE) + .addComponent(filenamesScrollPane, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE) + .addComponent(folderNamesScrollPane, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE)) + .addContainerGap()))))) + .addGroup(layout.createSequentialGroup() + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jSeparator1)))) + ); + + layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {deleteRuleButton, editRuleButton, newRuleButton}); + + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(configFileTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(ruleSetFileLabel)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(rulesScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 479, Short.MAX_VALUE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(newRuleButton) + .addComponent(editRuleButton) + .addComponent(deleteRuleButton))) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGap(30, 30, 30) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(descriptionEditTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(descriptionLabel)) + .addGap(9, 9, 9) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(extensionsTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(extensionsLabel))) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(ruleNameEditTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(ruleNameLabel))) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(filenamesScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE) + .addGroup(layout.createSequentialGroup() + .addComponent(filenamesLabel) + .addGap(0, 0, Short.MAX_VALUE))) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(folderNamesScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE) + .addGroup(layout.createSequentialGroup() + .addComponent(folderNamesLabel) + .addGap(0, 0, Short.MAX_VALUE))) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(fullPathsLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addComponent(fullPathsScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE) + .addGap(11, 11, 11))) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.CENTER) + .addComponent(minSizeLabel) + .addComponent(minSizeTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(maxSizeLabel) + .addComponent(maxSizeTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(fileSizeLabel)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.CENTER) + .addComponent(modifiedDateLabel) + .addComponent(modifiedWithinTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(daysIncludedLabel)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(shouldSaveCheckBox) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(shouldAlertCheckBox) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(jSeparator1, javax.swing.GroupLayout.PREFERRED_SIZE, 2, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(flagEncryptionProgramsCheckBox) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(finalizeImageWriter))) + .addContainerGap()) + ); + }// //GEN-END:initComponents + + private void rulesTableKeyReleased(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_rulesTableKeyReleased + updateForSelectedRule(); + }//GEN-LAST:event_rulesTableKeyReleased + + @NbBundle.Messages({ + "ConfigVisualPanel2.editRuleSet=Edit Rule", + "ConfigVisualPanel2.editRuleError=Edit rule error" + }) + private void editRuleButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_editRuleButtonActionPerformed + int row = rulesTable.getSelectedRow(); + if (row != -1) { + String ruleName = (String) rulesTable.getModel().getValueAt(row, 0); + LogicalImagerRule rule = getRuleSetFromCurrentConfig().getRules().get(row); + EditRulePanel editPanel = new EditRulePanel(okButton, cancelButton, ruleName, rule); + editPanel.setEnabled(true); + editPanel.setVisible(true); + + while (true) { + int option = JOptionPane.showOptionDialog(this, editPanel.getPanel(), Bundle.ConfigVisualPanel2_editRuleSet(), + JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, + null, new Object[]{okButton, cancelButton}, okButton); + if (option == JOptionPane.OK_OPTION) { + try { + ImmutablePair ruleMap = editPanel.toRule(); + if (!ruleName.equals(ruleMap.getKey()) && ruleExists(ruleMap)) { + JOptionPane.showMessageDialog(this, + Bundle.ConfigVisualPanel2_newRuleError_duplicateName(ruleMap.getKey()), + Bundle.ConfigVisualPanel2_editRuleError(), + JOptionPane.ERROR_MESSAGE); + continue; + } + updateRow(row, ruleMap); + break; + } catch (IOException | NumberFormatException ex) { + JOptionPane.showMessageDialog(this, + ex.getMessage(), + Bundle.ConfigVisualPanel2_editRuleError(), + JOptionPane.ERROR_MESSAGE); + // let user fix the error + } + } else { + break; + } + } + } + }//GEN-LAST:event_editRuleButtonActionPerformed + + @Messages({"ConfigVisualPanel2.newRule.name=New Rule", + "ConfigVisualPanel2.newRuleError.title=New rule error", + "# {0} - ruleName", + "ConfigVisualPanel2.newRuleError.duplicateName=A rule with name \"{0}\" already exists. Please enter a different rule name"}) + private void newRuleButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_newRuleButtonActionPerformed + NewRulePanel panel; + panel = new NewRulePanel(okButton, cancelButton); + panel.setEnabled(true); + panel.setVisible(true); + + while (true) { + int option = JOptionPane.showOptionDialog(this, panel, Bundle.ConfigVisualPanel2_newRule_name(), + JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, + null, new Object[]{okButton, cancelButton}, okButton); + if (option == JOptionPane.OK_OPTION) { + try { + + ImmutablePair ruleMap = panel.toRule(); + if (ruleExists(ruleMap)) { + JOptionPane.showMessageDialog(this, + Bundle.ConfigVisualPanel2_newRuleError_duplicateName(ruleMap.getKey()), + Bundle.ConfigVisualPanel2_newRuleError_title(), + JOptionPane.ERROR_MESSAGE); + continue; + } + // Save the new rule + appendRow(ruleMap); + break; + } catch (IOException | NumberFormatException ex) { + JOptionPane.showMessageDialog(this, + ex.getMessage(), + Bundle.ConfigVisualPanel2_newRuleError_title(), + JOptionPane.ERROR_MESSAGE); + // let user fix the error + } + } else { + break; + } + } + }//GEN-LAST:event_newRuleButtonActionPerformed + + @NbBundle.Messages({ + "ConfigVisualPanel2.deleteRuleSet=Delete rule ", + "ConfigVisualPanel2.deleteRuleSetConfirmation=Delete rule confirmation",}) + private void deleteRuleButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_deleteRuleButtonActionPerformed + int index = rulesTable.getSelectedRow(); + if (index != -1) { + String ruleName = (String) rulesTable.getModel().getValueAt(index, 0); + + int option = JOptionPane.showOptionDialog(this, + Bundle.ConfigVisualPanel2_deleteRuleSet() + ruleName, + Bundle.ConfigVisualPanel2_deleteRuleSetConfirmation(), + JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, null, null); + if (option == JOptionPane.NO_OPTION) { + return; + } + + getRuleSetFromCurrentConfig().getRules().remove(index); + updatePanel(configFilename, config); + if (rulesTable.getRowCount() > 0) { + rulesTable.setRowSelectionInterval(0, 0); + updateForSelectedRule(); + } + } + }//GEN-LAST:event_deleteRuleButtonActionPerformed + + private void rulesTableMouseReleased(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_rulesTableMouseReleased + updateForSelectedRule(); + }//GEN-LAST:event_rulesTableMouseReleased + + private void flagEncryptionProgramsCheckBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_flagEncryptionProgramsCheckBoxActionPerformed + flagEncryptionPrograms = flagEncryptionProgramsCheckBox.isSelected(); + setEnabledEncryptionProgramsRule(flagEncryptionPrograms); + }//GEN-LAST:event_flagEncryptionProgramsCheckBoxActionPerformed + + private void finalizeImageWriterActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_finalizeImageWriterActionPerformed + config.setFinalizeImageWriter(finalizeImageWriter.isSelected()); + }//GEN-LAST:event_finalizeImageWriterActionPerformed + + /** + * Set the whether the a rule for detecting encryption programs will be + * added to the rules in this config + * + * @param flagEncryptionPrograms true to have encryption programs rule + * added, false otherwise. + */ + private void setEnabledEncryptionProgramsRule(boolean flagEncryptionPrograms) { + if (flagEncryptionPrograms) { + // add the special rule + ImmutablePair ruleMap = createEncryptionProgramsRule(); + appendRow(ruleMap); + } else { + // remove it + int index = ((RulesTableModel) rulesTable.getModel()).findRow(EncryptionProgramsRule.getName()); + if (index != -1) { + getRuleSetFromCurrentConfig().getRules().remove(index); + updatePanel(configFilename, config); + if (rulesTable.getRowCount() > 0) { + rulesTable.setRowSelectionInterval(0, 0); + updateForSelectedRule(); + } + } + } + } + + /* + * Create an encryption programs rule + */ + private ImmutablePair createEncryptionProgramsRule() { + LogicalImagerRule.Builder builder = new LogicalImagerRule.Builder(); + builder.getName(EncryptionProgramsRule.getName()) + .getDescription(EncryptionProgramsRule.getDescription()) + .getShouldAlert(true) + .getShouldSave(true) + .getFilenames(EncryptionProgramsRule.getFilenames()); + LogicalImagerRule rule = builder.build(); + return new ImmutablePair<>(EncryptionProgramsRule.getName(), rule); + } + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JTextField configFileTextField; + private javax.swing.JLabel daysIncludedLabel; + private javax.swing.JButton deleteRuleButton; + private javax.swing.JTextField descriptionEditTextField; + private javax.swing.JLabel descriptionLabel; + private javax.swing.JButton editRuleButton; + private javax.swing.JLabel extensionsLabel; + private javax.swing.JTextField extensionsTextField; + private javax.swing.JLabel fileSizeLabel; + private javax.swing.JLabel filenamesLabel; + private javax.swing.JScrollPane filenamesScrollPane; + private javax.swing.JTable filenamesTable; + private javax.swing.JCheckBox finalizeImageWriter; + private javax.swing.JCheckBox flagEncryptionProgramsCheckBox; + private javax.swing.JLabel folderNamesLabel; + private javax.swing.JScrollPane folderNamesScrollPane; + private javax.swing.JTable folderNamesTable; + private javax.swing.JLabel fullPathsLabel; + private javax.swing.JScrollPane fullPathsScrollPane; + private javax.swing.JTable fullPathsTable; + private javax.swing.JSeparator jSeparator1; + private javax.swing.JLabel maxSizeLabel; + private javax.swing.JFormattedTextField maxSizeTextField; + private javax.swing.JLabel minSizeLabel; + private javax.swing.JFormattedTextField minSizeTextField; + private javax.swing.JLabel modifiedDateLabel; + private javax.swing.JTextField modifiedWithinTextField; + private javax.swing.JButton newRuleButton; + private javax.swing.JTextField ruleNameEditTextField; + private javax.swing.JLabel ruleNameLabel; + private javax.swing.JLabel ruleSetFileLabel; + private javax.swing.JScrollPane rulesScrollPane; + private javax.swing.JTable rulesTable; + private javax.swing.JCheckBox shouldAlertCheckBox; + private javax.swing.JCheckBox shouldSaveCheckBox; + // End of variables declaration//GEN-END:variables + + private LogicalImagerRuleSet getRuleSetFromCurrentConfig() { + if (config.getRuleSets().isEmpty()) { + List ruleSets = new ArrayList<>(); + ruleSets.add(new LogicalImagerRuleSet("no-set-name", new ArrayList<>())); // NON-NLS + config.setRuleSet(ruleSets); + } + return config.getRuleSets().get(0); + } + + /** + * Update the panel to reflect the rules in the current config + * + * @param configFilePath path of the config file being modified + * @param config contents of the config file being modifed + * @param rowSelectionkey the name of the rule to select by default + */ + private void updatePanel(String configFilePath, LogicalImagerConfig config, String rowSelectionkey) { + configFileTextField.setText(configFilePath); + finalizeImageWriter.setSelected(config.isFinalizeImageWriter()); + LogicalImagerRuleSet ruleSet = getRuleSetFromCurrentConfig(); + flagEncryptionProgramsCheckBox.setSelected(ruleSet.find(EncryptionProgramsRule.getName()) != null); + RulesTableModel rulesTableModel = new RulesTableModel(); + int row = 0; + int selectThisRow = 0; + + Collections.sort(ruleSet.getRules(), new SortRuleByName()); + + for (LogicalImagerRule rule : ruleSet.getRules()) { + rulesTableModel.setValueAt(rule.getName(), row, 0); + if (rowSelectionkey != null && rowSelectionkey.equals(rule.getName())) { + selectThisRow = row; + } + rulesTableModel.setValueAt(rule.getDescription(), row, 1); + rulesTableModel.setValueAt(rule, row, 2); + row++; + } + rulesTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN); + rulesTable.setModel(rulesTableModel); + // If there are any rules, select the first one + if (rulesTableModel.getRowCount() > 0) { + rulesTable.setRowSelectionInterval(selectThisRow, selectThisRow); + updateForSelectedRule(); + } else { + clearRuleDetails(); + updateRuleButtons(false); + } + } + + /** + * Private helper method to call updatePanel with no row to select specified + * + * @param configFilePath path of the config file being modified + * @param config contents of the config file being modifed + */ + private void updatePanel(String configFilePath, LogicalImagerConfig config) { + updatePanel(configFilePath, config, null); + } + + /** + * Update the panel to reflect the selected rule + */ + private void updateForSelectedRule() { + int index = rulesTable.getSelectedRow(); + if (index != -1) { + String ruleName = (String) rulesTable.getModel().getValueAt(index, 0); + String description = (String) rulesTable.getModel().getValueAt(index, 1); + updateRuleDetails(ruleName, description); + updateRuleButtons(!ruleName.equals(EncryptionProgramsRule.getName())); + } else { + clearRuleDetails(); + updateRuleButtons(false); + } + } + + /** + * Update the panel to display details of the specified rule + * + * @param ruleName the name of the rule to display + * @param description the description of the rule to display + */ + private void updateRuleDetails(String ruleName, String description) { + clearRuleDetails(); + LogicalImagerRule rule = getRuleSetFromCurrentConfig().find(ruleName); + shouldAlertCheckBox.setSelected(rule.isShouldAlert()); + shouldSaveCheckBox.setSelected(rule.isShouldSave()); + ruleNameEditTextField.setText(ruleName); + descriptionEditTextField.setText(description); + updateExtensions(rule.getExtensions()); + updateList(filenamesTable, rule.getFilenames()); + updateList(folderNamesTable, rule.getPaths()); + updateList(fullPathsTable, rule.getFullPaths()); + if (rule.getMinFileSize() == null) { + minSizeTextField.setText(""); + } else { + minSizeTextField.setText(rule.getMinFileSize().toString()); + } + if (rule.getMaxFileSize() == null) { + maxSizeTextField.setText(""); + } else { + maxSizeTextField.setText(rule.getMaxFileSize().toString()); + } + if (rule.getMinDays() == null) { + modifiedWithinTextField.setText(""); + } else { + modifiedWithinTextField.setText(Integer.toString(rule.getMinDays())); + } + } + + /** + * Reset rule details displayed to be blank or default + */ + private void clearRuleDetails() { + ruleNameEditTextField.setText(""); + descriptionEditTextField.setText(""); + extensionsTextField.setText(""); + updateExtensions(null); + updateList(filenamesTable, null); + updateList(folderNamesTable, null); + updateList(fullPathsTable, null); + minSizeTextField.setText(""); + maxSizeTextField.setText(""); + modifiedWithinTextField.setText(""); + shouldSaveCheckBox.setSelected(true); + shouldAlertCheckBox.setSelected(false); + } + + /** + * Update the extensions displayed + * + * @param extensions the list of extensions to display, null to display + * nothing + */ + private void updateExtensions(List extensions) { + extensionsTextField.setText(""); + if (extensions == null) { + return; + } + String content = ""; + boolean first = true; + for (String ext : extensions) { + content += (first ? "" : ",") + ext; + first = false; + } + extensionsTextField.setText(content); + } + + /** + * Update a JTable to display a list of Strings + * + * @param jTable the JTable to update + * @param list the list of Strings to display, null to display nothing + */ + private void updateList(javax.swing.JTable jTable, List list) { + SingleColumnTableModel tableModel = new SingleColumnTableModel(); + jTable.setTableHeader(null); + if (list == null) { + jTable.setModel(tableModel); + return; + } + int row = 0; + for (String s : list) { + tableModel.setValueAt(s, row, 0); + row++; + } + jTable.setModel(tableModel); + } + + void setConfiguration(String configFilename, LogicalImagerConfig config) { + this.configFilename = configFilename; + this.config = config; + updatePanel(configFilename, config); + } + + private void updateRow(int index, ImmutablePair ruleMap) { + getRuleSetFromCurrentConfig().getRules().remove(index); + getRuleSetFromCurrentConfig().getRules().add(ruleMap.getValue()); + updatePanel(configFilename, config, ruleMap.getKey()); + } + + /** + * Check if a rule with the same name as this rule already exists + * + * @param ruleMap the rule to check the name of + * + * @return true if it exists, false otherwise + */ + private boolean ruleExists(ImmutablePair ruleMap) { + for (LogicalImagerRule rule : getRuleSetFromCurrentConfig().getRules()) { + if (rule.getName().equals(ruleMap.getKey())) { + return true; + } + } + return false; + } + + private void appendRow(ImmutablePair ruleMap) { + getRuleSetFromCurrentConfig().getRules().add(ruleMap.getValue()); + updatePanel(configFilename, config, ruleMap.getKey()); + } + + /** + * Adjust the enabled status of the rule buttons to reflect wether there is + * a currently selected rule which can be modified + * + * @param isRowSelected true if a row is selected, false otherwise + */ + private void updateRuleButtons(boolean isRowSelected) { + newRuleButton.setEnabled(true); + editRuleButton.setEnabled(isRowSelected); + deleteRuleButton.setEnabled(isRowSelected); + } + + /** + * Sort rule by name + */ + private class SortRuleByName implements Comparator { + + @Override + public int compare(LogicalImagerRule a, LogicalImagerRule b) { + return a.getName().compareToIgnoreCase(b.getName()); + } + } + + /** + * RulesTableModel for rules table + */ + private class RulesTableModel extends AbstractTableModel { + + private static final long serialVersionUID = 1L; + private final List ruleName = new ArrayList<>(); + private final List ruleDescription = new ArrayList<>(); + private final List rule = new ArrayList<>(); + + int findRow(String name) { + return ruleName.indexOf(name); + } + + @Override + public int getRowCount() { + return ruleName.size(); + } + + @Override + public int getColumnCount() { + return 2; + } + + @NbBundle.Messages({ + "ConfigVisualPanel2.rulesTable.columnModel.title0=Rule Name", + "ConfigVisualPanel2.rulesTable.columnModel.title1=Description" + }) + @Override + public String getColumnName(int column) { + String colName = null; + switch (column) { + case 0: + colName = Bundle.ConfigVisualPanel2_rulesTable_columnModel_title0(); + break; + case 1: + colName = Bundle.ConfigVisualPanel2_rulesTable_columnModel_title1(); + break; + default: + break; + } + return colName; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + Object ret = null; + switch (columnIndex) { + case 0: + ret = ruleName.get(rowIndex); + break; + case 1: + ret = ruleDescription.get(rowIndex); + break; + case 2: + ret = rule.get(rowIndex); + break; + default: + throw new UnsupportedOperationException("Invalid table column index: " + columnIndex); //NON-NLS + } + return ret; + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + return false; + } + + @Override + public void setValueAt(Object aValue, int rowIndex, int columnIndex) { + switch (columnIndex) { + case 0: + ruleName.add((String) aValue); + break; + case 1: + ruleDescription.add((String) aValue); + break; + case 2: + rule.add((LogicalImagerRule) aValue); + break; + default: + throw new UnsupportedOperationException("Invalid table column index: " + columnIndex); //NON-NLS + } + // Only show the name and description column + if (columnIndex < 2) { + super.setValueAt(aValue, rowIndex, columnIndex); + } + } + } + + /** + * Table model for single column list table. + */ + private class SingleColumnTableModel extends AbstractTableModel { + + private static final long serialVersionUID = 1L; + + private final List list = new ArrayList<>(); + + @Override + public int getRowCount() { + return list.size(); + } + + @Override + public int getColumnCount() { + return 1; + } + + @Override + public String getColumnName(int column) { + return ""; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + Object ret = null; + if (columnIndex == 0) { + ret = list.get(rowIndex); + } else { + throw new UnsupportedOperationException("Invalid table column index: " + columnIndex); //NON-NLS + } + return ret; + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + return true; + } + + @Override + public void setValueAt(Object aValue, int rowIndex, int columnIndex) { + if (columnIndex == 0) { + list.add((String) aValue); + } else { + throw new UnsupportedOperationException("Invalid table column index: " + columnIndex); //NON-NLS + } + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/ConfigVisualPanel3.form b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/ConfigVisualPanel3.form new file mode 100644 index 0000000000..00c251301e --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/ConfigVisualPanel3.form @@ -0,0 +1,142 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/ConfigVisualPanel3.java b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/ConfigVisualPanel3.java new file mode 100644 index 0000000000..cf51247277 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/ConfigVisualPanel3.java @@ -0,0 +1,284 @@ +/* + * Autopsy + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.logicalimager.configuration; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonIOException; +import java.awt.Color; +import java.awt.Cursor; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.logging.Level; +import javax.swing.JOptionPane; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.FilenameUtils; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coreutils.Logger; +import static org.sleuthkit.autopsy.logicalimager.configuration.CreateLogicalImagerAction.getLogicalImagerExe; + +/** + * Configuration visual panel 3 + */ +class ConfigVisualPanel3 extends javax.swing.JPanel { + + private static final Logger logger = Logger.getLogger(ConfigVisualPanel3.class.getName()); + private static final String SAVED_LOGICAL_IMAGER = "SAVED_LOGICAL_IMAGER"; + private static final long serialVersionUID = 1L; + private boolean hasBeenSaved = false; + private String configFilename; + private LogicalImagerConfig config; + + /** + * Creates new form ConfigVisualPanel3 + */ + @NbBundle.Messages({"ConfigVisualPanel3.copyStatus.notSaved=File has not been saved", + "ConfigVisualPanel3.copyStatus.savingInProgress=Saving file, please wait", + "ConfigVisualPanel3.copyStatus.saved=Saved", + "ConfigVisualPanel3.copyStatus.error=Unable to save file"}) + ConfigVisualPanel3() { + initComponents(); + } + + final void resetPanel() { + hasBeenSaved = false; + configStatusLabel.setText(Bundle.ConfigVisualPanel3_copyStatus_notSaved()); + executableStatusLabel.setText(Bundle.ConfigVisualPanel3_copyStatus_notSaved()); + } + + @NbBundle.Messages({ + "ConfigVisualPanel3.saveConfigurationFile=Save imager" + }) + @Override + public String getName() { + return Bundle.ConfigVisualPanel3_saveConfigurationFile(); + } + + /** + * Identifies whether the configuration has been saved + * + * @return true if it has been saved, false otherwise + */ + boolean isSaved() { + return hasBeenSaved; + } + + /** + * Save the current configuration file and copy the logical imager + * executable to the same location. + */ + @NbBundle.Messages({ + "# {0} - configFilename", + "ConfigVisualPanel3.failedToSaveConfigMsg=Failed to save configuration file: {0}", + "# {0} - reason", + "ConfigVisualPanel3.reason=\nReason: {0}", + "ConfigVisualPanel3.failedToSaveExeMsg=Failed to save tsk_logical_imager.exe file" + }) + void saveConfigFile() { + boolean saveSuccess = true; + executableStatusLabel.setForeground(Color.BLACK); + configStatusLabel.setText(Bundle.ConfigVisualPanel3_copyStatus_savingInProgress()); + executableStatusLabel.setText(Bundle.ConfigVisualPanel3_copyStatus_savingInProgress()); + setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + GsonBuilder gsonBuilder = new GsonBuilder() + .setPrettyPrinting() + .excludeFieldsWithoutExposeAnnotation() + .disableHtmlEscaping(); + Gson gson = gsonBuilder.create(); + String toJson = gson.toJson(config); + try { + List lines = Arrays.asList(toJson.split("\\n")); + FileUtils.writeLines(new File(configFilename), "UTF-8", lines, System.getProperty("line.separator")); // NON-NLS + configStatusLabel.setText(Bundle.ConfigVisualPanel3_copyStatus_saved()); + } catch (IOException ex) { + saveSuccess = false; + configStatusLabel.setText(Bundle.ConfigVisualPanel3_copyStatus_error()); + configStatusLabel.setForeground(Color.RED); + JOptionPane.showMessageDialog(this, Bundle.ConfigVisualPanel3_failedToSaveConfigMsg(configFilename) + + Bundle.ConfigVisualPanel3_reason(ex.getMessage())); + } catch (JsonIOException jioe) { + saveSuccess = false; + executableStatusLabel.setText(Bundle.ConfigVisualPanel3_copyStatus_error()); + executableStatusLabel.setForeground(Color.RED); + logger.log(Level.SEVERE, "Failed to save configuration file: " + configFilename, jioe); // NON-NLS + JOptionPane.showMessageDialog(this, Bundle.ConfigVisualPanel3_failedToSaveConfigMsg(configFilename) + + Bundle.ConfigVisualPanel3_reason(jioe.getMessage())); + } + try { + writeTskLogicalImagerExe(Paths.get(configFilename).getParent()); + executableStatusLabel.setText(Bundle.ConfigVisualPanel3_copyStatus_saved()); + } catch (IOException ex) { + saveSuccess = false; + executableStatusLabel.setText(Bundle.ConfigVisualPanel3_copyStatus_error()); + executableStatusLabel.setForeground(Color.RED); + logger.log(Level.SEVERE, "Failed to save tsk_logical_imager.exe file", ex); // NON-NLS + JOptionPane.showMessageDialog(this, Bundle.ConfigVisualPanel3_failedToSaveExeMsg() + + Bundle.ConfigVisualPanel3_reason(ex.getMessage())); + } + if (saveSuccess) { + hasBeenSaved = true; + firePropertyChange(SAVED_LOGICAL_IMAGER, false, true); // NON-NLS + } + setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + + /** + * Writes the logical imager executable to the specified location. + * + * @param destDir The destination directory. + * + * @throws IOException If the executable cannot be found or copying fails. + */ + @NbBundle.Messages({ + "ConfigVisualPanel3.errorMsg.cannotFindLogicalImager=Cannot locate logical imager, cannot copy to destination" + }) + private void writeTskLogicalImagerExe(Path destDir) throws IOException { + File logicalImagerExe = getLogicalImagerExe(); + if (logicalImagerExe != null && logicalImagerExe.exists()) { + FileUtils.copyFileToDirectory(getLogicalImagerExe(), destDir.toFile()); + } else { + throw new IOException(Bundle.ConfigVisualPanel3_errorMsg_cannotFindLogicalImager()); + } + } + + /** + * The name of the event which is sent when the configuration and executable + * have been saved. + * + * @return SAVED_LOGICAL_IMAGER + */ + static String getSavedEventName() { + return SAVED_LOGICAL_IMAGER; + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + javax.swing.JButton saveButton = new javax.swing.JButton(); + descriptionScrollPane = new javax.swing.JScrollPane(); + descriptionTextArea = new javax.swing.JTextArea(); + javax.swing.JLabel configLabel = new javax.swing.JLabel(); + configStatusLabel = new javax.swing.JLabel(); + javax.swing.JLabel executableLabel = new javax.swing.JLabel(); + executableStatusLabel = new javax.swing.JLabel(); + + org.openide.awt.Mnemonics.setLocalizedText(saveButton, org.openide.util.NbBundle.getMessage(ConfigVisualPanel3.class, "ConfigVisualPanel3.saveButton.text")); // NOI18N + saveButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + saveButtonActionPerformed(evt); + } + }); + + descriptionTextArea.setEditable(false); + descriptionTextArea.setBackground(new java.awt.Color(240, 240, 240)); + descriptionTextArea.setColumns(20); + descriptionTextArea.setFont(new java.awt.Font("Tahoma", 0, 11)); // NOI18N + descriptionTextArea.setLineWrap(true); + descriptionTextArea.setRows(5); + descriptionTextArea.setWrapStyleWord(true); + descriptionTextArea.setEnabled(false); + descriptionScrollPane.setViewportView(descriptionTextArea); + + org.openide.awt.Mnemonics.setLocalizedText(configLabel, org.openide.util.NbBundle.getMessage(ConfigVisualPanel3.class, "ConfigVisualPanel3.configLabel.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(executableLabel, org.openide.util.NbBundle.getMessage(ConfigVisualPanel3.class, "ConfigVisualPanel3.executableLabel.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(executableStatusLabel, org.openide.util.NbBundle.getMessage(ConfigVisualPanel3.class, "ConfigVisualPanel3.executableStatusLabel.text")); // NOI18N + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING, false) + .addComponent(configLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(executableLabel)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(configStatusLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 237, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(executableStatusLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 238, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGap(0, 10, Short.MAX_VALUE)) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(descriptionScrollPane) + .addGroup(layout.createSequentialGroup() + .addComponent(saveButton, javax.swing.GroupLayout.PREFERRED_SIZE, 101, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(0, 0, Short.MAX_VALUE))) + .addContainerGap()))) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(descriptionScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(18, 18, 18) + .addComponent(saveButton) + .addGap(18, 18, 18) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addComponent(configLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(configStatusLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 14, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(executableLabel) + .addComponent(executableStatusLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 14, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addContainerGap(120, Short.MAX_VALUE)) + ); + }// //GEN-END:initComponents + + private void saveButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_saveButtonActionPerformed + saveConfigFile(); + }//GEN-LAST:event_saveButtonActionPerformed + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JLabel configStatusLabel; + private javax.swing.JScrollPane descriptionScrollPane; + private javax.swing.JTextArea descriptionTextArea; + private javax.swing.JLabel executableStatusLabel; + // End of variables declaration//GEN-END:variables + + /** + * Set the information necessary to save the configuration + * + * @param configFile the path to the json configuration file + * @param config the configuration to save + */ + @NbBundle.Messages({ + "# {0} - configurationLocation", + "ConfigVisualPanel3.description.text=Press Save to write the imaging tool and configuration file to the destination.\nDestination: {0}" + }) + void setConfigInfoForSaving(String configFile, LogicalImagerConfig config) { + this.configFilename = configFile; + this.config = config; + descriptionTextArea.setText(Bundle.ConfigVisualPanel3_description_text(FilenameUtils.getFullPath(configFilename))); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/ConfigWizardPanel1.java b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/ConfigWizardPanel1.java new file mode 100644 index 0000000000..c84301018e --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/ConfigWizardPanel1.java @@ -0,0 +1,142 @@ +/* + * Autopsy + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.logicalimager.configuration; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import org.openide.WizardDescriptor; +import org.openide.WizardValidationException; +import org.openide.util.HelpCtx; + +/** + * Configuration Wizard Panel 1 + */ +final class ConfigWizardPanel1 implements WizardDescriptor.ValidatingPanel { + + /** + * The visual component that displays this panel. If you need to access the + * component from this class, just use getComponent(). + */ + private ConfigVisualPanel1 component; + private boolean valid = false; + + // Get the visual component for the panel. In this template, the component + // is kept separate. This can be more efficient: if the wizard is created + // but never displayed, or not all panels are displayed, it is better to + // create only those which really need to be visible. + @Override + public ConfigVisualPanel1 getComponent() { + if (component == null) { + component = new ConfigVisualPanel1(); + component.addPropertyChangeListener(new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (evt.getPropertyName().equals(ConfigVisualPanel1.getUpdateEventName())) { // NON-NLS + valid = component.isPanelValid(); + fireChangeEvent(); + } + } + }); + + } + return component; + } + + @Override + public HelpCtx getHelp() { + // Show no Help button for this panel: + return HelpCtx.DEFAULT_HELP; + // If you have context help: + // return new HelpCtx("help.key.here"); + } + + @Override + public boolean isValid() { + return valid; + // If it depends on some condition (form filled out...) and + // this condition changes (last form field filled in...) then + // use ChangeSupport to implement add/removeChangeListener below. + // WizardDescriptor.ERROR/WARNING/INFORMATION_MESSAGE will also be useful. + } + + private final Set listeners = new HashSet<>(1); // or can use ChangeSupport in NB 6.0 + + /** + * Adds a listener to changes of the panel's validity. + * + * @param l the change listener to add + */ + @Override + public void addChangeListener(ChangeListener l) { + synchronized (listeners) { + listeners.add(l); + } + } + + /** + * Removes a listener to changes of the panel's validity. + * + * @param l the change listener to move + */ + @Override + public void removeChangeListener(ChangeListener l) { + synchronized (listeners) { + listeners.remove(l); + } + } + + /** + * This method is auto-generated. It seems that this method is used to + * listen to any change in this wizard panel. + */ + void fireChangeEvent() { + Iterator it; + synchronized (listeners) { + it = new HashSet<>(listeners).iterator(); + } + ChangeEvent ev = new ChangeEvent(this); + while (it.hasNext()) { + it.next().stateChanged(ev); + } + } + + @Override + public void readSettings(WizardDescriptor wiz) { + // use wiz.getProperty to retrieve previous panel state + component.setConfigFilename((String) wiz.getProperty("configFilename")); // NON-NLS + } + + @Override + public void storeSettings(WizardDescriptor wiz) { + // use wiz.putProperty to remember current panel state + wiz.putProperty("configFilename", component.getConfigPath()); // NON-NLS + wiz.putProperty("config", component.getConfig()); // NON-NLS + } + + @Override + public void validate() throws WizardValidationException { + valid = component.isPanelValid(); + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/ConfigWizardPanel2.java b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/ConfigWizardPanel2.java new file mode 100644 index 0000000000..4a7edcc3a8 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/ConfigWizardPanel2.java @@ -0,0 +1,91 @@ +/* + * Autopsy + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.logicalimager.configuration; + +import javax.swing.event.ChangeListener; +import org.openide.WizardDescriptor; +import org.openide.util.HelpCtx; + +/** + * Configuration Wizard Panel 2 + */ +final class ConfigWizardPanel2 implements WizardDescriptor.Panel { + + /** + * The visual component that displays this panel. If you need to access the + * component from this class, just use getComponent(). + */ + private ConfigVisualPanel2 component; + private String configFilename; + private LogicalImagerConfig config; + + // Get the visual component for the panel. In this template, the component + // is kept separate. This can be more efficient: if the wizard is created + // but never displayed, or not all panels are displayed, it is better to + // create only those which really need to be visible. + @Override + public ConfigVisualPanel2 getComponent() { + if (component == null) { + component = new ConfigVisualPanel2(); + } + return component; + } + + @Override + public HelpCtx getHelp() { + // Show no Help button for this panel: + return HelpCtx.DEFAULT_HELP; + // If you have context help: + // return new HelpCtx("help.key.here"); + } + + @Override + public boolean isValid() { + // If it is always OK to press Next or Finish, then: + return true; + // If it depends on some condition (form filled out...) and + // this condition changes (last form field filled in...) then + // use ChangeSupport to implement add/removeChangeListener below. + // WizardDescriptor.ERROR/WARNING/INFORMATION_MESSAGE will also be useful. + } + + @Override + public void readSettings(WizardDescriptor wiz) { + // use wiz.getProperty to retrieve previous panel state + configFilename = (String) wiz.getProperty("configFilename"); // NON-NLS + config = (LogicalImagerConfig) wiz.getProperty("config"); // NON-NLS + component.setConfiguration(configFilename, config); + } + + @Override + public void storeSettings(WizardDescriptor wiz) { + // use wiz.putProperty to remember current panel state + } + + @Override + public void addChangeListener(ChangeListener cl) { + // Not used + } + + @Override + public void removeChangeListener(ChangeListener cl) { + // Not used + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/ConfigWizardPanel3.java b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/ConfigWizardPanel3.java new file mode 100644 index 0000000000..1719f71745 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/ConfigWizardPanel3.java @@ -0,0 +1,106 @@ +/* + * Autopsy + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.logicalimager.configuration; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import org.openide.WizardDescriptor; +import org.openide.util.HelpCtx; + +/** + * Configuration Wizard Panel 3 + */ +final class ConfigWizardPanel3 implements WizardDescriptor.Panel { + + private final Set listeners = new HashSet<>(1); // or can use ChangeSupport in NB 6.0 + private ConfigVisualPanel3 component; + + @Override + public ConfigVisualPanel3 getComponent() { + if (component == null) { + component = new ConfigVisualPanel3(); + component.addPropertyChangeListener(new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (evt.getPropertyName().equals(ConfigVisualPanel3.getSavedEventName())) { // NON-NLS + fireChangeEvent(); + } + } + }); + } + return component; + } + + @Override + public HelpCtx getHelp() { + return HelpCtx.DEFAULT_HELP; + } + + @Override + public void readSettings(WizardDescriptor wiz) { + String configFilename = (String) wiz.getProperty("configFilename"); // NON-NLS + LogicalImagerConfig config = (LogicalImagerConfig) wiz.getProperty("config"); // NON-NLS + component.setConfigInfoForSaving(configFilename, config); + component.resetPanel(); + } + + @Override + public void storeSettings(WizardDescriptor data) { + //no settings to store + } + + @Override + public boolean isValid() { + return component.isSaved(); + } + + /** + * Fire an envent to indicate that state of the wizard panel has changed + */ + private void fireChangeEvent() { + Iterator it; + synchronized (listeners) { + it = new HashSet<>(listeners).iterator(); + } + ChangeEvent ev = new ChangeEvent(this); + while (it.hasNext()) { + it.next().stateChanged(ev); + } + } + + @Override + public void addChangeListener(ChangeListener cl) { + synchronized (listeners) { + listeners.add(cl); + } + } + + @Override + public void removeChangeListener(ChangeListener cl) { + synchronized (listeners) { + listeners.remove(cl); + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/CreateLogicalImagerAction.java b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/CreateLogicalImagerAction.java new file mode 100644 index 0000000000..002b9a526e --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/CreateLogicalImagerAction.java @@ -0,0 +1,111 @@ +/* + * Autopsy + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.logicalimager.configuration; + +import java.awt.Component; +import java.awt.Dialog; +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; +import javax.swing.JComponent; +import org.openide.DialogDisplayer; +import org.openide.WizardDescriptor; +import org.openide.awt.ActionID; +import org.openide.awt.ActionReference; +import org.openide.awt.ActionRegistration; +import org.openide.modules.InstalledFileLocator; +import org.openide.util.HelpCtx; +import org.openide.util.NbBundle; +import org.openide.util.NbBundle.Messages; +import org.openide.util.actions.CallableSystemAction; + +/** + * Configuration Logical Imager + */ +@ActionID(category = "Tools", id = "org.sleuthkit.autopsy.logicalimager.configuration.CreateLogicalImagerAction") +@ActionRegistration(displayName = "#CTL_CreateLogicalImagerAction", lazy = false) +@ActionReference(path = "Menu/Tools", position = 2000, separatorBefore = 1999) +@Messages("CTL_CreateLogicalImagerAction=Create Logical Imager") +public final class CreateLogicalImagerAction extends CallableSystemAction { + + private static final long serialVersionUID = 1L; + private static final String LOGICAL_IMAGER_DIR = "tsk_logical_imager"; // NON-NLS + private static final String LOGICAL_IMAGER_EXE = "tsk_logical_imager.exe"; // NON-NLS + private static final Path LOGICAL_IMAGER_EXE_PATH = Paths.get(LOGICAL_IMAGER_DIR, LOGICAL_IMAGER_EXE); + + /** + * Finds the installed logical imager executable. + * + * @return A File object for the executable or null if it is not found. + */ + static File getLogicalImagerExe() { + return InstalledFileLocator.getDefault().locate(LOGICAL_IMAGER_EXE_PATH.toString(), CreateLogicalImagerAction.class.getPackage().getName(), false); + } + + @NbBundle.Messages({ + "CreateLogicalImagerAction.title=Create Logical Imager" + }) + @Override + public void performAction() { + List> panels = new ArrayList<>(); + panels.add(new ConfigWizardPanel1()); + panels.add(new ConfigWizardPanel2()); + panels.add(new ConfigWizardPanel3()); + String[] steps = new String[panels.size()]; + for (int i = 0; i < panels.size(); i++) { + Component c = panels.get(i).getComponent(); + // Default step name to component name of panel. + steps[i] = c.getName(); + if (c instanceof JComponent) { // assume Swing components + JComponent jc = (JComponent) c; + jc.putClientProperty(WizardDescriptor.PROP_CONTENT_SELECTED_INDEX, i); + jc.putClientProperty(WizardDescriptor.PROP_CONTENT_DATA, steps); + jc.putClientProperty(WizardDescriptor.PROP_AUTO_WIZARD_STYLE, true); + jc.putClientProperty(WizardDescriptor.PROP_CONTENT_DISPLAYED, true); + jc.putClientProperty(WizardDescriptor.PROP_CONTENT_NUMBERED, true); + } + } + WizardDescriptor wiz = new WizardDescriptor(new WizardDescriptor.ArrayIterator<>(panels)); + // {0} will be replaced by WizardDesriptor.Panel.getComponent().getName() + wiz.setTitleFormat(new MessageFormat("{0}")); // NON-NLS + wiz.setTitle(Bundle.CreateLogicalImagerAction_title()); + Dialog dialog = DialogDisplayer.getDefault().createDialog(wiz); + dialog.setVisible(true); + } + + @Override + public String getName() { + return Bundle.CTL_CreateLogicalImagerAction(); + } + + @Override + public HelpCtx getHelpCtx() { + return HelpCtx.DEFAULT_HELP; + } + + @Override + public boolean isEnabled() { + File logicalImagerExe = getLogicalImagerExe(); + return logicalImagerExe != null && logicalImagerExe.exists(); + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/DefaultToEmptyNumberFormatter.java b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/DefaultToEmptyNumberFormatter.java new file mode 100644 index 0000000000..fbcf3a7e84 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/DefaultToEmptyNumberFormatter.java @@ -0,0 +1,40 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.sleuthkit.autopsy.logicalimager.configuration; + +import java.text.NumberFormat; +import java.text.ParseException; +import javax.swing.text.NumberFormatter; + +/** + * Number formatter which will reset to being a null value when an invalid value + * is entered + */ +final class DefaultToEmptyNumberFormatter extends NumberFormatter { + + private static final long serialVersionUID = 1L; + + /** + * Create a DefaultToEmptyNumberFormatter + * + * @param format the format for the numbers + */ + DefaultToEmptyNumberFormatter(NumberFormat format) { + super(format); + } + + @Override + public Object stringToValue(String string) + throws ParseException { + Object returnValue = null; + try { + returnValue = super.stringToValue(string); + } catch (ParseException ignored) { + //reset value to being empty since invalid value was entered + } + return returnValue; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/EditFullPathsRulePanel.form b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/EditFullPathsRulePanel.form new file mode 100644 index 0000000000..22cbb340e7 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/EditFullPathsRulePanel.form @@ -0,0 +1,164 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/EditFullPathsRulePanel.java b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/EditFullPathsRulePanel.java new file mode 100644 index 0000000000..c40d05cffc --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/EditFullPathsRulePanel.java @@ -0,0 +1,356 @@ +/* + * Autopsy + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.logicalimager.configuration; + +import java.awt.event.ActionEvent; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.io.IOException; +import java.util.List; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JOptionPane; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.SwingUtilities; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.openide.util.NbBundle; + +/** + * Edit full paths rule panel + */ +@SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives +final class EditFullPathsRulePanel extends javax.swing.JPanel { + + private JButton okButton; + private JButton cancelButton; + private final JTextArea fullPathsTextArea; + + /** + * Creates new form EditFullPathsRulePanel + */ + @NbBundle.Messages({ + "EditFullPathsRulePanel.example=Example: " + }) + EditFullPathsRulePanel(JButton okButton, JButton cancelButton, String ruleName, LogicalImagerRule rule, boolean editing) { + initComponents(); + + this.setRule(ruleName, rule); + this.setButtons(okButton, cancelButton); + + fullPathsTextArea = new JTextArea(); + initTextArea(fullPathsScrollPane, fullPathsTextArea); + setTextArea(fullPathsTextArea, rule.getFullPaths()); + + EditRulePanel.setTextFieldPrompts(fullPathsTextArea, + "" + Bundle.EditFullPathsRulePanel_example() + "
/Program Files/Common Files/system/wab32.dll
/Windows/System32/1033/VsGraphicsResources.dll"); // NON-NLS + ruleNameTextField.requestFocus(); + addDocumentListeners(); + validate(); + repaint(); + } + + /** + * Update the OK button when contents of a field change + */ + private void addDocumentListeners() { + SwingUtilities.invokeLater(() -> { + setOkButton(); //ensure initial state before listeners added is correct + }); + DocumentListener docListener; + docListener = new DocumentListener() { + @Override + public void changedUpdate(DocumentEvent e) { + setOkButton(); + } + + @Override + public void insertUpdate(DocumentEvent e) { + setOkButton(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + setOkButton(); + } + }; + ruleNameTextField.getDocument().addDocumentListener(docListener); + fullPathsTextArea.getDocument().addDocumentListener(docListener); + } + + /** + * Initialize the text area and the scroll pane viewing it + * + * @param pane the JScrollPane which will be viewing the JTextArea + * @param textArea the JTextArea being initialized + */ + private void initTextArea(JScrollPane pane, JTextArea textArea) { + textArea.setColumns(20); + textArea.setRows(5); + pane.setViewportView(textArea); + textArea.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_TAB) { + if (e.getModifiers() > 0) { + textArea.transferFocusBackward(); + } else { + textArea.transferFocus(); + } + e.consume(); + } + } + }); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + shouldSaveCheckBox = new javax.swing.JCheckBox(); + shouldAlertCheckBox = new javax.swing.JCheckBox(); + fullPathsLabel = new javax.swing.JLabel(); + descriptionTextField = new javax.swing.JTextField(); + ruleNameLabel = new javax.swing.JLabel(); + ruleNameTextField = new javax.swing.JTextField(); + fullPathsScrollPane = new javax.swing.JScrollPane(); + jSeparator1 = new javax.swing.JSeparator(); + jSeparator2 = new javax.swing.JSeparator(); + jLabel1 = new javax.swing.JLabel(); + jLabel2 = new javax.swing.JLabel(); + + shouldSaveCheckBox.setSelected(true); + org.openide.awt.Mnemonics.setLocalizedText(shouldSaveCheckBox, org.openide.util.NbBundle.getMessage(EditFullPathsRulePanel.class, "EditFullPathsRulePanel.shouldSaveCheckBox.text")); // NOI18N + shouldSaveCheckBox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + shouldSaveCheckBoxActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(shouldAlertCheckBox, org.openide.util.NbBundle.getMessage(EditFullPathsRulePanel.class, "EditFullPathsRulePanel.shouldAlertCheckBox.text")); // NOI18N + shouldAlertCheckBox.setActionCommand(org.openide.util.NbBundle.getMessage(EditFullPathsRulePanel.class, "EditFullPathsRulePanel.shouldAlertCheckBox.actionCommand")); // NOI18N + shouldAlertCheckBox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + shouldAlertCheckBoxActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(fullPathsLabel, org.openide.util.NbBundle.getMessage(EditFullPathsRulePanel.class, "EditFullPathsRulePanel.fullPathsLabel.text")); // NOI18N + fullPathsLabel.setToolTipText(org.openide.util.NbBundle.getMessage(EditFullPathsRulePanel.class, "EditFullPathsRulePanel.fullPathsLabel.toolTipText")); // NOI18N + fullPathsLabel.setPreferredSize(new java.awt.Dimension(112, 14)); + + org.openide.awt.Mnemonics.setLocalizedText(ruleNameLabel, org.openide.util.NbBundle.getMessage(EditFullPathsRulePanel.class, "EditFullPathsRulePanel.ruleNameLabel.text")); // NOI18N + ruleNameLabel.setPreferredSize(new java.awt.Dimension(112, 14)); + + org.openide.awt.Mnemonics.setLocalizedText(jLabel1, org.openide.util.NbBundle.getMessage(EditFullPathsRulePanel.class, "EditFullPathsRulePanel.jLabel1.text")); // NOI18N + jLabel1.setPreferredSize(new java.awt.Dimension(112, 14)); + + org.openide.awt.Mnemonics.setLocalizedText(jLabel2, org.openide.util.NbBundle.getMessage(EditFullPathsRulePanel.class, "EditFullPathsRulePanel.jLabel2.text")); // NOI18N + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(jSeparator2) + .addComponent(jSeparator1) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addComponent(ruleNameLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(fullPathsLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jLabel1, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addGap(0, 0, 0) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(ruleNameTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 648, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(descriptionTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 648, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(fullPathsScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 648, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addComponent(shouldSaveCheckBox) + .addComponent(shouldAlertCheckBox) + .addComponent(jLabel2)) + .addGap(0, 0, Short.MAX_VALUE))) + .addContainerGap()) + ); + + layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {fullPathsLabel, jLabel1, ruleNameLabel}); + + layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {descriptionTextField, fullPathsScrollPane, ruleNameTextField}); + + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGap(8, 8, 8) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(ruleNameLabel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(ruleNameTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(descriptionTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(jLabel1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jSeparator1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(fullPathsLabel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(0, 115, Short.MAX_VALUE)) + .addComponent(fullPathsScrollPane)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jSeparator2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabel2) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(shouldSaveCheckBox) + .addGap(3, 3, 3) + .addComponent(shouldAlertCheckBox) + .addContainerGap()) + ); + }// //GEN-END:initComponents + + private void shouldSaveCheckBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_shouldSaveCheckBoxActionPerformed + setOkButton(); + }//GEN-LAST:event_shouldSaveCheckBoxActionPerformed + + private void shouldAlertCheckBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_shouldAlertCheckBoxActionPerformed + setOkButton(); + }//GEN-LAST:event_shouldAlertCheckBoxActionPerformed + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JTextField descriptionTextField; + private javax.swing.JLabel fullPathsLabel; + private javax.swing.JScrollPane fullPathsScrollPane; + private javax.swing.JLabel jLabel1; + private javax.swing.JLabel jLabel2; + private javax.swing.JSeparator jSeparator1; + private javax.swing.JSeparator jSeparator2; + private javax.swing.JLabel ruleNameLabel; + private javax.swing.JTextField ruleNameTextField; + private javax.swing.JCheckBox shouldAlertCheckBox; + private javax.swing.JCheckBox shouldSaveCheckBox; + // End of variables declaration//GEN-END:variables + + /** + * Sets whether or not the OK button should be enabled based upon other UI + * elements + */ + void setOkButton() { + if (this.okButton != null) { + this.okButton.setEnabled(!StringUtils.isBlank(ruleNameTextField.getText()) && !StringUtils.isBlank(fullPathsTextArea.getText()) && (shouldAlertCheckBox.isSelected() || shouldSaveCheckBox.isSelected())); + } + } + + /** + * Gets the JOptionPane that is used to contain this panel if there is one + * + * @param parent + * + * @return + */ + private JOptionPane getOptionPane(JComponent parent) { + JOptionPane pane; + if (!(parent instanceof JOptionPane)) { + pane = getOptionPane((JComponent) parent.getParent()); + } else { + pane = (JOptionPane) parent; + } + return pane; + } + + /** + * Sets the buttons for ending the panel + * + * @param ok The ok button + * @param cancel The cancel button + */ + private void setButtons(JButton ok, JButton cancel) { + this.okButton = ok; + this.cancelButton = cancel; + okButton.addActionListener((ActionEvent e) -> { + JOptionPane pane = getOptionPane(okButton); + pane.setValue(okButton); + }); + cancelButton.addActionListener((ActionEvent e) -> { + JOptionPane pane = getOptionPane(cancelButton); + pane.setValue(cancelButton); + }); + } + + /** + * Set the name description and should alert / should save checkboxes + * + * @param ruleName the name of the rule + * @param rule the LogicalImagerRule + */ + private void setRule(String ruleName, LogicalImagerRule rule) { + ruleNameTextField.setText(ruleName); + descriptionTextField.setText(rule.getDescription()); + shouldAlertCheckBox.setSelected(rule.isShouldAlert()); + shouldSaveCheckBox.setSelected(rule.isShouldSave()); + } + + /** + * Set the text of a text area to contain the contents of the list each on + * its own line + * + * @param textArea the text area to set the contents of + * @param list the list of strings to display in the text area + */ + private void setTextArea(JTextArea textArea, List list) { + String text = ""; + for (String s : list) { + text += s + System.getProperty("line.separator"); // NON-NLS + } + textArea.setText(text); + } + + /** + * Convert the contents of this panel to a rule and return it as well as its + * name. + * + * @return an ImmutablePair containing the name of the rule and the rule + * + * @throws IOException + */ + @NbBundle.Messages({ + "EditFullPathsRulePanel.fullPaths=Full paths",}) + ImmutablePair toRule() throws IOException { + List fullPaths = EditRulePanel.validateTextList(fullPathsTextArea, Bundle.EditFullPathsRulePanel_fullPaths()); + String ruleName = EditRulePanel.validRuleName(ruleNameTextField.getText()); + LogicalImagerRule.Builder builder = new LogicalImagerRule.Builder(); + builder.getShouldAlert(shouldAlertCheckBox.isSelected()) + .getShouldSave(shouldSaveCheckBox.isSelected()) + .getName(ruleName) + .getDescription(descriptionTextField.getText()) + .getFullPaths(fullPaths); + LogicalImagerRule rule = builder.build(); + return new ImmutablePair<>(ruleName, rule); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/EditNonFullPathsRulePanel.form b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/EditNonFullPathsRulePanel.form new file mode 100644 index 0000000000..4eca4bcbac --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/EditNonFullPathsRulePanel.form @@ -0,0 +1,414 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/EditNonFullPathsRulePanel.java b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/EditNonFullPathsRulePanel.java new file mode 100644 index 0000000000..8b9ead7efb --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/EditNonFullPathsRulePanel.java @@ -0,0 +1,900 @@ +/* + * Autopsy + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.logicalimager.configuration; + +import java.awt.Color; +import java.awt.event.ActionEvent; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.io.IOException; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JOptionPane; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.SwingUtilities; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import org.apache.commons.lang.StringUtils; +import static org.apache.commons.lang.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.strip; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coreutils.Logger; + +/** + * Edit non-full paths rule panel + */ +@SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives +final class EditNonFullPathsRulePanel extends javax.swing.JPanel { + + private static final Logger logger = Logger.getLogger(EditNonFullPathsRulePanel.class.getName()); + private static final long serialVersionUID = 1L; + private static final Color DISABLED_COLOR = new Color(240, 240, 240); + private static final int BYTE_UNIT_CONVERSION = 1000; + private JButton okButton; + private JButton cancelButton; + private final javax.swing.JTextArea fileNamesTextArea; + private final javax.swing.JTextArea folderNamesTextArea; + + /** + * Creates new form EditRulePanel + */ + @NbBundle.Messages({ + "EditNonFullPathsRulePanel.example=Example: ", + "EditNonFullPathsRulePanel.units.bytes=Bytes", + "EditNonFullPathsRulePanel.units.kilobytes=Kilobytes", + "EditNonFullPathsRulePanel.units.megabytes=Megabytes", + "EditNonFullPathsRulePanel.units.gigabytes=Gigabytes" + }) + EditNonFullPathsRulePanel(JButton okButton, JButton cancelButton, String ruleName, LogicalImagerRule rule, boolean editing) { + initComponents(); + + this.setRule(ruleName, rule); + this.setButtons(okButton, cancelButton); + + setExtensions(rule.getExtensions()); + fileNamesTextArea = new JTextArea(); + initTextArea(filenamesScrollPane, fileNamesTextArea); + setTextArea(fileNamesTextArea, rule.getFilenames()); + if (rule.getExtensions() != null && !rule.getExtensions().isEmpty()) { + extensionsCheckbox.setSelected(true); + extensionsTextField.setEnabled(extensionsCheckbox.isSelected()); + } else if (rule.getFilenames() != null && !rule.getFilenames().isEmpty()) { + fileNamesCheckbox.setSelected(true); + fileNamesTextArea.setEnabled(fileNamesCheckbox.isSelected()); + } + updateExclusiveConditions(); + folderNamesTextArea = new JTextArea(); + initTextArea(folderNamesScrollPane, folderNamesTextArea); + setTextArea(folderNamesTextArea, rule.getPaths()); + folderNamesCheckbox.setSelected(!StringUtils.isBlank(folderNamesTextArea.getText())); + folderNamesTextArea.setEnabled(folderNamesCheckbox.isSelected()); + updateTextAreaBackgroundColor(folderNamesTextArea); + setModifiedWithin(rule.getMinDays()); + + setupMinMaxSizeOptions(rule); + ruleNameTextField.requestFocus(); + + EditRulePanel.setTextFieldPrompts(extensionsTextField, Bundle.EditNonFullPathsRulePanel_example() + "gif,jpg,png"); // NON-NLS + EditRulePanel.setTextFieldPrompts(fileNamesTextArea, "" + + Bundle.EditNonFullPathsRulePanel_example() + + "
filename.txt
readme.txt"); // NON-NLS + EditRulePanel.setTextFieldPrompts(folderNamesTextArea, "" + + Bundle.EditNonFullPathsRulePanel_example() + + "
[USER_FOLDER]/My Documents/Downloads" + + "
/Program Files/Common Files"); // NON-NLS + validate(); + repaint(); + addDocumentListeners(); + } + + /** + * Set the min and max size options + * + * @param rule the rule the min and max size options should reflect + */ + private void setupMinMaxSizeOptions(LogicalImagerRule rule) { + String savedMinSize = rule.getMinFileSize() == null ? "" : rule.getMinFileSize().toString(); + setSizeAndUnits(minSizeTextField, minSizeUnitsCombobox, savedMinSize); + minSizeCheckbox.setSelected(!StringUtils.isBlank(minSizeTextField.getText())); + minSizeTextField.setEnabled(minSizeCheckbox.isSelected()); + minSizeUnitsCombobox.setEnabled(minSizeCheckbox.isSelected()); + + String savedMaxSize = rule.getMaxFileSize() == null ? "" : rule.getMaxFileSize().toString(); + setSizeAndUnits(maxSizeTextField, maxSizeUnitsCombobox, savedMaxSize); + maxSizeCheckbox.setSelected(!StringUtils.isBlank(maxSizeTextField.getText())); + maxSizeTextField.setEnabled(maxSizeCheckbox.isSelected()); + maxSizeUnitsCombobox.setEnabled(maxSizeCheckbox.isSelected()); + } + + /** + * Update the OK button when contents of a field change + */ + private void addDocumentListeners() { + SwingUtilities.invokeLater(() -> { + setOkButton(); //ensure initial state before listeners added is correct + }); + DocumentListener docListener; + docListener = new DocumentListener() { + @Override + public void changedUpdate(DocumentEvent e) { + setOkButton(); + } + + @Override + public void insertUpdate(DocumentEvent e) { + setOkButton(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + setOkButton(); + } + }; + ruleNameTextField.getDocument().addDocumentListener(docListener); + extensionsTextField.getDocument().addDocumentListener(docListener); + fileNamesTextArea.getDocument().addDocumentListener(docListener); + folderNamesTextArea.getDocument().addDocumentListener(docListener); + minSizeTextField.getDocument().addDocumentListener(docListener); + maxSizeTextField.getDocument().addDocumentListener(docListener); + modifiedWithinTextField.getDocument().addDocumentListener(docListener); + } + + /** + * Initialize the text area and the scroll pane viewing it + * + * @param pane the JScrollPane which will be viewing the JTextArea + * @param textArea the JTextArea being initialized + */ + private void initTextArea(JScrollPane pane, JTextArea textArea) { + textArea.setColumns(20); + textArea.setRows(4); + pane.setViewportView(textArea); + textArea.setEnabled(false); + textArea.setEditable(false); + textArea.setBackground(DISABLED_COLOR); + textArea.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_TAB) { + if (e.getModifiers() > 0) { + textArea.transferFocusBackward(); + } else { + textArea.transferFocus(); + } + e.consume(); + } + } + }); + } + + /** + * Convert a long value and a string representing its units to Bytes + * + * @param value the numerical value to convert + * @param units the name of the units to convert to Kilobytes, Megabytes, or + * Gigabytes + * + * @return + */ + private long convertToBytes(long value, String units) { + long convertedValue = value; + if (units.equals(Bundle.EditNonFullPathsRulePanel_units_gigabytes())) { + convertedValue = convertedValue * BYTE_UNIT_CONVERSION * BYTE_UNIT_CONVERSION * BYTE_UNIT_CONVERSION; + } else if (units.equals(Bundle.EditNonFullPathsRulePanel_units_megabytes())) { + convertedValue = convertedValue * BYTE_UNIT_CONVERSION * BYTE_UNIT_CONVERSION; + } else if (units.equals(Bundle.EditNonFullPathsRulePanel_units_kilobytes())) { + convertedValue *= BYTE_UNIT_CONVERSION; + } + return convertedValue; + } + + /** + * Set the size and units for a specified sizeField and unitsComboBox + * + * @param sizeField the size field to set + * @param unitsComboBox the units combo box to set + * @param value the value as a string representation of the number + * of Bytes + */ + private void setSizeAndUnits(JTextField sizeField, JComboBox unitsComboBox, String value) { + if (StringUtils.isBlank(value)) { + unitsComboBox.setSelectedItem(Bundle.EditNonFullPathsRulePanel_units_bytes()); + sizeField.setText(""); + return; + } + long longValue = Long.valueOf(value); + if (longValue % BYTE_UNIT_CONVERSION != 0) { + unitsComboBox.setSelectedItem(Bundle.EditNonFullPathsRulePanel_units_bytes()); + sizeField.setText(value); //value stored in bytes is correct value to display + return; + } + longValue /= BYTE_UNIT_CONVERSION; + if (longValue % BYTE_UNIT_CONVERSION != 0) { + unitsComboBox.setSelectedItem(Bundle.EditNonFullPathsRulePanel_units_kilobytes()); + sizeField.setText(String.valueOf(longValue)); + return; + } + longValue /= BYTE_UNIT_CONVERSION; + if (longValue % BYTE_UNIT_CONVERSION != 0) { + unitsComboBox.setSelectedItem(Bundle.EditNonFullPathsRulePanel_units_megabytes()); + sizeField.setText(String.valueOf(longValue)); + return; + } + longValue /= BYTE_UNIT_CONVERSION; + unitsComboBox.setSelectedItem(Bundle.EditNonFullPathsRulePanel_units_gigabytes()); + sizeField.setText(String.valueOf(longValue)); + + } + + /** + * Set the modified within X days field + * + * @param minDays the number of days to include + */ + private void setModifiedWithin(Integer minDays) { + modifiedWithinTextField.setText(minDays == null ? "" : minDays.toString()); + modifiedWithinCheckbox.setSelected(!StringUtils.isBlank(modifiedWithinTextField.getText())); + modifiedWithinTextField.setEnabled(modifiedWithinCheckbox.isSelected()); + } + + /** + * Set the contents of a text area to display a list of Strings + * + * @param textArea the text area to set the text of + * @param list the list of Strings to display in the text area + */ + private void setTextArea(JTextArea textArea, List list) { + String text = ""; + if (list != null) { + text = list.stream().map((s) -> s + System.getProperty("line.separator")).reduce(text, String::concat); // NON-NLS + } + textArea.setText(text); + } + + /** + * Set the extensions textField to display a list of extensions + * + * @param extensions the list of extensions to display + */ + private void setExtensions(List extensions) { + extensionsTextField.setText(""); + String content = ""; + if (extensions != null) { + boolean first = true; + for (String ext : extensions) { + content += (first ? "" : ",") + ext; + first = false; + } + } + extensionsCheckbox.setSelected(!StringUtils.isBlank(content)); + extensionsTextField.setText(content); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + daysIncludedLabel = new javax.swing.JLabel(); + shouldSaveCheckBox = new javax.swing.JCheckBox(); + shouldAlertCheckBox = new javax.swing.JCheckBox(); + extensionsTextField = new javax.swing.JTextField(); + descriptionTextField = new javax.swing.JTextField(); + ruleNameLabel = new javax.swing.JLabel(); + ruleNameTextField = new javax.swing.JTextField(); + filenamesScrollPane = new javax.swing.JScrollPane(); + folderNamesScrollPane = new javax.swing.JScrollPane(); + minSizeTextField = new javax.swing.JFormattedTextField(); + maxSizeTextField = new javax.swing.JFormattedTextField(); + modifiedWithinTextField = new javax.swing.JFormattedTextField(); + userFolderNote = new javax.swing.JLabel(); + minSizeCheckbox = new javax.swing.JCheckBox(); + maxSizeCheckbox = new javax.swing.JCheckBox(); + modifiedWithinCheckbox = new javax.swing.JCheckBox(); + folderNamesCheckbox = new javax.swing.JCheckBox(); + fileNamesCheckbox = new javax.swing.JCheckBox(); + extensionsCheckbox = new javax.swing.JCheckBox(); + minSizeUnitsCombobox = new javax.swing.JComboBox<>(); + maxSizeUnitsCombobox = new javax.swing.JComboBox<>(); + jSeparator1 = new javax.swing.JSeparator(); + jSeparator2 = new javax.swing.JSeparator(); + descriptionLabel = new javax.swing.JLabel(); + jLabel2 = new javax.swing.JLabel(); + jLabel1 = new javax.swing.JLabel(); + extensionsInfoLabel = new javax.swing.JLabel(); + fileNamesInfoLabel = new javax.swing.JLabel(); + + org.openide.awt.Mnemonics.setLocalizedText(daysIncludedLabel, org.openide.util.NbBundle.getMessage(EditNonFullPathsRulePanel.class, "EditNonFullPathsRulePanel.daysIncludedLabel.text")); // NOI18N + + shouldSaveCheckBox.setSelected(true); + org.openide.awt.Mnemonics.setLocalizedText(shouldSaveCheckBox, org.openide.util.NbBundle.getMessage(EditNonFullPathsRulePanel.class, "EditNonFullPathsRulePanel.shouldSaveCheckBox.text")); // NOI18N + shouldSaveCheckBox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + shouldSaveCheckBoxActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(shouldAlertCheckBox, org.openide.util.NbBundle.getMessage(EditNonFullPathsRulePanel.class, "EditNonFullPathsRulePanel.shouldAlertCheckBox.text")); // NOI18N + shouldAlertCheckBox.setActionCommand(org.openide.util.NbBundle.getMessage(EditNonFullPathsRulePanel.class, "EditNonFullPathsRulePanel.shouldAlertCheckBox.actionCommand")); // NOI18N + shouldAlertCheckBox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + shouldAlertCheckBoxActionPerformed(evt); + } + }); + + extensionsTextField.setEnabled(false); + + org.openide.awt.Mnemonics.setLocalizedText(ruleNameLabel, org.openide.util.NbBundle.getMessage(EditNonFullPathsRulePanel.class, "EditNonFullPathsRulePanel.ruleNameLabel.text")); // NOI18N + ruleNameLabel.setPreferredSize(new java.awt.Dimension(112, 14)); + + filenamesScrollPane.setToolTipText(org.openide.util.NbBundle.getMessage(EditNonFullPathsRulePanel.class, "EditNonFullPathsRulePanel.filenamesScrollPane.toolTipText")); // NOI18N + filenamesScrollPane.setEnabled(false); + + folderNamesScrollPane.setEnabled(false); + + minSizeTextField.setFormatterFactory(new javax.swing.text.DefaultFormatterFactory(new DefaultToEmptyNumberFormatter(new java.text.DecimalFormat("#,###; ")))); + minSizeTextField.setEnabled(false); + + maxSizeTextField.setFormatterFactory(new javax.swing.text.DefaultFormatterFactory(new DefaultToEmptyNumberFormatter(new java.text.DecimalFormat("#,###; ")))); + maxSizeTextField.setEnabled(false); + + modifiedWithinTextField.setFormatterFactory(new javax.swing.text.DefaultFormatterFactory(new DefaultToEmptyNumberFormatter(new java.text.DecimalFormat("#,###; ")))); + modifiedWithinTextField.setEnabled(false); + + userFolderNote.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/info-icon-16.png"))); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(userFolderNote, org.openide.util.NbBundle.getMessage(EditNonFullPathsRulePanel.class, "EditNonFullPathsRulePanel.userFolderNote.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(minSizeCheckbox, org.openide.util.NbBundle.getMessage(EditNonFullPathsRulePanel.class, "EditNonFullPathsRulePanel.minSizeCheckbox.text")); // NOI18N + minSizeCheckbox.setPreferredSize(new java.awt.Dimension(112, 23)); + minSizeCheckbox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + minSizeCheckboxActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(maxSizeCheckbox, org.openide.util.NbBundle.getMessage(EditNonFullPathsRulePanel.class, "EditNonFullPathsRulePanel.maxSizeCheckbox.text")); // NOI18N + maxSizeCheckbox.setPreferredSize(new java.awt.Dimension(112, 23)); + maxSizeCheckbox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + maxSizeCheckboxActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(modifiedWithinCheckbox, org.openide.util.NbBundle.getMessage(EditNonFullPathsRulePanel.class, "EditNonFullPathsRulePanel.modifiedWithinCheckbox.text")); // NOI18N + modifiedWithinCheckbox.setPreferredSize(new java.awt.Dimension(112, 23)); + modifiedWithinCheckbox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + modifiedWithinCheckboxActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(folderNamesCheckbox, org.openide.util.NbBundle.getMessage(EditNonFullPathsRulePanel.class, "EditNonFullPathsRulePanel.folderNamesCheckbox.text")); // NOI18N + folderNamesCheckbox.setPreferredSize(new java.awt.Dimension(112, 23)); + folderNamesCheckbox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + folderNamesCheckboxActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(fileNamesCheckbox, org.openide.util.NbBundle.getMessage(EditNonFullPathsRulePanel.class, "EditNonFullPathsRulePanel.fileNamesCheckbox.text")); // NOI18N + fileNamesCheckbox.setPreferredSize(new java.awt.Dimension(112, 23)); + fileNamesCheckbox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + fileNamesCheckboxActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(extensionsCheckbox, org.openide.util.NbBundle.getMessage(EditNonFullPathsRulePanel.class, "EditNonFullPathsRulePanel.extensionsCheckbox.text")); // NOI18N + extensionsCheckbox.setPreferredSize(new java.awt.Dimension(112, 23)); + extensionsCheckbox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + extensionsCheckboxActionPerformed(evt); + } + }); + + minSizeUnitsCombobox.setModel(new javax.swing.DefaultComboBoxModel(new String[] { Bundle.EditNonFullPathsRulePanel_units_bytes(), Bundle.EditNonFullPathsRulePanel_units_kilobytes(), Bundle.EditNonFullPathsRulePanel_units_megabytes(), Bundle.EditNonFullPathsRulePanel_units_gigabytes()})); + minSizeUnitsCombobox.setEnabled(false); + + maxSizeUnitsCombobox.setModel(new javax.swing.DefaultComboBoxModel(new String[] { Bundle.EditNonFullPathsRulePanel_units_bytes(), Bundle.EditNonFullPathsRulePanel_units_kilobytes(), Bundle.EditNonFullPathsRulePanel_units_megabytes(), Bundle.EditNonFullPathsRulePanel_units_gigabytes()})); + maxSizeUnitsCombobox.setEnabled(false); + + org.openide.awt.Mnemonics.setLocalizedText(descriptionLabel, org.openide.util.NbBundle.getMessage(EditNonFullPathsRulePanel.class, "EditNonFullPathsRulePanel.descriptionLabel.text")); // NOI18N + descriptionLabel.setPreferredSize(new java.awt.Dimension(112, 14)); + + org.openide.awt.Mnemonics.setLocalizedText(jLabel2, org.openide.util.NbBundle.getMessage(EditNonFullPathsRulePanel.class, "EditNonFullPathsRulePanel.jLabel2.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(jLabel1, org.openide.util.NbBundle.getMessage(EditNonFullPathsRulePanel.class, "EditNonFullPathsRulePanel.jLabel1.text")); // NOI18N + + extensionsInfoLabel.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/info-icon-16.png"))); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(extensionsInfoLabel, org.openide.util.NbBundle.getMessage(EditNonFullPathsRulePanel.class, "EditNonFullPathsRulePanel.extensionsInfoLabel.text")); // NOI18N + + fileNamesInfoLabel.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/info-icon-16.png"))); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(fileNamesInfoLabel, org.openide.util.NbBundle.getMessage(EditNonFullPathsRulePanel.class, "EditNonFullPathsRulePanel.fileNamesInfoLabel.text")); // NOI18N + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(jSeparator2) + .addComponent(jSeparator1) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(shouldAlertCheckBox) + .addComponent(shouldSaveCheckBox) + .addComponent(fileNamesCheckbox, javax.swing.GroupLayout.PREFERRED_SIZE, 112, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING, false) + .addComponent(modifiedWithinCheckbox, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(maxSizeCheckbox, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addComponent(jLabel1, javax.swing.GroupLayout.PREFERRED_SIZE, 120, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGap(0, 0, Short.MAX_VALUE)) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(extensionsCheckbox, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(ruleNameLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(descriptionLabel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(minSizeCheckbox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(folderNamesCheckbox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGap(0, 0, 0) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(ruleNameTextField, javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(descriptionTextField, javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(extensionsTextField, javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(folderNamesScrollPane) + .addComponent(filenamesScrollPane, javax.swing.GroupLayout.Alignment.TRAILING) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(fileNamesInfoLabel) + .addComponent(extensionsInfoLabel) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(jLabel2, javax.swing.GroupLayout.PREFERRED_SIZE, 522, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(userFolderNote) + .addGroup(layout.createSequentialGroup() + .addComponent(minSizeTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 63, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(minSizeUnitsCombobox, javax.swing.GroupLayout.PREFERRED_SIZE, 110, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(modifiedWithinTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 63, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(maxSizeTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 63, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(maxSizeUnitsCombobox, javax.swing.GroupLayout.PREFERRED_SIZE, 110, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(daysIncludedLabel)))))) + .addGap(0, 11, Short.MAX_VALUE))))) + .addContainerGap()) + ); + + layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {maxSizeTextField, minSizeTextField, modifiedWithinTextField}); + + layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {descriptionLabel, extensionsCheckbox, fileNamesCheckbox, folderNamesCheckbox, maxSizeCheckbox, minSizeCheckbox, modifiedWithinCheckbox, ruleNameLabel}); + + layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {maxSizeUnitsCombobox, minSizeUnitsCombobox}); + + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGap(8, 8, 8) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.CENTER) + .addComponent(ruleNameLabel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(ruleNameTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(descriptionTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(descriptionLabel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jSeparator2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(extensionsTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(extensionsCheckbox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(extensionsInfoLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(filenamesScrollPane) + .addGroup(layout.createSequentialGroup() + .addComponent(fileNamesCheckbox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(0, 0, Short.MAX_VALUE))) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(fileNamesInfoLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(folderNamesScrollPane) + .addGroup(layout.createSequentialGroup() + .addComponent(folderNamesCheckbox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(0, 0, Short.MAX_VALUE))) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(userFolderNote, javax.swing.GroupLayout.PREFERRED_SIZE, 16, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabel2) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(minSizeTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(minSizeCheckbox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(minSizeUnitsCombobox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(maxSizeTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(maxSizeCheckbox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(maxSizeUnitsCombobox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.CENTER) + .addComponent(daysIncludedLabel) + .addComponent(modifiedWithinTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(modifiedWithinCheckbox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jSeparator1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabel1) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(shouldSaveCheckBox) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(shouldAlertCheckBox) + .addContainerGap()) + ); + }// //GEN-END:initComponents + + private void extensionsCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_extensionsCheckboxActionPerformed + if (fileNamesCheckbox.isSelected() && extensionsCheckbox.isSelected()) { + fileNamesCheckbox.setSelected(false); + } + updateExclusiveConditions(); + setOkButton(); + }//GEN-LAST:event_extensionsCheckboxActionPerformed + + private void fileNamesCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_fileNamesCheckboxActionPerformed + if (fileNamesCheckbox.isSelected() && extensionsCheckbox.isSelected()) { + extensionsCheckbox.setSelected(false); + } + updateExclusiveConditions(); + setOkButton(); + }//GEN-LAST:event_fileNamesCheckboxActionPerformed + + private void folderNamesCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_folderNamesCheckboxActionPerformed + folderNamesScrollPane.setEnabled(folderNamesCheckbox.isSelected()); + folderNamesTextArea.setEditable(folderNamesCheckbox.isSelected()); + folderNamesTextArea.setEnabled(folderNamesCheckbox.isSelected()); + updateTextAreaBackgroundColor(folderNamesTextArea); + setOkButton(); + + }//GEN-LAST:event_folderNamesCheckboxActionPerformed + + private void minSizeCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_minSizeCheckboxActionPerformed + minSizeTextField.setEnabled(minSizeCheckbox.isSelected()); + minSizeUnitsCombobox.setEnabled(minSizeCheckbox.isSelected()); + setOkButton(); + }//GEN-LAST:event_minSizeCheckboxActionPerformed + + private void maxSizeCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_maxSizeCheckboxActionPerformed + maxSizeTextField.setEnabled(maxSizeCheckbox.isSelected()); + maxSizeUnitsCombobox.setEnabled(maxSizeCheckbox.isSelected()); + setOkButton(); + }//GEN-LAST:event_maxSizeCheckboxActionPerformed + + private void modifiedWithinCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_modifiedWithinCheckboxActionPerformed + modifiedWithinTextField.setEnabled(modifiedWithinCheckbox.isSelected()); + setOkButton(); + }//GEN-LAST:event_modifiedWithinCheckboxActionPerformed + + private void shouldSaveCheckBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_shouldSaveCheckBoxActionPerformed + setOkButton(); + }//GEN-LAST:event_shouldSaveCheckBoxActionPerformed + + private void shouldAlertCheckBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_shouldAlertCheckBoxActionPerformed + setOkButton(); + }//GEN-LAST:event_shouldAlertCheckBoxActionPerformed + + /** + * Update the background area of a JTextArea to reflect whether it is + * enabled or not + * + * @param textArea the textArea to update the background color of + */ + private static void updateTextAreaBackgroundColor(JTextArea textArea) { + if (textArea.isEnabled()) { + textArea.setBackground(Color.WHITE); + } else { + textArea.setBackground(DISABLED_COLOR); + } + } + + /** + * Update the enabled status of conditions which are exclusive of each other + * when either one is changed + */ + private void updateExclusiveConditions() { + extensionsTextField.setEnabled(extensionsCheckbox.isSelected()); + filenamesScrollPane.setEnabled(fileNamesCheckbox.isSelected()); + fileNamesTextArea.setEditable(fileNamesCheckbox.isSelected()); + fileNamesTextArea.setEnabled(fileNamesCheckbox.isSelected()); + updateTextAreaBackgroundColor(fileNamesTextArea); + } + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JLabel daysIncludedLabel; + private javax.swing.JLabel descriptionLabel; + private javax.swing.JTextField descriptionTextField; + private javax.swing.JCheckBox extensionsCheckbox; + private javax.swing.JLabel extensionsInfoLabel; + private javax.swing.JTextField extensionsTextField; + private javax.swing.JCheckBox fileNamesCheckbox; + private javax.swing.JLabel fileNamesInfoLabel; + private javax.swing.JScrollPane filenamesScrollPane; + private javax.swing.JCheckBox folderNamesCheckbox; + private javax.swing.JScrollPane folderNamesScrollPane; + private javax.swing.JLabel jLabel1; + private javax.swing.JLabel jLabel2; + private javax.swing.JSeparator jSeparator1; + private javax.swing.JSeparator jSeparator2; + private javax.swing.JCheckBox maxSizeCheckbox; + private javax.swing.JFormattedTextField maxSizeTextField; + private javax.swing.JComboBox maxSizeUnitsCombobox; + private javax.swing.JCheckBox minSizeCheckbox; + private javax.swing.JFormattedTextField minSizeTextField; + private javax.swing.JComboBox minSizeUnitsCombobox; + private javax.swing.JCheckBox modifiedWithinCheckbox; + private javax.swing.JFormattedTextField modifiedWithinTextField; + private javax.swing.JLabel ruleNameLabel; + private javax.swing.JTextField ruleNameTextField; + private javax.swing.JCheckBox shouldAlertCheckBox; + private javax.swing.JCheckBox shouldSaveCheckBox; + private javax.swing.JLabel userFolderNote; + // End of variables declaration//GEN-END:variables + + /** + * Set the name description and should alert / should save checkboxes + * + * @param ruleName the name of the rule + * @param rule the LogicalImagerRule + */ + private void setRule(String ruleName, LogicalImagerRule rule) { + ruleNameTextField.setText(ruleName); + descriptionTextField.setText(rule.getDescription()); + shouldAlertCheckBox.setSelected(rule.isShouldAlert()); + shouldSaveCheckBox.setSelected(rule.isShouldSave()); + } + + /** + * Sets whether or not the OK button should be enabled based upon other UI + * elements + */ + void setOkButton() { + if (this.okButton != null) { + this.okButton.setEnabled(!StringUtils.isBlank(ruleNameTextField.getText()) && atLeastOneConditionSet() && (shouldAlertCheckBox.isSelected() || shouldSaveCheckBox.isSelected())); + } + } + + /** + * Checks that at least one condition has been selected and set to have a + * value + * + * @return true if at least one condition is set, false otherwise + */ + private boolean atLeastOneConditionSet() { + try { + return (extensionsCheckbox.isSelected() && !StringUtils.isBlank(extensionsTextField.getText()) && !validateExtensions(extensionsTextField).isEmpty()) + || (fileNamesCheckbox.isSelected() && !StringUtils.isBlank(fileNamesTextArea.getText())) + || (folderNamesCheckbox.isSelected() && !StringUtils.isBlank(folderNamesTextArea.getText())) + || (minSizeCheckbox.isSelected() && !StringUtils.isBlank(minSizeTextField.getText()) && isNonZeroLong(minSizeTextField.getValue())) + || (maxSizeCheckbox.isSelected() && !StringUtils.isBlank(maxSizeTextField.getText()) && isNonZeroLong(maxSizeTextField.getValue())) + || (modifiedWithinCheckbox.isSelected() && !StringUtils.isBlank(modifiedWithinTextField.getText()) && isNonZeroLong(modifiedWithinTextField.getValue())); + } catch (IOException ex) { + logger.log(Level.WARNING, "Invalid contents of extensionsTextField", ex); + return false; + } + } + + /** + * Check that value could be a non zero long + * + * @param numberObject the object to check + * + * @return true if the value is a non-zero long + */ + private boolean isNonZeroLong(Object numberObject) { + Long value = 0L; + try { + if (numberObject instanceof Number) { + value = ((Number) numberObject).longValue(); + } + } catch (NumberFormatException ignored) { + //The string was not a number, this method will return false becaue the value is still 0L + } + return (value != 0); + } + + /** + * Gets the JOptionPane that is used to contain this panel if there is one + * + * @param parent + * + * @return + */ + private JOptionPane getOptionPane(JComponent parent) { + JOptionPane pane; + if (!(parent instanceof JOptionPane)) { + pane = getOptionPane((JComponent) parent.getParent()); + } else { + pane = (JOptionPane) parent; + } + return pane; + } + + /** + * Sets the buttons for ending the panel + * + * @param ok The ok button + * @param cancel The cancel button + */ + private void setButtons(JButton ok, JButton cancel) { + this.okButton = ok; + this.cancelButton = cancel; + okButton.addActionListener((ActionEvent e) -> { + JOptionPane pane = getOptionPane(okButton); + pane.setValue(okButton); + }); + cancelButton.addActionListener((ActionEvent e) -> { + JOptionPane pane = getOptionPane(cancelButton); + pane.setValue(cancelButton); + }); + this.setOkButton(); + } + + /** + * Convert the contents of this panel to a rule and return it as well as its + * name. + * + * @return an ImmutablePair containing the name of the rule and the rule + * + * @throws IOException + */ + @NbBundle.Messages({ + "EditNonFullPathsRulePanel.modifiedDaysNotPositiveException=Modified days must be a positive", + "# {0} - message", + "EditNonFullPathsRulePanel.modifiedDaysMustBeNumberException=Modified days must be a number: {0}", + "EditNonFullPathsRulePanel.minFileSizeNotPositiveException=Minimum file size must be a positive", + "# {0} - message", + "EditNonFullPathsRulePanel.minFileSizeMustBeNumberException=Minimum file size must be a number: {0}", + "EditNonFullPathsRulePanel.maxFileSizeNotPositiveException=Maximum file size must be a positive", + "# {0} - message", + "EditNonFullPathsRulePanel.maxFileSizeMustBeNumberException=Maximum file size must be a number: {0}", + "# {0} - maxFileSize", + "# {1} - minFileSize", + "EditNonFullPathsRulePanel.maxFileSizeSmallerThanMinException=Maximum file size: {0} bytes must be bigger than minimum file size: {1} bytes", + "EditNonFullPathsRulePanel.fileNames=File names", + "EditNonFullPathsRulePanel.folderNames=Folder names",}) + ImmutablePair toRule() throws IOException { + String ruleName = EditRulePanel.validRuleName(ruleNameTextField.getText()); + List folderNames = folderNamesCheckbox.isSelected() ? EditRulePanel.validateTextList(folderNamesTextArea, Bundle.EditNonFullPathsRulePanel_folderNames()) : null; + + LogicalImagerRule.Builder builder = new LogicalImagerRule.Builder(); + builder.getName(ruleName) + .getDescription(descriptionTextField.getText()) + .getShouldAlert(shouldAlertCheckBox.isSelected()) + .getShouldSave(shouldSaveCheckBox.isSelected()) + .getPaths(folderNames); + + if (extensionsCheckbox.isSelected()) { + builder.getExtensions(validateExtensions(extensionsTextField)); + } else if (fileNamesCheckbox.isSelected()) { + builder.getFilenames(EditRulePanel.validateTextList(fileNamesTextArea, Bundle.EditNonFullPathsRulePanel_fileNames())); + } + + int minDays; + if (modifiedWithinCheckbox.isSelected() && !isBlank(modifiedWithinTextField.getText())) { + try { + modifiedWithinTextField.commitEdit(); + minDays = ((Number) modifiedWithinTextField.getValue()).intValue(); + if (minDays < 0) { + throw new IOException(Bundle.EditNonFullPathsRulePanel_modifiedDaysNotPositiveException()); + } + builder.getMinDays(minDays); + } catch (NumberFormatException | ParseException ex) { + throw new IOException(Bundle.EditNonFullPathsRulePanel_modifiedDaysMustBeNumberException(ex.getMessage()), ex); + } + } + + long minFileSize = 0; + if (minSizeCheckbox.isSelected() && !isBlank(minSizeTextField.getText())) { + try { + minSizeTextField.commitEdit(); + minFileSize = ((Number) minSizeTextField.getValue()).longValue(); + if (minFileSize < 0) { + throw new IOException(Bundle.EditNonFullPathsRulePanel_minFileSizeNotPositiveException()); + } + minFileSize = convertToBytes(minFileSize, minSizeUnitsCombobox.getItemAt(minSizeUnitsCombobox.getSelectedIndex())); + } catch (NumberFormatException | ParseException ex) { + throw new IOException(Bundle.EditNonFullPathsRulePanel_minFileSizeMustBeNumberException(ex.getMessage()), ex); + } + } + + long maxFileSize = 0; + if (maxSizeCheckbox.isSelected() && !isBlank(maxSizeTextField.getText())) { + try { + maxSizeTextField.commitEdit(); + maxFileSize = ((Number) maxSizeTextField.getValue()).longValue(); + if (maxFileSize < 0) { + throw new IOException(Bundle.EditNonFullPathsRulePanel_maxFileSizeNotPositiveException()); + } + maxFileSize = convertToBytes(maxFileSize, maxSizeUnitsCombobox.getItemAt(maxSizeUnitsCombobox.getSelectedIndex())); + } catch (NumberFormatException | ParseException ex) { + throw new IOException(Bundle.EditNonFullPathsRulePanel_maxFileSizeMustBeNumberException(ex.getMessage()), ex); + } + } + + if (maxFileSize != 0 && (maxFileSize < minFileSize)) { + throw new IOException(Bundle.EditNonFullPathsRulePanel_maxFileSizeSmallerThanMinException(maxFileSize, minFileSize)); + } + if (minSizeCheckbox.isSelected() && minFileSize != 0) { + builder.getMinFileSize(minFileSize); + } + if (maxSizeCheckbox.isSelected() && maxFileSize != 0) { + builder.getMaxFileSize(maxFileSize); + } + + LogicalImagerRule rule = builder.build(); + return new ImmutablePair<>(ruleName, rule); + } + + /** + * Validate the contents of the extensions textField contain a list of comma + * seperated strings which could be file extensions + * + * @param textField the JTextField which contains the extensions + * + * @return a List containing a string for each possible extension specified. + * + * @throws IOException + */ + @NbBundle.Messages({ + "EditNonFullPathsRulePanel.emptyExtensionException=Extensions cannot have an empty entry",}) + private List validateExtensions(JTextField textField) throws IOException { + if (isBlank(textField.getText())) { + return null; + } + List extensions = new ArrayList<>(); + for (String extension : textField.getText().split(",")) { + String strippedExtension = strip(extension); + if (strippedExtension.isEmpty()) { + throw new IOException(Bundle.EditNonFullPathsRulePanel_emptyExtensionException()); + } + extensions.add(strippedExtension); + } + if (extensions.isEmpty()) { + return null; + } + return extensions; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/EditRulePanel.java b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/EditRulePanel.java new file mode 100644 index 0000000000..5512af2b58 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/EditRulePanel.java @@ -0,0 +1,130 @@ +/* + * Autopsy + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.logicalimager.configuration; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import javax.swing.JButton; +import javax.swing.JPanel; +import javax.swing.JTextArea; +import javax.swing.text.JTextComponent; +import static org.apache.commons.lang.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.strip; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.corecomponents.TextPrompt; + +/** + * Edit rule panel + */ +final class EditRulePanel extends JPanel { + + private static final long serialVersionUID = 1L; + private final EditFullPathsRulePanel editFullPathsRulePanel; + private final EditNonFullPathsRulePanel editNonFullPathsRulePanel; + + /** + * Creates new form EditRulePanel + */ + EditRulePanel(JButton okButton, JButton cancelButton, String ruleName, LogicalImagerRule rule) { + if (rule.getFullPaths() != null && rule.getFullPaths().size() > 0) { + editFullPathsRulePanel = new EditFullPathsRulePanel(okButton, cancelButton, ruleName, rule, true); + editNonFullPathsRulePanel = null; + } else { + editNonFullPathsRulePanel = new EditNonFullPathsRulePanel(okButton, cancelButton, ruleName, rule, true); + editFullPathsRulePanel = null; + } + } + + JPanel getPanel() { + if (editFullPathsRulePanel != null) { + return editFullPathsRulePanel; + } else { + return editNonFullPathsRulePanel; + } + } + + ImmutablePair toRule() throws IOException, NumberFormatException { + ImmutablePair ruleMap; + if (editFullPathsRulePanel != null) { + ruleMap = editFullPathsRulePanel.toRule(); + } else { + ruleMap = editNonFullPathsRulePanel.toRule(); + } + return ruleMap; + } + + static void setTextFieldPrompts(JTextComponent textField, String text) { + /** + * Add text prompt to the text field. + */ + TextPrompt textPrompt; + if (textField instanceof JTextArea) { + textPrompt = new TextPrompt(text, textField, BorderLayout.NORTH); + } else { + textPrompt = new TextPrompt(text, textField); + } + + /** + * Sets the foreground color and transparency of the text prompt. + */ + textPrompt.setForeground(Color.LIGHT_GRAY); + textPrompt.changeAlpha(0.9f); // Mostly opaque + } + + @NbBundle.Messages({ + "EditRulePanel.emptyRuleName.message=Rule name cannot be empty", + "# {0} - ruleName", + "EditRulePanel.reservedRuleName.message=Rule name \"{0}\" is reserved for use with a predefined rule"}) + static String validRuleName(String name) throws IOException { + if (name.isEmpty()) { + throw new IOException(Bundle.EditRulePanel_emptyRuleName_message()); + } + if (name.equals(EncryptionProgramsRule.getName())) { + throw new IOException(Bundle.EditRulePanel_reservedRuleName_message(name)); + } + //TODO JIRA-5239 check if rule name exists already + return name; + } + + @NbBundle.Messages({ + "# {0} - fieldName", + "EditRulePanel.blankLineException={0} cannot have a blank line",}) + static List validateTextList(JTextArea textArea, String fieldName) throws IOException { + if (isBlank(textArea.getText())) { + return null; + } + List list = new ArrayList<>(); + for (String line : textArea.getText().split("\\n")) { // NON-NLS + String strippedLine = strip(line); + if (strippedLine.isEmpty()) { + throw new IOException(Bundle.EditRulePanel_blankLineException(fieldName)); + } + list.add(strippedLine); + } + if (list.isEmpty()) { + return null; + } + return list; + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/EncryptionProgramsRule.java b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/EncryptionProgramsRule.java new file mode 100644 index 0000000000..f94a958add --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/EncryptionProgramsRule.java @@ -0,0 +1,95 @@ +/* + * Autopsy + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.logicalimager.configuration; + +import java.util.ArrayList; +import java.util.List; +import org.openide.util.NbBundle; + +/** + * Encryption programs rule + */ +@NbBundle.Messages({ + "EncryptionProgramsRule.encryptionProgramsRuleName=Encryption Programs", + "EncryptionProgramsRule.encryptionProgramsRuleDescription=Find encryption programs" +}) +final class EncryptionProgramsRule { + + private static final String ENCRYPTION_PROGRAMS_RULE_NAME = Bundle.EncryptionProgramsRule_encryptionProgramsRuleName(); + private static final String ENCRYPTION_PROGRAMS_RULE_DESCRIPTION = Bundle.EncryptionProgramsRule_encryptionProgramsRuleDescription(); + private static final List FILENAMES = new ArrayList<>(); + + private EncryptionProgramsRule() { + //private no arg constructor intentionally blank + } + + static { + // Truecrypt + FILENAMES.add("truecrypt.exe"); // NON-NLS + + // AxCrypt + FILENAMES.add("AxCrypt.exe"); // NON-NLS + + // VeraCrypt + FILENAMES.add("VeraCrypt.exe"); // NON-NLS + FILENAMES.add("VeraCrypt Format.exe"); // NON-NLS + FILENAMES.add("VeraCrypt Setup.exe"); // NON-NLS + FILENAMES.add("VeraCryptExpander.exe"); // NON-NLS + + // GnuPG + FILENAMES.add("gpg-agent.exe"); // NON-NLS + FILENAMES.add("gpg-connect-agent.exe"); // NON-NLS + FILENAMES.add("gpg-preset-passphrase.exe"); // NON-NLS + FILENAMES.add("gpg-wks-client.exe"); // NON-NLS + FILENAMES.add("gpg.exe"); // NON-NLS + FILENAMES.add("gpgconf.exe"); // NON-NLS + FILENAMES.add("gpgme-w32spawn.exe"); // NON-NLS + FILENAMES.add("gpgsm.exe"); // NON-NLS + FILENAMES.add("gpgtar.exe"); // NON-NLS + FILENAMES.add("gpgv.exe"); // NON-NLS + + // Symantec Encryption Desktop aka PGP + FILENAMES.add("PGP Viewer.exe"); // NON-NLS + FILENAMES.add("PGPcbt64.exe"); // NON-NLS + FILENAMES.add("PGPdesk.exe"); // NON-NLS + FILENAMES.add("PGPfsd.exe"); // NON-NLS + FILENAMES.add("PGPmnApp.exe"); // NON-NLS + FILENAMES.add("pgpnetshare.exe"); // NON-NLS + FILENAMES.add("pgpp.exe"); // NON-NLS + FILENAMES.add("PGPpdCreate.exe"); // NON-NLS + FILENAMES.add("pgppe.exe"); // NON-NLS + FILENAMES.add("pgpstart.exe"); // NON-NLS + FILENAMES.add("PGPtray.exe"); // NON-NLS + FILENAMES.add("PGPwde.exe"); // NON-NLS + FILENAMES.add("PGP Portable.exe"); // NON-NLS + + } + + static String getName() { + return ENCRYPTION_PROGRAMS_RULE_NAME; + } + + static String getDescription() { + return ENCRYPTION_PROGRAMS_RULE_DESCRIPTION; + } + + static List getFilenames() { + return FILENAMES; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/LogicalImagerConfig.java b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/LogicalImagerConfig.java new file mode 100644 index 0000000000..685a2c8d35 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/LogicalImagerConfig.java @@ -0,0 +1,97 @@ +/* + * Autopsy + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.logicalimager.configuration; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import java.util.ArrayList; +import java.util.List; + +/** + * Logical Imager Configuration file JSON + */ +class LogicalImagerConfig { + + static private final String CURRENT_VERSION = "1.0"; + + @SerializedName("version") + @Expose(serialize = true) + private String version; + + @SerializedName("finalize-image-writer") + @Expose(serialize = true) + private boolean finalizeImageWriter; + + @SerializedName("rule-sets") + @Expose(serialize = true) + private List ruleSets; + + LogicalImagerConfig() { + this.version = CURRENT_VERSION; + this.finalizeImageWriter = false; + this.ruleSets = new ArrayList<>(); + } + + LogicalImagerConfig( + boolean finalizeImageWriter, + List ruleSets + ) { + this.version = CURRENT_VERSION; + this.finalizeImageWriter = finalizeImageWriter; + this.ruleSets = ruleSets; + } + + LogicalImagerConfig( + String version, + boolean finalizeImageWriter, + List ruleSets + ) { + this.version = version; + this.finalizeImageWriter = finalizeImageWriter; + this.ruleSets = ruleSets; + } + + String getVersion() { + return version; + } + + void setVersion(String version) { + this.version = version; + } + + static public String getCurrentVersion() { + return CURRENT_VERSION; + } + + boolean isFinalizeImageWriter() { + return finalizeImageWriter; + } + + void setFinalizeImageWriter(boolean finalizeImageWriter) { + this.finalizeImageWriter = finalizeImageWriter; + } + + List getRuleSets() { + return ruleSets; + } + + void setRuleSet(List ruleSets) { + this.ruleSets = ruleSets; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/LogicalImagerConfigDeserializer.java b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/LogicalImagerConfigDeserializer.java new file mode 100644 index 0000000000..a9c049ce08 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/LogicalImagerConfigDeserializer.java @@ -0,0 +1,213 @@ +/* + * Autopsy + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.logicalimager.configuration; + +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.openide.util.NbBundle; + +/** + * Logical Imager Configuration JSON deserializer + */ +@NbBundle.Messages({ + "LogicalImagerConfigDeserializer.missingRuleSetException=Missing rule-set", + "# {0} - key", + "LogicalImagerConfigDeserializer.unsupportedKeyException=Unsupported key: {0}", + "LogicalImagerConfigDeserializer.fullPathsException=A rule with full-paths cannot have other rule definitions",}) +class LogicalImagerConfigDeserializer implements JsonDeserializer { + + @Override + public LogicalImagerConfig deserialize(JsonElement je, Type type, JsonDeserializationContext jdc) throws JsonParseException { + String version = LogicalImagerConfig.getCurrentVersion(); + boolean finalizeImageWriter = false; + + final JsonObject jsonObject = je.getAsJsonObject(); + final JsonElement jsonVersion = jsonObject.get("version"); // NON-NLS + if (jsonVersion != null) { + version = jsonVersion.getAsString(); + } + + final JsonElement jsonFinalizeImageWriter = jsonObject.get("finalize-image-writer"); // NON-NLS + if (jsonFinalizeImageWriter != null) { + finalizeImageWriter = jsonFinalizeImageWriter.getAsBoolean(); + } + + JsonArray asJsonArray = jsonObject.get("rule-sets").getAsJsonArray(); // NON-NLS + if (asJsonArray == null) { + throw new JsonParseException(Bundle.LogicalImagerConfigDeserializer_missingRuleSetException()); + } + + List ruleSets = new ArrayList<>(); + for (JsonElement element : asJsonArray) { + String setName = null; + List rules = null; + JsonObject asJsonObject = element.getAsJsonObject(); + JsonElement setNameElement = asJsonObject.get("set-name"); + setName = setNameElement.getAsString(); + JsonElement rulesElement = asJsonObject.get("rules"); + rules = parseRules(rulesElement.getAsJsonArray()); + LogicalImagerRuleSet ruleSet = new LogicalImagerRuleSet(setName, rules); + ruleSets.add(ruleSet); + } + return new LogicalImagerConfig(version, finalizeImageWriter, ruleSets); + } + + private List parseRules(JsonArray asJsonArray) { + List rules = new ArrayList<>(); + + for (JsonElement element : asJsonArray) { + String key1; + Boolean shouldSave = false; + Boolean shouldAlert = true; + String name = null; + String description = null; + List extensions = null; + List paths = null; + List fullPaths = null; + List filenames = null; + Long minFileSize = null; + Long maxFileSize = null; + Integer minDays = null; + Integer minDate = null; + Integer maxDate = null; + + Set> entrySet = element.getAsJsonObject().entrySet(); + + for (Map.Entry entry1 : entrySet) { + key1 = entry1.getKey(); + switch (key1) { + case "shouldAlert": // NON-NLS + shouldAlert = entry1.getValue().getAsBoolean(); + break; + case "shouldSave": // NON-NLS + shouldSave = entry1.getValue().getAsBoolean(); + break; + case "name": // NON-NLS + name = entry1.getValue().getAsString(); + break; + case "description": // NON-NLS + description = entry1.getValue().getAsString(); + break; + case "extensions": // NON-NLS + JsonArray extensionsArray = entry1.getValue().getAsJsonArray(); + extensions = new ArrayList<>(); + for (JsonElement e : extensionsArray) { + extensions.add(e.getAsString()); + } + break; + case "folder-names": // NON-NLS + JsonArray pathsArray = entry1.getValue().getAsJsonArray(); + paths = new ArrayList<>(); + for (JsonElement e : pathsArray) { + paths.add(e.getAsString()); + } + break; + case "file-names": // NON-NLS + JsonArray filenamesArray = entry1.getValue().getAsJsonArray(); + filenames = new ArrayList<>(); + for (JsonElement e : filenamesArray) { + filenames.add(e.getAsString()); + } + break; + case "full-paths": // NON-NLS + JsonArray fullPathsArray = entry1.getValue().getAsJsonArray(); + fullPaths = new ArrayList<>(); + for (JsonElement e : fullPathsArray) { + fullPaths.add(e.getAsString()); + } + break; + case "size-range": // NON-NLS + JsonObject sizeRangeObject = entry1.getValue().getAsJsonObject(); + Set> entrySet1 = sizeRangeObject.entrySet(); + for (Map.Entry entry2 : entrySet1) { + String sizeKey = entry2.getKey(); + switch (sizeKey) { + case "min": // NON-NLS + minFileSize = entry2.getValue().getAsLong(); + break; + case "max": // NON-NLS + maxFileSize = entry2.getValue().getAsLong(); + break; + default: + throw new JsonParseException(Bundle.LogicalImagerConfigDeserializer_unsupportedKeyException(sizeKey)); + } + } + break; + case "date-range": // NON-NLS + JsonObject dateRangeObject = entry1.getValue().getAsJsonObject(); + Set> entrySet2 = dateRangeObject.entrySet(); + for (Map.Entry entry2 : entrySet2) { + String dateKey = entry2.getKey(); + switch (dateKey) { + case "min": // NON-NLS + minDate = entry2.getValue().getAsInt(); + break; + case "max": // NON-NLS + maxDate = entry2.getValue().getAsInt(); + break; + case "min-days": // NON-NLS + minDays = entry2.getValue().getAsInt(); + break; + default: + throw new JsonParseException(Bundle.LogicalImagerConfigDeserializer_unsupportedKeyException(dateKey)); + } + } + break; + default: + throw new JsonParseException(Bundle.LogicalImagerConfigDeserializer_unsupportedKeyException(key1)); + } + } + + // A rule with full-paths cannot have other rule definitions + if ((fullPaths != null && !fullPaths.isEmpty()) && ((extensions != null && !extensions.isEmpty()) + || (paths != null && !paths.isEmpty()) + || (filenames != null && !filenames.isEmpty()))) { + throw new JsonParseException(Bundle.LogicalImagerConfigDeserializer_fullPathsException()); + } + + LogicalImagerRule rule = new LogicalImagerRule.Builder() + .getShouldAlert(shouldAlert) + .getShouldSave(shouldSave) + .getName(name) + .getDescription(description) + .getExtensions(extensions) + .getPaths(paths) + .getFullPaths(fullPaths) + .getFilenames(filenames) + .getMinFileSize(minFileSize) + .getMaxFileSize(maxFileSize) + .getMinDays(minDays) + .getMinDate(minDate) + .getMaxDate(maxDate) + .build(); + rules.add(rule); + } // for + + return rules; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/LogicalImagerRule.java b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/LogicalImagerRule.java new file mode 100644 index 0000000000..36fbf18f87 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/LogicalImagerRule.java @@ -0,0 +1,255 @@ +/* + * Autopsy + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.logicalimager.configuration; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * + * The class definition for the Logical Imager Rule. + */ +class LogicalImagerRule { + + @Expose(serialize = true) + private final Boolean shouldAlert; + @Expose(serialize = true) + private final Boolean shouldSave; + @Expose(serialize = true) + private final String name; + @Expose(serialize = true) + private final String description; + @Expose(serialize = true) + private List extensions = new ArrayList<>(); + @SerializedName("file-names") + @Expose(serialize = true) + private List filenames = new ArrayList<>(); + @SerializedName("folder-names") + @Expose(serialize = true) + private List paths = new ArrayList<>(); + @SerializedName("full-paths") + @Expose(serialize = true) + private List fullPaths = new ArrayList<>(); + @SerializedName("size-range") + @Expose(serialize = true) + final private Map sizeRange = new HashMap<>(); + @SerializedName("date-range") + @Expose(serialize = true) + final private Map dateRange = new HashMap<>(); + + // The following fields should not be serialized, internal use only + @Expose(serialize = false) + private Long minFileSize; + @Expose(serialize = false) + private Long maxFileSize; + @Expose(serialize = false) + private Integer minDays; + @Expose(serialize = false) + private Integer minDate; + @Expose(serialize = false) + private Integer maxDate; + + private LogicalImagerRule(Boolean shouldAlert, Boolean shouldSave, String name, String description, + List extensions, + List filenames, + List paths, + List fullPaths, + Long minFileSize, + Long maxFileSize, + Integer minDays, + Integer minDate, + Integer maxDate + ) { + this.shouldAlert = shouldAlert; + this.shouldSave = shouldSave; + this.name = name; + this.description = description; + this.extensions = extensions; + this.filenames = filenames; + this.paths = paths; + this.fullPaths = fullPaths; + + this.sizeRange.put("min", minFileSize); // NON-NLS + this.minFileSize = minFileSize; + this.sizeRange.put("max", maxFileSize); // NON-NLS + this.maxFileSize = maxFileSize; + this.dateRange.put("min-days", minDays); // NON-NLS + this.minDays = minDays; + this.dateRange.put("min-date", minDate); // NON-NLS + this.minDate = minDate; + this.dateRange.put("max-date", maxDate); // NON-NLS + this.maxDate = maxDate; + } + + LogicalImagerRule() { + this.shouldAlert = false; // default + this.shouldSave = true; // default + this.description = null; + this.name = null; + } + + Boolean isShouldAlert() { + return shouldAlert; + } + + Boolean isShouldSave() { + return shouldSave; + } + + String getName() { + return name; + } + + String getDescription() { + return description; + } + + List getExtensions() { + return extensions; + } + + List getFilenames() { + return filenames; + } + + List getPaths() { + return paths; + } + + List getFullPaths() { + return fullPaths; + } + + Long getMinFileSize() { + return minFileSize; + } + + Long getMaxFileSize() { + return maxFileSize; + } + + Integer getMinDays() { + return minDays; + } + + Integer getMinDate() { + return minDate; + } + + Integer getMaxDate() { + return maxDate; + } + + /** + * Builder class + */ + static class Builder { + + private Boolean shouldAlert = null; + private Boolean shouldSave = null; + private String name = null; + private String description = null; + private List extensions = null; + private List filenames = null; + private List paths = null; + private List fullPaths = null; + private Long minFileSize = null; + private Long maxFileSize = null; + private Integer minDays = null; + private Integer minDate = null; + private Integer maxDate = null; + + Builder getShouldAlert(boolean shouldAlert) { + this.shouldAlert = shouldAlert; + return this; + } + + Builder getShouldSave(boolean shouldSave) { + this.shouldSave = shouldSave; + return this; + } + + Builder getName(String name) { + this.name = name; + return this; + } + + Builder getDescription(String description) { + this.description = description; + return this; + } + + Builder getExtensions(List extensions) { + this.extensions = extensions; + return this; + } + + Builder getFilenames(List filenames) { + this.filenames = filenames; + return this; + } + + Builder getPaths(List paths) { + this.paths = paths; + return this; + } + + Builder getFullPaths(List fullPaths) { + this.fullPaths = fullPaths; + return this; + } + + Builder getMinFileSize(Long minFileSize) { + this.minFileSize = minFileSize; + return this; + } + + Builder getMaxFileSize(Long maxFileSize) { + this.maxFileSize = maxFileSize; + return this; + } + + Builder getMinDays(Integer minDays) { + this.minDays = minDays; + return this; + } + + Builder getMinDate(Integer minDate) { + this.minDate = minDate; + return this; + } + + Builder getMaxDate(Integer maxDate) { + this.maxDate = maxDate; + return this; + } + + LogicalImagerRule build() { + return new LogicalImagerRule(shouldAlert, shouldSave, name, description, + extensions, filenames, paths, fullPaths, + minFileSize, maxFileSize, + minDays, minDate, maxDate + ); + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/LogicalImagerRuleSet.java b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/LogicalImagerRuleSet.java new file mode 100644 index 0000000000..c4fbb921bb --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/LogicalImagerRuleSet.java @@ -0,0 +1,63 @@ +/* + * Autopsy + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.logicalimager.configuration; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import java.util.List; + +/** + * Logical Imager Rule Set + */ +class LogicalImagerRuleSet { + + @SerializedName("set-name") + @Expose(serialize = true) + final private String setName; + + @SerializedName("rules") + @Expose(serialize = true) + private final List rules; + + LogicalImagerRuleSet(String setName, List rules) { + this.setName = setName; + this.rules = rules; + } + + String getSetName() { + return setName; + } + + List getRules() { + return rules; + } + + /* + * Find a rule with the given name. Return null if not found. + */ + LogicalImagerRule find(String name) { + for (LogicalImagerRule rule : rules) { + if (rule.getName().equals(name)) { + return rule; + } + } + return null; + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/NewRulePanel.form b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/NewRulePanel.form new file mode 100644 index 0000000000..71b01a4884 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/NewRulePanel.form @@ -0,0 +1,93 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/NewRulePanel.java b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/NewRulePanel.java new file mode 100644 index 0000000000..3b3da6dc5a --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/configuration/NewRulePanel.java @@ -0,0 +1,178 @@ +/* + * Autopsy + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.logicalimager.configuration; + +import java.awt.BorderLayout; +import java.io.IOException; +import java.util.logging.Level; +import javax.swing.JButton; +import javax.swing.JPanel; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.coreutils.Logger; + +/** + * New rule set panel + */ +@SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives +class NewRulePanel extends javax.swing.JPanel { + + private static final Logger logger = Logger.getLogger(NewRulePanel.class.getName()); + private final JPanel nonFullPathsJPanel; + private final EditNonFullPathsRulePanel editNonFullPathsRulePanel; + private final JPanel fullPathsPanel; + private final EditFullPathsRulePanel editFullPathsRulePanel; + + /** + * Creates new form NewRuleSetPanel + */ + NewRulePanel(JButton okButton, JButton cancelButton) { + initComponents(); + + nonFullPathsJPanel = createPanel(); + editNonFullPathsRulePanel = new EditNonFullPathsRulePanel(okButton, cancelButton, "", new LogicalImagerRule(), false); + nonFullPathsJPanel.add(editNonFullPathsRulePanel, BorderLayout.NORTH); + + fullPathsPanel = createPanel(); + editFullPathsRulePanel = new EditFullPathsRulePanel(okButton, cancelButton, "", new LogicalImagerRule(), false); + fullPathsPanel.add(editFullPathsRulePanel, BorderLayout.NORTH); + + sharedLayeredPane.add(nonFullPathsJPanel, Integer.valueOf(0)); + sharedLayeredPane.add(fullPathsPanel, Integer.valueOf(1)); + nonFullPathsJPanel.setVisible(true); + ruleDescription.setText(Bundle.NewRuleSetPanel_attributeRule_description()); + fullPathsPanel.setVisible(false); + } + + private JPanel createPanel() { + JPanel panel = new JPanel(new BorderLayout()); + panel.setSize(800, 650); + return panel; + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @Messages({"NewRuleSetPanel.attributeRule.name=Attribute", + "NewRuleSetPanel.fullPathRule.name=Full Path", + "NewRuleSetPanel.attributeRule.description=Search for files based on one or more attributes or metadata fields.", + "NewRuleSetPanel.fullPathRule.description=Search for files based on full exact match path."}) + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + chooseLabel = new javax.swing.JLabel(); + chooseComboBox = new javax.swing.JComboBox<>(); + sharedLayeredPane = new javax.swing.JLayeredPane(); + ruleDescription = new javax.swing.JLabel(); + + org.openide.awt.Mnemonics.setLocalizedText(chooseLabel, org.openide.util.NbBundle.getMessage(NewRulePanel.class, "NewRulePanel.chooseLabel.text")); // NOI18N + + chooseComboBox.setMaximumRowCount(2); + chooseComboBox.setModel(new javax.swing.DefaultComboBoxModel<>(new String[] {Bundle.NewRuleSetPanel_attributeRule_name(), Bundle.NewRuleSetPanel_fullPathRule_name()})); + chooseComboBox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + chooseComboBoxActionPerformed(evt); + } + }); + + javax.swing.GroupLayout sharedLayeredPaneLayout = new javax.swing.GroupLayout(sharedLayeredPane); + sharedLayeredPane.setLayout(sharedLayeredPaneLayout); + sharedLayeredPaneLayout.setHorizontalGroup( + sharedLayeredPaneLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGap(0, 0, Short.MAX_VALUE) + ); + sharedLayeredPaneLayout.setVerticalGroup( + sharedLayeredPaneLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGap(0, 467, Short.MAX_VALUE) + ); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGap(10, 10, 10) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addComponent(chooseLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(chooseComboBox, 0, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(ruleDescription, javax.swing.GroupLayout.PREFERRED_SIZE, 562, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap(114, Short.MAX_VALUE)) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(sharedLayeredPane) + .addContainerGap()) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(chooseLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(chooseComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(ruleDescription, javax.swing.GroupLayout.PREFERRED_SIZE, 20, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(sharedLayeredPane) + .addContainerGap()) + ); + }// //GEN-END:initComponents + + private void chooseComboBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_chooseComboBoxActionPerformed + int index = chooseComboBox.getSelectedIndex(); + if (chooseComboBox.getItemAt(index).equals(Bundle.NewRuleSetPanel_attributeRule_name())) { + nonFullPathsJPanel.setVisible(true); + editNonFullPathsRulePanel.setOkButton(); + ruleDescription.setText(Bundle.NewRuleSetPanel_attributeRule_description()); + fullPathsPanel.setVisible(false); + } else if (chooseComboBox.getItemAt(index).equals(Bundle.NewRuleSetPanel_fullPathRule_name())) { + nonFullPathsJPanel.setVisible(false); + ruleDescription.setText(Bundle.NewRuleSetPanel_fullPathRule_description()); + fullPathsPanel.setVisible(true); + editFullPathsRulePanel.setOkButton(); + } else { + logger.log(Level.WARNING, "Rule type selected was not one of the expected rule types"); + nonFullPathsJPanel.setVisible(false); + fullPathsPanel.setVisible(false); + ruleDescription.setText(""); + } + + }//GEN-LAST:event_chooseComboBoxActionPerformed + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JComboBox chooseComboBox; + private javax.swing.JLabel chooseLabel; + private javax.swing.JLabel ruleDescription; + private javax.swing.JLayeredPane sharedLayeredPane; + // End of variables declaration//GEN-END:variables + + ImmutablePair toRule() throws IOException, NumberFormatException { + ImmutablePair ruleMap; + if (chooseComboBox.getSelectedIndex() == 0) { + ruleMap = editNonFullPathsRulePanel.toRule(); + } else { + ruleMap = editFullPathsRulePanel.toRule(); + } + return ruleMap; + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/AddLogicalImageTask.java b/Core/src/org/sleuthkit/autopsy/logicalimager/dsp/AddLogicalImageTask.java similarity index 80% rename from Core/src/org/sleuthkit/autopsy/casemodule/AddLogicalImageTask.java rename to Core/src/org/sleuthkit/autopsy/logicalimager/dsp/AddLogicalImageTask.java index e4d2b89b17..9070dfc197 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/AddLogicalImageTask.java +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/dsp/AddLogicalImageTask.java @@ -1,7 +1,7 @@ /* - * Autopsy Forensic Browser + * Autopsy * - * Copyright 2013-2019 Basis Technology Corp. + * Copyright 2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.casemodule; +package org.sleuthkit.autopsy.logicalimager.dsp; import java.io.File; import java.io.IOException; @@ -27,49 +27,46 @@ import java.util.List; import java.util.logging.Level; import org.apache.commons.io.FileUtils; import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgressMonitor; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.imagewriter.ImageWriterSettings; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.TskCoreException; /** - * A runnable that - * - copy the logical image folder to a destination folder - * - add alert.txt and users.txt files to report - * - add an image data source to the case database. + * A runnable that - copy the logical image folder to a destination folder - add + * alert.txt and users.txt files to report - add an image data source to the + * case database. */ -public class AddLogicalImageTask extends AddImageTask { +final class AddLogicalImageTask extends AddMultipleImageTask { - private final static Logger logger = Logger.getLogger(AddLogicalImageTask.class.getName()); + private final static Logger logger = Logger.getLogger(AddLogicalImageTask.class.getName()); private final static String ALERT_TXT = "alert.txt"; //NON-NLS private final static String USERS_TXT = "users.txt"; //NON-NLS private final File src; private final File dest; private final DataSourceProcessorCallback callback; private final DataSourceProcessorProgressMonitor progressMonitor; - - public AddLogicalImageTask(String deviceId, String imagePath, int sectorSize, - String timeZone, boolean ignoreFatOrphanFiles, - String md5, String sha1, String sha256, - ImageWriterSettings imageWriterSettings, + + AddLogicalImageTask(String deviceId, + List imagePaths, + String timeZone, File src, File dest, - DataSourceProcessorProgressMonitor progressMonitor, + DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback - ) { - super(deviceId, imagePath, sectorSize, timeZone, ignoreFatOrphanFiles, - md5, sha1, sha256, imageWriterSettings, progressMonitor, callback); + ) throws NoCurrentCaseException { + super(deviceId, imagePaths, timeZone, progressMonitor, callback); this.src = src; this.dest = dest; this.progressMonitor = progressMonitor; this.callback = callback; } - + /** - * Copy the src directory to dest. - * Add alert.txt and users.txt to the case report - * Adds the image to the case database. + * Copy the src directory to dest. Add alert.txt and users.txt to the case + * report Adds the image to the case database. */ @Messages({ "# {0} - src", "# {1} - dest", "AddLogicalImageTask.copyingImageFromTo=Copying image from {0} to {1}", @@ -82,7 +79,7 @@ public class AddLogicalImageTask extends AddImageTask { public void run() { List errorList = new ArrayList<>(); List emptyDataSources = new ArrayList<>(); - + try { progressMonitor.setProgressText(Bundle.AddLogicalImageTask_copyingImageFromTo(src.toString(), dest.toString())); FileUtils.copyDirectory(src, dest); @@ -91,11 +88,11 @@ public class AddLogicalImageTask extends AddImageTask { // Copy directory failed String msg = Bundle.AddLogicalImageTask_failedToCopyDirectory(src.toString(), dest.toString()); errorList.add(msg); - logger.log(Level.SEVERE, String.format("Failed to copy directory {0} to {1}", src.toString(), dest.toString())); + logger.log(Level.SEVERE, String.format("Failed to copy directory %s to %s", src.toString(), dest.toString()), ex); callback.done(DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS, errorList, emptyDataSources); return; } - + // Add the alert.txt and users.txt to the case report progressMonitor.setProgressText(Bundle.AddLogicalImageTask_addingToReport(ALERT_TXT)); String status = addReport(Paths.get(dest.toString(), ALERT_TXT), ALERT_TXT + " " + src.getName()); @@ -114,15 +111,16 @@ public class AddLogicalImageTask extends AddImageTask { return; } progressMonitor.setProgressText(Bundle.AddLogicalImageTask_doneAddingToReport(USERS_TXT)); - + super.run(); } - - /** + + /** * Add a file specified by the reportPath to the case report. - * + * * @param reportPath Path to the report to be added * @param reportName Name associated the report + * * @returns null if success, or exception message if failure * */ @@ -138,8 +136,8 @@ public class AddLogicalImageTask extends AddImageTask { return null; } catch (TskCoreException ex) { String msg = Bundle.AddLogicalImageTask_failedToAddReport(reportPath.toString(), ex.getMessage()); - logger.log(Level.SEVERE, String.format("Failed to add report {0}. Reason= {1}", reportPath.toString(), ex.getMessage())); + logger.log(Level.SEVERE, String.format("Failed to add report %s. Reason= %s", reportPath.toString(), ex.getMessage()), ex); return msg; - } + } } } diff --git a/Core/src/org/sleuthkit/autopsy/logicalimager/dsp/AddMultipleImageTask.java b/Core/src/org/sleuthkit/autopsy/logicalimager/dsp/AddMultipleImageTask.java new file mode 100644 index 0000000000..ea09ee0e2e --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/dsp/AddMultipleImageTask.java @@ -0,0 +1,273 @@ +/* + * Autopsy + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.logicalimager.dsp; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback; +import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback.DataSourceProcessorResult; +import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgressMonitor; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.Image; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.SleuthkitJNI; +import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.TskDataException; +import org.sleuthkit.datamodel.TskFileRange; + +/** + * + * A runnable that adds multiple images to the case database + * + */ +@Messages({ + "AddMultipleImageTask.fsTypeUnknownErr=Cannot determine file system type" +}) +class AddMultipleImageTask implements Runnable { + + private static final Logger LOGGER = Logger.getLogger(AddMultipleImageTask.class.getName()); + public static final String TSK_FS_TYPE_UNKNOWN_ERR_MSG = Bundle.AddMultipleImageTask_fsTypeUnknownErr(); + private static final long TWO_GB = 2000000000L; + private final String deviceId; + private final List imageFilePaths; + private final String timeZone; + private final long chunkSize = TWO_GB; + private final DataSourceProcessorProgressMonitor progressMonitor; + private final DataSourceProcessorCallback callback; + private final Case currentCase; + + private boolean criticalErrorOccurred; + private volatile boolean cancelled; + + /** + * Constructs a runnable that adds multiple image files to a case database. + * If Sleuth Kit fails to find a filesystem in any of input image files, the + * file is added to the case as a local/logical file instead. + * + * @param deviceId An ASCII-printable identifier for the device + * associated with the data source that is intended + * to be unique across multiple cases (e.g., a UUID). + * @param imageFilePaths The paths of the multiple output files. + * @param timeZone The time zone to use when processing dates and + * times for the image, obtained from + * java.util.TimeZone.getID. + * @param progressMonitor Progress monitor for reporting progress during + * processing. + * @param callback Callback to call when processing is done. + * + * @throws NoCurrentCaseException The exception if there is no open case. + */ + @Messages({ + "# {0} - file", "AddMultipleImageTask.addingFileAsLogicalFile=Adding: {0} as an unallocated space file.", + "# {0} - deviceId", "# {1} - exceptionMessage", + "AddMultipleImageTask.errorAddingImgWithoutFileSystem=Error adding images without file systems for device %s: %s",}) + AddMultipleImageTask(String deviceId, List imageFilePaths, String timeZone, + DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) throws NoCurrentCaseException { + this.deviceId = deviceId; + this.imageFilePaths = imageFilePaths; + this.timeZone = timeZone; + this.callback = callback; + this.progressMonitor = progressMonitor; + currentCase = Case.getCurrentCaseThrows(); + } + + @Override + public void run() { + /* + * Try to add the input image files as images. + */ + List newDataSources = new ArrayList<>(); + List corruptedImageFilePaths = new ArrayList<>(); + List errorMessages = new ArrayList<>(); + currentCase.getSleuthkitCase().acquireSingleUserCaseWriteLock(); + try { + progressMonitor.setIndeterminate(true); + for (String imageFilePath : imageFilePaths) { + if (!cancelled) { + addImageToCase(imageFilePath, newDataSources, corruptedImageFilePaths, errorMessages); + } + } + } finally { + currentCase.getSleuthkitCase().releaseSingleUserCaseWriteLock(); + } + + /* + * Try to add any input image files that did not have file systems as a + * single an unallocated space file with the device id as the root virtual + * directory name. + */ + if (!cancelled && !corruptedImageFilePaths.isEmpty()) { + SleuthkitCase caseDatabase; + caseDatabase = currentCase.getSleuthkitCase(); + try { + progressMonitor.setProgressText(Bundle.AddMultipleImageTask_addingFileAsLogicalFile(corruptedImageFilePaths.toString())); + + caseDatabase.acquireSingleUserCaseWriteLock(); + + Image dataSource = caseDatabase.addImageInfo(0, corruptedImageFilePaths, timeZone); + newDataSources.add(dataSource); + List fileRanges = new ArrayList<>(); + + long imageSize = dataSource.getSize(); + int sequence = 0; + //start byte and end byte + long start = 0; + if (chunkSize > 0 && imageSize >= TWO_GB) { + for (double size = TWO_GB; size < dataSource.getSize(); size += TWO_GB) { + fileRanges.add(new TskFileRange(start, TWO_GB, sequence)); + start += TWO_GB; + sequence++; + } + + } + double leftoverSize = imageSize - sequence * TWO_GB; + fileRanges.add(new TskFileRange(start, (long)leftoverSize, sequence)); + + caseDatabase.addLayoutFiles(dataSource, fileRanges); + } catch (TskCoreException ex) { + errorMessages.add(Bundle.AddMultipleImageTask_errorAddingImgWithoutFileSystem(deviceId, ex.getLocalizedMessage())); + criticalErrorOccurred = true; + } finally { + caseDatabase.releaseSingleUserCaseWriteLock(); + } + } + + /* + * This appears to be the best that can be done to indicate completion + * with the DataSourceProcessorProgressMonitor in its current form. + */ + progressMonitor.setProgress(0); + progressMonitor.setProgress(100); + + /* + * Pass the results back via the callback. + */ + DataSourceProcessorResult result; + if (criticalErrorOccurred) { + result = DataSourceProcessorResult.CRITICAL_ERRORS; + } else if (!errorMessages.isEmpty()) { + result = DataSourceProcessorResult.NONCRITICAL_ERRORS; + } else { + result = DataSourceProcessorResult.NO_ERRORS; + } + callback.done(result, errorMessages, newDataSources); + criticalErrorOccurred = false; + } + + /** + * Attempts to cancel the processing of the input image files. May result in + * partial processing of the input. + */ + void cancelTask() { + LOGGER.log(Level.WARNING, "AddMultipleImageTask cancelled, processing may be incomplete"); // NON-NLS + cancelled = true; + } + + /** + * Attempts to add an input image to the case. + * + * @param imageFilePath The image file path. + * @param newDataSources If the image is added, a data source is + * added to this list for eventual return to + * the caller via the callback. + * @param corruptedImageFilePaths If the image cannot be added because + * Sleuth Kit cannot detect a filesystem, + * the image file path is added to this list + * for later addition as an unallocated space file. + * @param errorMessages If there are any error messages, the + * error messages are added to this list for + * eventual return to the caller via the + * callback. + */ + @Messages({ + "# {0} - imageFilePath", "AddMultipleImageTask.adding=Adding: {0}", + "# {0} - imageFilePath", "# {1} - deviceId", "# {2} - exceptionMessage", "AddMultipleImageTask.criticalErrorAdding=Critical error adding {0} for device {1}: {2}", + "# {0} - imageFilePath", "# {1} - deviceId", "# {2} - exceptionMessage", "AddMultipleImageTask.criticalErrorReverting=Critical error reverting add image process for {0} for device {1}: {2}", + "# {0} - imageFilePath", "# {1} - deviceId", "# {2} - exceptionMessage", "AddMultipleImageTask.nonCriticalErrorAdding=Non-critical error adding {0} for device {1}: {2}",}) + private void addImageToCase(String imageFilePath, List newDataSources, List corruptedImageFilePaths, List errorMessages) { + /* + * Try to add the image to the case database as a data source. + */ + progressMonitor.setProgressText(Bundle.AddMultipleImageTask_adding(imageFilePath)); + SleuthkitCase caseDatabase = currentCase.getSleuthkitCase(); + SleuthkitJNI.CaseDbHandle.AddImageProcess addImageProcess = caseDatabase.makeAddImageProcess(timeZone, false, false, ""); + try { + addImageProcess.run(deviceId, new String[]{imageFilePath}); + } catch (TskCoreException ex) { + if (ex.getMessage().contains(TSK_FS_TYPE_UNKNOWN_ERR_MSG)) { + /* + * If Sleuth Kit failed to add the image because it did not find + * a file system, save the image path so it can be added to the + * case as an unallocated space file. All other + * errors are critical. + */ + corruptedImageFilePaths.add(imageFilePath); + } else { + errorMessages.add(Bundle.AddMultipleImageTask_criticalErrorAdding(imageFilePath, deviceId, ex.getLocalizedMessage())); + criticalErrorOccurred = true; + } + /* + * Either way, the add image process needs to be reverted. + */ + try { + addImageProcess.revert(); + } catch (TskCoreException e) { + errorMessages.add(Bundle.AddMultipleImageTask_criticalErrorReverting(imageFilePath, deviceId, e.getLocalizedMessage())); + criticalErrorOccurred = true; + } + return; + } catch (TskDataException ex) { + errorMessages.add(Bundle.AddMultipleImageTask_nonCriticalErrorAdding(imageFilePath, deviceId, ex.getLocalizedMessage())); + } + + /* + * Try to commit the results of the add image process, retrieve the new + * image from the case database, and add it to the list of new data + * sources to be returned via the callback. + */ + try { + long imageId = addImageProcess.commit(); + Image dataSource = caseDatabase.getImageById(imageId); + newDataSources.add(dataSource); + + /* + * Verify the size of the new image. Note that it may not be what is + * expected, but at least part of it was added to the case. + */ + String verificationError = dataSource.verifyImageSize(); + if (!verificationError.isEmpty()) { + errorMessages.add(Bundle.AddMultipleImageTask_nonCriticalErrorAdding(imageFilePath, deviceId, verificationError)); + } + } catch (TskCoreException ex) { + /* + * The add image process commit failed or querying the case database + * for the newly added image failed. Either way, this is a critical + * error. + */ + errorMessages.add(Bundle.AddMultipleImageTask_criticalErrorAdding(imageFilePath, deviceId, ex.getLocalizedMessage())); + criticalErrorOccurred = true; + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/logicalimager/dsp/Bundle.properties b/Core/src/org/sleuthkit/autopsy/logicalimager/dsp/Bundle.properties new file mode 100644 index 0000000000..665b9f3f2e --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/dsp/Bundle.properties @@ -0,0 +1,11 @@ +# To change this license header, choose License Headers in Project Properties. +# To change this template file, choose Tools | Templates +# and open the template in the editor. + +LogicalImagerPanel.selectFromDriveLabel.text=Select Acquisition From Drive +LogicalImagerPanel.selectDriveLabel.text=Select Drive +LogicalImagerPanel.selectFolderLabel.text=Selected Folder: +LogicalImagerPanel.manualRadioButton.text=Manually Choose Folder +LogicalImagerPanel.importRadioButton.text=Import From External Drive +LogicalImagerPanel.browseButton.text=Browse +LogicalImagerPanel.refreshButton.text=Refresh diff --git a/Core/src/org/sleuthkit/autopsy/logicalimager/dsp/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/logicalimager/dsp/Bundle.properties-MERGED new file mode 100644 index 0000000000..32bd3c7868 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/dsp/Bundle.properties-MERGED @@ -0,0 +1,67 @@ +# To change this license header, choose License Headers in Project Properties. +# To change this template file, choose Tools | Templates +# and open the template in the editor. + +# {0} - file +AddLogicalImageTask.addingToReport=Adding {0} to report +# {0} - src +# {1} - dest +AddLogicalImageTask.copyingImageFromTo=Copying image from {0} to {1} +# {0} - file +AddLogicalImageTask.doneAddingToReport=Done adding {0} to report +AddLogicalImageTask.doneCopying=Done copying +# {0} - file +# {1} - exception message +AddLogicalImageTask.failedToAddReport=Failed to add report {0}. Reason= {1} +# {0} - src +# {1} - dest +AddLogicalImageTask.failedToCopyDirectory=Failed to copy directory {0} to {1} +# {0} - imageFilePath +AddMultipleImageTask.adding=Adding: {0} +# {0} - file +AddMultipleImageTask.addingFileAsLogicalFile=Adding: {0} as an unallocated space file. +# {0} - imageFilePath +# {1} - deviceId +# {2} - exceptionMessage +AddMultipleImageTask.criticalErrorAdding=Critical error adding {0} for device {1}: {2} +# {0} - imageFilePath +# {1} - deviceId +# {2} - exceptionMessage +AddMultipleImageTask.criticalErrorReverting=Critical error reverting add image process for {0} for device {1}: {2} +# {0} - deviceId +# {1} - exceptionMessage +AddMultipleImageTask.errorAddingImgWithoutFileSystem=Error adding images without file systems for device %s: %s +AddMultipleImageTask.fsTypeUnknownErr=Cannot determine file system type +# {0} - imageFilePath +# {1} - deviceId +# {2} - exceptionMessage +AddMultipleImageTask.nonCriticalErrorAdding=Non-critical error adding {0} for device {1}: {2} +LogicalImagerDSProcessor.dataSourceType=Autopsy Logical Imager Results +# {0} - directory +LogicalImagerDSProcessor.directoryAlreadyExists=Directory {0} already exists +# {0} - directory +LogicalImagerDSProcessor.failToCreateDirectory=Failed to create directory {0} +# {0} - file +LogicalImagerDSProcessor.failToGetCanonicalPath=Fail to get canonical path for {0} +# {0} - imageDirPath +LogicalImagerDSProcessor.imageDirPathNotFound={0} not found.\nUSB drive has been ejected. +LogicalImagerDSProcessor.noCurrentCase=No current case +LogicalImagerPanel.imageTable.columnModel.title0=Hostname +LogicalImagerPanel.imageTable.columnModel.title1=Extracted Date +LogicalImagerPanel.imageTable.columnModel.title2=Path +# {0} - sparseImageDirectory +LogicalImagerPanel.messageLabel.directoryDoesNotContainSparseImage=Directory {0} does not contain any images +# {0} - invalidFormatDirectory +LogicalImagerPanel.messageLabel.directoryFormatInvalid=Directory {0} does not match format Logical_Imager_HOSTNAME_yyyymmdd_HH_MM_SS +LogicalImagerPanel.messageLabel.driveHasNoImages=Drive has no images +LogicalImagerPanel.messageLabel.noExternalDriveFound=No drive found +LogicalImagerPanel.messageLabel.noImageSelected=No image selected +LogicalImagerPanel.messageLabel.scanningExternalDrives=Scanning external drives for images ... +LogicalImagerPanel.selectAcquisitionFromDriveLabel.text=Select acquisition from Drive +LogicalImagerPanel.selectFromDriveLabel.text=Select Acquisition From Drive +LogicalImagerPanel.selectDriveLabel.text=Select Drive +LogicalImagerPanel.selectFolderLabel.text=Selected Folder: +LogicalImagerPanel.manualRadioButton.text=Manually Choose Folder +LogicalImagerPanel.importRadioButton.text=Import From External Drive +LogicalImagerPanel.browseButton.text=Browse +LogicalImagerPanel.refreshButton.text=Refresh diff --git a/Core/src/org/sleuthkit/autopsy/logicalimager/dsp/DriveListUtils.java b/Core/src/org/sleuthkit/autopsy/logicalimager/dsp/DriveListUtils.java new file mode 100644 index 0000000000..d033d0fdce --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/dsp/DriveListUtils.java @@ -0,0 +1,86 @@ +/* + * Autopsy + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.logicalimager.dsp; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.Arrays; +import java.util.List; + +/** + * Utility class for displaying a list of drives + */ +public final class DriveListUtils { + + /** + * Convert a number of bytes to a human readable string + * + * @param bytes the number of bytes to convert + * @param si whether it takes 1000 or 1024 of a unit to reach the next + * unit + * + * @return a human readable string representing the number of bytes + */ + public static String humanReadableByteCount(long bytes, boolean si) { + int unit = si ? 1000 : 1024; + if (bytes < unit) { + return bytes + " B"; //NON-NLS + } + int exp = (int) (Math.log(bytes) / Math.log(unit)); + String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (si ? "" : "i"); //NON-NLS + return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre); //NON-NLS + } + + /** + * Empty private constructor for util class + */ + private DriveListUtils() { + //empty private constructor for util class + } + + /** Use the command net to determine what this drive is. + * net use will return an error for anything which isn't a share. + */ + public static boolean isNetworkDrive(String driveLetter) { + List cmd = Arrays.asList("cmd", "/c", "net", "use", driveLetter + ":"); + + try { + Process p = new ProcessBuilder(cmd) + .redirectErrorStream(true) + .start(); + + p.getOutputStream().close(); + + StringBuilder consoleOutput = new StringBuilder(); + + String line; + try (BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()))) { + while ((line = in.readLine()) != null) { + consoleOutput.append(line).append("\r\n"); + } + } + + int rc = p.waitFor(); + return rc == 0; + } catch(IOException | InterruptedException e) { + return false; // assume not a network drive + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/LogicalImagerDSProcessor.java b/Core/src/org/sleuthkit/autopsy/logicalimager/dsp/LogicalImagerDSProcessor.java similarity index 68% rename from Core/src/org/sleuthkit/autopsy/casemodule/LogicalImagerDSProcessor.java rename to Core/src/org/sleuthkit/autopsy/logicalimager/dsp/LogicalImagerDSProcessor.java index ff01e1c540..da9d5f7aa6 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/LogicalImagerDSProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/dsp/LogicalImagerDSProcessor.java @@ -1,7 +1,7 @@ /* - * Autopsy Forensic Browser + * Autopsy * - * Copyright 2011-2019 Basis Technology Corp. + * Copyright 2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,9 +16,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.casemodule; +package org.sleuthkit.autopsy.logicalimager.dsp; import java.io.File; +import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; @@ -29,27 +30,28 @@ import javax.swing.JPanel; import org.openide.util.NbBundle.Messages; import org.openide.util.lookup.ServiceProvider; import org.openide.util.lookup.ServiceProviders; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgressMonitor; import org.sleuthkit.datamodel.Content; /** - * A Logical Imager data source processor that implements the DataSourceProcessor service - * provider interface to allow integration with the add data source wizard. It - * also provides a run method overload to allow it to be used independently of - * the wizard. + * A Logical Imager data source processor that implements the + * DataSourceProcessor service provider interface to allow integration with the + * add data source wizard. It also provides a run method overload to allow it to + * be used independently of the wizard. */ -@ServiceProviders(value={ - @ServiceProvider(service=DataSourceProcessor.class)} +@ServiceProviders(value = { + @ServiceProvider(service = DataSourceProcessor.class)} ) -public class LogicalImagerDSProcessor implements DataSourceProcessor { +public final class LogicalImagerDSProcessor implements DataSourceProcessor { private static final String LOGICAL_IMAGER_DIR = "LogicalImager"; //NON-NLS - private static final String SPARSE_IMAGE_VHD = "sparse_image.vhd"; //NON-NLS private final LogicalImagerPanel configPanel; private AddLogicalImageTask addLogicalImageTask; - + /* * Constructs a Logical Imager data source processor that implements the * DataSourceProcessor service provider interface to allow integration with @@ -67,7 +69,7 @@ public class LogicalImagerDSProcessor implements DataSourceProcessor { * * @return A data source type display string for this data source processor. */ - @Messages({"LogicalImagerDSProcessor.dataSourceType=Autopsy Imager"}) + @Messages({"LogicalImagerDSProcessor.dataSourceType=Autopsy Logical Imager Results"}) public static String getType() { return Bundle.LogicalImagerDSProcessor_dataSourceType(); } @@ -128,15 +130,16 @@ public class LogicalImagerDSProcessor implements DataSourceProcessor { "# {0} - imageDirPath", "LogicalImagerDSProcessor.imageDirPathNotFound={0} not found.\nUSB drive has been ejected.", "# {0} - directory", "LogicalImagerDSProcessor.failToCreateDirectory=Failed to create directory {0}", "# {0} - directory", "LogicalImagerDSProcessor.directoryAlreadyExists=Directory {0} already exists", - }) + "# {0} - file", "LogicalImagerDSProcessor.failToGetCanonicalPath=Fail to get canonical path for {0}", + "LogicalImagerDSProcessor.noCurrentCase=No current case",}) @Override public void run(DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) { configPanel.storeSettings(); - + Path imageDirPath = configPanel.getImageDirPath(); List errorList = new ArrayList<>(); List emptyDataSources = new ArrayList<>(); - + if (!imageDirPath.toFile().exists()) { // This can happen if the USB drive was selected in the panel, but // was ejected before pressing the NEXT button @@ -146,7 +149,7 @@ public class LogicalImagerDSProcessor implements DataSourceProcessor { callback.done(DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS, errorList, emptyDataSources); return; } - + // Create the LogicalImager directory under ModuleDirectory String moduleDirectory = Case.getCurrentCase().getModuleDirectory(); File logicalImagerDir = Paths.get(moduleDirectory, LOGICAL_IMAGER_DIR).toFile(); @@ -167,45 +170,59 @@ public class LogicalImagerDSProcessor implements DataSourceProcessor { } File src = imageDirPath.toFile(); - String deviceId = UUID.randomUUID().toString(); - String timeZone = Calendar.getInstance().getTimeZone().getID(); - boolean ignoreFatOrphanFiles = false; - run(deviceId, Paths.get(src.toString(), SPARSE_IMAGE_VHD).toString(), 0, - timeZone, ignoreFatOrphanFiles, null, null, null, src, dest, - progressMonitor, callback); + // Get all VHD files in the src directory + List imagePaths = new ArrayList<>(); + for (File f : src.listFiles()) { + if (f.getName().endsWith(".vhd")) { + try { + imagePaths.add(f.getCanonicalPath()); + } catch (IOException ex) { + String msg = Bundle.LogicalImagerDSProcessor_failToGetCanonicalPath(f.getName()); + errorList.add(msg); + callback.done(DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS, errorList, emptyDataSources); + return; + } + } + } + try { + String deviceId = UUID.randomUUID().toString(); + String timeZone = Calendar.getInstance().getTimeZone().getID(); + run(deviceId, imagePaths, + timeZone, src, dest, + progressMonitor, callback); + } catch (NoCurrentCaseException ex) { + String msg = Bundle.LogicalImagerDSProcessor_noCurrentCase(); + errorList.add(msg); + callback.done(DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS, errorList, emptyDataSources); + return; + } } - + /** - * Adds a "Logical Imager" data source to the case database using a background task in - * a separate thread and the given settings instead of those provided by the - * selection and configuration panel. Returns as soon as the background task - * is started and uses the callback object to signal task completion and - * return results. + * Adds a "Logical Imager" data source to the case database using a + * background task in a separate thread and the given settings instead of + * those provided by the selection and configuration panel. Returns as soon + * as the background task is started and uses the callback object to signal + * task completion and return results. * - * @param deviceId An ASCII-printable identifier for the device - * associated with the data source that is - * intended to be unique across multiple cases - * (e.g., a UUID). - * @param imageFilePath Path to the image file. - * @param timeZone The time zone to use when processing dates - * and times for the image, obtained from - * java.util.TimeZone.getID. - * @param chunkSize The maximum size of each chunk of the raw - * data source as it is divided up into virtual - * unallocated space files. - * @param src The source directory of image. - * @param dest The destination directory to copy the source. - * @param progressMonitor Progress monitor for reporting progress - * during processing. - * @param callback Callback to call when processing is done. + * @param deviceId An ASCII-printable identifier for the device + * associated with the data source that is intended + * to be unique across multiple cases (e.g., a UUID). + * @param imagePaths Paths to the image files. + * @param timeZone The time zone to use when processing dates and + * times for the image, obtained from + * java.util.TimeZone.getID. + * @param src The source directory of image. + * @param dest The destination directory to copy the source. + * @param progressMonitor Progress monitor for reporting progress during + * processing. + * @param callback Callback to call when processing is done. */ - private void run(String deviceId, String imagePath, int sectorSize, String timeZone, - boolean ignoreFatOrphanFiles, String md5, String sha1, String sha256, + private void run(String deviceId, List imagePaths, String timeZone, File src, File dest, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback - ) { - addLogicalImageTask = new AddLogicalImageTask(deviceId, imagePath, sectorSize, - timeZone, ignoreFatOrphanFiles, md5, sha1, sha256, null, src, dest, + ) throws NoCurrentCaseException { + addLogicalImageTask = new AddLogicalImageTask(deviceId, imagePaths, timeZone, src, dest, progressMonitor, callback); new Thread(addLogicalImageTask).start(); } diff --git a/Core/src/org/sleuthkit/autopsy/logicalimager/dsp/LogicalImagerPanel.form b/Core/src/org/sleuthkit/autopsy/logicalimager/dsp/LogicalImagerPanel.form new file mode 100644 index 0000000000..8f864e1f20 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/dsp/LogicalImagerPanel.form @@ -0,0 +1,293 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/LogicalImagerPanel.java b/Core/src/org/sleuthkit/autopsy/logicalimager/dsp/LogicalImagerPanel.java similarity index 52% rename from Core/src/org/sleuthkit/autopsy/casemodule/LogicalImagerPanel.java rename to Core/src/org/sleuthkit/autopsy/logicalimager/dsp/LogicalImagerPanel.java index cfc0f87eb1..06722722d6 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/LogicalImagerPanel.java +++ b/Core/src/org/sleuthkit/autopsy/logicalimager/dsp/LogicalImagerPanel.java @@ -1,7 +1,7 @@ /* - * Autopsy Forensic Browser + * Autopsy * - * Copyright 2011-2019 Basis Technology Corp. + * Copyright 2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,10 +16,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.casemodule; +package org.sleuthkit.autopsy.logicalimager.dsp; import java.awt.Color; +import java.awt.Component; import java.io.File; +import java.io.FilenameFilter; import java.io.IOException; import java.nio.file.FileStore; import java.nio.file.Files; @@ -27,6 +29,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; +import java.util.logging.Level; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.JFileChooser; @@ -36,35 +39,35 @@ import javax.swing.ListSelectionModel; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.filechooser.FileSystemView; -import javax.swing.table.AbstractTableModel; -import javax.swing.table.TableModel; +import javax.swing.table.DefaultTableModel; +import javax.swing.table.TableColumn; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; +import org.sleuthkit.autopsy.coreutils.Logger; /** * Panel for adding an logical image file from drive letters. Allows the user to * select a file. */ @Messages({ - "LogicalImagerPanel.messageLabel.selectedImage=Selected folder", "LogicalImagerPanel.messageLabel.noImageSelected=No image selected", "LogicalImagerPanel.messageLabel.driveHasNoImages=Drive has no images", "LogicalImagerPanel.selectAcquisitionFromDriveLabel.text=Select acquisition from Drive",}) @SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives -public class LogicalImagerPanel extends JPanel implements DocumentListener { +final class LogicalImagerPanel extends JPanel implements DocumentListener { + private static final Logger logger = Logger.getLogger(LogicalImagerPanel.class.getName()); private static final long serialVersionUID = 1L; - private static final String SPARSE_IMAGE_VHD = "sparse_image.vhd"; //NON-NLS - private static final String SELECTED_IMAGE = Bundle.LogicalImagerPanel_messageLabel_selectedImage(); private static final String NO_IMAGE_SELECTED = Bundle.LogicalImagerPanel_messageLabel_noImageSelected(); private static final String DRIVE_HAS_NO_IMAGES = Bundle.LogicalImagerPanel_messageLabel_driveHasNoImages(); + private static final int COLUMN_TO_SORT_ON_INDEX = 1; + private static final int NUMBER_OF_VISIBLE_COLUMNS = 2; private static final String[] EMPTY_LIST_DATA = {}; private final JFileChooser fileChooser = new JFileChooser(); private final Pattern regex = Pattern.compile("Logical_Imager_(.+)_(\\d{4})(\\d{2})(\\d{2})_(\\d{2})_(\\d{2})_(\\d{2})"); - private final String contextName; - private Path choosenImageDirPath; - private TableModel imageTableModel; + private Path manualImageDirPath; + private DefaultTableModel imageTableModel; /** * Creates new form LogicalImagerPanel @@ -73,11 +76,29 @@ public class LogicalImagerPanel extends JPanel implements DocumentListener { * settings. */ private LogicalImagerPanel(String context) { - this.contextName = context; initComponents(); + configureImageTable(); + jScrollPane1.setBorder(null); clearImageTable(); } + /** + * Perform the Image Table configuration necessary when a new table model is + * set. + */ + private void configureImageTable() { + //hide path column while leaving it in model + if (imageTable.getColumnCount() > NUMBER_OF_VISIBLE_COLUMNS) { + TableColumn columnToHide = imageTable.getColumn(imageTableModel.getColumnName(NUMBER_OF_VISIBLE_COLUMNS)); + if (columnToHide != null) { + imageTable.removeColumn(columnToHide); + } + //sort on specified column in decending order, the first call will toggle to ascending order, the second to descending order + imageTable.getRowSorter().toggleSortOrder(COLUMN_TO_SORT_ON_INDEX); + imageTable.getRowSorter().toggleSortOrder(COLUMN_TO_SORT_ON_INDEX); + } + } + /** * Creates and returns an instance of a LogicalImagerPanel. * @@ -86,13 +107,9 @@ public class LogicalImagerPanel extends JPanel implements DocumentListener { * * @return instance of the LogicalImagerPanel */ - @Messages({ - "LogicalImagerPanel.messageLabel.clickScanOrBrowse=Click SCAN or BROWSE button to find images" - }) - public static synchronized LogicalImagerPanel createInstance(String context) { + static synchronized LogicalImagerPanel createInstance(String context) { LogicalImagerPanel instance = new LogicalImagerPanel(context); // post-constructor initialization of listener support without leaking references of uninitialized objects - instance.messageLabel.setText(Bundle.LogicalImagerPanel_messageLabel_clickScanOrBrowse()); instance.imageTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); return instance; } @@ -105,42 +122,65 @@ public class LogicalImagerPanel extends JPanel implements DocumentListener { // //GEN-BEGIN:initComponents private void initComponents() { - topLabel = new javax.swing.JLabel(); - jLabel1 = new javax.swing.JLabel(); - scanButton = new javax.swing.JButton(); - messageLabel = new javax.swing.JLabel(); + buttonGroup1 = new javax.swing.ButtonGroup(); + browseButton = new javax.swing.JButton(); + importRadioButton = new javax.swing.JRadioButton(); + manualRadioButton = new javax.swing.JRadioButton(); + pathTextField = new javax.swing.JTextField(); + selectFolderLabel = new javax.swing.JLabel(); selectDriveLabel = new javax.swing.JLabel(); + selectFromDriveLabel = new javax.swing.JLabel(); driveListScrollPane = new javax.swing.JScrollPane(); driveList = new javax.swing.JList<>(); - selectAcquisitionFromDriveLabel = new javax.swing.JLabel(); - jLabel6 = new javax.swing.JLabel(); - browseButton = new javax.swing.JButton(); + refreshButton = new javax.swing.JButton(); imageScrollPane = new javax.swing.JScrollPane(); imageTable = new javax.swing.JTable(); - jSeparator1 = new javax.swing.JSeparator(); + jSeparator2 = new javax.swing.JSeparator(); + jScrollPane1 = new javax.swing.JScrollPane(); + messageTextArea = new javax.swing.JTextArea(); setMinimumSize(new java.awt.Dimension(0, 65)); setPreferredSize(new java.awt.Dimension(403, 65)); - org.openide.awt.Mnemonics.setLocalizedText(topLabel, org.openide.util.NbBundle.getMessage(LogicalImagerPanel.class, "LogicalImagerPanel.topLabel.text")); // NOI18N - - org.openide.awt.Mnemonics.setLocalizedText(jLabel1, org.openide.util.NbBundle.getMessage(LogicalImagerPanel.class, "LogicalImagerPanel.jLabel1.text")); // NOI18N - - org.openide.awt.Mnemonics.setLocalizedText(scanButton, org.openide.util.NbBundle.getMessage(LogicalImagerPanel.class, "LogicalImagerPanel.scanButton.text")); // NOI18N - scanButton.addActionListener(new java.awt.event.ActionListener() { + org.openide.awt.Mnemonics.setLocalizedText(browseButton, org.openide.util.NbBundle.getMessage(LogicalImagerPanel.class, "LogicalImagerPanel.browseButton.text")); // NOI18N + browseButton.setEnabled(false); + browseButton.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { - scanButtonActionPerformed(evt); + browseButtonActionPerformed(evt); } }); - org.openide.awt.Mnemonics.setLocalizedText(messageLabel, org.openide.util.NbBundle.getMessage(LogicalImagerPanel.class, "LogicalImagerPanel.messageLabel.text")); // NOI18N + buttonGroup1.add(importRadioButton); + importRadioButton.setSelected(true); + org.openide.awt.Mnemonics.setLocalizedText(importRadioButton, org.openide.util.NbBundle.getMessage(LogicalImagerPanel.class, "LogicalImagerPanel.importRadioButton.text")); // NOI18N + importRadioButton.setToolTipText(""); + importRadioButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + importRadioButtonActionPerformed(evt); + } + }); + + buttonGroup1.add(manualRadioButton); + org.openide.awt.Mnemonics.setLocalizedText(manualRadioButton, org.openide.util.NbBundle.getMessage(LogicalImagerPanel.class, "LogicalImagerPanel.manualRadioButton.text")); // NOI18N + manualRadioButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + manualRadioButtonActionPerformed(evt); + } + }); + + pathTextField.setDisabledTextColor(java.awt.Color.black); + pathTextField.setEnabled(false); + + org.openide.awt.Mnemonics.setLocalizedText(selectFolderLabel, org.openide.util.NbBundle.getMessage(LogicalImagerPanel.class, "LogicalImagerPanel.selectFolderLabel.text")); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(selectDriveLabel, org.openide.util.NbBundle.getMessage(LogicalImagerPanel.class, "LogicalImagerPanel.selectDriveLabel.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(selectFromDriveLabel, org.openide.util.NbBundle.getMessage(LogicalImagerPanel.class, "LogicalImagerPanel.selectFromDriveLabel.text")); // NOI18N + driveList.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); driveList.addMouseListener(new java.awt.event.MouseAdapter() { - public void mouseClicked(java.awt.event.MouseEvent evt) { - driveListMouseClicked(evt); + public void mouseReleased(java.awt.event.MouseEvent evt) { + driveListMouseReleased(evt); } }); driveList.addKeyListener(new java.awt.event.KeyAdapter() { @@ -150,19 +190,16 @@ public class LogicalImagerPanel extends JPanel implements DocumentListener { }); driveListScrollPane.setViewportView(driveList); - org.openide.awt.Mnemonics.setLocalizedText(selectAcquisitionFromDriveLabel, org.openide.util.NbBundle.getMessage(LogicalImagerPanel.class, "LogicalImagerPanel.selectAcquisitionFromDriveLabel.text")); // NOI18N - - org.openide.awt.Mnemonics.setLocalizedText(jLabel6, org.openide.util.NbBundle.getMessage(LogicalImagerPanel.class, "LogicalImagerPanel.jLabel6.text")); // NOI18N - - org.openide.awt.Mnemonics.setLocalizedText(browseButton, org.openide.util.NbBundle.getMessage(LogicalImagerPanel.class, "LogicalImagerPanel.browseButton.text")); // NOI18N - browseButton.addActionListener(new java.awt.event.ActionListener() { + org.openide.awt.Mnemonics.setLocalizedText(refreshButton, org.openide.util.NbBundle.getMessage(LogicalImagerPanel.class, "LogicalImagerPanel.refreshButton.text")); // NOI18N + refreshButton.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { - browseButtonActionPerformed(evt); + refreshButtonActionPerformed(evt); } }); imageScrollPane.setPreferredSize(new java.awt.Dimension(346, 402)); + imageTable.setAutoCreateRowSorter(true); imageTable.setModel(new javax.swing.table.DefaultTableModel( new Object [][] { @@ -176,10 +213,9 @@ public class LogicalImagerPanel extends JPanel implements DocumentListener { imageTable.setShowHorizontalLines(false); imageTable.setShowVerticalLines(false); imageTable.getTableHeader().setReorderingAllowed(false); - imageTable.setUpdateSelectionOnSort(false); imageTable.addMouseListener(new java.awt.event.MouseAdapter() { - public void mouseClicked(java.awt.event.MouseEvent evt) { - imageTableMouseClicked(evt); + public void mouseReleased(java.awt.event.MouseEvent evt) { + imageTableMouseReleased(evt); } }); imageTable.addKeyListener(new java.awt.event.KeyAdapter() { @@ -190,135 +226,98 @@ public class LogicalImagerPanel extends JPanel implements DocumentListener { imageScrollPane.setViewportView(imageTable); imageTable.getColumnModel().getSelectionModel().setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); + jScrollPane1.setBorder(null); + + messageTextArea.setBackground(new java.awt.Color(240, 240, 240)); + messageTextArea.setColumns(20); + messageTextArea.setForeground(java.awt.Color.red); + messageTextArea.setLineWrap(true); + messageTextArea.setRows(3); + messageTextArea.setBorder(null); + messageTextArea.setDisabledTextColor(java.awt.Color.red); + messageTextArea.setEnabled(false); + messageTextArea.setMargin(new java.awt.Insets(0, 0, 0, 0)); + jScrollPane1.setViewportView(messageTextArea); + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() - .addGap(238, 238, 238) - .addComponent(topLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 163, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(0, 0, Short.MAX_VALUE)) - .addGroup(layout.createSequentialGroup() - .addGap(28, 28, 28) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(messageLabel, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addGroup(layout.createSequentialGroup() - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING, false) - .addComponent(jSeparator1, javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addComponent(selectDriveLabel) - .addGap(289, 289, 289)) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() - .addComponent(scanButton) - .addGap(126, 126, 126))) - .addGap(36, 36, 36) - .addComponent(browseButton)) - .addGroup(layout.createSequentialGroup() - .addComponent(driveListScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 211, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(28, 28, 28) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(selectAcquisitionFromDriveLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 305, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(imageScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 346, javax.swing.GroupLayout.PREFERRED_SIZE))) - .addGroup(layout.createSequentialGroup() - .addGap(346, 346, 346) - .addComponent(jLabel6, javax.swing.GroupLayout.PREFERRED_SIZE, 154, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addGroup(layout.createSequentialGroup() - .addGap(144, 144, 144) - .addComponent(jLabel1, javax.swing.GroupLayout.PREFERRED_SIZE, 116, javax.swing.GroupLayout.PREFERRED_SIZE)))) - .addContainerGap(48, Short.MAX_VALUE)))) + .addGap(10, 10, 10) + .addComponent(selectFolderLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 81, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(13, 13, 13) + .addComponent(pathTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 474, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING, false) + .addComponent(jSeparator2, javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGap(41, 41, 41) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(driveListScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 160, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(refreshButton)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(imageScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 377, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(layout.createSequentialGroup() + .addGap(20, 20, 20) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(manualRadioButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(browseButton)) + .addComponent(importRadioButton) + .addGroup(layout.createSequentialGroup() + .addGap(21, 21, 21) + .addComponent(selectDriveLabel) + .addGap(113, 113, 113) + .addComponent(selectFromDriveLabel)))))) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 568, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() - .addComponent(topLabel) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(jLabel1) - .addComponent(jLabel6)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(scanButton) - .addComponent(browseButton)) + .addGap(16, 16, 16) + .addComponent(importRadioButton) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addComponent(jSeparator1, javax.swing.GroupLayout.PREFERRED_SIZE, 4, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.CENTER) .addComponent(selectDriveLabel) - .addComponent(selectAcquisitionFromDriveLabel)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addComponent(selectFromDriveLabel)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(imageScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE) - .addComponent(driveListScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 194, Short.MAX_VALUE)) - .addGap(26, 26, 26) - .addComponent(messageLabel) - .addGap(154, 154, 154)) + .addComponent(driveListScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 23, Short.MAX_VALUE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(refreshButton) + .addGap(18, 18, 18) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(browseButton) + .addComponent(manualRadioButton)) + .addGap(18, 18, 18) + .addComponent(jSeparator2, javax.swing.GroupLayout.PREFERRED_SIZE, 16, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(selectFolderLabel) + .addComponent(pathTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 61, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(6, 6, 6)) ); }// //GEN-END:initComponents - public static String humanReadableByteCount(long bytes, boolean si) { - int unit = si ? 1000 : 1024; - if (bytes < unit) { - return bytes + " B"; //NON-NLS - } - int exp = (int) (Math.log(bytes) / Math.log(unit)); - String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (si ? "" : "i"); //NON-NLS - return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre); //NON-NLS - } - - @Messages({ - "LogicalImagerPanel.messageLabel.scanningExternalDrives=Scanning external drives for sparse_image.vhd ...", - "LogicalImagerPanel.messageLabel.noExternalDriveFound=No drive found" - }) - private void scanButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_scanButtonActionPerformed - // Scan external drives for sparse_image.vhd - clearImageTable(); - setNormalMessage(Bundle.LogicalImagerPanel_messageLabel_scanningExternalDrives()); - List listData = new ArrayList<>(); - File[] roots = File.listRoots(); - int firstRemovableDrive = -1; - int i = 0; - for (File root : roots) { - String description = FileSystemView.getFileSystemView().getSystemTypeDescription(root); - long spaceInBytes = root.getTotalSpace(); - String sizeWithUnit = humanReadableByteCount(spaceInBytes, false); - listData.add(root + " (" + description + ") (" + sizeWithUnit + ")"); - if (firstRemovableDrive == -1) { - try { - FileStore fileStore = Files.getFileStore(root.toPath()); - if ((boolean) fileStore.getAttribute("volume:isRemovable")) { //NON-NLS - firstRemovableDrive = i; - } - } catch (IOException ex) { - ; // skip - } - } - i++; - } - driveList.setListData(listData.toArray(new String[0])); - if (!listData.isEmpty()) { - // auto-select the first external drive, if any - driveList.setSelectedIndex(firstRemovableDrive == -1 ? 0 : firstRemovableDrive); - driveListMouseClicked(null); - driveList.requestFocusInWindow(); - } else { - setErrorMessage(Bundle.LogicalImagerPanel_messageLabel_noExternalDriveFound()); - } - firePropertyChange(DataSourceProcessor.DSP_PANEL_EVENT.UPDATE_UI.toString(), true, false); - }//GEN-LAST:event_scanButtonActionPerformed - @Messages({ "# {0} - sparseImageDirectory", - "# {1} - image", - "LogicalImagerPanel.messageLabel.directoryDoesNotContainSparseImage=Directory {0} does not contain {1}", + "LogicalImagerPanel.messageLabel.directoryDoesNotContainSparseImage=Directory {0} does not contain any images", "# {0} - invalidFormatDirectory", "LogicalImagerPanel.messageLabel.directoryFormatInvalid=Directory {0} does not match format Logical_Imager_HOSTNAME_yyyymmdd_HH_MM_SS" }) private void browseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_browseButtonActionPerformed imageTable.clearSelection(); - choosenImageDirPath = null; + manualImageDirPath = null; setErrorMessage(NO_IMAGE_SELECTED); fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); int retval = fileChooser.showOpenDialog(this); @@ -326,14 +325,20 @@ public class LogicalImagerPanel extends JPanel implements DocumentListener { String path = fileChooser.getSelectedFile().getPath(); Matcher m = regex.matcher(path); if (m.find()) { - Path vhdPath = Paths.get(path, SPARSE_IMAGE_VHD); - if (!vhdPath.toFile().exists()) { - setErrorMessage(Bundle.LogicalImagerPanel_messageLabel_directoryDoesNotContainSparseImage(path, SPARSE_IMAGE_VHD)); + File dir = Paths.get(path).toFile(); + String[] vhdFiles = dir.list(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.endsWith(".vhd"); + } + }); + if (vhdFiles.length == 0) { + setErrorMessage(Bundle.LogicalImagerPanel_messageLabel_directoryDoesNotContainSparseImage(path)); firePropertyChange(DataSourceProcessor.DSP_PANEL_EVENT.UPDATE_UI.toString(), true, false); return; } - choosenImageDirPath = Paths.get(path); - setNormalMessage(SELECTED_IMAGE + " " + path); + manualImageDirPath = Paths.get(path); + setNormalMessage(path); firePropertyChange(DataSourceProcessor.DSP_PANEL_EVENT.UPDATE_UI.toString(), false, true); } else { setErrorMessage(Bundle.LogicalImagerPanel_messageLabel_directoryFormatInvalid(path)); @@ -347,19 +352,23 @@ public class LogicalImagerPanel extends JPanel implements DocumentListener { private void imageTableSelect() { int index = imageTable.getSelectedRow(); if (index != -1) { - choosenImageDirPath = Paths.get((String) imageTableModel.getValueAt(index, 2)); - setNormalMessage(SELECTED_IMAGE + " " + choosenImageDirPath.toString()); + setNormalMessage((String) imageTableModel.getValueAt(imageTable.convertRowIndexToModel(index), 2)); firePropertyChange(DataSourceProcessor.DSP_PANEL_EVENT.UPDATE_UI.toString(), false, true); } else { - choosenImageDirPath = null; setErrorMessage(NO_IMAGE_SELECTED); firePropertyChange(DataSourceProcessor.DSP_PANEL_EVENT.UPDATE_UI.toString(), true, false); } } - private void imageTableMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_imageTableMouseClicked - imageTableSelect(); - }//GEN-LAST:event_imageTableMouseClicked + private boolean dirHasVhdFiles(File dir) { + File[] fList = dir.listFiles(new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.endsWith(".vhd"); + } + }); + return (fList != null && fList.length != 0); + } private void driveListSelect() { String selectedStr = driveList.getSelectedValue(); @@ -372,12 +381,10 @@ public class LogicalImagerPanel extends JPanel implements DocumentListener { if (fList != null) { imageTableModel = new ImageTableModel(); - int row = 0; // Find all directories with name like Logical_Imager_HOSTNAME_yyyymmdd_HH_MM_SS - // and has a sparse_image.vhd file in it + // and has vhd files in it for (File file : fList) { - if (file.isDirectory() - && Paths.get(driveLetter, file.getName(), SPARSE_IMAGE_VHD).toFile().exists()) { + if (file.isDirectory() && dirHasVhdFiles(file)) { String dir = file.getName(); Matcher m = regex.matcher(dir); if (m.find()) { @@ -391,26 +398,26 @@ public class LogicalImagerPanel extends JPanel implements DocumentListener { String second = m.group(7); String extractDate = year + "/" + month + "/" + day + " " + hour + ":" + minute + ":" + second; - imageTableModel.setValueAt(hostname, row, 0); - imageTableModel.setValueAt(extractDate, row, 1); - imageTableModel.setValueAt(imageDirPath, row, 2); - row++; + imageTableModel.addRow(new Object[]{hostname, extractDate, imageDirPath}); } } } - selectAcquisitionFromDriveLabel.setText(Bundle.LogicalImagerPanel_selectAcquisitionFromDriveLabel_text() + selectFromDriveLabel.setText(Bundle.LogicalImagerPanel_selectAcquisitionFromDriveLabel_text() + " " + driveLetter); imageTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN); imageTable.setModel(imageTableModel); + configureImageTable(); fixImageTableColumnWidth(); // If there are any images, select the first one if (imageTable.getRowCount() > 0) { imageTable.setRowSelectionInterval(0, 0); imageTableSelect(); } else { - choosenImageDirPath = null; setErrorMessage(DRIVE_HAS_NO_IMAGES); } + } else { + clearImageTable(); + setErrorMessage(DRIVE_HAS_NO_IMAGES); } } @@ -421,58 +428,158 @@ public class LogicalImagerPanel extends JPanel implements DocumentListener { } private void setErrorMessage(String msg) { - messageLabel.setForeground(Color.red); - messageLabel.setText(msg); + messageTextArea.setForeground(Color.red); + messageTextArea.setText(msg); + pathTextField.setText(""); } private void setNormalMessage(String msg) { - messageLabel.setForeground(Color.black); - messageLabel.setText(msg); + pathTextField.setText(msg); + messageTextArea.setText(""); } private void clearImageTable() { imageTableModel = new ImageTableModel(); imageTable.setModel(imageTableModel); + configureImageTable(); fixImageTableColumnWidth(); } - private void driveListMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_driveListMouseClicked - driveListSelect(); + private void toggleMouseAndKeyListeners(Component component, boolean isEnable) { + component.setEnabled(isEnable); + } + + private void manualRadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_manualRadioButtonActionPerformed + browseButton.setEnabled(true); + + // disable import panel + toggleMouseAndKeyListeners(driveList, false); + toggleMouseAndKeyListeners(driveListScrollPane, false); + toggleMouseAndKeyListeners(imageScrollPane, false); + toggleMouseAndKeyListeners(imageTable, false); + + refreshButton.setEnabled(false); + + manualImageDirPath = null; + setNormalMessage(""); firePropertyChange(DataSourceProcessor.DSP_PANEL_EVENT.UPDATE_UI.toString(), true, false); - }//GEN-LAST:event_driveListMouseClicked + }//GEN-LAST:event_manualRadioButtonActionPerformed + + private void importRadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_importRadioButtonActionPerformed + browseButton.setEnabled(false); + + toggleMouseAndKeyListeners(driveList, true); + toggleMouseAndKeyListeners(driveListScrollPane, true); + toggleMouseAndKeyListeners(imageScrollPane, true); + toggleMouseAndKeyListeners(imageTable, true); + + refreshButton.setEnabled(true); + + manualImageDirPath = null; + setNormalMessage(""); + refreshButton.doClick(); + }//GEN-LAST:event_importRadioButtonActionPerformed + + @Messages({ + "LogicalImagerPanel.messageLabel.scanningExternalDrives=Scanning external drives for images ...", + "LogicalImagerPanel.messageLabel.noExternalDriveFound=No drive found" + }) + private void refreshButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_refreshButtonActionPerformed + // Scan external drives for vhd images + clearImageTable(); + setNormalMessage(Bundle.LogicalImagerPanel_messageLabel_scanningExternalDrives()); + List listData = new ArrayList<>(); + File[] roots = File.listRoots(); + int firstRemovableDrive = -1; + int i = 0; + for (File root : roots) { + if (DriveListUtils.isNetworkDrive(root.toString().replace(":\\", ""))) { + continue; + } + String description = FileSystemView.getFileSystemView().getSystemTypeDescription(root); + long spaceInBytes = root.getTotalSpace(); + String sizeWithUnit = DriveListUtils.humanReadableByteCount(spaceInBytes, false); + listData.add(root + " (" + description + ") (" + sizeWithUnit + ")"); + if (firstRemovableDrive == -1) { + try { + FileStore fileStore = Files.getFileStore(root.toPath()); + if ((boolean) fileStore.getAttribute("volume:isRemovable")) { //NON-NLS + firstRemovableDrive = i; + } + } catch (IOException ignored) { + //unable to get this removable drive for default selection will try and select next removable drive by default + logger.log(Level.INFO, "Unable to select first removable drive found", ignored); + } + } + i++; + } + driveList.setListData(listData.toArray(new String[listData.size()])); + if (!listData.isEmpty()) { + // auto-select the first external drive, if any + driveList.setSelectedIndex(firstRemovableDrive == -1 ? 0 : firstRemovableDrive); + driveListMouseReleased(null); + driveList.requestFocusInWindow(); + } else { + setErrorMessage(Bundle.LogicalImagerPanel_messageLabel_noExternalDriveFound()); + } + firePropertyChange(DataSourceProcessor.DSP_PANEL_EVENT.UPDATE_UI.toString(), true, false); + }//GEN-LAST:event_refreshButtonActionPerformed private void driveListKeyReleased(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_driveListKeyReleased - driveListSelect(); - firePropertyChange(DataSourceProcessor.DSP_PANEL_EVENT.UPDATE_UI.toString(), true, false); + if (importRadioButton.isSelected()) { + driveListSelect(); + firePropertyChange(DataSourceProcessor.DSP_PANEL_EVENT.UPDATE_UI.toString(), true, false); + } }//GEN-LAST:event_driveListKeyReleased private void imageTableKeyReleased(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_imageTableKeyReleased - imageTableSelect(); - firePropertyChange(DataSourceProcessor.DSP_PANEL_EVENT.UPDATE_UI.toString(), true, false); + if (importRadioButton.isSelected()) { + imageTableSelect(); + } }//GEN-LAST:event_imageTableKeyReleased + private void imageTableMouseReleased(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_imageTableMouseReleased + if (importRadioButton.isSelected()) { + imageTableSelect(); + } + }//GEN-LAST:event_imageTableMouseReleased + + private void driveListMouseReleased(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_driveListMouseReleased + if (importRadioButton.isSelected()) { + driveListSelect(); + firePropertyChange(DataSourceProcessor.DSP_PANEL_EVENT.UPDATE_UI.toString(), true, false); + } + }//GEN-LAST:event_driveListMouseReleased + + // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JButton browseButton; + private javax.swing.ButtonGroup buttonGroup1; private javax.swing.JList driveList; private javax.swing.JScrollPane driveListScrollPane; private javax.swing.JScrollPane imageScrollPane; private javax.swing.JTable imageTable; - private javax.swing.JLabel jLabel1; - private javax.swing.JLabel jLabel6; - private javax.swing.JSeparator jSeparator1; - private javax.swing.JLabel messageLabel; - private javax.swing.JButton scanButton; - private javax.swing.JLabel selectAcquisitionFromDriveLabel; + private javax.swing.JRadioButton importRadioButton; + private javax.swing.JScrollPane jScrollPane1; + private javax.swing.JSeparator jSeparator2; + private javax.swing.JRadioButton manualRadioButton; + private javax.swing.JTextArea messageTextArea; + private javax.swing.JTextField pathTextField; + private javax.swing.JButton refreshButton; private javax.swing.JLabel selectDriveLabel; - private javax.swing.JLabel topLabel; + private javax.swing.JLabel selectFolderLabel; + private javax.swing.JLabel selectFromDriveLabel; // End of variables declaration//GEN-END:variables - public void reset() { + void reset() { //reset the UI elements to default - choosenImageDirPath = null; + manualImageDirPath = null; + setNormalMessage(""); driveList.setListData(EMPTY_LIST_DATA); clearImageTable(); - setNormalMessage(Bundle.LogicalImagerPanel_messageLabel_clickScanOrBrowse()); + if (importRadioButton.isSelected()) { + refreshButton.doClick(); + } } /** @@ -480,16 +587,25 @@ public class LogicalImagerPanel extends JPanel implements DocumentListener { * * @return true if a proper image has been selected, false otherwise */ - public boolean validatePanel() { - return choosenImageDirPath != null && choosenImageDirPath.toFile().exists(); + boolean validatePanel() { + if (manualRadioButton.isSelected()) { + return manualImageDirPath != null && manualImageDirPath.toFile().exists(); + } else if (imageTable.getSelectedRow() != -1) { + Path path = Paths.get((String) imageTableModel.getValueAt(imageTable.convertRowIndexToModel(imageTable.getSelectedRow()), 2)); + return path != null && path.toFile().exists(); + } else { + return false; + } } Path getImageDirPath() { - return choosenImageDirPath; - } - - public void setMessageLabel(String message) { - messageLabel.setText(message); + if (manualRadioButton.isSelected()) { + return manualImageDirPath; + } else if (imageTable.getSelectedRow() != -1) { + return Paths.get((String) imageTableModel.getValueAt(imageTable.convertRowIndexToModel(imageTable.getSelectedRow()), 2)); + } else { + return null; + } } @Override @@ -507,25 +623,22 @@ public class LogicalImagerPanel extends JPanel implements DocumentListener { void storeSettings() { } - private class ImageTableModel extends AbstractTableModel { + /** + * Image Table Model + */ + private class ImageTableModel extends DefaultTableModel { - private final List hostnames = new ArrayList<>(); - private final List extractDates = new ArrayList<>(); - private final List imageDirPaths = new ArrayList<>(); - - @Override - public int getRowCount() { - return hostnames.size(); - } + private static final long serialVersionUID = 1L; @Override public int getColumnCount() { - return 2; + return 3; } @Messages({ "LogicalImagerPanel.imageTable.columnModel.title0=Hostname", - "LogicalImagerPanel.imageTable.columnModel.title1=Extracted Date" + "LogicalImagerPanel.imageTable.columnModel.title1=Extracted Date", + "LogicalImagerPanel.imageTable.columnModel.title2=Path" }) @Override public String getColumnName(int column) { @@ -537,55 +650,18 @@ public class LogicalImagerPanel extends JPanel implements DocumentListener { case 1: colName = Bundle.LogicalImagerPanel_imageTable_columnModel_title1(); break; + case 2: + colName = Bundle.LogicalImagerPanel_imageTable_columnModel_title2(); + break; default: break; } return colName; } - @Override - public Object getValueAt(int rowIndex, int columnIndex) { - Object ret = null; - switch (columnIndex) { - case 0: - ret = hostnames.get(rowIndex); - break; - case 1: - ret = extractDates.get(rowIndex); - break; - case 2: - ret = imageDirPaths.get(rowIndex); - break; - default: - throw new UnsupportedOperationException("Invalid table column index: " + columnIndex); //NON-NLS - } - return ret; - } - @Override public boolean isCellEditable(int rowIndex, int columnIndex) { return false; } - - @Override - public void setValueAt(Object aValue, int rowIndex, int columnIndex) { - switch (columnIndex) { - case 0: - hostnames.add((String) aValue); - break; - case 1: - extractDates.add((String) aValue); - break; - case 2: - imageDirPaths.add((String) aValue); - break; - default: - throw new UnsupportedOperationException("Invalid table column index: " + columnIndex); //NON-NLS - } - // Only show the hostname and extractDates column - if (columnIndex < 2) { - super.setValueAt(aValue, rowIndex, columnIndex); - } - } } } diff --git a/Core/src/org/sleuthkit/autopsy/modules/dataSourceIntegrity/DataSourceIntegrityIngestSettingsPanel.form b/Core/src/org/sleuthkit/autopsy/modules/dataSourceIntegrity/DataSourceIntegrityIngestSettingsPanel.form index b9fc6f08b1..8c970ef7d9 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/dataSourceIntegrity/DataSourceIntegrityIngestSettingsPanel.form +++ b/Core/src/org/sleuthkit/autopsy/modules/dataSourceIntegrity/DataSourceIntegrityIngestSettingsPanel.form @@ -1,6 +1,11 @@
+ + + + + @@ -24,7 +29,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/modules/dataSourceIntegrity/DataSourceIntegrityIngestSettingsPanel.java b/Core/src/org/sleuthkit/autopsy/modules/dataSourceIntegrity/DataSourceIntegrityIngestSettingsPanel.java index 30e3b11b05..9fa465739f 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/dataSourceIntegrity/DataSourceIntegrityIngestSettingsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/modules/dataSourceIntegrity/DataSourceIntegrityIngestSettingsPanel.java @@ -64,6 +64,8 @@ final class DataSourceIntegrityIngestSettingsPanel extends IngestModuleIngestJob jLabel3 = new javax.swing.JLabel(); jLabel1 = new javax.swing.JLabel(); + setPreferredSize(new java.awt.Dimension(300, 155)); + org.openide.awt.Mnemonics.setLocalizedText(computeHashesCheckbox, org.openide.util.NbBundle.getMessage(DataSourceIntegrityIngestSettingsPanel.class, "DataSourceIntegrityIngestSettingsPanel.computeHashesCheckbox.text")); // NOI18N computeHashesCheckbox.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { @@ -89,7 +91,7 @@ final class DataSourceIntegrityIngestSettingsPanel extends IngestModuleIngestJob .addComponent(verifyHashesCheckbox) .addComponent(computeHashesCheckbox) .addComponent(jLabel3)) - .addContainerGap(47, Short.MAX_VALUE)) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) diff --git a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/Bundle.properties-MERGED index 2d105bc3a9..0b470ce6b1 100755 --- a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/Bundle.properties-MERGED @@ -26,15 +26,24 @@ HashDbSearchAction.noOpenCase.errMsg=No open case available. HashDbSearchPanel.noOpenCase.errMsg=No open case available. HashLookupSettingsPanel.centralRepo=Central Repository HashLookupSettingsPanel.editable=Editable +HashLookupSettingsPanel.indexNsrl.text=This hash set appears to be the NSRL, it will be removed from the list.\n +HashLookupSettingsPanel.indexNsrl.title=NSRL will not be indexed HashLookupSettingsPanel.notApplicable=N/A HashLookupSettingsPanel.promptMessage.deleteHashDb=This will make the hash database unavailable for lookup. Do you want to proceed?\n\nNote: The hash database can still be re-imported later. HashLookupSettingsPanel.promptTitle.deleteHashDb=Delete Hash Database from Configuration HashLookupSettingsPanel.readOnly=Read only # {0} - hash lookup name HashLookupSettingsPanel.removeDatabaseFailure.message=Failed to remove hash lookup: {0} +# {0} - nsrlUrlAddress +HashLookupSettingsPanel.removeUnindexedNsrl.text=Instead of indexing the NSRL, please download an already indexed version available here:\n{0} +HashLookupSettingsPanel.removeUnindexedNsrl.title=Unindexed NSRL(s) will be removed HashLookupSettingsPanel.saveFail.message=Couldn't save hash set settings. HashLookupSettingsPanel.saveFail.title=Save Fail HashLookupSettingsPanel.Title=Global Hash Lookup Settings +# {0} - nsrlHashSet +HashLookupSettingsPanel.unindexedNsrl.base=The following hash set appears to be an unindexed version of the NSRL, it will be removed from the list.\nHash set:{0}\n +# {0} - nsrlHashSets +HashLookupSettingsPanel.unindexedNsrls.base=The following hash sets appear to be unindexed versions of the NSRL, they will be removed from the list.\nHash sets:{0}\n HashLookupSettingsPanel.updateStatusError=Error reading status ImportCentralRepoDbProgressDialog.errorParsingFile.message=Error parsing hash set file ImportCentralRepoDbProgressDialog.linesProcessed.message=\ hashes processed diff --git a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashLookupSettingsPanel.java b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashLookupSettingsPanel.java index e6f399d38b..ebe59fe535 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashLookupSettingsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashLookupSettingsPanel.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"); @@ -25,7 +25,6 @@ import java.awt.Frame; import java.awt.event.KeyEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; -import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; @@ -45,6 +44,7 @@ import org.sleuthkit.autopsy.centralrepository.datamodel.EamDb; import org.sleuthkit.autopsy.centralrepository.datamodel.EamDbException; import org.sleuthkit.autopsy.corecomponents.OptionsPanel; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.PlatformUtil; import org.sleuthkit.autopsy.events.AutopsyEvent; import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.autopsy.ingest.IngestModuleGlobalSettingsPanel; @@ -61,12 +61,15 @@ import org.sleuthkit.autopsy.modules.hashdatabase.HashDbManager.HashDb; @SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives public final class HashLookupSettingsPanel extends IngestModuleGlobalSettingsPanel implements OptionsPanel { + private static final String NSRL_URL = "https://sourceforge.net/projects/autopsy/files/NSRL/"; + private static final String NSRL_NAME_STRING = "nsrl"; private static final String NO_SELECTION_TEXT = NbBundle .getMessage(HashLookupSettingsPanel.class, "HashDbConfigPanel.noSelectionText"); private static final String ERROR_GETTING_PATH_TEXT = NbBundle .getMessage(HashLookupSettingsPanel.class, "HashDbConfigPanel.errorGettingPathText"); private static final String ERROR_GETTING_INDEX_STATUS_TEXT = NbBundle .getMessage(HashLookupSettingsPanel.class, "HashDbConfigPanel.errorGettingIndexStatusText"); + private static final Logger logger = Logger.getLogger(HashLookupSettingsPanel.class.getName()); private final HashDbManager hashSetManager = HashDbManager.getInstance(); private final HashSetTableModel hashSetTableModel = new HashSetTableModel(); private final List newReferenceSetIDs = new ArrayList<>(); @@ -320,10 +323,8 @@ public final class HashLookupSettingsPanel extends IngestModuleGlobalSettingsPan @Override public void run() { //If unindexed ones are found, show a popup box that will either index them, or remove them. - if (unindexed.size() == 1) { - showInvalidIndex(false, unindexed); - } else if (unindexed.size() > 1) { - showInvalidIndex(true, unindexed); + if (!unindexed.isEmpty()) { + showInvalidIndex(unindexed); } } }); @@ -391,19 +392,47 @@ public final class HashLookupSettingsPanel extends IngestModuleGlobalSettingsPan * unindexed, along with solutions. This method is related to * ModalNoButtons, to be removed at a later date. * - * @param plural Whether or not there are multiple unindexed databases * @param unindexed The list of unindexed databases. Can be of size 1. */ - private void showInvalidIndex(boolean plural, List unindexed) { + @NbBundle.Messages({"# {0} - nsrlUrlAddress", + "HashLookupSettingsPanel.removeUnindexedNsrl.text=Instead of indexing the NSRL, please download an already indexed version available here:\n{0}", + "# {0} - nsrlHashSet", + "HashLookupSettingsPanel.unindexedNsrl.base=The following hash set appears to be an unindexed version of the NSRL, it will be removed from the list.\nHash set:{0}\n", + "# {0} - nsrlHashSets", + "HashLookupSettingsPanel.unindexedNsrls.base=The following hash sets appear to be unindexed versions of the NSRL, they will be removed from the list.\nHash sets:{0}\n", + "HashLookupSettingsPanel.removeUnindexedNsrl.title=Unindexed NSRL(s) will be removed"}) + private void showInvalidIndex(List unindexed) { String total = ""; - String message; - for (HashDb hdb : unindexed) { - total += "\n" + hdb.getHashSetName(); + String nsrlTotal = ""; + + List nsrlHashsets = new ArrayList<>(); + for (SleuthkitHashSet hdb : unindexed) { + //check if this is the NSRL if so point users toward already indexed versions + if (isWindows() && hdb.getHashSetName().toLowerCase().contains(NSRL_NAME_STRING)) { + nsrlHashsets.add(hdb); + nsrlTotal += "\n" + hdb.getHashSetName(); + } else { + total += "\n" + hdb.getHashSetName(); + } } - if (plural) { + if (!nsrlHashsets.isEmpty()) { + String message; + if (nsrlHashsets.size() > 1) { + message = Bundle.HashLookupSettingsPanel_unindexedNsrls_base(nsrlTotal) + Bundle.HashLookupSettingsPanel_removeUnindexedNsrl_text(NSRL_URL); + } else { + message = Bundle.HashLookupSettingsPanel_unindexedNsrl_base(nsrlTotal) + Bundle.HashLookupSettingsPanel_removeUnindexedNsrl_text(NSRL_URL); + } + JOptionPane.showMessageDialog(this, message, Bundle.HashLookupSettingsPanel_removeUnindexedNsrl_title(), JOptionPane.INFORMATION_MESSAGE); + for (SleuthkitHashSet hdb : nsrlHashsets) { + unindexed.remove(hdb); + } + removeThese(nsrlHashsets); + } + String message = NbBundle.getMessage(this.getClass(), "HashDbConfigPanel.dbNotIndexedMsg", total); + if (unindexed.isEmpty()) { + return; + } else if (unindexed.size() > 1) { message = NbBundle.getMessage(this.getClass(), "HashDbConfigPanel.dbsNotIndexedMsg", total); - } else { - message = NbBundle.getMessage(this.getClass(), "HashDbConfigPanel.dbNotIndexedMsg", total); } int res = JOptionPane.showConfirmDialog(this, message, NbBundle.getMessage(this.getClass(), @@ -948,6 +977,17 @@ public final class HashLookupSettingsPanel extends IngestModuleGlobalSettingsPan } }//GEN-LAST:event_sendIngestMessagesCheckBoxActionPerformed + /** + * Check if the current OS is windows + * + * @return true if running on windows, false otherwise + */ + private boolean isWindows() { + return PlatformUtil.getOSName().toLowerCase().startsWith("Windows"); + } + + @NbBundle.Messages({"HashLookupSettingsPanel.indexNsrl.text=This hash set appears to be the NSRL, it will be removed from the list.\n", + "HashLookupSettingsPanel.indexNsrl.title=NSRL will not be indexed"}) private void indexButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_indexButtonActionPerformed final HashDb hashDatabase = ((HashSetTable) hashSetTable).getSelection(); assert hashDatabase != null; @@ -956,28 +996,39 @@ public final class HashLookupSettingsPanel extends IngestModuleGlobalSettingsPan // Add a listener for the INDEXING_DONE event. This listener will update // the UI. SleuthkitHashSet hashDb = (SleuthkitHashSet) hashDatabase; - hashDb.addPropertyChangeListener(new PropertyChangeListener() { - @Override - public void propertyChange(PropertyChangeEvent evt) { - if (evt.getPropertyName().equals(SleuthkitHashSet.Event.INDEXING_DONE.toString())) { - HashDb selectedHashDb = ((HashSetTable) hashSetTable).getSelection(); - if (selectedHashDb != null && hashDb != null && hashDb.equals(selectedHashDb)) { - updateComponents(); - } - hashSetTableModel.refreshDisplay(); - } + if (isWindows() && hashDb.getHashSetName().toLowerCase().contains(NSRL_NAME_STRING)) { + JOptionPane.showMessageDialog(this, Bundle.HashLookupSettingsPanel_indexNsrl_text() + Bundle.HashLookupSettingsPanel_removeUnindexedNsrl_text(NSRL_URL), Bundle.HashLookupSettingsPanel_indexNsrl_title(), JOptionPane.INFORMATION_MESSAGE); + try { + hashSetManager.removeHashDatabaseNoSave(hashDatabase); + hashSetTableModel.refreshModel(); + firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null); + } catch (HashDbManager.HashDbManagerException ex) { + logger.log(Level.WARNING, "Unable to remove unindexed NSRL from hash set list", ex); } - }); + } else { + hashDb.addPropertyChangeListener(new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (evt.getPropertyName().equals(SleuthkitHashSet.Event.INDEXING_DONE.toString())) { + HashDb selectedHashDb = ((HashSetTable) hashSetTable).getSelection(); + if (selectedHashDb != null && hashDb != null && hashDb.equals(selectedHashDb)) { + updateComponents(); + } + hashSetTableModel.refreshDisplay(); + } + } + }); - // Display a modal dialog box to kick off the indexing on a worker thread - // and try to persuade the user to wait for the indexing task to finish. - // TODO: If the user waits, this defeats the purpose of doing the indexing on a worker thread. - // But if the user cancels the dialog, other operations on the database - // may be attempted when it is not in a suitable state. - ModalNoButtons indexDialog = new ModalNoButtons(this, new Frame(), hashDb); - indexDialog.setLocationRelativeTo(null); - indexDialog.setVisible(true); - indexDialog.setModal(true); + // Display a modal dialog box to kick off the indexing on a worker thread + // and try to persuade the user to wait for the indexing task to finish. + // TODO: If the user waits, this defeats the purpose of doing the indexing on a worker thread. + // But if the user cancels the dialog, other operations on the database + // may be attempted when it is not in a suitable state. + ModalNoButtons indexDialog = new ModalNoButtons(this, new Frame(), hashDb); + indexDialog.setLocationRelativeTo(null); + indexDialog.setVisible(true); + indexDialog.setModal(true); + } }//GEN-LAST:event_indexButtonActionPerformed private void importDatabaseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_importDatabaseButtonActionPerformed diff --git a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/Bundle.properties b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/Bundle.properties index 35378ab1a3..b3732ce149 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/Bundle.properties @@ -6,7 +6,7 @@ OptionsCategory_Name_InterestingItemDefinitions=Interesting Files OptionsCategory_Keywords_InterestingItemDefinitions=InterestingItemDefinitions InterestingItemsIdentifierIngestModule.moduleName=Interesting Files Identifier InterestingItemsIdentifierIngestModule.moduleDescription=Identifies interesting items as defined by interesting item rule sets. -FilesSetPanel.interesting.title=Interesting Files Set +FilesSetPanel.interesting.title=Interesting Files Set Rule FilesSetPanel.interesting.messages.filesSetsMustBeNamed=Interesting files sets must be named. FilesSetPanel.messages.filesSetsReservedName=You have chosen a name reserved by the software, please choose a different name. FilesSetPanel.ignoreKnownFilesCheckbox.text=Ignore Known Files @@ -31,13 +31,13 @@ FilesSetRulePanel.messages.invalidCharInName=The name cannot contain \\, /, :, * FilesSetRulePanel.messages.invalidCharInPath=The path cannot contain \\, :, *, ?, \", <, or > unless it is a regular expression. FilesSetRulePanel.messages.invalidPathRegex=The path regular expression is not valid:\n\n{0} FilesSetDefsPanel.doFileSetsDialog.duplicateRuleSet.text=Rule set with name {0} already exists. -FilesSetRulePanel.pathSeparatorInfoLabel.text=Use / as path separator +FilesSetRulePanel.pathSeparatorInfoLabel.text=Folder must be in parent path. Use '/' to give consecutive names FilesIdentifierIngestJobSettingsPanel.border.title=Select interesting files sets to enable during ingest: FilesSetRulePanel.jLabel1.text=Type: FilesSetRulePanel.interesting.jLabel5.text=Enter information about files that you want to find. FilesSetRulePanel.ingest.jLabel5.text=Enter information about files that you want to run ingest on. FilesSetRulePanel.nameCheck.text=Name: -FilesSetRulePanel.pathCheck.text=Path Substring: +FilesSetRulePanel.pathCheck.text=Folder Name: FilesSetRulePanel.filesRadioButton.text=Files FilesSetRulePanel.dirsRadioButton.text=Directories FilesSetDefsPanel.interesting.setsListLabel.text=Rule Sets: diff --git a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/Bundle.properties-MERGED index 7ca4901b1b..1279d3642b 100755 --- a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/Bundle.properties-MERGED @@ -37,6 +37,8 @@ FilesSetPanel.ingest.createNewFilter=Create/edit file ingest filters... FilesSetPanel.ingest.messages.filtersMustBeNamed=File ingest filters must be named. FilesSetPanel.rule.title=File Filter Rule FilesSetRulePanel.bytes=Bytes +# {0} - regex +FilesSetRulePanel.CommaInRegexWarning=Warning: Comma(s) in the file extension field will be interpreted as part of a regex and will not split the entry into multiple extensions (Entered: "{0}") FilesSetRulePanel.DaysIncludedEmptyError=Number of days included cannot be empty. FilesSetRulePanel.DaysIncludedInvalidError=Number of days included must be a positive integer. FilesSetRulePanel.gigaBytes=Gigabytes @@ -60,7 +62,7 @@ OptionsCategory_Name_InterestingItemDefinitions=Interesting Files OptionsCategory_Keywords_InterestingItemDefinitions=InterestingItemDefinitions InterestingItemsIdentifierIngestModule.moduleName=Interesting Files Identifier InterestingItemsIdentifierIngestModule.moduleDescription=Identifies interesting items as defined by interesting item rule sets. -FilesSetPanel.interesting.title=Interesting Files Set +FilesSetPanel.interesting.title=Interesting Files Set Rule FilesSetPanel.interesting.messages.filesSetsMustBeNamed=Interesting files sets must be named. FilesSetPanel.messages.filesSetsReservedName=You have chosen a name reserved by the software, please choose a different name. FilesSetPanel.ignoreKnownFilesCheckbox.text=Ignore Known Files @@ -85,13 +87,13 @@ FilesSetRulePanel.messages.invalidCharInName=The name cannot contain \\, /, :, * FilesSetRulePanel.messages.invalidCharInPath=The path cannot contain \\, :, *, ?, \", <, or > unless it is a regular expression. FilesSetRulePanel.messages.invalidPathRegex=The path regular expression is not valid:\n\n{0} FilesSetDefsPanel.doFileSetsDialog.duplicateRuleSet.text=Rule set with name {0} already exists. -FilesSetRulePanel.pathSeparatorInfoLabel.text=Use / as path separator +FilesSetRulePanel.pathSeparatorInfoLabel.text=Folder must be in parent path. Use '/' to give consecutive names FilesIdentifierIngestJobSettingsPanel.border.title=Select interesting files sets to enable during ingest: FilesSetRulePanel.jLabel1.text=Type: FilesSetRulePanel.interesting.jLabel5.text=Enter information about files that you want to find. FilesSetRulePanel.ingest.jLabel5.text=Enter information about files that you want to run ingest on. FilesSetRulePanel.nameCheck.text=Name: -FilesSetRulePanel.pathCheck.text=Path Substring: +FilesSetRulePanel.pathCheck.text=Folder Name: FilesSetRulePanel.filesRadioButton.text=Files FilesSetRulePanel.dirsRadioButton.text=Directories FilesSetDefsPanel.interesting.setsListLabel.text=Rule Sets: diff --git a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.form b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.form index 416aad285c..7497f936bb 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.form +++ b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.form @@ -832,6 +832,7 @@ + diff --git a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.java b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.java index e919bf189f..5ff9d2a172 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.java @@ -22,6 +22,8 @@ import java.awt.EventQueue; import java.io.File; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -278,6 +280,7 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp //enable the new button FilesSetDefsPanel.this.newSetButton.setEnabled(canBeEnabled); FilesSetDefsPanel.this.importSetButton.setEnabled(canBeEnabled); + // Get the selected interesting files set and populate the set // components. FilesSet selectedSet = FilesSetDefsPanel.this.setsList.getSelectedValue(); @@ -294,14 +297,26 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp FilesSetDefsPanel.this.copySetButton.setEnabled(canBeEnabled); FilesSetDefsPanel.this.exportSetButton.setEnabled(true); // Populate the rule definitions list, sorted by name. - TreeMap rules = new TreeMap<>(selectedSet.getRules()); - rules.values().forEach((rule) -> { + List rules = new ArrayList<>(selectedSet.getRules().values()); + Collections.sort(rules, new Comparator() { + @Override + public int compare(FilesSet.Rule rule1, FilesSet.Rule rule2) { + return rule1.toString().compareTo(rule2.toString()); + } + }); + rules.forEach((rule) -> { FilesSetDefsPanel.this.rulesListModel.addElement(rule); }); // Select the first rule by default. if (!FilesSetDefsPanel.this.rulesListModel.isEmpty()) { FilesSetDefsPanel.this.rulesList.setSelectedIndex(0); } + } else { + // Disable the edit, delete, copy, and export buttons + FilesSetDefsPanel.this.editSetButton.setEnabled(false); + FilesSetDefsPanel.this.deleteSetButton.setEnabled(false); + FilesSetDefsPanel.this.copySetButton.setEnabled(false); + FilesSetDefsPanel.this.exportSetButton.setEnabled(false); } } @@ -796,6 +811,7 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp org.openide.awt.Mnemonics.setLocalizedText(jLabel7, org.openide.util.NbBundle.getMessage(FilesSetDefsPanel.class, "FilesSetDefsPanel.jLabel7.text")); // NOI18N mimeTypeComboBox.setBackground(new java.awt.Color(240, 240, 240)); + mimeTypeComboBox.setEditable(true); mimeTypeComboBox.setModel(new javax.swing.DefaultComboBoxModel(new String[] {""})); mimeTypeComboBox.setEnabled(false); mimeTypeComboBox.setMinimumSize(new java.awt.Dimension(0, 20)); diff --git a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetRulePanel.java b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetRulePanel.java index 9744b0c5bf..b30249bc69 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetRulePanel.java +++ b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetRulePanel.java @@ -57,7 +57,9 @@ final class FilesSetRulePanel extends javax.swing.JPanel { "FilesSetRulePanel.NoPathError=Path cannot be empty", "FilesSetRulePanel.DaysIncludedEmptyError=Number of days included cannot be empty.", "FilesSetRulePanel.DaysIncludedInvalidError=Number of days included must be a positive integer.", - "FilesSetRulePanel.ZeroFileSizeError=File size condition value must not be 0 (Unless = is selected)." + "FilesSetRulePanel.ZeroFileSizeError=File size condition value must not be 0 (Unless = is selected).", + "# {0} - regex", + "FilesSetRulePanel.CommaInRegexWarning=Warning: Comma(s) in the file extension field will be interpreted as part of a regex and will not split the entry into multiple extensions (Entered: \"{0}\")", }) private static final long serialVersionUID = 1L; @@ -130,6 +132,7 @@ final class FilesSetRulePanel extends javax.swing.JPanel { this.setButtons(okButton, cancelButton); updateNameTextFieldPrompt(); + setComponentsForSearchType(); } /** @@ -358,6 +361,16 @@ final class FilesSetRulePanel extends javax.swing.JPanel { return false; } if (this.nameRegexCheckbox.isSelected()) { + + // If extension is also selected and the regex contains a comma, display a warning + // since it is unclear whether the comma is part of a regex or is separating extensions. + if (this.extensionRadioButton.isSelected() && this.nameTextField.getText().contains(",")) { + NotifyDescriptor notifyDesc = new NotifyDescriptor.Message( + Bundle.FilesSetRulePanel_CommaInRegexWarning(this.nameTextField.getText()), + NotifyDescriptor.WARNING_MESSAGE); + DialogDisplayer.getDefault().notify(notifyDesc); + } + try { Pattern.compile(this.nameTextField.getText()); } catch (PatternSyntaxException ex) { diff --git a/Core/src/org/sleuthkit/autopsy/rejview/HexView.java b/Core/src/org/sleuthkit/autopsy/rejview/HexView.java index f78f9ac1ad..932bcf627a 100644 --- a/Core/src/org/sleuthkit/autopsy/rejview/HexView.java +++ b/Core/src/org/sleuthkit/autopsy/rejview/HexView.java @@ -251,10 +251,10 @@ final class HexView extends JPanel { this.setHighlight(startByte, endByte); if (startByte != endByte) { - /** - * @param 1 Start - * @param 2 End - * @param 3 Len + /* + * param 1 Start + * param 2 End + * param 3 Len */ int length = endByte - startByte; String text = Bundle.HexView_statusTemplate_nonZeroLength( @@ -266,8 +266,8 @@ final class HexView extends JPanel { String.format("0x%1$x", length)); statusLabel.setText(text); } else { - /** - * @param 1 Start + /* + * param 1 Start */ String text = Bundle.HexView_statusTemplate_zeroLength(startByte, String.format("0x%1$x", startByte)); statusLabel.setText(text); diff --git a/Core/src/org/sleuthkit/autopsy/rejview/RejTreeKeyView.java b/Core/src/org/sleuthkit/autopsy/rejview/RejTreeKeyView.java index dacb2e8177..a74d4d26b4 100644 --- a/Core/src/org/sleuthkit/autopsy/rejview/RejTreeKeyView.java +++ b/Core/src/org/sleuthkit/autopsy/rejview/RejTreeKeyView.java @@ -58,10 +58,10 @@ public final class RejTreeKeyView extends RejTreeNodeView { public RejTreeKeyView(RejTreeKeyNode node) { super(new BorderLayout()); - /** - * @param 1 Name - * @param 2 Number of subkeys - * @param 3 Number of values + /* + * param 1 Name + * param 2 Number of subkeys + * param 3 Number of values */ String metadataTemplate = "" + Bundle.RejTreeKeyView_template_name() diff --git a/Core/src/org/sleuthkit/autopsy/rejview/RejTreeValueView.java b/Core/src/org/sleuthkit/autopsy/rejview/RejTreeValueView.java index 8b18d38bf5..d2b4e7cf5c 100644 --- a/Core/src/org/sleuthkit/autopsy/rejview/RejTreeValueView.java +++ b/Core/src/org/sleuthkit/autopsy/rejview/RejTreeValueView.java @@ -51,9 +51,9 @@ public final class RejTreeValueView extends RejTreeNodeView { "RejTreeValueView.valueBorder.title=Value",}) public RejTreeValueView(RejTreeValueNode node) { super(new BorderLayout()); - /** - * @param 1 Name - * @param 2 Type + /* + * param 1 Name + * param 2 Type */ String metadataTemplate = "" + Bundle.RejTreeValueView_template_name() @@ -63,8 +63,8 @@ public final class RejTreeValueView extends RejTreeNodeView { String valueName; String valueType; - /** - * @param 1 Value + /* + * param 1 Value */ String valueTemplate = "%1$s"; try { diff --git a/Core/src/org/sleuthkit/autopsy/report/Bundle.properties b/Core/src/org/sleuthkit/autopsy/report/Bundle.properties index 16bc87c22e..45e66b1092 100644 --- a/Core/src/org/sleuthkit/autopsy/report/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/report/Bundle.properties @@ -258,11 +258,9 @@ CreatePortableCasePanel.outputFolderTextField.text=jTextField1 CreatePortableCasePanel.chooseOutputFolderButton.text=Choose folder CreatePortableCasePanel.jLabel1.text=Export files tagged as: CreatePortableCasePanel.jLabel2.text=Select output folder: -CreatePortableCasePanel.compressCheckbox.text=Compress case -CreatePortableCasePanel.chunkSizeLabel.text=Split into chunks of size: CreatePortableCasePanel.errorLabel.text_1=Windows only ReportWizardPortableCaseOptionsVisualPanel.errorLabel.text=Windows only -ReportWizardPortableCaseOptionsVisualPanel.compressCheckbox.text=Package case into chunks of size: +ReportWizardPortableCaseOptionsVisualPanel.compressCheckbox.text=Package case into an archive: PortableCaseTagsListPanel.deselectButton.text=Deselect All PortableCaseTagsListPanel.selectButton.text=Select All PortableCaseInterestingItemsListPanel.selectButton.text=Select All @@ -271,3 +269,4 @@ ReportFileTextConfigurationPanel.tabDelimitedButton.text=Tab delimited ReportFileTextConfigurationPanel.commaDelimitedButton.text=Comma delimited PortableCaseTagsListPanel.descLabel.text=Include the following tags: PortableCaseInterestingItemsListPanel.descLabel.text=Include Interesting Items from these sets: +ReportWizardPortableCaseOptionsVisualPanel.compressCheckbox.toolTipText= diff --git a/Core/src/org/sleuthkit/autopsy/report/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/report/Bundle.properties-MERGED old mode 100644 new mode 100755 index f9813728d5..53aa1acfbd --- a/Core/src/org/sleuthkit/autopsy/report/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/report/Bundle.properties-MERGED @@ -318,11 +318,9 @@ CreatePortableCasePanel.outputFolderTextField.text=jTextField1 CreatePortableCasePanel.chooseOutputFolderButton.text=Choose folder CreatePortableCasePanel.jLabel1.text=Export files tagged as: CreatePortableCasePanel.jLabel2.text=Select output folder: -CreatePortableCasePanel.compressCheckbox.text=Compress case -CreatePortableCasePanel.chunkSizeLabel.text=Split into chunks of size: CreatePortableCasePanel.errorLabel.text_1=Windows only ReportWizardPortableCaseOptionsVisualPanel.errorLabel.text=Windows only -ReportWizardPortableCaseOptionsVisualPanel.compressCheckbox.text=Package case into chunks of size: +ReportWizardPortableCaseOptionsVisualPanel.compressCheckbox.text=Package case into an archive: PortableCaseTagsListPanel.deselectButton.text=Deselect All PortableCaseTagsListPanel.selectButton.text=Select All PortableCaseInterestingItemsListPanel.selectButton.text=Select All @@ -331,5 +329,6 @@ ReportFileTextConfigurationPanel.tabDelimitedButton.text=Tab delimited ReportFileTextConfigurationPanel.commaDelimitedButton.text=Comma delimited PortableCaseTagsListPanel.descLabel.text=Include the following tags: PortableCaseInterestingItemsListPanel.descLabel.text=Include Interesting Items from these sets: +ReportWizardPortableCaseOptionsVisualPanel.compressCheckbox.toolTipText= ReportWizardPortableCaseOptionsVisualPanel.getName.title=Choose Portable Case settings TableReportGenerator.StatusColumn.Header=Review Status diff --git a/Core/src/org/sleuthkit/autopsy/report/FileReportText.java b/Core/src/org/sleuthkit/autopsy/report/FileReportText.java index 9d3de2c945..ea0f3452c2 100644 --- a/Core/src/org/sleuthkit/autopsy/report/FileReportText.java +++ b/Core/src/org/sleuthkit/autopsy/report/FileReportText.java @@ -24,6 +24,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Writer; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -63,9 +64,12 @@ class FileReportText implements FileReportModule { public void startReport(String baseReportDir) { this.reportPath = baseReportDir + FILE_NAME; try { - out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(this.reportPath))); + out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(this.reportPath), StandardCharsets.UTF_8)); + out.write('\ufeff'); } catch (FileNotFoundException ex) { logger.log(Level.WARNING, "Failed to create report text file", ex); //NON-NLS + } catch (IOException ex) { + logger.log(Level.WARNING, "Failed to write BOM to report text file", ex); //NON-NLS } } diff --git a/Core/src/org/sleuthkit/autopsy/report/PortableCaseReportModule.java b/Core/src/org/sleuthkit/autopsy/report/PortableCaseReportModule.java index 6566634b1c..ae2fc88766 100644 --- a/Core/src/org/sleuthkit/autopsy/report/PortableCaseReportModule.java +++ b/Core/src/org/sleuthkit/autopsy/report/PortableCaseReportModule.java @@ -983,7 +983,7 @@ class PortableCaseReportModule implements ReportModule { enum ChunkSize { NONE("Do not split", ""), // NON-NLS - DVD("4.5 GB (DVD)", "4500m"); // NON-NLS + DVD("Split into 4.5 GB chunks (DVD)", "4500m"); // NON-NLS private final String displayName; private final String sevenZipParam; diff --git a/Core/src/org/sleuthkit/autopsy/report/ReportHTML.java b/Core/src/org/sleuthkit/autopsy/report/ReportHTML.java index b54af094ad..e7bb2f313d 100644 --- a/Core/src/org/sleuthkit/autopsy/report/ReportHTML.java +++ b/Core/src/org/sleuthkit/autopsy/report/ReportHTML.java @@ -45,21 +45,22 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; +import java.util.concurrent.ExecutionException; import java.util.logging.Level; import javax.imageio.ImageIO; import javax.swing.JPanel; +import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.StringEscapeUtils; import org.openide.filesystems.FileUtil; import org.openide.util.NbBundle; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; -import org.sleuthkit.autopsy.casemodule.services.Services; import org.sleuthkit.autopsy.casemodule.services.TagsManager; -import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationCase; -import org.sleuthkit.autopsy.centralrepository.datamodel.EamDb; -import org.sleuthkit.autopsy.centralrepository.datamodel.EamDbException; -import org.sleuthkit.autopsy.centralrepository.datamodel.EamOrganization; +import org.sleuthkit.autopsy.casemodule.services.contentviewertags.ContentViewerTagManager; +import org.sleuthkit.autopsy.casemodule.services.contentviewertags.ContentViewerTagManager.ContentViewerTag; +import org.sleuthkit.autopsy.contentviewers.imagetagging.ImageTagRegion; +import org.sleuthkit.autopsy.contentviewers.imagetagging.ImageTagsUtil; import org.sleuthkit.autopsy.coreutils.EscapeUtil; import org.sleuthkit.autopsy.coreutils.ImageUtils; import org.sleuthkit.autopsy.coreutils.Logger; @@ -731,6 +732,29 @@ class ReportHTML implements TableReportModule { logger.log(Level.SEVERE, "Output writer is null. Page was not initialized before writing.", ex); //NON-NLS } } + + /** + * Finds all associated image tags. + * + * @param contentTags + * @return + */ + private List getTaggedRegions(List contentTags) { + ArrayList tagRegions = new ArrayList<>(); + contentTags.forEach((contentTag) -> { + try { + ContentViewerTag contentViewerTag = ContentViewerTagManager + .getTag(contentTag, ImageTagRegion.class); + if (contentViewerTag != null) { + tagRegions.add(contentViewerTag.getDetails()); + } + } catch (TskCoreException | NoCurrentCaseException ex) { + logger.log(Level.WARNING, "Could not get content viewer tag " + + "from case db for content_tag with id %d", contentTag.getId()); + } + }); + return tagRegions; + } /** * Add the body of the thumbnails table. @@ -770,13 +794,54 @@ class ReportHTML implements TableReportModule { } AbstractFile file = (AbstractFile) content; + List contentTags = new ArrayList<>(); + + String thumbnailPath = null; + String imageWithTagsFullPath = null; + try { + //Get content tags and all image tags + contentTags = Case.getCurrentCase().getServices() + .getTagsManager().getContentTagsByContent(file); + List imageTags = getTaggedRegions(contentTags); + + if(!imageTags.isEmpty()) { + //Write the tags to the fullsize and thumbnail images + BufferedImage fullImageWithTags = ImageTagsUtil.getImageWithTags(file, imageTags); + + BufferedImage thumbnailWithTags = ImageTagsUtil.getThumbnailWithTags(file, + imageTags, ImageTagsUtil.IconSize.MEDIUM); + + String fileName = org.sleuthkit.autopsy.coreutils.FileUtil.escapeFileName(file.getName()); + + //Create paths in report to write tagged images + File thumbnailImageWithTagsFile = Paths.get(thumbsPath, FilenameUtils.removeExtension(fileName) + ".png").toFile(); + String fullImageWithTagsPath = makeCustomUniqueFilePath(file, "thumbs_fullsize"); + fullImageWithTagsPath = FilenameUtils.removeExtension(fullImageWithTagsPath) + ".png"; + File fullImageWithTagsFile = Paths.get(fullImageWithTagsPath).toFile(); + + //Save images + ImageIO.write(thumbnailWithTags, "png", thumbnailImageWithTagsFile); + ImageIO.write(fullImageWithTags, "png", fullImageWithTagsFile); + + thumbnailPath = THUMBS_REL_PATH + thumbnailImageWithTagsFile.getName(); + //Relative path + imageWithTagsFullPath = fullImageWithTagsPath.substring(subPath.length()); + } + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "Could not get tags for file.", ex); //NON-NLS + } catch (IOException | InterruptedException | ExecutionException ex) { + logger.log(Level.WARNING, "Could make marked up thumbnail.", ex); //NON-NLS + } // save copies of the orginal image and thumbnail image - String thumbnailPath = prepareThumbnail(file); + if(thumbnailPath == null) { + thumbnailPath = prepareThumbnail(file); + } + if (thumbnailPath == null) { continue; } - String contentPath = saveContent(file, "thumbs_fullsize"); //NON-NLS + String contentPath = saveContent(file, "original"); //NON-NLS String nameInImage; try { nameInImage = file.getUniquePath(); @@ -785,32 +850,27 @@ class ReportHTML implements TableReportModule { } StringBuilder linkToThumbnail = new StringBuilder(); - linkToThumbnail.append("
+ + + + + + + +
TypeSubstring/RegexTextDescriptionSample match
Full Namefalse\verbatim test.txt \endverbatimWill match files named "test.txt"text.txt
Full Nametrue\verbatim bomb \endverbatimWill match files with "bomb" anywhere their namePipe bomb.png
Full Nametrue\verbatim virus.*\.exe \endverbatimWill match files with "virus" followed by ".exe" anywhere their namebad_virus.exe
Extension Onlyfalse\verbatim zip \endverbatimWill match .zip filesmyArchive.zip
Extension Onlyfalse\verbatim zip,rar,7z \endverbatimWill match .zip, .rar, and .7z filesanotherArchive.rar
Extension Onlytrue\verbatim jp \endverbatimWill match .jpg, .jpeg files, and any others with "jp" in the extensionmyImage.jpg
+
  • Path Substring - Enter a folder name that must be part of file's path for it to be a match. If you only want to specify that a word appears somewhere in the path, use the regex option. + + + + + +
    RegexTextDescriptionSample match
    false\verbatim Documents \endverbatimMatch any file that has a folder named "Documents" in its path/folder1/Documents/fileA.doc
    true\verbatim bomb \endverbatimMatch any file with "bomb" in the path/folder1/bomb making/file2.doc
    true\verbatim Users/.*/Downloads \endverbatimMatch any file with "Users" and "Downloads" in the pathC:/Users/user1/Downloads/myFile.txt
    -Using the Module -====== +
  • MIME Type - Use the pull-down list to select a MIME type. Only a single MIME type can be selected. -When you enable the Interesting Files module, you can choose what rule sets to enable. To add rules, use the "Advanced" button from the ingest module panel. +
  • File Size - Select whether you want to match files equal to, smaller than, or larger than a given size. -When files are found, they will be in the Interesting Files area of the tree. You should see the set and rule names with the match. +
  • Modified Within - Select how recently a file must have been modified to match the rule. + +Finally you can optionally enter a name for the rule. This will be displayed in the UI for each match. -Ingest Settings ------- +\subsection interesting_files_examples Examples +Here are a few examples of rules being created. -When running the ingest modules, the user can choose which interesting file rules to enable . -
    -\image html interesting_files_ingest_settings.PNG +This is a rule that matches any file with "bomb" in the name that also has an "image/png" MIME type. -Seeing Results ------- -The results show up in the tree under "Results", "Interesting Items". +\image html InterestingFiles/bomb_png.png -\image html interesting_files_results.PNG +This is a rule that matches folders named "Private". + +\image html InterestingFiles/private_folder.png + +This rule is looking for archives in the user download directory. It requires "Users" and "Downloads" in the file's path, and an extension of .zip, .rar, or .7z. + +\image html InterestingFiles/download_archive.png + +This is a rule that matches files with size at least 50MB that have been modified in the last week. + +\image html InterestingFiles/new_large_files.png + +\section interesting_files_running Running the Module + +At runtime, you can select which rule sets you would like to run on your data source. + +\image html InterestingFiles/ingest.png + +\section interesting_files_results Viewing Results + +Files that match any of the rules in the enabled rule sets will be shown in the Results section of the \ref tree_viewer_page under "Interesting Items" and then the name of the rule set that matched. Note that other modules besides Interesting Files put results in this section of the tree, so there may be more than just what matched your rule sets. Selecting the "Interesting Files" node under one of your rule sets will display all matching files in the \ref result_viewer_page. + +\image html InterestingFiles/results.png + +You can see which rule matched in the "Category" column. You can export some or all of the files for further analysis. To do this, first use the standard Windows file +selection methods to highlight the files you want to export in the \ref result_viewer_page : +
      +
    • Hold down Ctrl and click on each file you want to export +
    • Hold down Shift to select a range of files +
    • Click on any file in the Result Viewer and then hit Ctrl+A to select all the files +
    +Once you have your desired files selected, right click and select “Extract Files” to save copies of them. */ diff --git a/docs/doxygen-user/logical_imager.dox b/docs/doxygen-user/logical_imager.dox new file mode 100644 index 0000000000..5f46068418 --- /dev/null +++ b/docs/doxygen-user/logical_imager.dox @@ -0,0 +1,124 @@ +/*! \page logical_imager_page Logical Imager + +\section logical_imager_overview Overview + +The logical imager allows you to collect files from a live Windows computer. The imager is configured with rules that specify what files to collect. Rules can be based on file attributes such as folder names, extensions, and sizes. You can use this feature when you do not have time or authorization to perform a full drive acquisition. + +The logical imager produces one or more sparse VHD images that contain all of the file system data that was read. These VHD images can be imported into Autopsy or mounted by Windows. The imager also enumerates the user accounts on the system and can generate alerts if encryption programs exist. + +The general workflow is: +
      +
    • Configure logical imager using Autopsy. This will copy a configuration file specifying which files to collect and the logical imager executable to the target drive. +
    • Insert the drive into the target system and run logical imager. This will give you a folder containing the sparse VHD copy of the target system (or multiple VHDs if more than one drive was analyzed), a file containing user account information, and a record of which files generated alerts. +
    • Load the result of running logical imager into Autopsy to browse any matching files and see user account information. +
    + +\section logical_imager_config Configuration + +To start, open Autopsy and go to Tools->Create Logical Imager. + +\image html LogicalImager/tools_menu.png + +
      +
    • Configuring an external drive + +The normal use case is to select a drive from the list under "Configure selected external drive." This will put the logical imager executable and a configuration file into the root directory of that drive once you finish the configuration. It is important to run the executable from the root of your drive because its presence on the drive makes the imager skip that drive during processing. MOVE THIS?? + +\image html LogicalImager/configure_drive.png + +
    • Configuring in a folder + +If you're not ready to set up your drive yet, or if you want to create a different configuration file, you can use the second option to browse to a folder or an existing configuration file. If you're creating a new file, browse to the folder you want to create it in. +Notice that the configuration file has the default name "logical-imager-config.json". You can change this, but if you do you'll need to rename it after you copy it to your drive or use the command prompt to run the imager. See the section on \ref logical_imager_custom_run. + +\image html LogicalImager/select_folder.png +
    + +In either case you can now configure your imager. If the configuration file already exists, this screen will be loaded with the current settings from the file. + +\image html LogicalImager/main_config_panel.png + +On the left side you can see each rule in the configuration file. Each of these rules will be applied against the live system. A rule has a name, an optional description, one or more conditions, and settings for what should happen when a file matching the rule is found. When you select a rule you'll see all the settings for that rule on the right side of the panel. You can edit or delete rules once you select them. There are also two global settings in the bottom right that apply to the configuration file as a whole: +
      +
    • Alert if encryption programs are found - This will add a predefined rule to find encryption programs and alert and export any that are found. You will not be able to edit this rule. +
    • Continue imaging after searches are performed - By default, the logical imager will only copy sectors that it uses or that are part of matching files being exported. If this option is selected, logical imager will go back through the image after the rule matching is complete and copy over any remaining sectors. This will take longer to run and result in much larger VHD images. +
    + +To make a new rule, click on the "New Rule" button. + +\image html LogicalImager/new_attr_rule.png + +There are two rule types to choose from: +
      +
    • Attribute rules allow you enter multiple conditions that must be true for a file to match +
    • Full path rules allow you to enter one or more full paths (path and file name) which must match exactly +
    + +For either rule type, you start by entering a rule name and optional description. You will also need to choose at least one action to take when a match is found. +
      +
    • Alert in Imager console if rule matches - this will display the file data in the console and add it to the "alerts.txt" output file. +
    • Extract file if it matches a rule - this will ensure that the matching file's contents will be copied to the sparse VHD +
    + +Attribute rules can have one or more conditions. All conditions must be true for a rule to match. +
      +
    • Extensions - File must match one of the given extensions (comma-separated). Extensions are case-insensitive. +
    • File names - File must match one of the given file names (new line-separated). File names should include extensions and are case-insensitive. +
    • Folder names - File must match one of the given paths (new line-separated). The given path may be a substring of the file path. You can use "[USER_FOLDER]" to match any user folder on the system. For example, "[USER_FOLDER]/Downloads" will match the downloads folder in any user folder, such as "Users/username/Downloads". +
    • Minimum size / Maximum size - File must be in the given range. You can either both fields to specify a range or use just one to match all files larger or smaller than the given size. +
    • Modified Within - File must have been changed within the specified last number of days +
    + +Full path rules have a single condition. +
      +
    • Full paths: File must exactly match one of the given full paths (new line-separated) +
    + +\image html LogicalImager/full_path_rule.png + +\section logical_imager_running Running Logical Imager + +\subsection logical_imager_default_run Running with the Default Configuration + +Using the defaults in the configuration process will create a drive with the config file (named "logical-imager-config.json") and the logical imager executable in the root folder of your drive. + +\image html LogicalImager/exe_folder.png + +The default case is to run the logical imager on every drive except the one containing it. Note that the logical imager executable must be in the root directory for the drive to be skipped. To run the imager, right-click on "tsk_logical_imager.exe" and select "Run as administrator". This will open a console window where you'll see some information about the processing and if you set any rules to create alerts, you'll see matches in the console window as well. The window will close automatically when the processing is complete. + +The logical imager will start writing the sparse VHD(s) and any other data to a directory next to the executable. + +\image html LogicalImager/output_folder.png + +\subsection logical_imager_custom_run Running from a Command Prompt + +To run the logical imager with custom settings, you'll need to first open a command prompt in administrator mode (right-click and then select "Run as administrator"). Then switch to the drive where logical imager is located. You can run using the default configuration by simply typing "tsk_logical_imager.exe". + +\image html LogicalImager/command_prompt.png + +If your configuration file is not named "logical-imager-config.json" (for example, if you have multiple configuration files for different situations), you'll need to specify the file name using the "-c" flag. + +\image html LogicalImager/config_flag.png + +If you want to specify the drive to run on, you can use the "-i" flag. This can be helpful for testing your configuration file - you can create a small USB drive with files that should match your rules to ensure that everything is working correctly before using it on a real system. The following example shows how to only run on the "G" drive on this system: + +\image html LogicalImager/image_flag.png + +\section logical_imager_results Viewing Results + +The logical imager results can be added to an Autopsy case as a \ref ds_page "data source". This brings in the sparse VHD(s) as a disk image and also adds the other files created by the logical imager. Select the "Autopsy Imager" option and proceed to the next page. + +\image html LogicalImager/dsp_select.png + +In the top section, you can see all the logical imager result folders in the root folder of each drive. Select the one you want to add and then hit the "Next" button. + +\image html LogicalImager/import.png + +If your logical imager results are in a different location, select "Manually Choose Folder" and use the "Browse" button to locate your results. + +In either case you'll get to configure the \ref ingest_page "ingest modules" to run. You can run any of them, but since your disk image may not be complete you may see more errors than normal. For example, the sparse VHD may contain the entire file allocation table but the actual data that goes with the files will be missing. + +The alert and user files created by the logical imager can be found under the Reports section of the Tree Viewer. + + +*/ \ No newline at end of file diff --git a/docs/doxygen-user/machine_translation.dox b/docs/doxygen-user/machine_translation.dox new file mode 100644 index 0000000000..ee3fbd07da --- /dev/null +++ b/docs/doxygen-user/machine_translation.dox @@ -0,0 +1,37 @@ +/*! \page machine_translation_page Machine Translation + +You can configure a machine translation service to use with Autopsy. If one is configured, you will be able to translate text in the \ref content_viewer_page and file names in the \ref result_viewer_page and \ref tree_viewer_page. + +\section mt_config Configuration + +To set up a machine translation service, go to Options->Tools and then select the Machine Translation tab. Select the service you want to use from the drop down menu at the top. + +\image html mt_config.png + +Each service will require slightly different configuration steps. After setting everything up, you can run a quick check that the service is set up correctly using the "Test" button. + +\section mt_file_names Translating File Names + +You can use machine translation to automatically translate file and folder names, such as the ones seen below: + +\image html mt_file_name_original.png + +To enable file name translation, go to the \ref view_options_page and check the box under "Translate Text". + +\image html mt_file_name_enable.png + +Once enabled, the translated versions of the file and folder names will be shown in the \ref tree_viewer_page and in the first column of the \ref result_viewer_page. The original name will be displayed in the new "Original Name" column. + +\image html mt_file_names_translated.png + +\section mt_content_viewer Translating File Content + +After you set up a machine translation service, the Translation tab under the Text tab in the Content Viewer will be active. The Translation tab allows you to use your service to translate the beginning of a file. For example, you might see the following in the default Indexed Text tab: + +\image html mt_content_viewer_untranslated_text.png + +Switching to the Translation tab will display the results of running the text through the machine translation service. + +\image html mt_content_viewer_translated.png + +*/ \ No newline at end of file diff --git a/docs/doxygen-user/main.dox b/docs/doxygen-user/main.dox index e148279aa6..5d5e3063cd 100644 --- a/docs/doxygen-user/main.dox +++ b/docs/doxygen-user/main.dox @@ -49,6 +49,7 @@ The following topics are available here: - \subpage common_properties_page - \subpage search_all_cases_page - \subpage logs_and_output_page + - \subpage machine_translation_page - Reporting - \subpage tagging_page - \subpage reporting_page @@ -68,6 +69,7 @@ The following topics are available here: - \subpage advanced_page - \subpage experimental_page - \subpage command_line_ingest_page +- \subpage logical_imager_page - \subpage translations_page If the topic you need is not listed, refer to the Autopsy Wiki or join the SleuthKit User List at SourceForge. diff --git a/docs/doxygen-user/portable_case.dox b/docs/doxygen-user/portable_case.dox index 4f4e809ae8..b476206a38 100644 --- a/docs/doxygen-user/portable_case.dox +++ b/docs/doxygen-user/portable_case.dox @@ -9,7 +9,7 @@ The general use case is as follows:
    1. Alice is analyzing one or more data sources using Autopsy. She tags files and results that are of particular interest.
    2. Alice wants to share her findings with Bob but is unable to send him the original data sources because he shouldn't see them or the originals are too big. -
    3. Alice creates a portable case which will contain only her tagged files and results, plus any files associated with those results, and sends it to Bob. +
    4. Alice creates a portable case which will contain only her tagged files and results, plus any files associated with those results, and sends it to Bob. She could also choose to include \ref interesting_files_identifier_page "interesting files" or results.
    5. Bob can open the portable case in Autopsy and view all files and results Alice tagged, and run any of the normal Autopsy features.
    @@ -21,25 +21,38 @@ The portable version could like this: \image html portable_case_portable_version.png -Alice only tagged eight files and results, so most of the original content is no longer in the case. Some of the data sources had no tagged items so they're not included at all. The file structure of any tagged files is preserved - you can see that the tagged image in the screenshot is still in the same location, but the non-tagged files are gone. Note that although the original images (such as "image1.vhd") appear in the tree, their contents are not included in the portable case. +Alice only tagged eight files and results and her case had no interesting items, so most of the original content is no longer in the case. Some of the data sources had no tagged items so they're not included at all. The file structure of any tagged files is preserved - you can see that the tagged image in the screenshot is still in the same location, but the non-tagged files are gone. Note that although the original images (such as "image1.vhd") appear in the tree, their contents are not included in the portable case. \section portable_case_creation Creating a Portable Case -First you'll want to make sure that all the files and results you want included in the portable case are tagged - see the \ref tagging_page page for more details. -You can see what tags you've added in the \ref tree_viewer_page. +A portable case can contain tagged files and results and data from the Interesting Items section of the \ref tree_viewer_page. You'll be able to choose which of the Interesting Item sets you want to include in the portable case. + +\image html portable_case_interesting_items.png + +You can tag any additional files you want to include in the portable case. See the \ref tagging_page page for details on how to create tags. You can see what tags you've added in the \ref tree_viewer_page. \image html portable_case_tags.png -Portable cases are created through the \ref reporting_page feature. The Generate Report dialog will display a list of all tags that are in use in the current case and you can choose which ones you would like to include. At the bottom you can select the output folder for the new case. By default it will be placed in the "Reports" folder in the current case. +Portable cases are created through the \ref reporting_page feature. The Generate Report dialog will display a list of all tags and interesting file sets that are in use in the current case and you can choose which ones you would like to include. At the bottom you can choose to optionally package the case. Choosing to package the case without chunking will simply compress the portable case in a single archive that can be extracted with common compression programs. If you choose split the packaged case into multiple files, you will need to use the "Unpackage Portable Case" option before loading it. This will be discussed in the next section. + +The portable case will be placed in the "Reports" folder in the current case. \image html portable_case_report_panel.png -Here you can see the new portable case. It will be named with the original case name plus "(Portable)". The portable case is initially missing many of the normal Autopsy folders - these will be created the first time a user opens it. The portable case folder can be zipped and sent to a different user. +Here you can see an unpackaged portable case. It will be named with the original case name plus "(Portable)". The portable case is initially missing many of the normal Autopsy folders - these will be created the first time a user opens it. \image html portable_case_folder.png +If you packaged the portable case but did not choose to split it into chunks, you'll have a single .zip file. If you chose to split the packaged case, you'll have one or more files starting with extension .zip.001. + +\image html portable_case_chunks.png + \section portable_case_usage Using a Portable Case +If your portable case was packaged, you'll first need to unpackage it. Open the "Case" menu and then select "Unpackage Portable Case". This will bring up a dialog where you can browse to your packaged case and select where to extract it to. Once unpackaged you can open it normally. + +\image html portable_case_unpackage.png + Portable cases generally behave like any other Autopsy case. You can run ingest, do keyword searches, use the timeline viewer, etc. One point to note is that while the original data source names appear in the case, the data sources themselves were not copied into the portable case. \image html portable_case_empty_image.png diff --git a/docs/doxygen-user/reporting.dox b/docs/doxygen-user/reporting.dox index 63c1917036..c1065983e7 100644 --- a/docs/doxygen-user/reporting.dox +++ b/docs/doxygen-user/reporting.dox @@ -64,13 +64,17 @@ show up as Hashset Hits. \subsection report_case_uco CASE-UCO -This module creates an JSON output file in CASE-UCO format from a single data source. +This module creates a JSON output file in CASE-UCO format for a single data source. \image html reports_case.png \subsection report_files Files - Text -This report module allows you create a tab delimited text file from all files in the current case. You can select which fields should be exported. +This report module allows you create a tab or comma delimited text file report of all of the files in the current case. Start by selecting which delimiter you would like to use. + +\image html reports_files_delimiter.png + +You can then select which fields should be reported. \image html reports_files_config.png
    @@ -84,7 +88,7 @@ This report module generates a KML file from any GPS data in the case. This file \subsection report_portable_case Portable Case -This report module generates a new Autopsy case from any tagged files and results. See the \ref portable_case_page page for additional information. +This report module generates a new Autopsy case that includes tagged and/or interesting items. See the \ref portable_case_page page for additional information. \subsection report_stix STIX diff --git a/docs/doxygen-user/result_viewer.dox b/docs/doxygen-user/result_viewer.dox index 1c813d9f81..fae9ebb360 100644 --- a/docs/doxygen-user/result_viewer.dox +++ b/docs/doxygen-user/result_viewer.dox @@ -1,37 +1,61 @@ /*! \page result_viewer_page Result Viewer -The Result Viewer is located on the top right of the Autopsy screen. It shows lists of files and their corresponding attributes such as time, path, size, checksum, etc. +The Result Viewer is located on the top right of the Autopsy screen and shows the the contents of what was selected in the \ref tree_viewer_page. -
    +\section result_viewer_table Table Viewers + +The main table viewer in the "Listing" tab displays the contents of the current selection as a table with selected details (properties) of each item. For files, some examples of the properties that this viewer shows are: name, time (modified, changed, accessed, and created), size, flags (directory and meta), mode, user ID, group ID, metadata address, attribute address, and type (directory and meta). For other data types, the columns will be different. Click the "Table" tab to select this view. + +The following shows the main table viewer when a folder is selected in the Data Source section of the \ref tree_viewer_page. \image html result-viewer-example-1.PNG -
    -By default, the first three columns after the file name in the results viewer are named "S", "C" and "O". +As mentioned above, a table viewer is context-aware which means it will show applicable columns for the data type selected. The following shows the data in the "Web Bookmarks" node in the \ref tree_viewer_page. + +\image html result-viewer-example-3.PNG + +\subsection result_viewer_sco SCO Columns +By default, the first three columns after the file name in a table viewer are named "S", "C" and "O". \image html view_options_sco.png -These columns display the following: +These columns display the following information:
      -
    • (S)core column - indicates whether the item is interesting or notable +
    • (S)core column - indicates whether the item is interesting or notable.
        -
      • Displays a red icon if the file is a match for a notable hash set or has been tagged with a notable tag -
      • Displays a yellow icon if the file has an interesting item match or has been tagged with a non-notable tag +
      • Displays a red icon if the file is a match for a notable hashset or has been tagged with a notable tag. +
      • Displays a yellow icon if the file has an interesting item match or has been tagged with a non-notable tag.
      -
    • (C)omment column - indicates whether the item has a comment in the Central Repository or has a comment associated with a tag +
    • (C)omment column - indicates whether the item has a comment in the Central Repository or has a comment associated with a tag.
    • (O)ther occurrences column - indicates how many data sources in the Central Repository contain this item. The count will include the selected item.
    -To display more information about why an icon has appeared, you can hover over it. The Comment and Other occurrences columns query the Central Repository. If this seems to be having a performance impact, it can be disabled through the \ref view_options_page. This will remove the Other occurrences column entirely and the Comment column will be based only on tags. +To display more information about why an icon has appeared, you can hover over it. These columns query the Central Repository as well as the case database. If this seems to be having a performance impact, you can disable them through the \ref view_options_page. This will remove the Other occurrences column entirely, the Comment column will be based only on tags, and the Score column will no longer be able to reflect Notable items. -You can also switch it to Thumbnail view to see thumbnails of the content in the selected folder. +\subsection export_csv Exporting to CSV -
    -\image html result-viewer-example-2.PNG -
    +You can export the contents of a table viewer to a CSV file in two ways. The "Save table as CSV" button in the upper left will save the entire contents of the table viewer to a CSV file. You can also select rows in the table viewer and then right-click and select "Export selected rows to CSV" to save only a subset of the rows: -The Result Viewer is context-aware, meaning it will show applicable columns for the data type selected. -
    -\image html result-viewer-example-3.PNG -
    +\image html result_viewer_csv.PNG + +\subsection right_click_functions Right Click Functions +Table viewers in the Results Viewer have certain right-click functions built-in into them that can be accessed when a row of a particular type is selected (a file, a directory, or a result). +Here are some examples that you may see: +\li Open File in External Viewer: Opens the selected file in an "external" application as defined by the local OS or through the External Viewer tab that you can navigate to by selecting the Options menu item from the Tools menu. For example, HTML files may be opened by Chrome or Firefox or some other browser, depending on what the local system is configured to use. +\li View in New Window: Opens the selected item in a new content viewer (instead of in the default location in the lower right area of the main window). +\li Extract: Makes a local copy of the selected file or directory for further analysis. + + +\section thumbnail_result_viewer Thumbnail Viewers +Thumbnail viewers display items selected in the \ref tree_viewer_page as a table of thumbnail images in adjustable sizes. This viewer only supports "picture" files (it currently only supports the JPG, GIF, and PNG formats). Click on the Thumbnail tab in the Listing tab to select this view. Note that for a large number of images in a directory selected in the Data Sources area of the \ref tree_viewer_page, or for a selection in the Views area of the \ref tree_viewer_page that contains a large number of images, it might take a while to populate the thumbnail viewer for the first time, i.e., before the thumbnails are cached. + +\image html thumbnail-result-viewer-tab.PNG + +\section result_viewer_paging Paging + +A table viewer can perform slowly when displaying a large numbers of rows. To address this, when there are over a certain numer of rows (10,000 by default), the results will be split into pages. The paging controls at the top right of the table view allow you to browse the different pages. + +\image html result_viewer_paging.PNG + +You can adust the page sizes through \ref view_options_page or turn paging off entirely. */ diff --git a/docs/doxygen-user/tagging.dox b/docs/doxygen-user/tagging.dox index 107dd7700a..ada9f5d0c5 100644 --- a/docs/doxygen-user/tagging.dox +++ b/docs/doxygen-user/tagging.dox @@ -1,6 +1,6 @@ /*! \page tagging_page Tagging -Tagging (or Bookmarking) allows you to create a reference to a file or object and easily find it later. Tagging is also used by the \ref central_repo_page "central repository" to mark items as notable. +Tagging (or Bookmarking) allows you to create a reference to a file or object and easily find it later or include it in a \ref reporting_page "report". Tagging is also used by the \ref central_repo_page "central repository" to mark items as notable. \section tagging_items Tagging items @@ -41,6 +41,48 @@ Tagged results are shown in the "Results" portion of the tree under "Tags". Tagg \image html tagging-4.PNG +\section image_tagging Image tagging + +When you have an image selected in the \ref result_viewer_page, you'll see a "Tags Menu" option in the upper right of the "Application" \ref content_viewer_page "Content Viewer". This allows you to tag only a selected area of the image. Image tagging is currently only enabled on Windows. + +\image html tagging_image_menu.png + +\subsection image_tagging_creation Creating an image tag + +To start, select the "Create" option on the tags menu. You may then left-click and drag on the image to create a rectangle (which will be your 'tag'). When you release the mouse, you'll be able to apply a tag name (and optionally a comment) to your image tag. + +\image html tagging_image_select.png + +You can add a new tag name using the "New Tag" button. + +\image html tagging_image_create_tag.png + +Once you choose the tag name you'll see a red outline in the image around the section you chose. + +\image html tagging_image_one_tag.png + +You can create multiple tags in the same image. + +\image html tagging_image_multiple.png + +If you want to temporarily hide the tag outlines, select "Hide" from the tags menu. You can then select "Show" to see them again. The outlines will also reappear if you switch to a different item in the Result Viewer and come back. + +\subsection image_tagging_editing Selecting, resizing and deleting an image tag + +In order to resize or delete an image tag, you will need to first select it. You can do this by left clicking anywhere inside (or on) the image tag. Selected tags can be resized by dragging any of the 8 handles that appear. The resized dimensions will automatically be saved when the mouse is released. + +\image html tagging_image_edit_tag.png + +Selecting a tag will also enable the "Delete" option in the tags menu. Deleting a tag is an irreversible operation, so please use caution. + +\subsection image_tagging_report Exporting and reporting on image tags + +If you want to save the image with the tag outline, select "Export" from the tags menu. The result will always be a PNG file. Note that using the "Extract File(s)" option from the right-click menu in the Result Viewer will export the original image. + +You'll also be able to see the image tags in an \ref report_html "HTML report". + +\image html tagging_image_report.png + \section managing_tags Managing tags The list of tags can be edited through the Tags tab on the Options menu. diff --git a/docs/doxygen-user/tree_viewer.dox b/docs/doxygen-user/tree_viewer.dox index 9e309476d0..b163018f4f 100644 --- a/docs/doxygen-user/tree_viewer.dox +++ b/docs/doxygen-user/tree_viewer.dox @@ -1,8 +1,47 @@ /*! \page tree_viewer_page Tree Viewer -The Tree Viewer shows the discovered folders by the data sources they come from, as well as a list of files in the folders. It is located on the left side of the Autopsy screen. The "Group by Data Source" option on the top left moves all views, results, and tags under their corresponding data source. +The tree on the left-hand side of the main window is where you can browse the files in the data sources in the case and find saved results from automated analyis (ingest). The tree has five main areas: +- Data Sources: This shows the directory tree hierarchy of the data sources. You can navigate to a specific file or directory here. Each data source added to the case is represented as a distinct sub tree. If you add a data source multiple times, it shows up multiple times. +- Views: Specific types of files from the data sources are shown here, aggregated by type or other properties. Files here can come from more than one data source. +- Results: This is where you can see the results from both the automated analysis (ingest) running in the background and your search results. +- Tags: This is where files and results that have been \ref tagging_page "tagged" are shown. +- Reports: Reports that you have generated, or that ingest modules have created, show up here. -Each folder in the tree on the left shows how many items are contained within it in parentheses after the directory name. See the picture below. +You can also use the "Group by data source" option available through the \ref view_options_page to move the Views, Results, and Tags tree nodes under their corresponding data sources. This can be helpful on very large cases to reduce the size of each sub tree. For example: + +\image html ui_layout_group_tree.PNG + +\section ui_tree_ds Data Sources + +The Data Sources area shows each data source that has been added to the case, in order added (top one is first). +Right clicking on the various nodes in the Data Sources area of the tree will allow you to get more options for each data source and its contents. + +Unallocated space is the chunks of a file system that are currently not being used for anything. Unallocated space can hold deleted files and other interesting artifacts. In an image data source, unallocated space is stored in blocks with distinct locations in the file system. However, because of the way carving tools work, it is better to feed these tools a single, large unallocated space file. Autopsy provides access to both methods of looking at unallocated space. +\li Individual blocks in a volume For each volume, there is a "virtual" folder named "$Unalloc". This folder contains all the individual unallocated blocks in contiguous runs (unallocated space files) as the image is storing them. You can right click and extract any unallocated space file the same way you can extract any other type of file in the Data Sources area. +\li Single files Right click on a volume and select "Extract Unallocated Space as Single File" to concatenate all of the unallocated space files in the volume into a single, continuous file. (If desired, you can right click on an image, and select "Extract Unallocated Space to Single Files" which will do the same thing, but once for each volume in the image). + +An example of the single file extraction option is shown below. +\image html extracting-unallocated-space.PNG + +\section ui_tree_views Views + +Views filter all the files in the case by some property of the file. +- File Types Sorts files by file extension or by MIME type, and shows them in the appropriate group. For example, files with .mp3 and .wav extensions end up in the "Audio" group. +- Deleted Files Displays files that have been deleted, but the names have been recovered. +- File Size Sorts files based on size. + + +\section ui_tree_results Results +- Extracted Content: Many ingest modules will place results here; EXIF metadata, GPS locations, or Web history for example. +- Keyword Hits: Keyword search hits show up here. +- Hashset Hits: Hashset hits show up here. +- E-Mail Messages: Email messages show up here. +- Interesting Items: Things deemed interesting show up here. +- Accounts: Credit card accounts show up here. +- Tags: Any item you tag shows up here so you can find it again easily. + +\section ui_tree_reports Reports + +Reports can be added by \subpage ingest_page or created using the \subpage reporting_page tool. -\image html directory-tree.PNG */ diff --git a/docs/doxygen-user/triage.dox b/docs/doxygen-user/triage.dox index 6f27b02683..3750f92434 100644 --- a/docs/doxygen-user/triage.dox +++ b/docs/doxygen-user/triage.dox @@ -48,7 +48,11 @@ In a triage situation, there is generally not time to make a full image of the s \subsubsection triage_vhd Making a Sparse Image -With any of the above methods for analyzing live systems and devices there is still the problem that your Autopsy case won't be very useful after you disconnect from the drive. To solve this problem you can choose to make a "sparse VHD" as Autopsy is processing the device. This is a file format used by Microsoft Virtual Machines that is readable by Windows and other forensic tools. Instead of copying each sector sequentially, sparse VHDs allow us to copy sectors in any order. This lets us copy each sector as Autopsy reads it, so the sparse VHD will contain all of the files that have been processed so far. We will also have the data associated with volumes and file systems since Autopsy has to process those in the course of analyzing the system. +With any of the above methods for analyzing live systems and devices there is still the problem that your Autopsy case won't be very useful after you disconnect from the drive. It will refer to a device that no longer exists and, more importantly, you may not have a copy of any files of interest that you observed during the triage. + +To solve this problem you can choose to make a "sparse VHD" as Autopsy is processing the device. This will save a copy of every sector that Autopsy reads, which will include file system structures (such as Master File Tables) and files that passed the ingest filters (such as all pictures). + +VHD is a file format used by Microsoft Virtual Machines that is readable by Windows and other forensic tools. The VHD will grow in size as Autopsy reads more data from the target drive. To create a sparse VHD, check the box for "Make a VHD image..." when selecting the disk to analyze. @@ -60,7 +64,7 @@ To create a sparse VHD, check the box for "Make a VHD image..." when selecting t In this scenario, you are trying to answer whether child exploitation images exist in a knock and talk type situation where you will have a limited amount of time with the target system. -Preparaton at the office: +Preparation at the office:
    • Create a \ref live_triage_page "live triage drive" on your USB drive
    • Launch Autopsy from that USB drive and create an \ref ingest_profiles "ingest profile" that: @@ -75,7 +79,7 @@ In this scenario, you are trying to answer whether child exploitation images exi
      • Start the analysis:
          -
        • Plus the live triage drive you made at the office into their laptop +
        • Plug the live triage drive you made at the office into their laptop
        • Launch Autopsy from the .bat file
        • \ref cases_page "Create a case" (saving to your USB drive)
        • Add a \ref ds_local "local drive data source" diff --git a/docs/doxygen-user/uilayout.dox b/docs/doxygen-user/uilayout.dox index c0906dd0cc..0f39386afe 100644 --- a/docs/doxygen-user/uilayout.dox +++ b/docs/doxygen-user/uilayout.dox @@ -5,115 +5,37 @@ \section ui_overview Overview The major areas in the Autopsy User Interface (UI) are: -- \ref ui_tree, shown outlined in green below -- \ref ui_results, shown outlined in blue below -- \ref ui_content, shown outlined in red below -- \ref ui_keyword, shown outlined in yellow below -- \ref ui_status, shown in solid purple below +- \ref ui_tree, shown in green on the left side +- \ref ui_results, shown in blue on the upper right side +- \ref ui_content, shown in red on the lower right side +- \ref ui_keyword, shown in magenta in the upper right corner +- \ref ui_status, shown in purple in the lower right corner You can customize how data is shown in the UI through the \ref view_options_page panel. \image html ui-layout-1.PNG -
          -
          -
          \section ui_tree Tree Viewer -\subpage tree_viewer_page "More..." -
          +The Tree Viewer on the left-hand side is the top level of the UI. Selecting items in the Tree Viewer will cause their contents to be displayed in the Result Viewer to the right. You can browse the files in the image, find saved results generated by the \ref ingest_page "ingest modules", and see the results from \ref tagging_page and \ref reporting_page. See the \subpage tree_viewer_page page for additional information. -The tree on the left-hand side is where you can browse the files in the image and find saved results from automated procedures (ingest). The tree has five main areas: -- Data Sources: This shows the directory tree hierarchy of the file systems in the images. You can navigate to a specific file or directory here. Each data source added is represented as a drive. If you add a data source multiple times, it shows up multiple times. -- Views: Specific types of files from the data sources are shown here, aggregated by type or other properties. Files here can come from more than one data source. Look here for files of a specific type or property. -- Results: Where you can see the results from the background ingest tasks and you can see your previous search results. Go here to see what was found by the ingest modules and to find your previous search results. -- Tags: Where files and results that have been \ref tagging_page "tagged" are shown -- Reports: References to reports that you have generated or that ingest modules have created show up here - -You can also use the "Group by data source" option available through the \ref view_options_page to move the views, results, and tags subtrees under their corresponding data sources. This can be helpful on very large cases to reduce the size of each node. - -\image html ui_layout_group_tree.PNG - -\subsection ui_tree_ds Data Sources - -The Data Sources section shows each data source that has been added to the case, in order added (top one is first). -Right clicking on the various nodes in the Data Sources section of the tree will allow you to get more options for each data source and its contents. - -Unallocated space is chunks of the file system that is currently not being used for anything. Unallocated space can store deleted files and other interesting artifacts. On the actual image, Unallocated space is stored in blocks with distinct locations on the system. However, because of the way various carving tools work, it is more ideal to feed them a single, large unallocated file. Autopsy provides access to both methods of looking at unallocated space. -\li Individual blocks in a volume There is a folder named "Unalloc". This folder contains all the individual unallocated blocks as the image is storing them. You can right click and extract them the same way you can extract any other type of file in the Directory Tree. -\li Single files Right click on a volume and select "Extract Unallocated Space as Single File" to concatenate all the unallocated files in the volume into a single, continuous file. (If desired, you can right click on an image, and select "Extract Unallocated Space to Single Files" which will do the same thing, but once for each volume in the image). - -An example of the single file extraction option is shown below. -\image html extracting-unallocated-space.PNG - -\subsection ui_tree_views Views - -Views filter all the files in the case by some external property of the file, not by any internal analysis of the file. -- File Type Sorts files by file extension or MIME type, and shows them in the appropriate group. For example, .mp3 and .wav both end up in the "Audio" group. -- Recent Files Displays files that are accessed within the last seven days the user had the device. -- Deleted Files Displays files that have been deleted but the names have been recovered. -- File Size Sorts files based upon size. This can give you an idea where to look for files you are interested in. - - -\subsection ui_tree_results Results -- Extracted Content: Many ingest modules will place results here; EXIF data, GPS locations, or Web History for example -- Keyword Hits: Keyword search hits show up here -- Hashset Hits: Hashset hits show up here -- E-Mail Messages: Email messages show up here -- Interesting Items: Things deemed interesting show up here -- Accounts: Credit card accounts show up here -- Tags: Any item you tag shows up here so you can find it again easily - -\subsection ui_tree_reports Reports - -Reports can be added by \subpage ingest_page or created using the \subpage reporting_page tool. - -
          -
          -
          \section ui_results Result Viewer -\subpage result_viewer_page "More..." -
          - -The Result Viewer windows are in the upper right area of the interface and display the results from selecting something in the tree. You will have the option to display the results in a variety of formats. - -\subsection right_click_functions Right Click Functions -Viewers in Result Viewers have certain right-click functions built-in into them that can be accessed when a node a certain type is selected (a file, directory or a result). -Here are some examples that you may see: -\li Open File in External Viewer: Opens the selected file in an "external" application as defined by the local OS or through the External Viewer tab on the Options menu. For example, HTML files may be opened by IE or Firefox, depending on what the local system is configured to use. -\li View in New Window: Opens the content in a new internal Content Viewer (instead of in the default location in the lower right). -\li Extract: Make a local copy of the file or directory for further analysis. -\li Search for files with the same MD5 Hash: Searches the entire file-system for any files with the same MD5 Hash as the one selected. - -\subsection thumbnail_result_viewer Thumbnail Result Viewers -Thumbnail Results Viewer displays the data catalog as a table of thumbnail images in adjustable sizes. This viewer only supports picture files (Currently, only supports JPG, GIF, and PNG formats). Click the Thumbnail tab to select this view. Note that for a large number of images in a directory selected in the Data Explorer, or for a View selected that contains a large number of images, it might take a while to populate this view for the first time before the images are cached. - -Example\n -Below is an example of "Thumbnail Results Viewer" window: -\image html thumbnail-result-viewer-tab.PNG - -\subsection table_result_viewer Table Result Viewers -Table Results Viewer (Directory Listing) displays the data catalog as a table with some details (properties) of each file. The properties that it shows are: name, time (modified, changed, accessed, and created), size, flags (directory and meta), mode, user ID, group ID, metadata address, attribute address, and type (directory and meta). Click the Table Viewer tab to select this view. - -The Results Viewer can be also activated for saved results and it can show a high level results grouped, or a results at a file level, depending on which node on the Directory Tree is selected to populate the Table Results Viewer. - -Example\n -Below is an example of a "Table Results Viewer" window: -\image html table-result-viewer-tab.PNG +The Result Viewer windows are in the upper right area of the interface and display the results from selecting something in the Tree Viewer. The columns displayed will depend on what was selected in the Tree Viewer - files will show things like path, size, and creation date while a contact would show its name, phone number, and email. Selecting an item in the Result Viewer will show details about the item in the Content Viewer below. See the \ref result_viewer_page for additional information. \section ui_content Content Viewer -The \ref content_viewer_page area is in the lower right area of the interface. This area is used to view a specific file in a variety of formats. There are different tabs for different viewers. Not all tabs support all file types, so only some of them will be enabled. To display data in this area, a file must be selected from the Result Viewer window. +The Content Viewer area is in the lower right area of the interface. This area is used to view a specific file in a variety of formats. There are different tabs for different viewers. Not all tabs support all file types, so only some of them will be enabled. To display data in this area, a file must be selected from the Result Viewer window. -The Content Viewer area is part of a plug-in framework. You can install modules that will add more viewer types. For additional information on the built-in content viewers, see the \ref content_viewer_page page. +The Content Viewer area is part of a plug-in framework, meaning you can install modules that will add more viewer types. +For additional information see the \ref content_viewer_page page. \section ui_keyword Keyword Search -Keyword Search allows the user to search for keywords in the data source. It is covered in more detail here: \subpage keyword_search_page +The Keyword Search feature allows the user to search for keywords in one or more data sources contained in the current case. It is covered in more detail here: \ref keyword_search_page \section ui_status Status Area -The Status area will show progress bars while ingest is occuring. This visually indicates to the user what portion of the processing is already complete. The user can click on the progress bars to see further detail or to cancel ingest jobs. +The Status area will show progress bars while ingest is occurring. This visually indicates what portion of the processing is already complete. You can click on the progress bars to see further detail or to cancel ingest jobs.
          */ diff --git a/docs/doxygen-user/view_options.dox b/docs/doxygen-user/view_options.dox index 67cb5853d4..14e14d76a4 100644 --- a/docs/doxygen-user/view_options.dox +++ b/docs/doxygen-user/view_options.dox @@ -35,9 +35,13 @@ Similarly, the option to hide slack in the views area will prevent slack files f This option allows you to hide tags from other users in the Tagging section of the tree. See \ref user_tags for more details. -\subsection view_options_cr_columns Do not use the Central Repository to populate columns +\subsection view_options_cr_columns Do not add columns for S(core), C(omments) and (O)ccurrences -By default, the first three columns in the result viewer after the file name in the results viewer are named "S", "C" and "O". These are described in more detail on the \ref result_viewer_page page. The Comment and Other occurrences columns query the Central Repository. If this seems to be having a performance impact, it can be disabled using the checkbox. This will remove the Other occurrences column entirely and the Comment column will be based only on tags. +By default, the first three columns in the result viewer after the file name in the results viewer are named "S", "C" and "O". Populating these columns can increase loading times. See the \ref result_viewer_sco section for additional information. + +\subsection view_options_paging Paging + +By default, only 10,000 results will be show in the Result Viewer. You can change that threshold here. Setting it to zero will disable paging. \subsection view_options_content_viewer Content viewer selection @@ -53,7 +57,7 @@ Timestamps can be viewed in either local time or in a time zone selected in the \subsection view_options_translate Translate text -If you have a custom translation module installed, this option will add a column to the \ref result_viewer_page to show the translated name of files and folders. +If you have a \ref machine_translation_page module installed, this option will add a column to the \ref result_viewer_page to show the translated name of files and folders. \section view_options_case Current Case Settings diff --git a/docs/doxygen/Doxyfile b/docs/doxygen/Doxyfile index a7463ac441..bf66159b48 100644 --- a/docs/doxygen/Doxyfile +++ b/docs/doxygen/Doxyfile @@ -38,7 +38,7 @@ PROJECT_NAME = "Autopsy" # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 4.11.0 +PROJECT_NUMBER = 4.12.0 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears a the top of each page and should give viewer a @@ -1063,7 +1063,7 @@ GENERATE_HTML = YES # The default directory is: html. # This tag requires that the tag GENERATE_HTML is set to YES. -HTML_OUTPUT = api-docs/4.11.0/ +HTML_OUTPUT = api-docs/4.12.0/ # The HTML_FILE_EXTENSION tag can be used to specify the file extension for each # generated HTML page (for example: .htm, .php, .asp). diff --git a/nbproject/project.properties b/nbproject/project.properties index dbc771a543..5d59189544 100644 --- a/nbproject/project.properties +++ b/nbproject/project.properties @@ -4,7 +4,7 @@ app.title=Autopsy ### lowercase version of above app.name=${branding.token} ### if left unset, version will default to today's date -app.version=4.11.0 +app.version=4.12.0 ### build.type must be one of: DEVELOPMENT, RELEASE #build.type=RELEASE build.type=DEVELOPMENT diff --git a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/EmailMessage.java b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/EmailMessage.java index 7b0b2eb30e..40f2fc0933 100644 --- a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/EmailMessage.java +++ b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/EmailMessage.java @@ -47,7 +47,10 @@ class EmailMessage { private long id = -1L; private String messageID = ""; private String inReplyToID = ""; - private List references = null; + private List references = new ArrayList<>(); + private String simplifiedSubject = ""; + private boolean replySubject = false; + private String messageThreadID = ""; boolean hasAttachment() { return hasAttachment; @@ -80,8 +83,34 @@ class EmailMessage { void setSubject(String subject) { if (subject != null) { this.subject = subject; + if(subject.matches("^[R|r][E|e].*?:.*")) { + this.simplifiedSubject = subject.replaceAll("[R|r][E|e].*?:", "").trim(); + replySubject = true; + } else { + this.simplifiedSubject = subject; + } + } else { + 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() { + return replySubject; + } String getHeaders() { return headers; @@ -222,22 +251,40 @@ class EmailMessage { /** * Returns a list of Message-IDs listing the parent, grandparent, - * great-grandparent, and so on, of this message. + * great-grandparent, and so on, of this message. * - * @return reference list or empty string if non is available. + * @return The reference list or empty string if none is available. */ List getReferences() { return references; } /** - * Set the list of reference message-IDs. + * Set the list of reference message-IDs from the email message header. * * @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() { + return this.messageThreadID; + } /** * A Record to hold generic information about attachments. diff --git a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/EmailMessageThreader.java b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/EmailMessageThreader.java new file mode 100755 index 0000000000..431c7cc74c --- /dev/null +++ b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/EmailMessageThreader.java @@ -0,0 +1,699 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.thunderbirdparser; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +/** + * Given a list of email messages arranges the message into threads using the + * message reference lists. + * + * This threader is based heavely off of the algorithum found at + * + * "message threading." by Jamie Zawinski + * + */ +final class EmailMessageThreader { + + private int bogus_id_count = 0; + + private EmailMessageThreader(){} + + public static void threadMessages(List emailMessages) { + EmailMessageThreader instance = new EmailMessageThreader(); + + Map id_table = instance.createIDTable(emailMessages); + Set rootSet = instance.getRootSet(id_table); + + instance.pruneEmptyContainers(rootSet); + + Set finalRootSet = instance.groupBySubject(rootSet); + + instance.assignThreadIDs(finalRootSet); + } + + /** + * Walks the list of emailMessages creating a Container object for each + * unique message ID found. Adds the emailMessage to the container where + * possible. + * + * @param emailMessages + * + * @return - HashMap of all message where the key is the message-ID of the message + */ + private Map createIDTable(List emailMessages) { + HashMap id_table = new HashMap<>(); + + for (EmailMessage message : emailMessages) { + String messageID = message.getMessageID(); + + // Check the id_table for an existing Container for message-id + EmailContainer container = id_table.get(messageID); + + // An existing container for message-id was found + if (container != null) { + // If the existing Container has a message already assocated with it + // emailMessage maybe a duplicate, so we don't lose the existance of + // the duplicate message assign it a bogus message-id + if (container.hasMessage()) { + messageID = String.format("", bogus_id_count++); + container = null; + } else { + container.setMessage(message); + } + } + + if (container == null) { + container = new EmailContainer(message); + id_table.put(messageID, container); + } + + processMessageReferences(message, container, id_table); + } + + return id_table; + } + + /** + * Loops throught message's list of references, creating objects as needed + * and setting up the parent child relationships amoung the messages. + * + * @param message The current email messags + * @param container Container object for message + * @param id_table Hashtable of known message-id\container pairs + */ + void processMessageReferences(EmailMessage message, EmailContainer container, Map id_table) { + List referenceList = message.getReferences(); + + // Make sure the inReplyToID is in the list of references + String inReplyToID = message.getInReplyToID(); + if (inReplyToID != null && !inReplyToID.isEmpty()) { + if (referenceList == null) { + referenceList = new ArrayList<>(); + } + + referenceList.add(inReplyToID); + } + + // No references, nothing to do + if (referenceList == null) { + return; + } + + EmailContainer parent_ref = null; + EmailContainer ref; + + for (String refID : referenceList) { + // Check id_table to see if there is already a container for this + // reference id, if not create a new Container and add to table + ref = id_table.get(refID); + + if (ref == null) { + ref = new EmailContainer(); + id_table.put(refID, ref); + } + + // Set the parent\child relationship between parent_ref and ref + if (parent_ref != null + && !ref.hasParent() + && !parent_ref.equals(ref) + && !parent_ref.isChild(ref)) { + ref.setParent(parent_ref); + parent_ref.addChild(ref); + } + + parent_ref = ref; + } + + // If the parent_ref and container are already linked, don't change + // anything + if (parent_ref != null + && (parent_ref.equals(container) + || container.isChild(parent_ref))) { + parent_ref = null; + } + + // If container already has a parent, the parent was assumed based on + // the list of references from another message. parent_ref will be + // the real parent of container so throw away the old parent and set a + // new one. + if (container.hasParent()) { + container.getParent().removeChild(container); + container.setParent(null); + } + + if (parent_ref != null) { + container.setParent(container); + parent_ref.addChild(container); + } + } + + /** + * Creates a set of root container messages from the message-ID hashtable. A + * root Container is container that does not have a parent container. + * + * @param id_table Table of all known Containers + * + * @return A set of the root containers. + */ + Set getRootSet(Map id_table) { + HashSet rootSet = new HashSet<>(); + + id_table.values().stream().filter((container) + -> (!container.hasParent())).forEachOrdered((container) -> { + rootSet.add(container); + }); + + return rootSet; + } + + /** + * Remove Containers from containerSet if they do not have a message or + * children. + * + * @param containerSet A set of Container objects + */ + void pruneEmptyContainers(Set containerSet) { + Set containersToRemove = new HashSet<>(); + containerSet.forEach((container) -> { + if (!container.hasMessage() && !container.hasChildren()) { + containersToRemove.add(container); + } else { + pruneChildren(container); + } + }); + + containerSet.removeAll(containersToRemove); + } + + /** + * Recursively work through the list of parent's children removing empty + * containers. If the passed in container does not have a message + * associated with it, it will get removed and its children will be assigned + * to their grandparent. + * + * @param parent returns true if their where children pruned, otherwise false. + */ + boolean pruneChildren(EmailContainer parent) { + if (parent == null) { + return false; + } + + Set children = parent.getChildren(); + + if (children == null) { + return false; + } + + EmailContainer grandParent = parent.getParent(); + Set remove = new HashSet<>(); + Set add = new HashSet<>(); + for (EmailContainer child : parent.getChildren()) { + if (pruneChildren(child)) { + remove.add(child); + add.addAll(child.getChildren()); + child.setParent(null); + child.clearChildren(); + + } + } + + parent.addChildren(add); + parent.removeChildren(remove); + + if (!parent.hasMessage() && grandParent != null) { + children.forEach((child) -> { + child.setParent(grandParent); + }); + return true; + } + + return false; + } + + /** + * Now that the emails are grouped together by references\message ID take + * another pass through and group together messages with the same simplified + * subject. + * + * The purpose of grouping by subject is to bring threads together that may + * been separated due to missing messages. Group by subject to put attempt + * to put these threads together. + * + * This may cause "root" messages with identical subjects to get grouped + * together as children of an empty container. The code that uses the thread + * information can decide what to do in that situation as those message + * maybe part of a common thread or maybe their own unique messages. + * + * @param rootSet + * + * @return Final set of threaded messages. + */ + Set groupBySubject(Set rootSet) { + Map subject_table = createSubjectTable(rootSet); + + Set finalSet = new HashSet<>(); + + for (EmailContainer rootSetContainer : rootSet) { + String rootSubject = rootSetContainer.getSimplifiedSubject(); + + EmailContainer tableContainer = subject_table.get(rootSubject); + if (tableContainer == null || tableContainer.equals(rootSetContainer)) { + finalSet.add(rootSetContainer); + continue; + } + + // If both containers are dummy/empty append the children of one to the other + if (tableContainer.getMessage() == null && rootSetContainer.getMessage() == null) { + tableContainer.addChildren(rootSetContainer.getChildren()); + rootSetContainer.clearChildren(); + continue; + } + + // one container is empty, but the other is not, make the non-empty one be a + // child of the empty + if ((tableContainer.getMessage() == null && rootSetContainer.getMessage() != null) + || (tableContainer.getMessage() != null && rootSetContainer.getMessage() == null)) { + + if (tableContainer.getMessage() == null) { + tableContainer.addChild(rootSetContainer); + + } else { + rootSetContainer.addChild(tableContainer); + subject_table.remove(rootSubject, tableContainer); + subject_table.put(rootSubject, rootSetContainer); + + finalSet.add(rootSetContainer); + } + + continue; + } + + // tableContainer is non-empty and it's message's subject does not begin + // with 'RE:' but rootSetContainer's message does begin with 'RE:', then + // make rootSetContainer a child of tableContainer + if (tableContainer.getMessage() != null + && !tableContainer.isReplySubject() + && rootSetContainer.isReplySubject()) { + tableContainer.addChild(rootSetContainer); + continue; + } + + // If table container is non-empy, and table container's subject does + // begin with 'RE:', but rootSetContainer does not start with 'RE:' + // make tableContainer a child of rootSetContainer + if (tableContainer.getMessage() != null + && tableContainer.isReplySubject() + && !rootSetContainer.isReplySubject()) { + rootSetContainer.addChild(tableContainer); + subject_table.put(rootSubject, rootSetContainer); + finalSet.add(rootSetContainer); + continue; + } + + // rootSetContainer and tableContainer either both have 'RE' or + // don't. Create a new dummy container with both containers as + // children. + EmailContainer newParent = new EmailContainer(); + newParent.addChild(tableContainer); + newParent.addChild(rootSetContainer); + subject_table.remove(rootSubject, tableContainer); + subject_table.put(rootSubject, newParent); + + finalSet.add(newParent); + } + return finalSet; + } + + /** + * Creates a Hashtable of Container unique subjects. There will be one + * Container subject pair for each unique subject. + * + * @param rootSet The set of "root" Containers + * + * @return The subject hashtable + */ + Map createSubjectTable(Set rootSet) { + HashMap subject_table = new HashMap<>(); + + for (EmailContainer rootSetContainer : rootSet) { + String subject = ""; + boolean reSubject = false; + + if (rootSetContainer.hasMessage()) { + subject = rootSetContainer.getMessage().getSimplifiedSubject(); + reSubject = rootSetContainer.getMessage().isReplySubject(); + } else if (rootSetContainer.hasChildren()) { + Iterator childrenIterator = rootSetContainer.getChildren().iterator(); + while (childrenIterator.hasNext()) { + EmailMessage childMessage = childrenIterator.next().getMessage(); + if (childMessage != null) { + subject = childMessage.getSimplifiedSubject(); + if (!subject.isEmpty()) { + reSubject = childMessage.isReplySubject(); + break; + } + } + } + } + + if (subject == null || subject.isEmpty()) { + continue; // Give up on this container + } + + EmailContainer tableContainer = subject_table.get(subject); + +// A container will be added to the table, if a container for its "simplified" subject +// does not currently exist in the table. Or if there is more than one container with the same +// subject, but one is an "empty container" the empty one will be added +// the table or in the one in the table has "RE" in the subject it will be replaced +// by the one that does not have "RE" in the subject (if it exists) +// + if (tableContainer == null || + (tableContainer.getMessage() != null && rootSetContainer.getMessage() == null) || + (!reSubject && (tableContainer.getMessage() != null && tableContainer.getMessage().isReplySubject()))) { + subject_table.put(subject, rootSetContainer); + } + + } + + return subject_table; + } + + /** + * Assign "threadIDs" for each thread. It is assumed that each member of + * containerSet is a unique message thread. + * + * ThreadIDs will only be unique between runs if "IDPrefix" is unique between + * runs of the algorithm. + * + * @param containerSet A set of "root" containers + * + * @param IDPrefix A string to make the threadIDs unique. + */ + private void assignThreadIDs(Set containerSet) { + for(EmailContainer container: containerSet) { + // Generate a threadID + String threadID = UUID.randomUUID().toString(); + // Add the IDs to this thread + addThreadID(container, threadID); + } + } + + /** + * Recursively walk container's children adding the thread ID to + * the EmailMessage objects. + * + * @param container The root container of a set of related container objects + * @param threadID The String to assign as the "threadId" for this set of + * messages + */ + private void addThreadID(EmailContainer container, String threadID) { + if(container == null) { + return; + } + + EmailMessage message = container.getMessage(); + if(message != null) { + message.setMessageThreadID(threadID); + } + + if(container.hasChildren()) { + for(EmailContainer child: container.getChildren()) { + addThreadID(child, threadID); + } + } + } + + /** + * The container object is used to wrap and email message and track the + * messages parent and child messages. + */ + final class EmailContainer { + + private EmailMessage message; + private EmailContainer parent; + private Set children; + + /** + * Constructs an empty container. + */ + EmailContainer() { + // This constructor is intentially empty to allow for the creation of + // an EmailContainer without a message + } + + /** + * Constructs a new Container object with the given EmailMessage. + * + * @param message Returns the message, or null if one was not set + */ + EmailContainer(EmailMessage message) { + this.message = message; + } + + /** + * Returns the EmailMessage object. + * + * @return Then EmailMessage object or null if one was not set + */ + EmailMessage getMessage() { + return message; + } + + /** + * Set the Container EmailMessage object. + * + * @param message - The container EmailMessage + */ + void setMessage(EmailMessage message) { + this.message = message; + } + + /** + * Return whether or not this Container has a valid EmailMessage object. + * + * @return True if EmailMessage has been set otherwise false + */ + boolean hasMessage() { + return message != null; + } + + /** + * Returns the Simplified Subject (original subject without RE:) of the + * EmailMessage or if this is an empty Container with Children, return + * the simplified subject of one of the children. + * + * @return Simplified subject of this Container + */ + String getSimplifiedSubject() { + String subject = ""; + if (message != null) { + subject = message.getSimplifiedSubject(); + } else if (children != null) { + for (EmailContainer child : children) { + if (child.hasMessage()) { + subject = child.getSimplifiedSubject(); + } + + if (subject != null && !subject.isEmpty()) { + break; + } + } + } + return subject; + } + + /** + * Simialar to getSimplifiedSubject, isReplySubject is a helper function + * that will return the isReplySubject of the Containers message or if + * this is an empty container, the state of one of the children. + * + * @return + */ + boolean isReplySubject() { + if (message != null) { + return message.isReplySubject(); + } else if (children != null) { + for (EmailContainer child : children) { + if (child.hasMessage()) { + boolean isReply = child.isReplySubject(); + + if (isReply) { + return isReply; + } + } + } + } + + return false; + } + + /** + * Returns the parent Container of this Container. + * + * @return The Container parent or null if one is not set + */ + EmailContainer getParent() { + return parent; + } + + /** + * Sets the given container as the parent of this object. + * + * @param container - the object to set as the parent + */ + void setParent(EmailContainer container) { + parent = container; + } + + /** + * Returns true if a parent object is current set. + * + * @return True if this container has a parent otherwise false + */ + boolean hasParent() { + return parent != null; + } + + /** + * Adds the specified Container to the list of children. + * + * @param child - the Container to add to the child list + * + * @return true, if the element was added to the children list + */ + boolean addChild(EmailContainer child) { + if (children == null) { + children = new HashSet<>(); + } + + return children.add(child); + } + + /** + * Adds to the list of children all of the elements that are contained + * in the specified collection. + * + * @param children - set containing the Containers to be added to the + * list of children + * + * @return true if the list of children was changed as a result of this + * call + */ + boolean addChildren(Set children) { + if (children == null || children.isEmpty()) { + return false; + } + + if (this.children == null) { + this.children = new HashSet<>(); + } + + return this.children.addAll(children); + } + + /** + * Removes from the children list all of the elements that are contained + * in the specified collection. + * + * @param children - set containing the elements to be removed from the + * list of children + * + * @return true if the set was changed as a result of this call + */ + boolean removeChildren(Set children) { + if (children != null) { + return this.children.removeAll(children); + } + + return false; + } + + /** + * Clears the Containers list of children. + * + */ + void clearChildren() { + if( children != null ) { + children.clear(); + } + } + + /** + * Removes the given container from the list of children. + * + * @param child - the container to remove from the children list + * + * @return - True if the given container successfully removed from the + * list of children + */ + boolean removeChild(EmailContainer child) { + if(children != null) { + return children.remove(child); + } else { + return false; + } + } + + /** + * Returns whether or not this container has children. + * + * @return True if the child list is not null or empty + */ + boolean hasChildren() { + return children != null && !children.isEmpty(); + } + + /** + * Returns the list of children of this container. + * + * @return The child list or null if a child has not been added. + */ + Set getChildren() { + return children; + } + + /** + * Search all of this containers children to make sure that the given + * container is not a related. + * + * @param container - the container object to search for + * + * @return True if the given container is in the child tree of this + * container, false otherwise. + */ + boolean isChild(EmailContainer container) { + if (children == null || children.isEmpty()) { + return false; + } else if (children.contains(container)) { + return true; + } else { + return children.stream().anyMatch((child) -> (child.isChild(container))); + } + } + } +} diff --git a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java index d5cb95bcc2..40580dc346 100644 --- a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java +++ b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java @@ -409,8 +409,14 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { */ private void processEmails(List emails, AbstractFile abstractFile) { List derivedFiles = new ArrayList<>(); - - + + // Putting try/catch around this to catch any exception and still allow + // the creation of the artifacts to continue. + try{ + EmailMessageThreader.threadMessages(emails); + } catch(Exception ex) { + logger.log(Level.WARNING, String.format("Exception thrown parsing emails from %s", abstractFile.getName()), ex); + } for (EmailMessage email : emails) { BlackboardArtifact msgArtifact = addEmailArtifact(email, abstractFile); @@ -510,6 +516,7 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { String subject = email.getSubject(); long id = email.getId(); String localPath = email.getLocalPath(); + String threadID = email.getMessageThreadID(); List senderAddressList = new ArrayList<>(); String senderAddress; @@ -567,6 +574,7 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { addArtifactAttribute(cc, ATTRIBUTE_TYPE.TSK_EMAIL_CC, bbattributes); addArtifactAttribute(bodyHTML, ATTRIBUTE_TYPE.TSK_EMAIL_CONTENT_HTML, bbattributes); addArtifactAttribute(rtf, ATTRIBUTE_TYPE.TSK_EMAIL_CONTENT_RTF, bbattributes); + addArtifactAttribute(threadID, ATTRIBUTE_TYPE.TSK_THREAD_ID, bbattributes); try { diff --git a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/VcardParser.java b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/VcardParser.java index 15d52b536e..7e5a321313 100755 --- a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/VcardParser.java +++ b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/VcardParser.java @@ -143,8 +143,9 @@ final class VcardParser { * @throws NoCurrentCaseException If there is no open case. */ void parse(File vcardFile, AbstractFile abstractFile) throws IOException, NoCurrentCaseException { - VCard vcard = Ezvcard.parse(vcardFile).first(); - addContactArtifact(vcard, abstractFile); + for (VCard vcard: Ezvcard.parse(vcardFile).all()) { + addContactArtifact(vcard, abstractFile); + } } diff --git a/unix_setup.sh b/unix_setup.sh index 0b252b2598..eecf1ed8c5 100644 --- a/unix_setup.sh +++ b/unix_setup.sh @@ -5,7 +5,7 @@ # NOTE: update_sleuthkit_version.pl updates this value and relies # on it keeping the same name and whitespace. Don't change it. -TSK_VERSION=4.6.6 +TSK_VERSION=4.6.7 # In the beginning...