diff --git a/Core/build.xml b/Core/build.xml index 4b6c32d56d..b7b0e202c7 100644 --- a/Core/build.xml +++ b/Core/build.xml @@ -40,6 +40,24 @@ + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED index 0444c30101..64dfb6481a 100755 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED @@ -11,7 +11,7 @@ Case.deleteCaseFailureMessageBox.message=Error deleting case: {0} Case.deleteCaseFailureMessageBox.title=Failed to Delete Case Case.exceptionMessage.cancelledByUser=Cancelled by user. Case.exceptionMessage.cannotDeleteCurrentCase=Cannot delete current case, it must be closed first. -Case.exceptionMessage.cannotGetLockToDeleteCase=Cannot delete case because it is open for another user or there is a problem with the coordination service. +Case.exceptionMessage.cannotGetLockToDeleteCase=Cannot delete case because it is open for another user or host. Case.exceptionMessage.cannotLocateMainWindow=Cannot locate main application window Case.exceptionMessage.cannotOpenMultiUserCaseNoSettings=Multi-user settings are missing (see Tools, Options, Multi-user tab), cannot open a multi-user case. # {0} - exception message @@ -32,12 +32,19 @@ Case.exceptionMessage.couldNotSaveCaseMetadata=Failed to save case metadata:\n{0 Case.exceptionMessage.couldNotSaveDbNameToMetadataFile=Failed to save case database name to case metadata file:\n{0}. # {0} - exception message Case.exceptionMessage.couldNotUpdateCaseNodeData=Failed to update coordination service node data:\n{0}. +# {0} - case display name +Case.exceptionMessage.deletionInterrupted=Deletion of the case {0} was cancelled. Case.exceptionMessage.emptyCaseDir=Must specify a case directory path. Case.exceptionMessage.emptyCaseName=Must specify a case name. -Case.exceptionMessage.errorsDeletingCase=Errors occured while deleting the case. See the application log for details +Case.exceptionMessage.errorsDeletingCase=Errors occured while deleting the case. See the application log for details. # {0} - exception message Case.exceptionMessage.execExceptionWrapperMessage={0} -Case.exceptionMessage.failedToDeleteCoordinationServiceNodes=Failed to delete the coordination service nodes for the case. +# {0} - exception message +Case.exceptionMessage.failedToConnectToCoordSvc=Failed to connect to coordination service:\n{0}. +# {0} - exception message +Case.exceptionMessage.failedToFetchCoordSvcNodeData=Failed to fetch coordination service node data:\n{0}. +# {0} - exception message +Case.exceptionMessage.failedToLockCaseForDeletion=Failed to exclusively lock case for deletion:\n{0}. # {0} - exception message Case.exceptionMessage.failedToReadMetadata=Failed to read case metadata:\n{0}. Case.exceptionMessage.metadataUpdateError=Failed to update case metadata @@ -50,23 +57,26 @@ Case.progressIndicatorTitle.creatingCase=Creating Case Case.progressIndicatorTitle.deletingCase=Deleting Case Case.progressIndicatorTitle.openingCase=Opening Case Case.progressMessage.cancelling=Cancelling... -Case.progressMessage.checkingForOtherUser=Checking to see if another user has the case open... Case.progressMessage.clearingTempDirectory=Clearing case temp directory... Case.progressMessage.closingApplicationServiceResources=Closing case-specific application service resources... Case.progressMessage.closingCaseDatabase=Closing case database... Case.progressMessage.closingCaseLevelServices=Closing case-level services... +Case.progressMessage.connectingToCoordSvc=Connecting to coordination service... Case.progressMessage.creatingCaseDatabase=Creating case database... Case.progressMessage.creatingCaseDirectory=Creating case directory... Case.progressMessage.creatingCaseNodeData=Creating coordination service node data... Case.progressMessage.deletingCaseDatabase=Deleting case database... +Case.progressMessage.deletingCaseDirCoordSvcNode=Deleting case directory coordination service node... Case.progressMessage.deletingCaseDirectory=Deleting case directory... -Case.progressMessage.deletingCoordinationServiceNodes=Deleting coordination service nodes... +Case.progressMessage.deletingResourcesCoordSvcNode=Deleting case resources coordination service node... Case.progressMessage.deletingTextIndex=Deleting text index... +Case.progressMessage.fetchingCoordSvcNodeData=Fetching coordination service node data for the case... Case.progressMessage.openingApplicationServiceResources=Opening application service case resources... Case.progressMessage.openingCaseDatabase=Opening case database... Case.progressMessage.openingCaseLevelServices=Opening case-level services... Case.progressMessage.preparing=Preparing... Case.progressMessage.preparingToOpenCaseResources=Preparing to open case resources.
This may take time if another user is upgrading the case. +Case.progressMessage.removingCaseFromRecentCases=Removing case from Recent Cases menu... Case.progressMessage.savingCaseMetadata=Saving case metadata to file... Case.progressMessage.settingUpNetworkCommunications=Setting up network communications... Case.progressMessage.shuttingDownNetworkCommunications=Shutting down network communications... diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java index 521566d294..c9640f1a13 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java @@ -18,6 +18,7 @@ */ package org.sleuthkit.autopsy.casemodule; +import com.google.common.annotations.Beta; import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; import java.awt.Frame; import java.awt.event.ActionEvent; @@ -31,6 +32,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.sql.Connection; import java.sql.DriverManager; +import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.text.ParseException; @@ -79,6 +81,8 @@ import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; import org.sleuthkit.autopsy.casemodule.events.DataSourceAddedEvent; import org.sleuthkit.autopsy.casemodule.events.DataSourceNameChangedEvent; import org.sleuthkit.autopsy.casemodule.events.ReportAddedEvent; +import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData.CaseNodeDataException; +import org.sleuthkit.autopsy.casemodule.multiusercases.CoordinationServiceUtils; import org.sleuthkit.autopsy.casemodule.services.Services; import org.sleuthkit.autopsy.commonpropertiessearch.CommonAttributeSearchAction; import org.sleuthkit.autopsy.communications.OpenCommVisualizationToolAction; @@ -119,7 +123,6 @@ import org.sleuthkit.datamodel.Report; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskUnsupportedSchemaVersionException; -import org.sleuthkit.autopsy.coreutils.StopWatch; /** * An Autopsy case. Currently, only one case at a time may be open. @@ -139,6 +142,7 @@ public class Case { private static final String MODULE_FOLDER = "ModuleOutput"; //NON-NLS private static final String CASE_ACTION_THREAD_NAME = "%s-case-action"; private static final String CASE_RESOURCES_THREAD_NAME = "%s-manage-case-resources"; + private static final String NO_NODE_ERROR_MSG_FRAGMENT = "KeeperErrorCode = NoNode"; private static final Logger logger = Logger.getLogger(Case.class.getName()); private static final AutopsyEventPublisher eventPublisher = new AutopsyEventPublisher(); private static final Object caseActionSerializationLock = new Object(); @@ -699,38 +703,28 @@ public class Case { } /** - * Deletes a case. This method cannot be used to delete the current case; - * deleting the current case must be done by calling Case.deleteCurrentCase. + * Deletes a case. The case to be deleted must not be the "current case." + * Deleting the current case must be done by calling Case.deleteCurrentCase. * - * @param metadata The metadata for the case to delete. + * @param metadata The case metadata. * - * @throws CaseActionException if there is a problem deleting the case. The - * exception will have a user-friendly message - * and may be a wrapper for a lower-level - * exception. + * @throws CaseActionException If there were one or more errors deleting the + * case. The exception will have a user-friendly + * message and may be a wrapper for a + * lower-level exception. */ @Messages({ "Case.progressIndicatorTitle.deletingCase=Deleting Case", "Case.exceptionMessage.cannotDeleteCurrentCase=Cannot delete current case, it must be closed first.", - "Case.progressMessage.checkingForOtherUser=Checking to see if another user has the case open...", - "Case.exceptionMessage.cannotGetLockToDeleteCase=Cannot delete case because it is open for another user or there is a problem with the coordination service.", - "Case.exceptionMessage.failedToDeleteCoordinationServiceNodes=Failed to delete the coordination service nodes for the case." + "# {0} - case display name", "Case.exceptionMessage.deletionInterrupted=Deletion of the case {0} was cancelled." }) public static void deleteCase(CaseMetadata metadata) throws CaseActionException { - StopWatch stopWatch = new StopWatch(); - stopWatch.start(); synchronized (caseActionSerializationLock) { if (null != currentCase) { throw new CaseActionException(Bundle.Case_exceptionMessage_cannotDeleteCurrentCase()); } } - stopWatch.stop(); - logger.log(Level.INFO, String.format("Used %d s to acquire caseActionSerializationLock (Java monitor in Case class) for %s (%s) in %s", stopWatch.getElapsedTimeSecs(), metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory())); - /* - * Set up either a GUI progress indicator without a cancel button (can't - * cancel deleting a case) or a logging progress indicator. - */ ProgressIndicator progressIndicator; if (RuntimeProperties.runningWithGUI()) { progressIndicator = new ModalDialogProgressIndicator(mainFrame, Bundle.Case_progressIndicatorTitle_deletingCase()); @@ -740,32 +734,17 @@ public class Case { progressIndicator.start(Bundle.Case_progressMessage_preparing()); try { if (CaseType.SINGLE_USER_CASE == metadata.getCaseType()) { - deleteCase(metadata, progressIndicator); + deleteSingleUserCase(metadata, progressIndicator); } else { - /* - * First, acquire an exclusive case directory lock. The case - * cannot be deleted if another node has it open. - */ - progressIndicator.progress(Bundle.Case_progressMessage_checkingForOtherUser()); - stopWatch.reset(); - stopWatch.start(); - try (CoordinationService.Lock dirLock = CoordinationService.getInstance().tryGetExclusiveLock(CategoryNode.CASES, metadata.getCaseDirectory())) { - stopWatch.stop(); - logger.log(Level.INFO, String.format("Used %d s to acquire case directory coordination service lock for %s (%s) in %s", stopWatch.getElapsedTimeSecs(), metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory())); - if (dirLock != null) { - deleteCase(metadata, progressIndicator); - } else { - throw new CaseActionException(Bundle.Case_creationException_couldNotAcquireDirLock()); - } - } catch (CoordinationServiceException ex) { - stopWatch.stop(); - logger.log(Level.INFO, String.format("Used %d s to fail to acquire case directory coordination service lock for %s (%s) in %s", stopWatch.getElapsedTimeSecs(), metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory())); - throw new CaseActionException(Bundle.Case_exceptionMessage_failedToDeleteCoordinationServiceNodes(), ex); - } try { - deleteCoordinationServiceNodes(metadata, progressIndicator); - } catch (CoordinationServiceException ex) { - throw new CaseActionException(Bundle.Case_creationException_couldNotAcquireDirLock(), ex); + deleteMultiUserCase(metadata, progressIndicator); + } catch (InterruptedException ex) { + /* + * Note that task cancellation is not currently supported + * for this code path, so this catch block is not expected + * to be executed. + */ + throw new CaseActionException(Bundle.Case_exceptionMessage_deletionInterrupted(metadata.getCaseDisplayName()), ex); } } } finally { @@ -773,43 +752,6 @@ public class Case { } } - /** - * Deletes the coordination nodes for a multi-user case. - * - * @param metadata The metadata for the case to delete. - * @param progressIndicator The progress indicator for the deletion - * operation. - * - * @throws CoordinationServiceException If there is a problem getting the - * coordination service. - */ - @Messages({ - "Case.progressMessage.deletingCoordinationServiceNodes=Deleting coordination service nodes..." - }) - static void deleteCoordinationServiceNodes(CaseMetadata metadata, ProgressIndicator progressIndicator) throws CoordinationServiceException { - progressIndicator.progress(Bundle.Case_progressMessage_deletingCoordinationServiceNodes()); - CoordinationService coordinationService; - coordinationService = CoordinationService.getInstance(); - String resourcesLockNodePath = metadata.getCaseDirectory() + "_resources"; - try { - coordinationService.deleteNode(CategoryNode.CASES, resourcesLockNodePath); - } catch (CoordinationServiceException ex) { - /* - * Log but do not notify the user. - */ - logger.log(Level.SEVERE, String.format("Failed to delete resources lock coordination service node for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); - } - String caseDirectoryLockNodePath = metadata.getCaseDirectory(); - try { - coordinationService.deleteNode(CategoryNode.CASES, caseDirectoryLockNodePath); - } catch (CoordinationServiceException ex) { - /* - * Log but do not notify the user. - */ - logger.log(Level.SEVERE, String.format("Failed to delete case directory lock coordination service node for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); - } - } - /** * Opens a new or existing case as the current case. * @@ -987,105 +929,6 @@ public class Case { return imgPaths; } - /** - * - * Deletes the case directory of a deleted case and removes the case form - * the Recent Cases menu. - * - * @param metadata The case metadata. - * @param progressIndicator A progress indicator. - * - * @throws UserPreferencesException if there is a problem getting the case - * databse connection info for a multi-user - * case. - * @throws ClassNotFoundException if there is a problem loading the JDBC - * driver for PostgreSQL for a multi-user - * case. - * @throws SQLException If there is a problem - */ - @Messages({ - "Case.progressMessage.deletingTextIndex=Deleting text index...", - "Case.progressMessage.deletingCaseDatabase=Deleting case database...", - "Case.progressMessage.deletingCaseDirectory=Deleting case directory...", - "Case.exceptionMessage.errorsDeletingCase=Errors occured while deleting the case. See the application log for details" - }) - private static void deleteCase(CaseMetadata metadata, ProgressIndicator progressIndicator) throws CaseActionException { - StopWatch stopWatch = new StopWatch(); - boolean errorsOccurred = false; - if (CaseType.MULTI_USER_CASE == metadata.getCaseType()) { - /* - * Delete the case database from the database server. - */ - stopWatch.start(); - try { - progressIndicator.progress(Bundle.Case_progressMessage_deletingCaseDatabase()); - CaseDbConnectionInfo db; - db = UserPreferences.getDatabaseConnectionInfo(); - Class.forName("org.postgresql.Driver"); //NON-NLS - try (Connection connection = DriverManager.getConnection("jdbc:postgresql://" + db.getHost() + ":" + db.getPort() + "/postgres", db.getUserName(), db.getPassword()); //NON-NLS - Statement statement = connection.createStatement();) { - String deleteCommand = "DROP DATABASE \"" + metadata.getCaseDatabaseName() + "\""; //NON-NLS - statement.execute(deleteCommand); - stopWatch.stop(); - logger.log(Level.INFO, String.format("Used %d s to delete case database for %s (%s) in %s", stopWatch.getElapsedTimeSecs(), metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory())); - } - } catch (UserPreferencesException | ClassNotFoundException | SQLException ex) { - logger.log(Level.SEVERE, String.format("Failed to delete case database %s for %s (%s) in %s", metadata.getCaseDatabaseName(), metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); - errorsOccurred = true; - stopWatch.stop(); - logger.log(Level.INFO, String.format("Used %d s to fail delete case database for %s (%s) in %s", stopWatch.getElapsedTimeSecs(), metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory())); - } - } - - /* - * Delete the text index. - */ - progressIndicator.progress(Bundle.Case_progressMessage_deletingTextIndex()); - for (KeywordSearchService searchService : Lookup.getDefault().lookupAll(KeywordSearchService.class)) { - try { - stopWatch.reset(); - stopWatch.start(); - searchService.deleteTextIndex(metadata); - stopWatch.stop(); - logger.log(Level.INFO, String.format("Used %d s to delete text index for %s (%s) in %s", stopWatch.getElapsedTimeSecs(), metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory())); - } catch (KeywordSearchServiceException ex) { - logger.log(Level.SEVERE, String.format("Failed to delete text index for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); - errorsOccurred = true; - stopWatch.stop(); - logger.log(Level.INFO, String.format("Used %d s to fail to delete text index for %s (%s) in %s", stopWatch.getElapsedTimeSecs(), metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory())); - } - } - - /* - * Delete the case directory. - */ - progressIndicator.progress(Bundle.Case_progressMessage_deletingCaseDirectory()); - stopWatch.reset(); - stopWatch.start(); - if (!FileUtil.deleteDir(new File(metadata.getCaseDirectory()))) { - logger.log(Level.SEVERE, String.format("Failed to delete case directory for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory())); - errorsOccurred = true; - stopWatch.stop(); - logger.log(Level.INFO, String.format("Used %d s to fail to delete case directory for %s (%s) in %s", stopWatch.getElapsedTimeSecs(), metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory())); - } else { - stopWatch.stop(); - logger.log(Level.INFO, String.format("Used %d s to delete case directory for %s (%s) in %s", stopWatch.getElapsedTimeSecs(), metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory())); - } - - /* - * If running in a GUI, remove the case from the Recent Cases menu - */ - if (RuntimeProperties.runningWithGUI()) { - SwingUtilities.invokeLater(() -> { - RecentCases.getInstance().removeRecentCase(metadata.getCaseDisplayName(), metadata.getFilePath().toString()); - }); - } - - if (errorsOccurred) { - throw new CaseActionException(Bundle.Case_exceptionMessage_errorsDeletingCase()); - } - } - /** * Acquires an exclusive case resources lock. * @@ -1101,7 +944,8 @@ public class Case { }) private static CoordinationService.Lock acquireExclusiveCaseResourcesLock(String caseDir) throws CaseActionException { try { - String resourcesNodeName = caseDir + "_resources"; + Path caseDirPath = Paths.get(caseDir); + String resourcesNodeName = CoordinationServiceUtils.getCaseResourcesNodePath(caseDirPath); Lock lock = CoordinationService.getInstance().tryGetExclusiveLock(CategoryNode.CASES, resourcesNodeName, RESOURCES_LOCK_TIMOUT_HOURS, TimeUnit.HOURS); return lock; } catch (InterruptedException ex) { @@ -1777,11 +1621,10 @@ public class Case { } if (getCaseType() == CaseType.MULTI_USER_CASE && !oldCaseDetails.getCaseDisplayName().equals(caseDetails.getCaseDisplayName())) { try { - CoordinationService coordinationService = CoordinationService.getInstance(); - CaseNodeData nodeData = new CaseNodeData(coordinationService.getNodeData(CategoryNode.CASES, metadata.getCaseDirectory())); + CaseNodeData nodeData = CaseNodeData.readCaseNodeData(metadata.getCaseDirectory()); nodeData.setDisplayName(caseDetails.getCaseDisplayName()); - coordinationService.setNodeData(CategoryNode.CASES, metadata.getCaseDirectory(), nodeData.toArray()); - } catch (CoordinationServiceException | InterruptedException | IOException ex) { + CaseNodeData.writeCaseNodeData(nodeData); + } catch (CaseNodeDataException | InterruptedException ex) { throw new CaseActionException(Bundle.Case_exceptionMessage_couldNotUpdateCaseNodeData(ex.getLocalizedMessage()), ex); } } @@ -2162,10 +2005,8 @@ public class Case { if (getCaseType() == CaseType.MULTI_USER_CASE) { progressIndicator.progress(Bundle.Case_progressMessage_creatingCaseNodeData()); try { - CoordinationService coordinationService = CoordinationService.getInstance(); - CaseNodeData nodeData = new CaseNodeData(metadata); - coordinationService.setNodeData(CategoryNode.CASES, metadata.getCaseDirectory(), nodeData.toArray()); - } catch (CoordinationServiceException | InterruptedException | ParseException | IOException ex) { + CaseNodeData.createCaseNodeData(metadata); + } catch (CaseNodeDataException | InterruptedException ex) { throw new CaseActionException(Bundle.Case_exceptionMessage_couldNotCreateCaseNodeData(ex.getLocalizedMessage()), ex); } } @@ -2175,6 +2016,8 @@ public class Case { * Updates the node data for the case directory lock coordination service * node. * + * @param progressIndicator A progress indicator. + * * @throws CaseActionException If there is a problem completing the * operation. The exception will have a * user-friendly message and may be a wrapper @@ -2187,15 +2030,12 @@ public class Case { private void updateCaseNodeData(ProgressIndicator progressIndicator) throws CaseActionException { if (getCaseType() == CaseType.MULTI_USER_CASE) { progressIndicator.progress(Bundle.Case_progressMessage_updatingCaseNodeData()); - if (getCaseType() == CaseType.MULTI_USER_CASE) { - try { - CoordinationService coordinationService = CoordinationService.getInstance(); - CaseNodeData nodeData = new CaseNodeData(coordinationService.getNodeData(CategoryNode.CASES, metadata.getCaseDirectory())); - nodeData.setLastAccessDate(new Date()); - coordinationService.setNodeData(CategoryNode.CASES, metadata.getCaseDirectory(), nodeData.toArray()); - } catch (CoordinationServiceException | InterruptedException | IOException ex) { - throw new CaseActionException(Bundle.Case_exceptionMessage_couldNotUpdateCaseNodeData(ex.getLocalizedMessage()), ex); - } + try { + CaseNodeData nodeData = CaseNodeData.readCaseNodeData(metadata.getCaseDirectory()); + nodeData.setLastAccessDate(new Date()); + CaseNodeData.writeCaseNodeData(nodeData); + } catch (CaseNodeDataException | InterruptedException ex) { + throw new CaseActionException(Bundle.Case_exceptionMessage_couldNotUpdateCaseNodeData(ex.getLocalizedMessage()), ex); } } } @@ -2330,7 +2170,6 @@ public class Case { "# {0} - service name", "Case.servicesException.notificationTitle={0} Error" }) private void openAppServiceCaseResources(ProgressIndicator progressIndicator) throws CaseActionException { - progressIndicator.progress(Bundle.Case_progressMessage_openingApplicationServiceResources()); /* * Each service gets its own independently cancellable/interruptible * task, running in a named thread managed by an executor service, with @@ -2339,6 +2178,7 @@ public class Case { * possible to ensure that each service task completes before the next * one starts by awaiting termination of the executor service. */ + progressIndicator.progress(Bundle.Case_progressMessage_openingApplicationServiceResources()); for (AutopsyService service : Lookup.getDefault().lookupAll(AutopsyService.class)) { /* * Create a progress indicator for the task and start the task. If @@ -2688,6 +2528,363 @@ public class Case { } + /** + * Deletes a single-user case. + * + * @param metadata The case metadata. + * @param progressIndicator A progress indicator. + * + * @throws CaseActionException If there were one or more errors deleting the + * case. The exception will have a user-friendly + * message and may be a wrapper for a + * lower-level exception. + */ + @Messages({ + "Case.exceptionMessage.errorsDeletingCase=Errors occured while deleting the case. See the application log for details." + }) + private static void deleteSingleUserCase(CaseMetadata metadata, ProgressIndicator progressIndicator) throws CaseActionException { + boolean errorsOccurred = false; + try { + deleteTextIndex(metadata, progressIndicator); + } catch (KeywordSearchServiceException ex) { + errorsOccurred = true; + logger.log(Level.WARNING, String.format("Failed to delete text index for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); //NON-NLS + } + + try { + deleteCaseDirectory(metadata, progressIndicator); + } catch (CaseActionException ex) { + errorsOccurred = true; + logger.log(Level.WARNING, String.format("Failed to delete case directory for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); //NON-NLS + } + + deleteFromRecentCases(metadata, progressIndicator); + + if (errorsOccurred) { + throw new CaseActionException(Bundle.Case_exceptionMessage_errorsDeletingCase()); + } + } + + /** + * Deletes a multi-user case. This method does so after acquiring the case + * directory coordination service lock and is intended to be used for + * deleting simple multi-user cases without auto ingest input. Note that the + * case directory coordination service node for the case is only deleted if + * no errors occurred. + * + * @param metadata The case metadata. + * @param progressIndicator A progress indicator. + * + * @throws CaseActionException If there were one or more errors deleting + * the case. The exception will have a + * user-friendly message and may be a wrapper + * for a lower-level exception. + * @throws InterruptedException If the thread this code is running in is + * interrupted while blocked, i.e., if + * cancellation of the operation is detected + * during a wait. + */ + @Messages({ + "Case.progressMessage.connectingToCoordSvc=Connecting to coordination service...", + "# {0} - exception message", "Case.exceptionMessage.failedToConnectToCoordSvc=Failed to connect to coordination service:\n{0}.", + "Case.exceptionMessage.cannotGetLockToDeleteCase=Cannot delete case because it is open for another user or host.", + "# {0} - exception message", "Case.exceptionMessage.failedToLockCaseForDeletion=Failed to exclusively lock case for deletion:\n{0}.", + "Case.progressMessage.fetchingCoordSvcNodeData=Fetching coordination service node data for the case...", + "# {0} - exception message", "Case.exceptionMessage.failedToFetchCoordSvcNodeData=Failed to fetch coordination service node data:\n{0}.", + "Case.progressMessage.deletingResourcesCoordSvcNode=Deleting case resources coordination service node...", + "Case.progressMessage.deletingCaseDirCoordSvcNode=Deleting case directory coordination service node..." + }) + private static void deleteMultiUserCase(CaseMetadata metadata, ProgressIndicator progressIndicator) throws CaseActionException, InterruptedException { + progressIndicator.progress(Bundle.Case_progressMessage_connectingToCoordSvc()); + CoordinationService coordinationService; + try { + coordinationService = CoordinationService.getInstance(); + } catch (CoordinationServiceException ex) { + logger.log(Level.SEVERE, String.format("Failed to connect to coordination service when attempting to delete %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); //NON-NLS + throw new CaseActionException(Bundle.Case_exceptionMessage_failedToConnectToCoordSvc(ex.getLocalizedMessage())); + } + + CaseNodeData caseNodeData; + boolean errorsOccurred = false; + try (CoordinationService.Lock dirLock = coordinationService.tryGetExclusiveLock(CategoryNode.CASES, metadata.getCaseDirectory())) { + if (dirLock == null) { + logger.log(Level.INFO, String.format("Could not delete %s (%s) in %s because a case directory lock was held by another host", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory())); //NON-NLS + throw new CaseActionException(Bundle.Case_exceptionMessage_cannotGetLockToDeleteCase()); + } + + progressIndicator.progress(Bundle.Case_progressMessage_fetchingCoordSvcNodeData()); + try { + caseNodeData = CaseNodeData.readCaseNodeData(metadata.getCaseDirectory()); + } catch (CaseNodeDataException | InterruptedException ex) { + logger.log(Level.SEVERE, String.format("Failed to get coordination service node data %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); //NON-NLS + throw new CaseActionException(Bundle.Case_exceptionMessage_failedToFetchCoordSvcNodeData(ex.getLocalizedMessage())); + } + + errorsOccurred = deleteMultiUserCase(caseNodeData, metadata, progressIndicator, logger); + + progressIndicator.progress(Bundle.Case_progressMessage_deletingResourcesCoordSvcNode()); + try { + String resourcesLockNodePath = CoordinationServiceUtils.getCaseResourcesNodePath(caseNodeData.getDirectory()); + coordinationService.deleteNode(CategoryNode.CASES, resourcesLockNodePath); + } catch (CoordinationServiceException ex) { + if (!isNoNodeException(ex)) { + errorsOccurred = true; + logger.log(Level.WARNING, String.format("Error deleting the case resources coordination service node for the case at %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); //NON-NLS + } + } catch (InterruptedException ex) { + logger.log(Level.WARNING, String.format("Error deleting the case resources coordination service node for the case at %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); //NON-NLS + } + + } catch (CoordinationServiceException ex) { + logger.log(Level.SEVERE, String.format("Error exclusively locking the case directory for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); //NON-NLS + throw new CaseActionException(Bundle.Case_exceptionMessage_failedToLockCaseForDeletion(ex.getLocalizedMessage())); + } + + if (!errorsOccurred) { + progressIndicator.progress(Bundle.Case_progressMessage_deletingCaseDirCoordSvcNode()); + try { + String casDirNodePath = CoordinationServiceUtils.getCaseDirectoryNodePath(caseNodeData.getDirectory()); + coordinationService.deleteNode(CategoryNode.CASES, casDirNodePath); + } catch (CoordinationServiceException | InterruptedException ex) { + logger.log(Level.SEVERE, String.format("Error deleting the case directory lock node for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); //NON-NLS + errorsOccurred = true; + } + } + + if (errorsOccurred) { + throw new CaseActionException(Bundle.Case_exceptionMessage_errorsDeletingCase()); + } + } + + /** + * IMPORTANT: This is a "beta" method and is subject to change or removal + * without notice! + * + * Deletes a mulit-user case by attempting to delete the case database, the + * text index, the case directory, and the case resources coordination + * service node for a case, and removes the case from the recent cases menu + * of the main application window. Callers of this method MUST acquire and + * release the case directory lock for the case and are responsible for + * deleting the corresponding coordination service nodes, if desired. + * + * @param caseNodeData The coordination service node data for the case. + * @param metadata The case metadata. + * @param progressIndicator A progress indicator. + * @param logger A logger. + * + * @return True if one or more errors occurred (see log for details), false + * otherwise. + * + * @throws InterruptedException If the thread this code is running in is + * interrupted while blocked, i.e., if + * cancellation of the operation is detected + * during a wait. + */ + @Beta + public static boolean deleteMultiUserCase(CaseNodeData caseNodeData, CaseMetadata metadata, ProgressIndicator progressIndicator, Logger logger) throws InterruptedException { + boolean errorsOccurred = false; + try { + deleteMultiUserCaseDatabase(caseNodeData, metadata, progressIndicator, logger); + deleteMultiUserCaseTextIndex(caseNodeData, metadata, progressIndicator, logger); + deleteMultiUserCaseDirectory(caseNodeData, metadata, progressIndicator, logger); + deleteFromRecentCases(metadata, progressIndicator); + } catch (UserPreferencesException | ClassNotFoundException | SQLException ex) { + errorsOccurred = true; + logger.log(Level.WARNING, String.format("Failed to delete the case database for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); //NON-NLS + } catch (KeywordSearchServiceException ex) { + errorsOccurred = true; + logger.log(Level.WARNING, String.format("Failed to delete the text index for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); //NON-NLS + } catch (CaseActionException ex) { + errorsOccurred = true; + logger.log(Level.WARNING, String.format("Failed to delete the case directory for %s (%s) in %s", metadata.getCaseDisplayName(), metadata.getCaseName(), metadata.getCaseDirectory()), ex); //NON-NLS + } + return errorsOccurred; + } + + /** + * Attempts to delete the case database for a multi-user case. + * + * @param caseNodeData The coordination service node data for the case. + * @param metadata The case metadata. + * @param progressIndicator A progress indicator. + * @param logger A logger. + * + * @throws UserPreferencesException if there is an error getting the + * database server connection info. + * @throws ClassNotFoundException if there is an error gettting the + * required JDBC driver. + * @throws SQLException if there is an error executing the SQL + * to drop the database from the database + * server. + * @throws InterruptedException If interrupted while blocked waiting for + * coordination service data to be written + * to the coordination service node + * database. + */ + @Messages({ + "Case.progressMessage.deletingCaseDatabase=Deleting case database..." + }) + private static void deleteMultiUserCaseDatabase(CaseNodeData caseNodeData, CaseMetadata metadata, ProgressIndicator progressIndicator, Logger logger) throws UserPreferencesException, ClassNotFoundException, SQLException, InterruptedException { + if (!caseNodeData.isDeletedFlagSet(CaseNodeData.DeletedFlags.CASE_DB)) { + progressIndicator.progress(Bundle.Case_progressMessage_deletingCaseDatabase()); + logger.log(Level.INFO, String.format("Deleting case database for %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory())); //NON-NLS + CaseDbConnectionInfo info = UserPreferences.getDatabaseConnectionInfo(); + String url = "jdbc:postgresql://" + info.getHost() + ":" + info.getPort() + "/postgres"; //NON-NLS + Class.forName("org.postgresql.Driver"); //NON-NLS + try (Connection connection = DriverManager.getConnection(url, info.getUserName(), info.getPassword()); Statement statement = connection.createStatement()) { + String dbExistsQuery = "SELECT 1 from pg_database WHERE datname = '" + metadata.getCaseDatabaseName() + "'"; //NON-NLS + try (ResultSet queryResult = statement.executeQuery(dbExistsQuery)) { + if (queryResult.next()) { + String deleteCommand = "DROP DATABASE \"" + metadata.getCaseDatabaseName() + "\""; //NON-NLS + statement.execute(deleteCommand); + } + } + } + setDeletedItemFlag(caseNodeData, CaseNodeData.DeletedFlags.CASE_DB); + } + } + + /** + * Attempts to delete the text index for a multi-user case. + * + * @param caseNodeData The coordination service node data for the case. + * @param metadata The case metadata. + * @param progressIndicator A progress indicator. + * @param logger A logger. + * + * @throws KeywordSearchServiceException If there is an error deleting the + * text index. + * @throws InterruptedException If interrupted while blocked + * waiting for coordination service + * data to be written to the + * coordination service node database. + */ + private static void deleteMultiUserCaseTextIndex(CaseNodeData caseNodeData, CaseMetadata metadata, ProgressIndicator progressIndicator, Logger logger) throws KeywordSearchServiceException, InterruptedException { + if (!caseNodeData.isDeletedFlagSet(CaseNodeData.DeletedFlags.TEXT_INDEX)) { + logger.log(Level.INFO, String.format("Deleting text index for %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory())); //NON-NLS + deleteTextIndex(metadata, progressIndicator); + setDeletedItemFlag(caseNodeData, CaseNodeData.DeletedFlags.TEXT_INDEX); + } + } + + /** + * Attempts to delete the text index for a case. + * + * @param metadata The case metadata. + * @param progressIndicator A progress indicator. + * + * @throws KeywordSearchServiceException If there is an error deleting the + * text index. + */ + @Messages({ + "Case.progressMessage.deletingTextIndex=Deleting text index..." + }) + private static void deleteTextIndex(CaseMetadata metadata, ProgressIndicator progressIndicator) throws KeywordSearchServiceException { + progressIndicator.progress(Bundle.Case_progressMessage_deletingTextIndex()); + for (KeywordSearchService searchService : Lookup.getDefault().lookupAll(KeywordSearchService.class)) { + searchService.deleteTextIndex(metadata); + } + } + + /** + * Attempts to delete the case directory for a multi-user case. + * + * @param caseNodeData The coordination service node data for the case. + * @param metadata The case metadata. + * @param progressIndicator A progress indicator. + * @param logger A logger. + * + * @throws CaseActionException if there is an error deleting the case + * directory. + * @throws InterruptedException If interrupted while blocked waiting for + * coordination service data to be written to + * the coordination service node database. + */ + private static void deleteMultiUserCaseDirectory(CaseNodeData caseNodeData, CaseMetadata metadata, ProgressIndicator progressIndicator, Logger logger) throws CaseActionException, InterruptedException { + if (!caseNodeData.isDeletedFlagSet(CaseNodeData.DeletedFlags.CASE_DIR)) { + logger.log(Level.INFO, String.format("Deleting case directory for %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory())); //NON-NLS + deleteCaseDirectory(metadata, progressIndicator); + setDeletedItemFlag(caseNodeData, CaseNodeData.DeletedFlags.CASE_DIR); + } + } + + /** + * Attempts to delete the case directory for a case. + * + * @param metadata The case metadata. + * @param progressIndicator A progress indicator. + * + * @throws CaseActionException If there is an error deleting the case + * directory. + */ + @Messages({ + "Case.progressMessage.deletingCaseDirectory=Deleting case directory..." + }) + private static void deleteCaseDirectory(CaseMetadata metadata, ProgressIndicator progressIndicator) throws CaseActionException { + progressIndicator.progress(Bundle.Case_progressMessage_deletingCaseDirectory()); + if (!FileUtil.deleteDir(new File(metadata.getCaseDirectory()))) { + throw new CaseActionException(String.format("Failed to delete %s", metadata.getCaseDirectory())); //NON-NLS + } + } + + /** + * Attempts to remove a case from the recent cases menu if the main + * application window is present. + * + * @param metadata The case metadata. + * @param progressIndicator A progress indicator. + */ + @Messages({ + "Case.progressMessage.removingCaseFromRecentCases=Removing case from Recent Cases menu..." + }) + private static void deleteFromRecentCases(CaseMetadata metadata, ProgressIndicator progressIndicator) { + if (RuntimeProperties.runningWithGUI()) { + progressIndicator.progress(Bundle.Case_progressMessage_removingCaseFromRecentCases()); + SwingUtilities.invokeLater(() -> { + RecentCases.getInstance().removeRecentCase(metadata.getCaseDisplayName(), metadata.getFilePath().toString()); + }); + } + } + + /** + * Examines a coordination service exception to try to determine if it is a + * "no node" exception, i.e., an operation was attempted on a node that does + * not exist. + * + * @param ex A coordination service exception. + * + * @return True or false. + */ + private static boolean isNoNodeException(CoordinationServiceException ex) { + boolean isNodeNodeEx = false; + Throwable cause = ex.getCause(); + if (cause != null) { + String causeMessage = cause.getMessage(); + isNodeNodeEx = causeMessage.contains(NO_NODE_ERROR_MSG_FRAGMENT); + } + return isNodeNodeEx; + } + + /** + * Sets a deleted item flag in the coordination service node data for a + * multi-user case. + * + * @param caseNodeData The coordination service node data for the case. + * @param flag The flag to set. + * + * @throws InterruptedException If interrupted while blocked waiting for + * coordination service data to be written to + * the coordination service node database. + */ + private static void setDeletedItemFlag(CaseNodeData caseNodeData, CaseNodeData.DeletedFlags flag) throws InterruptedException { + try { + caseNodeData.setDeletedFlag(flag); + CaseNodeData.writeCaseNodeData(caseNodeData); + } catch (CaseNodeDataException ex) { + logger.log(Level.SEVERE, String.format("Error updating deleted item flag %s for %s (%s) in %s", flag.name(), caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory()), ex); + } + } + /** * A case operation Cancel button listener for use with a * ModalDialogProgressIndicator when running with a GUI. diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java b/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java index abd2edbccf..20dfabd93c 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java @@ -95,13 +95,13 @@ public final class CaseMetadata { private final static String EXAMINER_ELEMENT_PHONE = "ExaminerPhone"; //NON-NLS private final static String EXAMINER_ELEMENT_EMAIL = "ExaminerEmail"; //NON-NLS private final static String CASE_ELEMENT_NOTES = "CaseNotes"; //NON-NLS - + /* * Fields from schema version 5 */ private static final String SCHEMA_VERSION_FIVE = "5.0"; private final static String ORIGINAL_CASE_ELEMENT_NAME = "OriginalCase"; //NON-NLS - + /* * Unread fields, regenerated on save. */ @@ -138,16 +138,16 @@ public final class CaseMetadata { public static DateFormat getDateFormat() { return new SimpleDateFormat(DATE_FORMAT_STRING, Locale.US); } - + /** * Constructs a CaseMetadata object for a new case. The metadata is not * persisted to the case metadata file until writeFile or a setX method is * called. * - * @param caseType The type of case. - * @param caseDirectory The case directory. - * @param caseName The immutable name of the case. - * @param caseDetails The details for the case + * @param caseType The type of case. + * @param caseDirectory The case directory. + * @param caseName The immutable name of the case. + * @param caseDetails The details for the case */ CaseMetadata(Case.CaseType caseType, String caseDirectory, String caseName, CaseDetails caseDetails) { this(caseType, caseDirectory, caseName, caseDetails, null); @@ -158,11 +158,11 @@ public final class CaseMetadata { * persisted to the case metadata file until writeFile or a setX method is * called. * - * @param caseType The type of case. - * @param caseDirectory The case directory. - * @param caseName The immutable name of the case. - * @param caseDetails The details for the case - * @param originalMetadata The metadata object from the original case + * @param caseType The type of case. + * @param caseDirectory The case directory. + * @param caseName The immutable name of the case. + * @param caseDetails The details for the case + * @param originalMetadata The metadata object from the original case */ CaseMetadata(Case.CaseType caseType, String caseDirectory, String caseName, CaseDetails caseDetails, CaseMetadata originalMetadata) { metadataFilePath = Paths.get(caseDirectory, caseDetails.getCaseDisplayName() + FILE_EXTENSION); @@ -190,25 +190,25 @@ public final class CaseMetadata { this.metadataFilePath = metadataFilePath; readFromFile(); } - + /** - * Locate the case meta data file in the supplied directory. If the file does - * not exist, null is returned. - * - * @param directoryPath Directory path to search - * @return case meta data file path or null + * Locate the case meta data file in the supplied directory. If the file + * does not exist, null is returned. + * + * @param directoryPath Directory path to search. + * + * @return Case metadata file path or null. */ - public static Path getCaseMetadataFile(Path directoryPath) { - final File[] caseFiles = directoryPath.toFile().listFiles(); - if(caseFiles != null) { - for (File file : caseFiles) { + public static Path getCaseMetadataFilePath(Path directoryPath) { + final File[] files = directoryPath.toFile().listFiles(); + if (files != null) { + for (File file : files) { final String fileName = file.getName().toLowerCase(); - if (fileName.endsWith(CaseMetadata.getFileExtension())) { + if (fileName.endsWith(CaseMetadata.getFileExtension()) && file.isFile()) { return file.toPath(); } } } - return null; } @@ -460,7 +460,7 @@ public final class CaseMetadata { * Create the children of the case element. */ createCaseElements(doc, caseElement, this); - + /* * Add original case element */ @@ -472,15 +472,15 @@ public final class CaseMetadata { originalCaseElement.appendChild(originalCaseDetailsElement); createCaseElements(doc, originalCaseDetailsElement, originalMetadata); } - + } - + /** * Write the case element children for the given metadata object - * - * @param doc The document. - * @param caseElement The case element parent - * @param metadataToWrite The CaseMetadata object to read from + * + * @param doc The document. + * @param caseElement The case element parent + * @param metadataToWrite The CaseMetadata object to read from */ private void createCaseElements(Document doc, Element caseElement, CaseMetadata metadataToWrite) { CaseDetails caseDetailsToWrite = metadataToWrite.caseDetails; @@ -572,9 +572,8 @@ public final class CaseMetadata { examinerEmail = getElementTextContent(caseElement, EXAMINER_ELEMENT_EMAIL, false); caseNotes = getElementTextContent(caseElement, CASE_ELEMENT_NOTES, false); } - - this.caseDetails = new CaseDetails(caseDisplayName, caseNumber, examinerName, examinerPhone, examinerEmail, - caseNotes); + + this.caseDetails = new CaseDetails(caseDisplayName, caseNumber, examinerName, examinerPhone, examinerEmail, caseNotes); this.caseType = Case.CaseType.fromString(getElementTextContent(caseElement, CASE_TYPE_ELEMENT_NAME, true)); if (null == this.caseType) { throw new CaseMetadataException("Case metadata file corrupted"); diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercases/CaseNodeData.java b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercases/CaseNodeData.java index 3869e364d0..4431e0b8bd 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercases/CaseNodeData.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercases/CaseNodeData.java @@ -22,25 +22,33 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; +import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.text.ParseException; import java.util.Date; +import java.util.logging.Level; import org.sleuthkit.autopsy.casemodule.CaseMetadata; +import org.sleuthkit.autopsy.casemodule.CaseMetadata.CaseMetadataException; +import org.sleuthkit.autopsy.coordinationservice.CoordinationService; +import org.sleuthkit.autopsy.coordinationservice.CoordinationService.CoordinationServiceException; +import org.sleuthkit.autopsy.coreutils.Logger; /** - * An object that converts data for a case directory lock coordination service - * node to and from byte arrays. + * Case data stored in a case directory coordination service node. */ public final class CaseNodeData { - private static final int CURRENT_VERSION = 1; + private static final int MAJOR_VERSION = 2; + private static final int MINOR_VERSION = 0; + private static final Logger logger = Logger.getLogger(CaseNodeData.class.getName()); /* - * Version 0 fields. + * Version 0 fields. Note that version 0 node data was only written to the + * coordination service node if an auto ingest job error occurred. */ - private final int version; + private int version; private boolean errorsOccurred; /* @@ -53,28 +61,191 @@ public final class CaseNodeData { private String displayName; private short deletedItemFlags; - /** - * Gets the current version of the case directory lock coordination service - * node data. - * - * @return The version number. + /* + * Version 2 fields. */ - public static int getCurrentVersion() { - return CaseNodeData.CURRENT_VERSION; + private int minorVersion; + + /** + * Creates case node data from the metadata for a case and writes it to the + * appropriate case directory coordination service node, which must already + * exist. + * + * @param metadata The case metadata. + * + * @return The case node data that was written to the coordination service + * node. + * + * @throws CaseNodeDataException If there is an error creating or writing + * the case node data. + * @throws InterruptedException If the current thread is interrupted while + * waiting for the coordination service. + */ + public static CaseNodeData createCaseNodeData(final CaseMetadata metadata) throws CaseNodeDataException, InterruptedException { + try { + final CaseNodeData nodeData = new CaseNodeData(metadata); + CoordinationService.getInstance().setNodeData(CoordinationService.CategoryNode.CASES, nodeData.getDirectory().toString(), nodeData.toArray()); + return nodeData; + + } catch (ParseException | IOException | CoordinationServiceException ex) { + throw new CaseNodeDataException(String.format("Error creating case node data for coordination service node with path %s", metadata.getCaseDirectory().toUpperCase()), ex); //NON-NLS + } } /** - * Uses a CaseMetadata object to construct an object that converts data for - * a case directory lock coordination service node to and from byte arrays. + * Reads case data from a case directory coordination service node. If the + * data is missing, corrupted, or from an older version of the software, an + * attempt is made to remedy the situation using the case metadata. + * + * @param nodePath The case directory coordination service node path. + * + * @return The case node data. + * + * @throws CaseNodeDataException If there is an error reading or writing the + * case node data. + * @throws InterruptedException If the current thread is interrupted while + * waiting for the coordination service. + */ + public static CaseNodeData readCaseNodeData(String nodePath) throws CaseNodeDataException, InterruptedException { + try { + CaseNodeData nodeData; + final byte[] nodeBytes = CoordinationService.getInstance().getNodeData(CoordinationService.CategoryNode.CASES, nodePath); + if (nodeBytes != null && nodeBytes.length > 0) { + try { + nodeData = new CaseNodeData(nodeBytes); + } catch (IOException ex) { + /* + * The existing case node data is corrupted. + */ + logger.log(Level.WARNING, String.format("Error reading node data for coordination service node with path %s, will attempt to replace it", nodePath.toUpperCase()), ex); //NON-NLS + final CaseMetadata metadata = getCaseMetadata(nodePath); + nodeData = createCaseNodeData(metadata); + logger.log(Level.INFO, String.format("Replaced corrupt node data for coordination service node with path %s", nodePath.toUpperCase())); //NON-NLS + } + } else { + /* + * The case node data is missing. Version 0 node data was only + * written to the coordination service node if an auto ingest + * job error occurred. + */ + logger.log(Level.INFO, String.format("Missing node data for coordination service node with path %s, will attempt to create it", nodePath.toUpperCase())); //NON-NLS + final CaseMetadata metadata = getCaseMetadata(nodePath); + nodeData = createCaseNodeData(metadata); + logger.log(Level.INFO, String.format("Created node data for coordination service node with path %s", nodePath.toUpperCase())); //NON-NLS + } + if (nodeData.getVersion() < CaseNodeData.MAJOR_VERSION) { + nodeData = upgradeCaseNodeData(nodePath, nodeData); + } + return nodeData; + + } catch (CaseNodeDataException | CaseMetadataException | ParseException | IOException | CoordinationServiceException ex) { + throw new CaseNodeDataException(String.format("Error reading/writing node data coordination service node with path %s", nodePath.toUpperCase()), ex); //NON-NLS + } + } + + /** + * Writes case data to a case directory coordination service node. Obtain + * the case data to be updated and written by calling createCaseNodeData() + * or readCaseNodeData(). + * + * @param nodeData The case node data. + * + * @throws CaseNodeDataException If there is an error writing the case node + * data. + * @throws InterruptedException If the current thread is interrupted while + * waiting for the coordination service. + */ + public static void writeCaseNodeData(CaseNodeData nodeData) throws CaseNodeDataException, InterruptedException { + try { + CoordinationService.getInstance().setNodeData(CoordinationService.CategoryNode.CASES, nodeData.getDirectory().toString(), nodeData.toArray()); + + } catch (IOException | CoordinationServiceException ex) { + throw new CaseNodeDataException(String.format("Error writing node data coordination service node with path %s", nodeData.getDirectory().toString().toUpperCase()), ex); //NON-NLS + } + } + + /** + * Upgrades older versions of node data to the current version and writes + * the data back to the case directory coordination service node. + * + * @param nodePath The case directory coordination service node path. + * @param oldNodeData The outdated node data. + * + * @return The updated node data. + * + * @throws CaseNodeDataException If the case meta data file or case + * directory do not exist. + * @throws CaseMetadataException If the case metadata cannot be read. + */ + private static CaseNodeData upgradeCaseNodeData(String nodePath, CaseNodeData oldNodeData) throws CaseNodeDataException, CaseMetadataException, ParseException, IOException, CoordinationServiceException, InterruptedException { + CaseNodeData nodeData; + switch (oldNodeData.getVersion()) { + case 0: + /* + * Version 0 node data consisted of only the version number and + * the errors occurred flag and was only written when an auto + * ingest job error occurred. To upgrade from version 0, the + * version 1 fields need to be set from the case metadata and + * the errors occurred flag needs to be carried forward. Note + * that the last accessed date gets advanced to now, since it is + * otherwise unknown. + */ + final CaseMetadata metadata = getCaseMetadata(nodePath); + nodeData = new CaseNodeData(metadata); + nodeData.setErrorsOccurred(oldNodeData.getErrorsOccurred()); + break; + case 1: + /* + * Version 1 node data did not have a minor version number + * field. + */ + oldNodeData.setMinorVersion(MINOR_VERSION); + nodeData = oldNodeData; + break; + default: + nodeData = oldNodeData; + break; + } + writeCaseNodeData(nodeData); + return nodeData; + } + + /** + * Gets the metadata for a case. + * + * @param nodePath The case directory coordination service node path for the + * case. + * + * @return The case metadata. + * + * @throws CaseNodeDataException If the case metadata file or the case + * directory does not exist. + * @throws CaseMetadataException If the case metadata cannot be read. + */ + private static CaseMetadata getCaseMetadata(String nodePath) throws CaseNodeDataException, CaseMetadataException { + final Path caseDirectoryPath = Paths.get(nodePath); + final File caseDirectory = caseDirectoryPath.toFile(); + if (!caseDirectory.exists()) { + throw new CaseNodeDataException("Case directory does not exist"); // NON-NLS + } + final Path metadataFilePath = CaseMetadata.getCaseMetadataFilePath(caseDirectoryPath); + if (metadataFilePath == null) { + throw new CaseNodeDataException("Case meta data file does not exist"); // NON-NLS + } + return new CaseMetadata(metadataFilePath); + } + + /** + * Uses case metadata to construct the case data to store in a case + * directory coordination service node. * * @param metadata The case meta data. * - * @throws java.text.ParseException If there is an error parsing dates from - * string representations of dates in the - * meta data. + * @throws ParseException If there is an error parsing dates from string + * representations of dates in the meta data. */ - public CaseNodeData(CaseMetadata metadata) throws ParseException { - this.version = CURRENT_VERSION; + private CaseNodeData(CaseMetadata metadata) throws ParseException { + this.version = MAJOR_VERSION; this.errorsOccurred = false; this.directory = Paths.get(metadata.getCaseDirectory()); this.createDate = CaseMetadata.getDateFormat().parse(metadata.getCreatedDate()); @@ -82,51 +253,64 @@ public final class CaseNodeData { this.name = metadata.getCaseName(); this.displayName = metadata.getCaseDisplayName(); this.deletedItemFlags = 0; + this.minorVersion = MINOR_VERSION; } /** - * Uses coordination service node data to construct an object that converts - * data for a case directory lock coordination service node to and from byte - * arrays. + * Uses the raw bytes from a case directory coordination service node to + * construct a case node data object. * * @param nodeData The raw bytes received from the coordination service. * * @throws IOException If there is an error reading the node data. */ - public CaseNodeData(byte[] nodeData) throws IOException { + private CaseNodeData(byte[] nodeData) throws IOException { if (nodeData == null || nodeData.length == 0) { throw new IOException(null == nodeData ? "Null node data byte array" : "Zero-length node data byte array"); } - DataInputStream inputStream = new DataInputStream(new ByteArrayInputStream(nodeData)); - this.version = inputStream.readInt(); - if (this.version > 0) { - this.errorsOccurred = inputStream.readBoolean(); - } else { - short legacyErrorsOccurred = inputStream.readByte(); - this.errorsOccurred = (legacyErrorsOccurred < 0); - } - if (this.version > 0) { - this.directory = Paths.get(inputStream.readUTF()); - this.createDate = new Date(inputStream.readLong()); - this.lastAccessDate = new Date(inputStream.readLong()); - this.name = inputStream.readUTF(); - this.displayName = inputStream.readUTF(); - this.deletedItemFlags = inputStream.readShort(); + try (ByteArrayInputStream byteStream = new ByteArrayInputStream(nodeData); DataInputStream inputStream = new DataInputStream(byteStream)) { + this.version = inputStream.readInt(); + if (this.version == 1) { + this.errorsOccurred = inputStream.readBoolean(); + } else { + byte errorsOccurredByte = inputStream.readByte(); + this.errorsOccurred = (errorsOccurredByte < 0); + } + if (this.version > 0) { + this.directory = Paths.get(inputStream.readUTF()); + this.createDate = new Date(inputStream.readLong()); + this.lastAccessDate = new Date(inputStream.readLong()); + this.name = inputStream.readUTF(); + this.displayName = inputStream.readUTF(); + this.deletedItemFlags = inputStream.readShort(); + } + if (this.version > 1) { + this.minorVersion = inputStream.readInt(); + } } } /** - * Gets the node data version number of this node. + * Gets the version number of this node data. * * @return The version number. */ - public int getVersion() { + private int getVersion() { return this.version; } + /** + * Sets the minor version number of this node data. + * + * @param minorVersion The version number. + */ + private void setMinorVersion(int minorVersion) { + this.minorVersion = minorVersion; + } + /** * Gets whether or not any errors occurred during the processing of any auto - * ingest job for the case represented by this node data. + * ingest job for the case. * * @return True or false. */ @@ -136,7 +320,7 @@ public final class CaseNodeData { /** * Sets whether or not any errors occurred during the processing of any auto - * ingest job for the case represented by this node data. + * ingest job for the case. * * @param errorsOccurred True or false. */ @@ -145,8 +329,7 @@ public final class CaseNodeData { } /** - * Gets the path of the case directory of the case represented by this node - * data. + * Gets the path of the case directory. * * @return The case directory path. */ @@ -155,17 +338,7 @@ public final class CaseNodeData { } /** - * Sets the path of the case directory of the case represented by this node - * data. - * - * @param caseDirectory The case directory path. - */ - public void setDirectory(Path caseDirectory) { - this.directory = caseDirectory; - } - - /** - * Gets the date the case represented by this node data was created. + * Gets the date the case was created. * * @return The create date. */ @@ -174,16 +347,7 @@ public final class CaseNodeData { } /** - * Sets the date the case represented by this node data was created. - * - * @param createDate The create date. - */ - public void setCreateDate(Date createDate) { - this.createDate = new Date(createDate.getTime()); - } - - /** - * Gets the date the case represented by this node data last accessed. + * Gets the date the case was last accessed. * * @return The last access date. */ @@ -192,7 +356,7 @@ public final class CaseNodeData { } /** - * Sets the date the case represented by this node data was last accessed. + * Sets the date the case was last accessed. * * @param lastAccessDate The last access date. */ @@ -201,8 +365,7 @@ public final class CaseNodeData { } /** - * Gets the unique and immutable (user cannot change it) name of the case - * represented by this node data. + * Gets the unique and immutable name of the case. * * @return The case name. */ @@ -211,17 +374,7 @@ public final class CaseNodeData { } /** - * Sets the unique and immutable (user cannot change it) name of the case - * represented by this node data. - * - * @param name The case name. - */ - public void setName(String name) { - this.name = name; - } - - /** - * Gets the display name of the case represented by this node data. + * Gets the display name of the case. * * @return The case display name. */ @@ -230,7 +383,7 @@ public final class CaseNodeData { } /** - * Sets the display name of the case represented by this node data. + * Sets the display name of the case. * * @param displayName The case display name. */ @@ -238,40 +391,113 @@ public final class CaseNodeData { this.displayName = displayName; } + /** + * Checks whether a given deleted item flag is set for the case. + * + * @param flag The flag to check. + * + * @return True or false. + */ + public boolean isDeletedFlagSet(DeletedFlags flag) { + return (this.deletedItemFlags & flag.getValue()) == flag.getValue(); + } + + /** + * Sets a given deleted item flag. + * + * @param flag The flag to set. + */ + public void setDeletedFlag(DeletedFlags flag) { + this.deletedItemFlags |= flag.getValue(); + } + /** * Gets the node data as a byte array that can be sent to the coordination * service. * * @return The node data as a byte array. * - * @throws IOException If there is an error writing the node data. + * @throws IOException If there is an error writing the node data to the + * array. */ - public byte[] toArray() throws IOException { - ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); - DataOutputStream outputStream = new DataOutputStream(byteStream); - outputStream.writeInt(this.version); - outputStream.writeBoolean(this.errorsOccurred); - outputStream.writeUTF(this.directory.toString()); - outputStream.writeLong(this.createDate.getTime()); - outputStream.writeLong(this.lastAccessDate.getTime()); - outputStream.writeUTF(this.name); - outputStream.writeUTF(this.displayName); - outputStream.writeShort(this.deletedItemFlags); - outputStream.flush(); - byteStream.flush(); - return byteStream.toByteArray(); + private byte[] toArray() throws IOException { + try (ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); DataOutputStream outputStream = new DataOutputStream(byteStream)) { + outputStream.writeInt(this.version); + outputStream.writeByte((byte) (this.errorsOccurred ? 0x80 : 0)); + outputStream.writeUTF(this.directory.toString()); + outputStream.writeLong(this.createDate.getTime()); + outputStream.writeLong(this.lastAccessDate.getTime()); + outputStream.writeUTF(this.name); + outputStream.writeUTF(this.displayName); + outputStream.writeShort(this.deletedItemFlags); + outputStream.writeInt(this.minorVersion); + outputStream.flush(); + byteStream.flush(); + return byteStream.toByteArray(); + } } - public final static class InvalidDataException extends Exception { + /** + * Flags for the various components of a case that can be deleted. + */ + public enum DeletedFlags { + + TEXT_INDEX(1), + CASE_DB(2), + CASE_DIR(4), + DATA_SOURCES(8), + MANIFEST_FILE_NODES(16); + + private final short value; + + /** + * Constructs a flag for a case component that can be deleted. + * + * @param value + */ + private DeletedFlags(int value) { + this.value = (short) value; + } + + /** + * Gets the value of the flag. + * + * @return The value as a short. + */ + private short getValue() { + return value; + } + + } + + /** + * Exception thrown when there is an error reading or writing case node + * data. + */ + public static final class CaseNodeDataException extends Exception { private static final long serialVersionUID = 1L; - private InvalidDataException(String message) { + /** + * Constructs an exception to throw when there is an error reading or + * writing case node data. + * + * @param message The exception message. + */ + private CaseNodeDataException(String message) { super(message); } - private InvalidDataException(String message, Throwable cause) { + /** + * Constructs an exception to throw when there is an error reading or + * writing case node data. + * + * @param message The exception message. + * @param cause The cause of the exception. + */ + private CaseNodeDataException(String message, Throwable cause) { super(message, cause); } } + } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercases/CaseNodeDataCollector.java b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercases/CaseNodeDataCollector.java new file mode 100755 index 0000000000..cfb542d967 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercases/CaseNodeDataCollector.java @@ -0,0 +1,86 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019-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.multiusercases; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData.CaseNodeDataException; +import static org.sleuthkit.autopsy.casemodule.multiusercases.CoordinationServiceUtils.isCaseAutoIngestLogNodePath; +import static org.sleuthkit.autopsy.casemodule.multiusercases.CoordinationServiceUtils.isCaseNameNodePath; +import static org.sleuthkit.autopsy.casemodule.multiusercases.CoordinationServiceUtils.isCaseResourcesNodePath; +import org.sleuthkit.autopsy.coordinationservice.CoordinationService; +import org.sleuthkit.autopsy.coordinationservice.CoordinationService.CoordinationServiceException; +import org.sleuthkit.autopsy.coreutils.Logger; + +/** + * Collects the multi-user case node data stored in the case directory + * coordination service nodes. + */ +final public class CaseNodeDataCollector { + + private static final Logger logger = Logger.getLogger(CaseNodeDataCollector.class.getName()); + + /** + * Collects the multi-user case node data stored in the case directory + * coordination service nodes. + * + * @return The node data for the multi-user cases known to the coordination + * service. + * + * @throws CoordinationServiceException If there is an error interacting + * with the coordination service. + * @throws InterruptedException If the current thread is interrupted + * while waiting for the coordination + * service. + */ + public static List getNodeData() throws CoordinationServiceException, InterruptedException { + final List nodeDataList = new ArrayList<>(); + final CoordinationService coordinationService = CoordinationService.getInstance(); + final List nodePaths = coordinationService.getNodeList(CoordinationService.CategoryNode.CASES); + for (String nodePath : nodePaths) { + /* + * Skip the case name, case resources, and case auto ingest log + * coordination service nodes. They are not used to store case data. + */ + if (isCaseNameNodePath(nodePath) || isCaseResourcesNodePath(nodePath) || isCaseAutoIngestLogNodePath(nodePath)) { + continue; + } + + /* + * Get the case node data from the case directory coordination service node. + */ + try { + final CaseNodeData nodeData = CaseNodeData.readCaseNodeData(nodePath); + nodeDataList.add(nodeData); + } catch (CaseNodeDataException | InterruptedException ex) { + logger.log(Level.WARNING, String.format("Error reading case node data from %s", nodePath), ex); + } + + } + return nodeDataList; + } + + /** + * Private constructor to prevent instantiation of this utility class. + */ + private CaseNodeDataCollector() { + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercases/CoordinationServiceUtils.java b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercases/CoordinationServiceUtils.java new file mode 100755 index 0000000000..d4b0399ff8 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercases/CoordinationServiceUtils.java @@ -0,0 +1,133 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019-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.multiusercases; + +import java.nio.file.Path; +import java.nio.file.Paths; +import org.sleuthkit.autopsy.coreutils.TimeStampUtils; + +/** + * Utility methods for using the coordination service for multi-user cases. + */ +public final class CoordinationServiceUtils { + + private static final String CASE_AUTO_INGEST_LOG_NAME = "AUTO_INGEST_LOG.TXT"; //NON-NLS + private static final String RESOURCES_LOCK_SUFFIX = "_RESOURCES"; //NON-NLS + + /** + * Gets the path of the case resources coordination service node for a case. + * This coordiantion service node is used for case resource locking. + * + * @param caseDirectoryPath The case directory path. + * + * @return The case resources coordination service node path. + */ + public static String getCaseResourcesNodePath(Path caseDirectoryPath) { + return caseDirectoryPath + RESOURCES_LOCK_SUFFIX; + } + + /** + * Gets the path of the case auto ingest log coordination service node for a + * case. This coordination service node is used for serializing case auto + * ingest log writes. + * + * @param caseDirectoryPath The case directory path. + * + * @return The case auto ingest log coordination service node path. + */ + public static String getCaseAutoIngestLogNodePath(Path caseDirectoryPath) { + return Paths.get(caseDirectoryPath.toString(), CASE_AUTO_INGEST_LOG_NAME).toString(); + } + + /** + * Gets the path of the case directory coordination service node for a case. + * This coordination service node is used for locking the case directory and + * for storing data about the case. + * + * @param caseDirectoryPath The case directory path. + * + * @return The case directory coordination service node path. + */ + public static String getCaseDirectoryNodePath(Path caseDirectoryPath) { + return caseDirectoryPath.toString(); + } + + /** + * Gets the path of the case name coordination service node for a case. This + * coordination service node is used to lock the case name so that only one + * node at a time can create a case with a particular name. + * + * @param caseDirectoryPath The case directory path. + * + * @return The case name coordination service node path. + */ + public static String getCaseNameNodePath(Path caseDirectoryPath) { + String caseName = caseDirectoryPath.getFileName().toString(); + if (TimeStampUtils.endsWithTimeStamp(caseName)) { + caseName = TimeStampUtils.removeTimeStamp(caseName); + if (caseName.endsWith("_")) { + caseName = caseName.substring(0, caseName.length() - 1); + } + } + return caseName; + } + + /** + * Determines whether or not a coordination service node path is a case auto + * ingest node path. + * + * @param nodePath The node path. + * + * @return True or false. + */ + public static boolean isCaseAutoIngestLogNodePath(String nodePath) { + return Paths.get(nodePath).getFileName().toString().equals(CASE_AUTO_INGEST_LOG_NAME); + } + + /** + * Determines whether or not a coordination service node path is a case + * resources node path. + * + * @param nodePath The node path. + * + * @return True or false. + */ + public static boolean isCaseResourcesNodePath(String nodePath) { + return Paths.get(nodePath).getFileName().toString().endsWith(RESOURCES_LOCK_SUFFIX); + } + + /** + * Determines whether or not a coordination service node path is a case name + * node path. + * + * @param nodePath The node path. + * + * @return True or false. + */ + public static boolean isCaseNameNodePath(String nodePath) { + return !(nodePath.contains("\\") || nodePath.contains("//")); + } + + /** + * Prevents instantiation of this uitlity class. + */ + private CoordinationServiceUtils() { + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercases/MultiUserCaseNodeDataCollector.java b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercases/MultiUserCaseNodeDataCollector.java deleted file mode 100755 index 63b11728ab..0000000000 --- a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercases/MultiUserCaseNodeDataCollector.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2019-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.multiusercases; - -import java.io.File; -import java.io.IOException; -import java.nio.file.LinkOption; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.text.ParseException; -import java.util.ArrayList; -import java.util.List; -import java.util.logging.Level; -import org.sleuthkit.autopsy.casemodule.CaseMetadata; -import org.sleuthkit.autopsy.coordinationservice.CoordinationService; -import org.sleuthkit.autopsy.coreutils.Logger; - -/** - * Queries the coordination service to collect the multi-user case node data - * stored in the case directory lock ZooKeeper nodes. - */ -final public class MultiUserCaseNodeDataCollector { - - private static final Logger logger = Logger.getLogger(MultiUserCaseNodeDataCollector.class.getName()); - private static final String CASE_AUTO_INGEST_LOG_NAME = "AUTO_INGEST_LOG.TXT"; //NON-NLS - private static final String RESOURCES_LOCK_SUFFIX = "_RESOURCES"; //NON-NLS - - /** - * Queries the coordination service to collect the multi-user case node data - * stored in the case directory lock ZooKeeper nodes. - * - * @return A list of CaseNodedata objects that convert data for a case - * directory lock coordination service node to and from byte arrays. - * - * @throws CoordinationServiceException If there is an error - */ - public static List getNodeData() throws CoordinationService.CoordinationServiceException { - final List cases = new ArrayList<>(); - final CoordinationService coordinationService = CoordinationService.getInstance(); - final List nodeList = coordinationService.getNodeList(CoordinationService.CategoryNode.CASES); - for (String nodeName : nodeList) { - /* - * Ignore auto ingest case name lock nodes. - */ - final Path nodeNameAsPath = Paths.get(nodeName); - if (!(nodeNameAsPath.toString().contains("\\") || nodeNameAsPath.toString().contains("//"))) { - continue; - } - - /* - * Ignore case auto ingest log lock nodes and resource lock nodes. - */ - final String lastNodeNameComponent = nodeNameAsPath.getFileName().toString(); - if (lastNodeNameComponent.equals(CASE_AUTO_INGEST_LOG_NAME)) { - continue; - } - - /* - * Ignore case resources lock nodes. - */ - if (lastNodeNameComponent.endsWith(RESOURCES_LOCK_SUFFIX)) { - continue; - } - - /* - * Get the data from the case directory lock node. This data may not - * exist for "legacy" nodes. If it is missing, create it. - */ - try { - CaseNodeData nodeData; - byte[] nodeBytes = coordinationService.getNodeData(CoordinationService.CategoryNode.CASES, nodeName); - if (nodeBytes != null && nodeBytes.length > 0) { - nodeData = new CaseNodeData(nodeBytes); - if (nodeData.getVersion() == 0) { - /* - * Version 0 case node data was only written if errors - * occurred during an auto ingest job and consisted of - * only the set errors flag. - */ - nodeData = createNodeDataFromCaseMetadata(nodeName, true); - } - } else { - nodeData = createNodeDataFromCaseMetadata(nodeName, false); - } - cases.add(nodeData); - - } catch (CoordinationService.CoordinationServiceException | InterruptedException | IOException | ParseException | CaseMetadata.CaseMetadataException ex) { - logger.log(Level.SEVERE, String.format("Error getting coordination service node data for %s", nodeName), ex); - } - - } - return cases; - } - - /** - * Creates and saves case directory lock coordination service node data from - * the metadata file for the case associated with the node. - * - * @param nodeName The coordination service node name, i.e., the case - * directory path. - * @param errorsOccurred Whether or not errors occurred during an auto - * ingest job for the case. - * - * @return A CaseNodedata object. - * - * @throws IOException If there is an error writing the - * node data to a byte array. - * @throws CaseMetadataException If there is an error reading the - * case metadata file. - * @throws ParseException If there is an error parsing a date - * from the case metadata file. - * @throws CoordinationServiceException If there is an error interacting - * with the coordination service. - * @throws InterruptedException If a coordination service operation - * is interrupted. - */ - private static CaseNodeData createNodeDataFromCaseMetadata(String nodeName, boolean errorsOccurred) throws IOException, CaseMetadata.CaseMetadataException, ParseException, CoordinationService.CoordinationServiceException, InterruptedException { - CaseNodeData nodeData = null; - Path caseDirectoryPath = Paths.get(nodeName).toRealPath(LinkOption.NOFOLLOW_LINKS); - File caseDirectory = caseDirectoryPath.toFile(); - if (caseDirectory.exists()) { - File[] files = caseDirectory.listFiles(); - for (File file : files) { - String name = file.getName().toLowerCase(); - if (name.endsWith(CaseMetadata.getFileExtension())) { - CaseMetadata metadata = new CaseMetadata(Paths.get(file.getAbsolutePath())); - nodeData = new CaseNodeData(metadata); - nodeData.setErrorsOccurred(errorsOccurred); - break; - } - } - } - if (nodeData != null) { - CoordinationService coordinationService = CoordinationService.getInstance(); - coordinationService.setNodeData(CoordinationService.CategoryNode.CASES, nodeName, nodeData.toArray()); - return nodeData; - } else { - throw new IOException(String.format("Could not find case metadata file for %s", nodeName)); - } - } - - /** - * Private constructor to prevent instantiation of this utility class. - */ - private MultiUserCaseNodeDataCollector() { - } - -} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/Bundle.properties-MERGED index 7ca1d937a3..c4801e56ed 100755 --- a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/Bundle.properties-MERGED @@ -1,5 +1,12 @@ +MultiUserCaseBrowserCustomizer.column.caseDbDeleteStatus=Case Database Deleted +MultiUserCaseBrowserCustomizer.column.caseDirDeleteStatus=Case Directory Deleted MultiUserCaseBrowserCustomizer.column.createTime=Create Time +MultiUserCaseBrowserCustomizer.column.dataSourcesDeleteStatus=Data Sources Deleted MultiUserCaseBrowserCustomizer.column.directory=Directory MultiUserCaseBrowserCustomizer.column.displayName=Name MultiUserCaseBrowserCustomizer.column.lastAccessTime=Last Access Time +MultiUserCaseBrowserCustomizer.column.manifestFileZNodesDeleteStatus=Manifest Znodes Deleted +MultiUserCaseBrowserCustomizer.column.textIndexDeleteStatus=Text Index Deleted +MultiUserCaseNode.column.createTime=False +MultiUserCaseNode.columnValue.true=True MultiUserCasesBrowserPanel.waitNode.message=Please Wait... diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCaseBrowserCustomizer.java b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCaseBrowserCustomizer.java index 7653da89db..692e2f2cf1 100755 --- a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCaseBrowserCustomizer.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCaseBrowserCustomizer.java @@ -141,13 +141,23 @@ public interface MultiUserCaseBrowserCustomizer { "MultiUserCaseBrowserCustomizer.column.displayName=Name", "MultiUserCaseBrowserCustomizer.column.createTime=Create Time", "MultiUserCaseBrowserCustomizer.column.directory=Directory", - "MultiUserCaseBrowserCustomizer.column.lastAccessTime=Last Access Time" + "MultiUserCaseBrowserCustomizer.column.lastAccessTime=Last Access Time", + "MultiUserCaseBrowserCustomizer.column.manifestFileZNodesDeleteStatus=Manifest Znodes Deleted", + "MultiUserCaseBrowserCustomizer.column.dataSourcesDeleteStatus=Data Sources Deleted", + "MultiUserCaseBrowserCustomizer.column.textIndexDeleteStatus=Text Index Deleted", + "MultiUserCaseBrowserCustomizer.column.caseDbDeleteStatus=Case Database Deleted", + "MultiUserCaseBrowserCustomizer.column.caseDirDeleteStatus=Case Directory Deleted" }) public enum Column { DISPLAY_NAME(Bundle.MultiUserCaseBrowserCustomizer_column_displayName()), CREATE_DATE(Bundle.MultiUserCaseBrowserCustomizer_column_createTime()), DIRECTORY(Bundle.MultiUserCaseBrowserCustomizer_column_directory()), - LAST_ACCESS_DATE(Bundle.MultiUserCaseBrowserCustomizer_column_lastAccessTime()); + LAST_ACCESS_DATE(Bundle.MultiUserCaseBrowserCustomizer_column_lastAccessTime()), + MANIFEST_FILE_ZNODES_DELETE_STATUS(Bundle.MultiUserCaseBrowserCustomizer_column_manifestFileZNodesDeleteStatus()), + DATA_SOURCES_DELETE_STATUS(Bundle.MultiUserCaseBrowserCustomizer_column_dataSourcesDeleteStatus()), + TEXT_INDEX_DELETE_STATUS(Bundle.MultiUserCaseBrowserCustomizer_column_textIndexDeleteStatus()), + CASE_DB_DELETE_STATUS(Bundle.MultiUserCaseBrowserCustomizer_column_caseDbDeleteStatus()), + CASE_DIR_DELETE_STATUS(Bundle.MultiUserCaseBrowserCustomizer_column_caseDirDeleteStatus()); private final String displayName; diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCaseNode.java b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCaseNode.java index b54cae2238..11e73cfb12 100755 --- a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCaseNode.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCaseNode.java @@ -25,8 +25,10 @@ import javax.swing.Action; import org.openide.nodes.AbstractNode; import org.openide.nodes.Children; import org.openide.nodes.Sheet; +import org.openide.util.NbBundle; import org.openide.util.lookup.Lookups; import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; +import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData.DeletedFlags; import org.sleuthkit.autopsy.casemodule.multiusercasesbrowser.MultiUserCaseBrowserCustomizer.Column; import org.sleuthkit.autopsy.datamodel.NodeProperty; @@ -75,6 +77,21 @@ final class MultiUserCaseNode extends AbstractNode { case LAST_ACCESS_DATE: sheetSet.put(new NodeProperty<>(propName, propName, propName, caseNodeData.getLastAccessDate())); break; + case MANIFEST_FILE_ZNODES_DELETE_STATUS: + sheetSet.put(new NodeProperty<>(propName, propName, propName, isDeleted(DeletedFlags.MANIFEST_FILE_NODES))); + break; + case DATA_SOURCES_DELETE_STATUS: + sheetSet.put(new NodeProperty<>(propName, propName, propName, isDeleted(DeletedFlags.DATA_SOURCES))); + break; + case TEXT_INDEX_DELETE_STATUS: + sheetSet.put(new NodeProperty<>(propName, propName, propName, isDeleted(DeletedFlags.TEXT_INDEX))); + break; + case CASE_DB_DELETE_STATUS: + sheetSet.put(new NodeProperty<>(propName, propName, propName, isDeleted(DeletedFlags.CASE_DB))); + break; + case CASE_DIR_DELETE_STATUS: + sheetSet.put(new NodeProperty<>(propName, propName, propName, isDeleted(DeletedFlags.CASE_DIR))); + break; default: break; } @@ -95,4 +112,20 @@ final class MultiUserCaseNode extends AbstractNode { return customizer.getPreferredAction(caseNodeData); } + /** + * Interprets the deletion status of part of a case. + * + * @param flag The coordination service node data deleted items flag + * to interpret. + * + * @return A string stating "True" or "False." + */ + @NbBundle.Messages({ + "MultiUserCaseNode.columnValue.true=True", + "MultiUserCaseNode.column.createTime=False", + }) + private String isDeleted(CaseNodeData.DeletedFlags flag) { + return caseNodeData.isDeletedFlagSet(flag) ? "True" : "False"; + } + } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCasesBrowserPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCasesBrowserPanel.java index 1f19fe2652..154e692663 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCasesBrowserPanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCasesBrowserPanel.java @@ -43,6 +43,8 @@ import org.sleuthkit.autopsy.casemodule.multiusercasesbrowser.MultiUserCaseBrows public final class MultiUserCasesBrowserPanel extends javax.swing.JPanel implements ExplorerManager.Provider { private static final long serialVersionUID = 1L; + private static final int NAME_COLUMN_INDEX = 0; + private static final int NAME_COLUMN_WIDTH = 150; private final ExplorerManager explorerManager; private final MultiUserCaseBrowserCustomizer customizer; private final OutlineView outlineView; @@ -103,6 +105,11 @@ public final class MultiUserCasesBrowserPanel extends javax.swing.JPanel impleme } } + /* + * Give the case name column a greater width. + */ + outline.getColumnModel().getColumn(NAME_COLUMN_INDEX).setPreferredWidth(NAME_COLUMN_WIDTH); + /* * Hide the root node and configure the node selection mode. */ diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCasesRootNode.java b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCasesRootNode.java index 0d214e856d..45e8c02be4 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCasesRootNode.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCasesRootNode.java @@ -25,7 +25,7 @@ import org.openide.nodes.ChildFactory; import org.openide.nodes.Children; import org.openide.nodes.Node; import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; -import org.sleuthkit.autopsy.casemodule.multiusercases.MultiUserCaseNodeDataCollector; +import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeDataCollector; import org.sleuthkit.autopsy.coordinationservice.CoordinationService; import org.sleuthkit.autopsy.coreutils.Logger; @@ -63,9 +63,9 @@ final class MultiUserCasesRootNode extends AbstractNode { @Override protected boolean createKeys(List keys) { try { - List caseNodeData = MultiUserCaseNodeDataCollector.getNodeData(); + List caseNodeData = CaseNodeDataCollector.getNodeData(); keys.addAll(caseNodeData); - } catch (CoordinationService.CoordinationServiceException ex) { + } catch (CoordinationService.CoordinationServiceException | InterruptedException ex) { logger.log(Level.SEVERE, "Failed to get case node data from coodination service", ex); } return true; 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 fc30931457..21c7b81c76 100755 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/Bundle.properties-MERGED @@ -26,13 +26,29 @@ DataContentViewerOtherCases.earliestCaseLabel.text=Central Repository Starting D DataContentViewerOtherCases.foundInLabel.text= DataContentViewerOtherCases.title=Other Occurrences DataContentViewerOtherCases.toolTip=Displays instances of the selected file/artifact from other occurrences. +DataContentViewerOtherCasesModel.csvHeader.attribute=Matched Attribute +DataContentViewerOtherCasesModel.csvHeader.case=Case +DataContentViewerOtherCasesModel.csvHeader.comment=Comment +DataContentViewerOtherCasesModel.csvHeader.dataSource=Data Source +DataContentViewerOtherCasesModel.csvHeader.device=Device +DataContentViewerOtherCasesModel.csvHeader.known=Known +DataContentViewerOtherCasesModel.csvHeader.path=Path +DataContentViewerOtherCasesModel.csvHeader.value=Attribute Value +OccurrencePanel.caseCreatedDateLabel.text=Created Date: +OccurrencePanel.caseDetails.text=Case Details +OccurrencePanel.caseNameLabel.text=Name: +OccurrencePanel.commonProperties.text=Common Properties +OccurrencePanel.commonPropertyCommentLabel.text=Comment: +OccurrencePanel.commonPropertyKnownStatusLabel.text=Known Status: +OccurrencePanel.commonPropertyTypeLabel.text=Type: +OccurrencePanel.commonPropertyValueLabel.text=Value: +OccurrencePanel.dataSourceDetails.text=Data Source Details +OccurrencePanel.dataSourceNameLabel.text=Name: +OccurrencePanel.fileDetails.text=File Details +OccurrencePanel.filePathLabel.text=File Path: OtherOccurrencesCasesTableModel.case=Case OtherOccurrencesCasesTableModel.noData=No Data. -OtherOccurrencesFilesTableModel.attribute=Matched Attribute -OtherOccurrencesFilesTableModel.comment=Comment -OtherOccurrencesFilesTableModel.dataSource=Data Source -OtherOccurrencesFilesTableModel.device=Device -OtherOccurrencesFilesTableModel.known=Known +OtherOccurrencesDataSourcesTableModel.dataSourceName=Data Source Name +OtherOccurrencesDataSourcesTableModel.noData=No Data. +OtherOccurrencesFilesTableModel.fileName=File Name OtherOccurrencesFilesTableModel.noData=No Data. -OtherOccurrencesFilesTableModel.path=Path -OtherOccurrencesFilesTableModel.value=Attribute Value diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.form b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.form index 9b30385d0f..7f1449178e 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.form +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.form @@ -46,11 +46,11 @@ - + - + @@ -68,123 +68,114 @@ - + - + + + + - + - + + - - - - - + + + + + + + + + + + - - - - - + + + + + + + + + + + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + - + + @@ -195,6 +186,11 @@ + + + + + @@ -217,6 +213,11 @@ + + + + + @@ -231,11 +232,8 @@ - - - - -
+ +
@@ -243,10 +241,10 @@
- + - + @@ -261,7 +259,7 @@ - + @@ -273,7 +271,7 @@ - + @@ -284,6 +282,21 @@
+ + + + + + + + + + + + + + +
diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.java b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.java index 34254f902b..5427b2be42 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.java @@ -1,7 +1,7 @@ /* * Central Repository * - * Copyright 2015-2019 Basis Technology Corp. + * Copyright 2017-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,6 +21,8 @@ package org.sleuthkit.autopsy.centralrepository.contentviewer; import java.awt.Component; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; import java.io.BufferedWriter; import java.io.File; import java.io.IOException; @@ -48,9 +50,6 @@ import static javax.swing.JOptionPane.PLAIN_MESSAGE; import static javax.swing.JOptionPane.ERROR_MESSAGE; import javax.swing.JPanel; import javax.swing.filechooser.FileNameExtensionFilter; -import javax.swing.table.DefaultTableModel; -import javax.swing.table.TableCellRenderer; -import javax.swing.table.TableColumn; import javax.swing.table.TableModel; import javax.swing.table.TableRowSorter; import org.joda.time.DateTimeZone; @@ -93,50 +92,58 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi 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_noArtifacts()); - private static final int DEFAULT_MIN_CELL_WIDTH = 15; + private static final CorrelationCaseWrapper NO_RESULTS_CASE = new CorrelationCaseWrapper(Bundle.DataContentViewerOtherCases_table_noResultsFound()); - private final OtherOccurrencesFilesTableModel tableModel; + private final OtherOccurrencesFilesTableModel filesTableModel; private final OtherOccurrencesCasesTableModel casesTableModel; + private final OtherOccurrencesDataSourcesTableModel dataSourcesTableModel; + private OccurrencePanel occurrencePanel; private final Collection correlationAttributes; - private String dataSourceName = ""; - private String deviceId = ""; + private String dataSourceName = ""; //the data source of the file which the content viewer is being populated for + private String deviceId = ""; //the device id of the data source for the file which the content viewer is being populated for /** * Could be null. */ - private AbstractFile file; + private AbstractFile file; //the file which the content viewer is being populated for /** * Creates new form DataContentViewerOtherCases */ public DataContentViewerOtherCases() { - this.tableModel = new OtherOccurrencesFilesTableModel(); + this.filesTableModel = new OtherOccurrencesFilesTableModel(); this.casesTableModel = new OtherOccurrencesCasesTableModel(); + this.dataSourcesTableModel = new OtherOccurrencesDataSourcesTableModel(); this.correlationAttributes = new ArrayList<>(); - + occurrencePanel = new OccurrencePanel(); initComponents(); customizeComponents(); + + detailsPanelScrollPane.addComponentListener(new ComponentAdapter() { + @Override + public void componentResized(ComponentEvent componentEvent) { + //when its resized make sure the width of the panel resizes to match the scroll pane width to avoid a horizontal scroll bar + occurrencePanel.setPreferredSize(new java.awt.Dimension(detailsPanelScrollPane.getPreferredSize().width, occurrencePanel.getPreferredSize().height)); + detailsPanelScrollPane.setViewportView(occurrencePanel); + } + }); reset(); } private void customizeComponents() { - ActionListener actList = new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - JMenuItem jmi = (JMenuItem) e.getSource(); - if (jmi.equals(selectAllMenuItem)) { - filesTable.selectAll(); - } else if (jmi.equals(showCaseDetailsMenuItem)) { - showCaseDetails(filesTable.getSelectedRow()); - } else if (jmi.equals(exportToCSVMenuItem)) { - try { - saveToCSV(); - } catch (NoCurrentCaseException ex) { - LOGGER.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS - } - } else if (jmi.equals(showCommonalityMenuItem)) { - showCommonalityDetails(); + ActionListener actList = (ActionEvent e) -> { + JMenuItem jmi = (JMenuItem) e.getSource(); + if (jmi.equals(selectAllMenuItem)) { + filesTable.selectAll(); + } else if (jmi.equals(showCaseDetailsMenuItem)) { + showCaseDetails(filesTable.getSelectedRow()); + } else if (jmi.equals(exportToCSVMenuItem)) { + try { + saveToCSV(); + } catch (NoCurrentCaseException ex) { + LOGGER.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS } + } else if (jmi.equals(showCommonalityMenuItem)) { + showCommonalityDetails(); } }; @@ -145,10 +152,6 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi showCaseDetailsMenuItem.addActionListener(actList); showCommonalityMenuItem.addActionListener(actList); - // Set background of every nth row as light grey. - TableCellRenderer renderer = new OtherOccurrencesFilesTableCellRenderer(); - filesTable.setDefaultRenderer(Object.class, renderer); - // Configure column sorting. TableRowSorter sorter = new TableRowSorter<>(filesTable.getModel()); filesTable.setRowSorter(sorter); @@ -162,8 +165,18 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi updateOnDataSourceSelection(); } }); + + //alows resizing of the 4th section + filesTable.getSelectionModel().addListSelectionListener((e) -> { + if (Case.isCaseOpen()) { + occurrencePanel = new OccurrencePanel(); + updateOnFileSelection(); + } + }); + //sort tables alphabetically initially casesTable.getRowSorter().toggleSortOrder(0); dataSourcesTable.getRowSorter().toggleSortOrder(0); + filesTable.getRowSorter().toggleSortOrder(0); } @Messages({"DataContentViewerOtherCases.correlatedArtifacts.isEmpty=There are no files or artifacts to correlate.", @@ -218,54 +231,43 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi "DataContentViewerOtherCases.caseDetailsDialog.noCaseNameError=Error", "DataContentViewerOtherCases.noOpenCase.errMsg=No open case available."}) private void showCaseDetails(int selectedRowViewIdx) { - String caseDisplayName = Bundle.DataContentViewerOtherCases_caseDetailsDialog_noCaseNameError(); + String details = Bundle.DataContentViewerOtherCases_caseDetailsDialog_noDetails(); try { if (-1 != selectedRowViewIdx) { EamDb dbManager = EamDb.getInstance(); int selectedRowModelIdx = filesTable.convertRowIndexToModel(selectedRowViewIdx); - OtherOccurrenceNodeInstanceData nodeData = (OtherOccurrenceNodeInstanceData) tableModel.getRow(selectedRowModelIdx); - CorrelationCase eamCasePartial = nodeData.getCorrelationAttributeInstance().getCorrelationCase(); - if (eamCasePartial == null) { - JOptionPane.showConfirmDialog(showCaseDetailsMenuItem, - Bundle.DataContentViewerOtherCases_caseDetailsDialog_noDetailsReference(), - caseDisplayName, - DEFAULT_OPTION, PLAIN_MESSAGE); - return; + List rowList = filesTableModel.getListOfNodesForFile(selectedRowModelIdx); + if (!rowList.isEmpty()) { + if (rowList.get(0) instanceof OtherOccurrenceNodeInstanceData) { + CorrelationCase eamCasePartial = ((OtherOccurrenceNodeInstanceData) rowList.get(0)).getCorrelationAttributeInstance().getCorrelationCase(); + caseDisplayName = eamCasePartial.getDisplayName(); + // query case details + CorrelationCase eamCase = dbManager.getCaseByUUID(eamCasePartial.getCaseUUID()); + if (eamCase != null) { + details = eamCase.getCaseDetailsOptionsPaneDialog(); + } else { + details = Bundle.DataContentViewerOtherCases_caseDetailsDialog_noDetails(); + } + } else { + details = Bundle.DataContentViewerOtherCases_caseDetailsDialog_notSelected(); + } + } else { + details = Bundle.DataContentViewerOtherCases_caseDetailsDialog_noDetailsReference(); } - caseDisplayName = eamCasePartial.getDisplayName(); - // query case details - CorrelationCase eamCase = dbManager.getCaseByUUID(eamCasePartial.getCaseUUID()); - if (eamCase == null) { - JOptionPane.showConfirmDialog(showCaseDetailsMenuItem, - Bundle.DataContentViewerOtherCases_caseDetailsDialog_noDetails(), - caseDisplayName, - DEFAULT_OPTION, PLAIN_MESSAGE); - return; - } - - // display case details - JOptionPane.showConfirmDialog(showCaseDetailsMenuItem, - eamCase.getCaseDetailsOptionsPaneDialog(), - caseDisplayName, - DEFAULT_OPTION, PLAIN_MESSAGE); - } else { - JOptionPane.showConfirmDialog(showCaseDetailsMenuItem, - Bundle.DataContentViewerOtherCases_caseDetailsDialog_notSelected(), - caseDisplayName, - DEFAULT_OPTION, PLAIN_MESSAGE); } } catch (EamDbException ex) { LOGGER.log(Level.SEVERE, "Error loading case details", ex); + } finally { JOptionPane.showConfirmDialog(showCaseDetailsMenuItem, - Bundle.DataContentViewerOtherCases_caseDetailsDialog_noDetails(), + details, caseDisplayName, DEFAULT_OPTION, PLAIN_MESSAGE); } } private void saveToCSV() throws NoCurrentCaseException { - if (0 != filesTable.getSelectedRowCount()) { + if (casesTableModel.getRowCount() > 0) { Calendar now = Calendar.getInstance(); String fileName = String.format("%1$tY%1$tm%1$te%1$tI%1$tM%1$tS_other_data_sources.csv", now); CSVFileChooser.setCurrentDirectory(new File(Case.getCurrentCaseThrows().getExportDirectory())); @@ -279,45 +281,46 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi if (!selectedFile.getName().endsWith(".csv")) { // NON-NLS selectedFile = new File(selectedFile.toString() + ".csv"); // NON-NLS } - - writeSelectedRowsToFileAsCSV(selectedFile); + writeOtherOccurrencesToFileAsCSV(selectedFile); } } } - private void writeSelectedRowsToFileAsCSV(File destFile) { - StringBuilder content; - int[] selectedRowViewIndices = filesTable.getSelectedRows(); - int colCount = tableModel.getColumnCount(); - + @Messages({ + "DataContentViewerOtherCasesModel.csvHeader.case=Case", + "DataContentViewerOtherCasesModel.csvHeader.device=Device", + "DataContentViewerOtherCasesModel.csvHeader.dataSource=Data Source", + "DataContentViewerOtherCasesModel.csvHeader.attribute=Matched Attribute", + "DataContentViewerOtherCasesModel.csvHeader.value=Attribute Value", + "DataContentViewerOtherCasesModel.csvHeader.known=Known", + "DataContentViewerOtherCasesModel.csvHeader.path=Path", + "DataContentViewerOtherCasesModel.csvHeader.comment=Comment" + }) + /** + * Write data for all cases in the content viewer to a CSV file + */ + private void writeOtherOccurrencesToFileAsCSV(File destFile) { try (BufferedWriter writer = Files.newBufferedWriter(destFile.toPath())) { - - // write column names - content = new StringBuilder(""); - for (int colIdx = 0; colIdx < colCount; colIdx++) { - content.append('"').append(tableModel.getColumnName(colIdx)).append('"'); - if (colIdx < (colCount - 1)) { - content.append(","); + //write headers + StringBuilder headers = new StringBuilder("\""); + headers.append(Bundle.DataContentViewerOtherCasesModel_csvHeader_case()) + .append(OtherOccurrenceNodeInstanceData.getCsvItemSeparator()).append(Bundle.DataContentViewerOtherCasesModel_csvHeader_dataSource()) + .append(OtherOccurrenceNodeInstanceData.getCsvItemSeparator()).append(Bundle.DataContentViewerOtherCasesModel_csvHeader_attribute()) + .append(OtherOccurrenceNodeInstanceData.getCsvItemSeparator()).append(Bundle.DataContentViewerOtherCasesModel_csvHeader_value()) + .append(OtherOccurrenceNodeInstanceData.getCsvItemSeparator()).append(Bundle.DataContentViewerOtherCasesModel_csvHeader_known()) + .append(OtherOccurrenceNodeInstanceData.getCsvItemSeparator()).append(Bundle.DataContentViewerOtherCasesModel_csvHeader_path()) + .append(OtherOccurrenceNodeInstanceData.getCsvItemSeparator()).append(Bundle.DataContentViewerOtherCasesModel_csvHeader_comment()) + .append('"').append(System.getProperty("line.separator")); + writer.write(headers.toString()); + //write content + for (CorrelationAttributeInstance corAttr : correlationAttributes) { + Map correlatedNodeDataMap = new HashMap<>(0); + // get correlation and reference set instances from DB + correlatedNodeDataMap.putAll(getCorrelatedInstances(corAttr, dataSourceName, deviceId)); + for (OtherOccurrenceNodeInstanceData nodeData : correlatedNodeDataMap.values()) { + writer.write(nodeData.toCsvString()); } } - - content.append(System.getProperty("line.separator")); - writer.write(content.toString()); - - // write rows - for (int rowViewIdx : selectedRowViewIndices) { - content = new StringBuilder(""); - for (int colIdx = 0; colIdx < colCount; colIdx++) { - int rowModelIdx = filesTable.convertRowIndexToModel(rowViewIdx); - content.append('"').append(tableModel.getValueAt(rowModelIdx, colIdx)).append('"'); - if (colIdx < (colCount - 1)) { - content.append(","); - } - } - content.append(System.getProperty("line.separator")); - writer.write(content.toString()); - } - } catch (IOException ex) { LOGGER.log(Level.SEVERE, "Error writing selected rows to CSV.", ex); } @@ -329,11 +332,15 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi private void reset() { // start with empty table casesTableModel.clearTable(); - ((DefaultTableModel) dataSourcesTable.getModel()).setRowCount(0); - tableModel.clearTable(); + dataSourcesTableModel.clearTable(); + filesTableModel.clearTable(); correlationAttributes.clear(); earliestCaseDate.setText(Bundle.DataContentViewerOtherCases_earliestCaseNotAvailable()); foundInLabel.setText(""); + //calling getPreferredSize has a side effect of ensuring it has a preferred size which reflects the contents which are visible + occurrencePanel = new OccurrencePanel(); + occurrencePanel.getPreferredSize(); + detailsPanelScrollPane.setViewportView(occurrencePanel); } @Override @@ -364,6 +371,7 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi @Override public int isPreferred(Node node) { return 1; + } /** @@ -558,7 +566,6 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi try { final Case openCase = Case.getCurrentCaseThrows(); String caseUUID = openCase.getName(); - HashMap nodeDataMap = new HashMap<>(); if (EamDb.isEnabled()) { @@ -583,7 +590,6 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi } } } - if (corAttr.getCorrelationType().getDisplayName().equals("Files")) { List caseDbFiles = getCaseDbMatches(corAttr, openCase); @@ -591,7 +597,6 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi addOrUpdateNodeData(openCase, nodeDataMap, caseDbFile); } } - return nodeDataMap; } catch (EamDbException ex) { LOGGER.log(Level.SEVERE, "Error getting artifact instances from database.", ex); // NON-NLS @@ -748,14 +753,14 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi dataSources.add(makeDataSourceString(nodeData.getCorrelationAttributeInstance().getCorrelationCase().getCaseUUID(), nodeData.getDeviceID(), nodeData.getDataSourceName())); caseNames.put(nodeData.getCorrelationAttributeInstance().getCorrelationCase().getCaseUUID(), nodeData.getCorrelationAttributeInstance().getCorrelationCase()); } catch (EamDbException ex) { - LOGGER.log(Level.WARNING, "Unable to get correlation case for displaying other occurrence for case: " + nodeData.getCaseName()); + LOGGER.log(Level.WARNING, "Unable to get correlation case for displaying other occurrence for case: " + nodeData.getCaseName(), ex); } } else { try { dataSources.add(makeDataSourceString(Case.getCurrentCaseThrows().getName(), nodeData.getDeviceID(), nodeData.getDataSourceName())); caseNames.put(Case.getCurrentCaseThrows().getName(), new CorrelationCase(Case.getCurrentCaseThrows().getName(), Case.getCurrentCaseThrows().getDisplayName())); } catch (NoCurrentCaseException ex) { - LOGGER.log(Level.WARNING, "No current case open for other occurrences"); + LOGGER.log(Level.WARNING, "No current case open for other occurrences", ex); } } totalCount++; @@ -770,7 +775,6 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi } else if (caseCount == 0) { casesTableModel.addCorrelationCase(NO_RESULTS_CASE); } - setColumnWidths(); setEarliestCaseDate(); foundInLabel.setText(String.format(Bundle.DataContentViewerOtherCases_foundIn_text(), totalCount, caseCount, dataSources.size())); if (caseCount > 0) { @@ -791,33 +795,40 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi */ private void updateOnCaseSelection() { int[] selectedCaseIndexes = casesTable.getSelectedRows(); - DefaultTableModel dataSourceModel = (DefaultTableModel) dataSourcesTable.getModel(); - dataSourceModel.setRowCount(0); - tableModel.clearTable(); - for (CorrelationAttributeInstance corAttr : correlationAttributes) { - Map correlatedNodeDataMap = new HashMap<>(0); + dataSourcesTableModel.clearTable(); + filesTableModel.clearTable(); - // get correlation and reference set instances from DB - correlatedNodeDataMap.putAll(getCorrelatedInstances(corAttr, dataSourceName, deviceId)); - for (OtherOccurrenceNodeInstanceData nodeData : correlatedNodeDataMap.values()) { - for (int selectedRow : selectedCaseIndexes) { - try { - if (nodeData.isCentralRepoNode()) { - if (casesTableModel.getCorrelationCase(casesTable.convertRowIndexToModel(selectedRow)) != null - && ((CorrelationCase) casesTableModel.getCorrelationCase(casesTable.convertRowIndexToModel(selectedRow))).getCaseUUID().equals(nodeData.getCorrelationAttributeInstance().getCorrelationCase().getCaseUUID())) { - dataSourceModel.addRow(new Object[]{nodeData.getDataSourceName(), nodeData.getDeviceID()}); + if (selectedCaseIndexes.length == 0) { + //special case when no cases are selected + occurrencePanel = new OccurrencePanel(); + occurrencePanel.getPreferredSize(); + detailsPanelScrollPane.setViewportView(occurrencePanel); + } else { + for (CorrelationAttributeInstance corAttr : correlationAttributes) { + Map correlatedNodeDataMap = new HashMap<>(0); + + // get correlation and reference set instances from DB + correlatedNodeDataMap.putAll(getCorrelatedInstances(corAttr, dataSourceName, deviceId)); + for (OtherOccurrenceNodeInstanceData nodeData : correlatedNodeDataMap.values()) { + for (int selectedRow : selectedCaseIndexes) { + try { + if (nodeData.isCentralRepoNode()) { + if (casesTableModel.getCorrelationCase(casesTable.convertRowIndexToModel(selectedRow)) != null + && casesTableModel.getCorrelationCase(casesTable.convertRowIndexToModel(selectedRow)).getCaseUUID().equals(nodeData.getCorrelationAttributeInstance().getCorrelationCase().getCaseUUID())) { + dataSourcesTableModel.addNodeData(nodeData); + } + } else { + dataSourcesTableModel.addNodeData(nodeData); } - } else { - dataSourceModel.addRow(new Object[]{nodeData.getDataSourceName(), nodeData.getDeviceID()}); + } 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()); } } } - } - if (dataSourcesTable.getRowCount() > 0) { - dataSourcesTable.setRowSelectionInterval(0, 0); + if (dataSourcesTable.getRowCount() > 0) { + dataSourcesTable.setRowSelectionInterval(0, 0); + } } } @@ -827,9 +838,8 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi */ private void updateOnDataSourceSelection() { int[] selectedCaseIndexes = casesTable.getSelectedRows(); - DefaultTableModel dataSourceModel = (DefaultTableModel) dataSourcesTable.getModel(); int[] selectedDataSources = dataSourcesTable.getSelectedRows(); - tableModel.clearTable(); + filesTableModel.clearTable(); for (CorrelationAttributeInstance corAttr : correlationAttributes) { Map correlatedNodeDataMap = new HashMap<>(0); @@ -841,43 +851,94 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi try { if (nodeData.isCentralRepoNode()) { if (casesTableModel.getCorrelationCase(casesTable.convertRowIndexToModel(selectedCaseRow)) != null - && ((CorrelationCase) casesTableModel.getCorrelationCase(casesTable.convertRowIndexToModel(selectedCaseRow))).getCaseUUID().equals(nodeData.getCorrelationAttributeInstance().getCorrelationCase().getCaseUUID()) - && dataSourceModel.getValueAt(dataSourcesTable.convertRowIndexToModel(selectedDataSourceRow), 1).toString().equals(nodeData.getDeviceID())) { - tableModel.addNodeData(nodeData); + && casesTableModel.getCorrelationCase(casesTable.convertRowIndexToModel(selectedCaseRow)).getCaseUUID().equals(nodeData.getCorrelationAttributeInstance().getCorrelationCase().getCaseUUID()) + && dataSourcesTableModel.getDeviceIdForRow(dataSourcesTable.convertRowIndexToModel(selectedDataSourceRow)).equals(nodeData.getDeviceID())) { + filesTableModel.addNodeData(nodeData); } } else { - if (dataSourceModel.getValueAt(dataSourcesTable.convertRowIndexToModel(selectedDataSourceRow), 1).toString().equals(nodeData.getDeviceID())) { - tableModel.addNodeData(nodeData); + 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()); + LOGGER.log(Level.WARNING, "Unable to get correlation attribute instance from OtherOccurrenceNodeInstanceData for case " + nodeData.getCaseName(), ex); } } } } } + if (filesTable.getRowCount() > 0) { + filesTable.setRowSelectionInterval(0, 0); + } } /** - * Adjust column widths to their preferred values. + * Update the data displayed in the details section to be correct for the + * currently selected File */ - private void setColumnWidths() { - for (int idx = 0; idx < tableModel.getColumnCount(); idx++) { - TableColumn column = filesTable.getColumnModel().getColumn(idx); - column.setMinWidth(DEFAULT_MIN_CELL_WIDTH); - int columnWidth = tableModel.getColumnPreferredWidth(idx); - if (columnWidth > 0) { - column.setPreferredWidth(columnWidth); + private void updateOnFileSelection() { + if (filesTable.getSelectedRowCount() == 1) { + //if there is one file selected update the deatils to show the data for that file + occurrencePanel = new OccurrencePanel(filesTableModel.getListOfNodesForFile(filesTable.convertRowIndexToModel(filesTable.getSelectedRow()))); + } else if (dataSourcesTable.getSelectedRowCount() == 1) { + //if no files were selected and only one data source is selected update the information to reflect the data source + String caseName = dataSourcesTableModel.getCaseNameForRow(dataSourcesTable.convertRowIndexToModel(dataSourcesTable.getSelectedRow())); + String dsName = dataSourcesTableModel.getValueAt(dataSourcesTable.convertRowIndexToModel(dataSourcesTable.getSelectedRow()), 0).toString(); + String caseCreatedDate = ""; + for (int row : casesTable.getSelectedRows()) { + if (casesTableModel.getValueAt(casesTable.convertRowIndexToModel(row), 0).toString().equals(caseName)) { + caseCreatedDate = getCaseCreatedDate(row); + break; + } } - } - for (int idx = 0; idx < dataSourcesTable.getColumnCount(); idx++) { - if (dataSourcesTable.getColumnModel().getColumn(idx).getHeaderValue().toString().equals(Bundle.DataContentViewerOtherCases_dataSources_header_text())) { - dataSourcesTable.getColumnModel().getColumn(idx).setPreferredWidth(100); + occurrencePanel = new OccurrencePanel(caseName, caseCreatedDate, dsName); + } else if (casesTable.getSelectedRowCount() == 1) { + //if no files were selected and a number of data source other than 1 are selected + //update the information to reflect the case + String createdDate = ""; + String caseName = ""; + if (casesTable.getRowCount() > 0) { + caseName = casesTableModel.getValueAt(casesTable.convertRowIndexToModel(casesTable.getSelectedRow()), 0).toString(); + } + if (caseName.isEmpty()) { + occurrencePanel = new OccurrencePanel(); } else { - dataSourcesTable.getColumnModel().getColumn(idx).setPreferredWidth(210); + createdDate = getCaseCreatedDate(casesTable.getSelectedRow()); + occurrencePanel = new OccurrencePanel(caseName, createdDate); } + } else { + //else display an empty details area + occurrencePanel = new OccurrencePanel(); } + //calling getPreferredSize has a side effect of ensuring it has a preferred size which reflects the contents which are visible + occurrencePanel.getPreferredSize(); + detailsPanelScrollPane.setViewportView(occurrencePanel); + } + + /** + * Get the date a case was created + * + * @param caseTableRowIdx the row from the casesTable representing the case + * + * @return A string representing the date the case was created or an empty + * string if the date could not be determined + */ + private String getCaseCreatedDate(int caseTableRowIdx) { + try { + if (EamDb.isEnabled()) { + CorrelationCase partialCase; + partialCase = casesTableModel.getCorrelationCase(casesTable.convertRowIndexToModel(caseTableRowIdx)); + if (partialCase == null){ + return ""; + } + return EamDb.getInstance().getCaseByUUID(partialCase.getCaseUUID()).getCreationDate(); + } else { + return Case.getCurrentCase().getCreatedDate(); + } + } catch (EamDbException ex) { + LOGGER.log(Level.WARNING, "Error getting case created date for row: " + caseTableRowIdx, ex); + } + return ""; } /** @@ -895,19 +956,20 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi showCaseDetailsMenuItem = new javax.swing.JMenuItem(); showCommonalityMenuItem = new javax.swing.JMenuItem(); CSVFileChooser = new javax.swing.JFileChooser(); - otherCasesPanel = new javax.swing.JPanel(); tableContainerPanel = new javax.swing.JPanel(); earliestCaseLabel = new javax.swing.JLabel(); earliestCaseDate = new javax.swing.JLabel(); foundInLabel = new javax.swing.JLabel(); - jSplitPane2 = new javax.swing.JSplitPane(); - jSplitPane3 = new javax.swing.JSplitPane(); + tablesViewerSplitPane = new javax.swing.JSplitPane(); + caseDatasourceFileSplitPane = new javax.swing.JSplitPane(); + caseDatasourceSplitPane = new javax.swing.JSplitPane(); caseScrollPane = new javax.swing.JScrollPane(); casesTable = new javax.swing.JTable(); dataSourceScrollPane = new javax.swing.JScrollPane(); dataSourcesTable = new javax.swing.JTable(); - propertiesTableScrollPane = new javax.swing.JScrollPane(); + filesTableScrollPane = new javax.swing.JScrollPane(); filesTable = new javax.swing.JTable(); + detailsPanelScrollPane = new javax.swing.JScrollPane(); rightClickPopupMenu.addPopupMenuListener(new javax.swing.event.PopupMenuListener() { public void popupMenuCanceled(javax.swing.event.PopupMenuEvent evt) { @@ -931,13 +993,12 @@ 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(1500, 10)); + setMinimumSize(new java.awt.Dimension(600, 10)); setOpaque(false); - setPreferredSize(new java.awt.Dimension(1500, 44)); + setPreferredSize(new java.awt.Dimension(600, 63)); - otherCasesPanel.setPreferredSize(new java.awt.Dimension(921, 62)); - - tableContainerPanel.setPreferredSize(new java.awt.Dimension(1500, 63)); + tableContainerPanel.setPreferredSize(new java.awt.Dimension(600, 63)); + tableContainerPanel.setRequestFocusEnabled(false); org.openide.awt.Mnemonics.setLocalizedText(earliestCaseLabel, org.openide.util.NbBundle.getMessage(DataContentViewerOtherCases.class, "DataContentViewerOtherCases.earliestCaseLabel.text")); // NOI18N earliestCaseLabel.setToolTipText(org.openide.util.NbBundle.getMessage(DataContentViewerOtherCases.class, "DataContentViewerOtherCases.earliestCaseLabel.toolTipText")); // NOI18N @@ -946,49 +1007,50 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi org.openide.awt.Mnemonics.setLocalizedText(foundInLabel, org.openide.util.NbBundle.getMessage(DataContentViewerOtherCases.class, "DataContentViewerOtherCases.foundInLabel.text")); // NOI18N - jSplitPane2.setDividerLocation(470); + tablesViewerSplitPane.setDividerLocation(450); + tablesViewerSplitPane.setResizeWeight(0.5); - jSplitPane3.setDividerLocation(150); + caseDatasourceFileSplitPane.setDividerLocation(300); + caseDatasourceFileSplitPane.setResizeWeight(0.6); + caseDatasourceFileSplitPane.setToolTipText(""); + + caseDatasourceSplitPane.setDividerLocation(150); + caseDatasourceSplitPane.setResizeWeight(0.5); + + caseScrollPane.setPreferredSize(new java.awt.Dimension(140, 30)); casesTable.setAutoCreateRowSorter(true); casesTable.setModel(casesTableModel); caseScrollPane.setViewportView(casesTable); - jSplitPane3.setLeftComponent(caseScrollPane); + caseDatasourceSplitPane.setLeftComponent(caseScrollPane); + + dataSourceScrollPane.setPreferredSize(new java.awt.Dimension(140, 30)); dataSourcesTable.setAutoCreateRowSorter(true); - dataSourcesTable.setModel(new javax.swing.table.DefaultTableModel( - new Object [][] { - - }, - new String [] { - "Data Source Name", "Device ID" - } - ) { - boolean[] canEdit = new boolean [] { - false, false - }; - - public boolean isCellEditable(int rowIndex, int columnIndex) { - return canEdit [columnIndex]; - } - }); + dataSourcesTable.setModel(dataSourcesTableModel); dataSourceScrollPane.setViewportView(dataSourcesTable); - jSplitPane3.setRightComponent(dataSourceScrollPane); + caseDatasourceSplitPane.setRightComponent(dataSourceScrollPane); - jSplitPane2.setLeftComponent(jSplitPane3); + caseDatasourceFileSplitPane.setLeftComponent(caseDatasourceSplitPane); - propertiesTableScrollPane.setPreferredSize(new java.awt.Dimension(1000, 30)); + filesTableScrollPane.setPreferredSize(new java.awt.Dimension(140, 30)); filesTable.setAutoCreateRowSorter(true); - filesTable.setModel(tableModel); + filesTable.setModel(filesTableModel); filesTable.setToolTipText(org.openide.util.NbBundle.getMessage(DataContentViewerOtherCases.class, "DataContentViewerOtherCases.table.toolTip.text")); // NOI18N filesTable.setComponentPopupMenu(rightClickPopupMenu); - filesTable.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_INTERVAL_SELECTION); - propertiesTableScrollPane.setViewportView(filesTable); + filesTable.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); + filesTableScrollPane.setViewportView(filesTable); - jSplitPane2.setRightComponent(propertiesTableScrollPane); + caseDatasourceFileSplitPane.setRightComponent(filesTableScrollPane); + + tablesViewerSplitPane.setLeftComponent(caseDatasourceFileSplitPane); + + detailsPanelScrollPane.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + detailsPanelScrollPane.setPreferredSize(new java.awt.Dimension(200, 100)); + tablesViewerSplitPane.setRightComponent(detailsPanelScrollPane); javax.swing.GroupLayout tableContainerPanelLayout = new javax.swing.GroupLayout(tableContainerPanel); tableContainerPanel.setLayout(tableContainerPanelLayout); @@ -997,65 +1059,49 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi .addGroup(tableContainerPanelLayout.createSequentialGroup() .addGroup(tableContainerPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(tableContainerPanelLayout.createSequentialGroup() - .addComponent(earliestCaseLabel) + .addComponent(earliestCaseLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 161, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(earliestCaseDate) - .addGap(66, 66, 66) - .addComponent(foundInLabel)) - .addComponent(jSplitPane2, javax.swing.GroupLayout.DEFAULT_SIZE, 911, Short.MAX_VALUE)) + .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, 590, Short.MAX_VALUE)) .addContainerGap()) ); tableContainerPanelLayout.setVerticalGroup( tableContainerPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, tableContainerPanelLayout.createSequentialGroup() - .addComponent(jSplitPane2, javax.swing.GroupLayout.DEFAULT_SIZE, 31, Short.MAX_VALUE) + .addGroup(tableContainerPanelLayout.createSequentialGroup() + .addGap(0, 0, 0) + .addComponent(tablesViewerSplitPane, javax.swing.GroupLayout.DEFAULT_SIZE, 33, Short.MAX_VALUE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(tableContainerPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(earliestCaseLabel) - .addComponent(earliestCaseDate) - .addComponent(foundInLabel)) + .addGroup(tableContainerPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addGroup(tableContainerPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(earliestCaseLabel) + .addComponent(earliestCaseDate)) + .addComponent(foundInLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 14, javax.swing.GroupLayout.PREFERRED_SIZE)) .addContainerGap()) ); - javax.swing.GroupLayout otherCasesPanelLayout = new javax.swing.GroupLayout(otherCasesPanel); - otherCasesPanel.setLayout(otherCasesPanelLayout); - otherCasesPanelLayout.setHorizontalGroup( - otherCasesPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGap(0, 921, Short.MAX_VALUE) - .addGroup(otherCasesPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(otherCasesPanelLayout.createSequentialGroup() - .addComponent(tableContainerPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGap(0, 0, 0))) - ); - otherCasesPanelLayout.setVerticalGroup( - otherCasesPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGap(0, 62, Short.MAX_VALUE) - .addGroup(otherCasesPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(otherCasesPanelLayout.createSequentialGroup() - .addComponent(tableContainerPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 62, Short.MAX_VALUE) - .addGap(0, 0, 0))) - ); - javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(otherCasesPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 1500, Short.MAX_VALUE) + .addComponent(tableContainerPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(otherCasesPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(layout.createSequentialGroup() + .addComponent(tableContainerPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 64, Short.MAX_VALUE) + .addGap(0, 0, 0)) ); }// //GEN-END:initComponents private void rightClickPopupMenuPopupMenuWillBecomeVisible(javax.swing.event.PopupMenuEvent evt) {//GEN-FIRST:event_rightClickPopupMenuPopupMenuWillBecomeVisible boolean enableCentralRepoActions = false; - if (EamDb.isEnabled() && filesTable.getSelectedRowCount() == 1) { int rowIndex = filesTable.getSelectedRow(); - OtherOccurrenceNodeData selectedNode = (OtherOccurrenceNodeData) tableModel.getRow(rowIndex); - if (selectedNode instanceof OtherOccurrenceNodeInstanceData) { - OtherOccurrenceNodeInstanceData instanceData = (OtherOccurrenceNodeInstanceData) selectedNode; + List selectedFile = filesTableModel.getListOfNodesForFile(rowIndex); + if (!selectedFile.isEmpty() && selectedFile.get(0) instanceof OtherOccurrenceNodeInstanceData) { + OtherOccurrenceNodeInstanceData instanceData = (OtherOccurrenceNodeInstanceData) selectedFile.get(0); enableCentralRepoActions = instanceData.isCentralRepoNode(); } } @@ -1065,24 +1111,25 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JFileChooser CSVFileChooser; + private javax.swing.JSplitPane caseDatasourceFileSplitPane; + private javax.swing.JSplitPane caseDatasourceSplitPane; private javax.swing.JScrollPane caseScrollPane; private javax.swing.JTable casesTable; private javax.swing.JScrollPane dataSourceScrollPane; private javax.swing.JTable dataSourcesTable; + private javax.swing.JScrollPane detailsPanelScrollPane; private javax.swing.JLabel earliestCaseDate; private javax.swing.JLabel earliestCaseLabel; private javax.swing.JMenuItem exportToCSVMenuItem; private javax.swing.JTable filesTable; + private javax.swing.JScrollPane filesTableScrollPane; private javax.swing.JLabel foundInLabel; - private javax.swing.JSplitPane jSplitPane2; - private javax.swing.JSplitPane jSplitPane3; - private javax.swing.JPanel otherCasesPanel; - private javax.swing.JScrollPane propertiesTableScrollPane; 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; + private javax.swing.JSplitPane tablesViewerSplitPane; // End of variables declaration//GEN-END:variables /** @@ -1119,9 +1166,6 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi @Override public int hashCode() { - //int hash = 7; - //hash = 67 * hash + this.dataSourceID.hashCode(); - //hash = 67 * hash + this.filePath.hashCode(); return Objects.hash(getDataSourceID(), getFilePath(), getType()); } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchTopComponent.form b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OccurrencePanel.form old mode 100755 new mode 100644 similarity index 64% rename from KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchTopComponent.form rename to Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OccurrencePanel.form index cbd81a1c12..f889b3180a --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchTopComponent.form +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OccurrencePanel.form @@ -2,8 +2,8 @@
- - + + @@ -16,18 +16,8 @@ + - - - - - - - - - - - - + diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OccurrencePanel.java b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OccurrencePanel.java new file mode 100644 index 0000000000..d8b18d5f82 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OccurrencePanel.java @@ -0,0 +1,370 @@ +/* + * Central Repository + * + * 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.centralrepository.contentviewer; + +import java.awt.Color; +import java.awt.Font; +import java.util.ArrayList; +import java.util.Map; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Level; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.casemodule.Case; +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.coreutils.Logger; +import org.sleuthkit.datamodel.TskData; + +/** + * Panel for displaying other occurrence details. + */ +final class OccurrencePanel extends javax.swing.JPanel { + + private static final Logger LOGGER = Logger.getLogger(OccurrencePanel.class.getName()); + private static final int LEFT_INSET = 10; + private static final int RIGHT_INSET = 10; + private static final int TOP_INSET = 10; + private static final int BOTTOM_INSET = 10; + private static final int VERTICAL_GAP = 6; + private static final int HORIZONTAL_GAP = 4; + private static final long serialVersionUID = 1L; + + private int gridY = 0; + private final List nodeDataList; + private final Map caseNamesAndDates = new HashMap<>(); + private final Set dataSourceNames = new HashSet<>(); + private final Set filePaths = new HashSet<>(); + + /** + * Construct an empty OccurrencePanel + */ + OccurrencePanel() { + nodeDataList = new ArrayList<>(); + customizeComponents(); + } + + /** + * Construct an OccurrencePanel which will display only Case information + * + * @param caseName the name of the case + * @param caseCreatedDate the date the case was created + */ + OccurrencePanel(String caseName, String caseCreatedDate) { + nodeDataList = new ArrayList<>(); + caseNamesAndDates.put(caseName, caseCreatedDate); + customizeComponents(); + } + + /** + * Construct an OccurrencePanel which will display only case and data source + * information + * + * @param caseName the name of the case + * @param caseCreatedDate the date the case was created + * @param dataSourceName the name of the data source + */ + OccurrencePanel(String caseName, String caseCreatedDate, String dataSourceName) { + nodeDataList = new ArrayList<>(); + caseNamesAndDates.put(caseName, caseCreatedDate); + dataSourceNames.add(dataSourceName); + customizeComponents(); + } + + /** + * Construct a OccurrencePanel which will display details for all other + * occurrences associated with a file + * + * @param nodeDataList the list of OtherOccurrenceNodeData representing + * common properties for the file + */ + OccurrencePanel(List nodeDataList) { + this.nodeDataList = nodeDataList; + customizeComponents(); + } + + /** + * Do all the construction of gui elements and adding of the appropriate + * elements to the gridbaglayout + */ + private void customizeComponents() { + initComponents(); + if (!this.nodeDataList.isEmpty()) { + //if addInstanceDetails is going to be called it should be called + // before addFileDetails, addDataSourceDetails, and addCaseDetails + //because it also collects the information they display + addInstanceDetails(); + if (!filePaths.isEmpty()) { + addFileDetails(); + } + } + if (!dataSourceNames.isEmpty()) { + addDataSourceDetails(); + } + if (!caseNamesAndDates.keySet().isEmpty()) { + addCaseDetails(); + } + //add filler to keep everything else at the top + addItemToBag(gridY, 0, 0, 0, new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 32767))); + } + + @Messages({ + "OccurrencePanel.commonProperties.text=Common Properties", + "OccurrencePanel.commonPropertyTypeLabel.text=Type:", + "OccurrencePanel.commonPropertyValueLabel.text=Value:", + "OccurrencePanel.commonPropertyKnownStatusLabel.text=Known Status:", + "OccurrencePanel.commonPropertyCommentLabel.text=Comment:" + }) + /** + * Add the Common Property instance details to the gridbaglayout supports + * adding multiple common properties + * + * Also collects the case, data source, and file path information to be + * displayed + */ + private void addInstanceDetails() { + javax.swing.JLabel commonPropertiesLabel = new javax.swing.JLabel(); + org.openide.awt.Mnemonics.setLocalizedText(commonPropertiesLabel, Bundle.OccurrencePanel_commonProperties_text()); + commonPropertiesLabel.setFont(commonPropertiesLabel.getFont().deriveFont(Font.BOLD, commonPropertiesLabel.getFont().getSize())); + addItemToBag(gridY, 0, TOP_INSET, 0, commonPropertiesLabel); + gridY++; + //for each other occurrence + for (OtherOccurrenceNodeData occurrence : nodeDataList) { + if (occurrence instanceof OtherOccurrenceNodeInstanceData) { + String type = ((OtherOccurrenceNodeInstanceData) occurrence).getType(); + if (!type.isEmpty()) { + javax.swing.JLabel typeLabel = new javax.swing.JLabel(); + org.openide.awt.Mnemonics.setLocalizedText(typeLabel, Bundle.OccurrencePanel_commonPropertyTypeLabel_text()); + addItemToBag(gridY, 0, VERTICAL_GAP, 0, typeLabel); + javax.swing.JLabel typeFieldValue = new javax.swing.JLabel(); + typeFieldValue.setText(type); + addItemToBag(gridY, 1, VERTICAL_GAP, 0, typeFieldValue); + gridY++; + } + String value = ((OtherOccurrenceNodeInstanceData) occurrence).getValue(); + if (!value.isEmpty()) { + javax.swing.JLabel valueLabel = new javax.swing.JLabel(); + org.openide.awt.Mnemonics.setLocalizedText(valueLabel, Bundle.OccurrencePanel_commonPropertyValueLabel_text()); + addItemToBag(gridY, 0, 0, 0, valueLabel); + javax.swing.JLabel valueFieldValue = new javax.swing.JLabel(); + valueFieldValue.setText(value); + addItemToBag(gridY, 1, 0, 0, valueFieldValue); + gridY++; + } + TskData.FileKnown knownStatus = ((OtherOccurrenceNodeInstanceData) occurrence).getKnown(); + javax.swing.JLabel knownStatusLabel = new javax.swing.JLabel(); + 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()); + if (knownStatus == TskData.FileKnown.BAD) { + knownStatusValue.setForeground(Color.RED); + } + addItemToBag(gridY, 1, 0, 0, knownStatusValue); + gridY++; + String comment = ((OtherOccurrenceNodeInstanceData) occurrence).getComment(); + if (!comment.isEmpty()) { + javax.swing.JLabel commentLabel = new javax.swing.JLabel(); + org.openide.awt.Mnemonics.setLocalizedText(commentLabel, Bundle.OccurrencePanel_commonPropertyCommentLabel_text()); + addItemToBag(gridY, 0, 0, VERTICAL_GAP, commentLabel); + javax.swing.JTextArea commentValue = new javax.swing.JTextArea(); + commentValue.setText(comment); + commentValue.setEditable(false); + commentValue.setColumns(20); + commentValue.setLineWrap(true); + commentValue.setRows(3); + commentValue.setTabSize(4); + commentValue.setWrapStyleWord(true); + commentValue.setBorder(javax.swing.BorderFactory.createEtchedBorder()); + commentValue.setBackground(javax.swing.UIManager.getDefaults().getColor("TextArea.disabledBackground")); + addItemToBag(gridY, 1, 0, VERTICAL_GAP, commentValue); + gridY++; + } + String caseDate = ""; + try { + OtherOccurrenceNodeInstanceData nodeData = ((OtherOccurrenceNodeInstanceData) occurrence); + if (nodeData.isCentralRepoNode()) { + if (EamDb.isEnabled()) { + CorrelationCase partialCase = nodeData.getCorrelationAttributeInstance().getCorrelationCase(); + caseDate = EamDb.getInstance().getCaseByUUID(partialCase.getCaseUUID()).getCreationDate(); + } + } else { + caseDate = Case.getCurrentCase().getCreatedDate(); + } + } catch (EamDbException ex) { + LOGGER.log(Level.WARNING, "Error getting case created date for other occurrence content viewer", ex); + } + //Collect the data that is necessary for the other sections + caseNamesAndDates.put(((OtherOccurrenceNodeInstanceData) occurrence).getCaseName(), caseDate); + dataSourceNames.add(((OtherOccurrenceNodeInstanceData) occurrence).getDataSourceName()); + filePaths.add(((OtherOccurrenceNodeInstanceData) occurrence).getFilePath()); + } + } + //end for each + } + + @Messages({ + "OccurrencePanel.fileDetails.text=File Details", + "OccurrencePanel.filePathLabel.text=File Path:" + }) + /** + * Add the File specific details such as file path to the gridbaglayout + */ + private void addFileDetails() { + String filePath = filePaths.size() > 1 ? "" : filePaths.iterator().next(); + if (!filePath.isEmpty()) { + javax.swing.JLabel fileDetailsLabel = new javax.swing.JLabel(); + org.openide.awt.Mnemonics.setLocalizedText(fileDetailsLabel, Bundle.OccurrencePanel_fileDetails_text()); + fileDetailsLabel.setFont(fileDetailsLabel.getFont().deriveFont(Font.BOLD, fileDetailsLabel.getFont().getSize())); + addItemToBag(gridY, 0, TOP_INSET, 0, fileDetailsLabel); + gridY++; + javax.swing.JLabel filePathLabel = new javax.swing.JLabel(); + org.openide.awt.Mnemonics.setLocalizedText(filePathLabel, Bundle.OccurrencePanel_filePathLabel_text()); + addItemToBag(gridY, 0, VERTICAL_GAP, VERTICAL_GAP, filePathLabel); + javax.swing.JTextArea filePathValue = new javax.swing.JTextArea(); + filePathValue.setText(filePath); + filePathValue.setEditable(false); + filePathValue.setColumns(20); + filePathValue.setLineWrap(true); + filePathValue.setRows(3); + filePathValue.setTabSize(4); + filePathValue.setWrapStyleWord(true); + filePathValue.setBorder(javax.swing.BorderFactory.createEtchedBorder()); + filePathValue.setBackground(javax.swing.UIManager.getDefaults().getColor("TextArea.disabledBackground")); + addItemToBag(gridY, 1, VERTICAL_GAP, VERTICAL_GAP, filePathValue); + gridY++; + } + } + + @Messages({ + "OccurrencePanel.dataSourceDetails.text=Data Source Details", + "OccurrencePanel.dataSourceNameLabel.text=Name:" + }) + /** + * Add the data source specific details such as data source name to the + * gridbaglayout + */ + private void addDataSourceDetails() { + String dataSourceName = dataSourceNames.size() > 1 ? "" : dataSourceNames.iterator().next(); + if (!dataSourceName.isEmpty()) { + javax.swing.JLabel dataSourceDetailsLabel = new javax.swing.JLabel(); + org.openide.awt.Mnemonics.setLocalizedText(dataSourceDetailsLabel, Bundle.OccurrencePanel_dataSourceDetails_text()); + dataSourceDetailsLabel.setFont(dataSourceDetailsLabel.getFont().deriveFont(Font.BOLD, dataSourceDetailsLabel.getFont().getSize())); + addItemToBag(gridY, 0, TOP_INSET, 0, dataSourceDetailsLabel); + gridY++; + javax.swing.JLabel dataSourceNameLabel = new javax.swing.JLabel(); + org.openide.awt.Mnemonics.setLocalizedText(dataSourceNameLabel, Bundle.OccurrencePanel_dataSourceNameLabel_text()); + addItemToBag(gridY, 0, VERTICAL_GAP, VERTICAL_GAP, dataSourceNameLabel); + javax.swing.JLabel dataSourceNameValue = new javax.swing.JLabel(); + dataSourceNameValue.setText(dataSourceName); + addItemToBag(gridY, 1, VERTICAL_GAP, VERTICAL_GAP, dataSourceNameValue); + gridY++; + } + } + + @Messages({ + "OccurrencePanel.caseDetails.text=Case Details", + "OccurrencePanel.caseNameLabel.text=Name:", + "OccurrencePanel.caseCreatedDateLabel.text=Created Date:" + }) + /** + * Add the case specific details such as case name to the gridbaglayout + */ + private void addCaseDetails() { + javax.swing.JLabel caseDetailsLabel = new javax.swing.JLabel(); + org.openide.awt.Mnemonics.setLocalizedText(caseDetailsLabel, Bundle.OccurrencePanel_caseDetails_text()); + caseDetailsLabel.setFont(caseDetailsLabel.getFont().deriveFont(Font.BOLD, caseDetailsLabel.getFont().getSize())); + addItemToBag(gridY, 0, TOP_INSET, 0, caseDetailsLabel); + gridY++; + String caseName = caseNamesAndDates.keySet().size() > 1 ? "" : caseNamesAndDates.keySet().iterator().next(); + if (!caseName.isEmpty()) { + javax.swing.JLabel caseNameLabel = new javax.swing.JLabel(); + org.openide.awt.Mnemonics.setLocalizedText(caseNameLabel, Bundle.OccurrencePanel_caseNameLabel_text()); + addItemToBag(gridY, 0, VERTICAL_GAP, 0, caseNameLabel); + javax.swing.JLabel caseNameValue = new javax.swing.JLabel(); + caseNameValue.setText(caseName); + addItemToBag(gridY, 1, VERTICAL_GAP, 0, caseNameValue); + gridY++; + } + String caseCreatedDate = caseNamesAndDates.keySet().size() > 1 ? "" : caseNamesAndDates.get(caseName); + if (caseCreatedDate != null && !caseCreatedDate.isEmpty()) { + javax.swing.JLabel caseCreatedLabel = new javax.swing.JLabel(); + org.openide.awt.Mnemonics.setLocalizedText(caseCreatedLabel, Bundle.OccurrencePanel_caseCreatedDateLabel_text()); + addItemToBag(gridY, 0, 0, BOTTOM_INSET, caseCreatedLabel); + javax.swing.JLabel caseCreatedValue = new javax.swing.JLabel(); + caseCreatedValue.setText(caseCreatedDate); + addItemToBag(gridY, 1, 0, BOTTOM_INSET, caseCreatedValue); + gridY++; + } + } + + /** + * Add a JComponent to the gridbaglayout + * + * @param gridYLocation the row number the item should be added at + * @param gridXLocation the column number the item should be added at + * @param topInset the gap from the top of the cell which should exist + * @param bottomInset the gap from the bottom of the cell which should + * exist + * @param item the JComponent to add to the gridbaglayout + */ + private void addItemToBag(int gridYLocation, int gridXLocation, int topInset, int bottomInset, javax.swing.JComponent item) { + java.awt.GridBagConstraints gridBagConstraints; + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = gridXLocation; + gridBagConstraints.gridy = gridYLocation; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.FIRST_LINE_START; + int leftInset = LEFT_INSET; + int rightInset = HORIZONTAL_GAP; + //change formating a bit if it is the value instead of the label + if (gridXLocation == 1) { + leftInset = 0; + rightInset = RIGHT_INSET; + gridBagConstraints.weightx = 0.1; + gridBagConstraints.gridwidth = 2; + } + gridBagConstraints.insets = new java.awt.Insets(topInset, leftInset, bottomInset, rightInset); + //if the item is a filler item ensure it will resize vertically + if (item instanceof javax.swing.Box.Filler) { + gridBagConstraints.weighty = 0.1; + } + add(item, gridBagConstraints); + } + + /** + * 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() { + + setMinimumSize(new java.awt.Dimension(50, 30)); + setPreferredSize(null); + setLayout(new java.awt.GridBagLayout()); + }// //GEN-END:initComponents + + // Variables declaration - do not modify//GEN-BEGIN:variables + // End of variables declaration//GEN-END:variables +} diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrenceNodeInstanceData.java b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrenceNodeInstanceData.java index 79dcf21f64..7703e19e62 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrenceNodeInstanceData.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrenceNodeInstanceData.java @@ -1,7 +1,7 @@ /* * Central Repository * - * Copyright 2018 Basis Technology Corp. + * Copyright 2018-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -31,11 +31,12 @@ import org.sleuthkit.datamodel.TskDataException; * Class for populating the Other Occurrences tab */ class OtherOccurrenceNodeInstanceData implements OtherOccurrenceNodeData { - + // For now hard code the string for the central repo files type, since // getting it dynamically can fail. private static final String FILE_TYPE_STR = "Files"; - + private static final String CSV_ITEM_SEPARATOR = "\",\""; + private final String caseName; private String deviceID; private String dataSourceName; @@ -44,12 +45,13 @@ class OtherOccurrenceNodeInstanceData implements OtherOccurrenceNodeData { private final String value; private TskData.FileKnown known; private String comment; - + private AbstractFile originalAbstractFile = null; private CorrelationAttributeInstance originalCorrelationInstance = null; - + /** * Create a node from a central repo instance. + * * @param instance The central repo instance * @param type The type of the instance * @param value The value of the instance @@ -63,15 +65,17 @@ class OtherOccurrenceNodeInstanceData implements OtherOccurrenceNodeData { this.value = value; known = instance.getKnownStatus(); comment = instance.getComment(); - + originalCorrelationInstance = instance; } - + /** * Create a node from an abstract file. + * * @param newFile The abstract file * @param autopsyCase The current case - * @throws EamDbException + * + * @throws EamDbException */ OtherOccurrenceNodeInstanceData(AbstractFile newFile, Case autopsyCase) throws EamDbException { caseName = autopsyCase.getDisplayName(); @@ -82,115 +86,129 @@ class OtherOccurrenceNodeInstanceData implements OtherOccurrenceNodeData { } catch (TskDataException | TskCoreException ex) { throw new EamDbException("Error loading data source for abstract file ID " + newFile.getId(), ex); } - + filePath = newFile.getParentPath() + newFile.getName(); typeStr = FILE_TYPE_STR; value = newFile.getMd5Hash(); known = newFile.getKnown(); comment = ""; - + originalAbstractFile = newFile; } - + /** * Check if this node is a "file" type + * * @return true if it is a file type */ boolean isFileType() { return FILE_TYPE_STR.equals(typeStr); } - + /** * Update the known status for this node + * * @param newKnownStatus The new known status */ void updateKnown(TskData.FileKnown newKnownStatus) { known = newKnownStatus; } - + /** * Update the comment for this node + * * @param newComment The new comment */ void updateComment(String newComment) { comment = newComment; } - + /** * Check if this is a central repo node. - * @return true if this node was created from a central repo instance, false otherwise + * + * @return true if this node was created from a central repo instance, false + * otherwise */ boolean isCentralRepoNode() { return (originalCorrelationInstance != null); - } - + } + /** * Get the case name + * * @return the case name */ String getCaseName() { return caseName; } - + /** * Get the device ID + * * @return the device ID */ String getDeviceID() { return deviceID; } - + /** * Get the data source name + * * @return the data source name */ String getDataSourceName() { return dataSourceName; } - + /** * Get the file path + * * @return the file path */ String getFilePath() { return filePath; } - + /** * Get the type (as a string) + * * @return the type */ String getType() { return typeStr; } - + /** * Get the value (MD5 hash for files) + * * @return the value */ String getValue() { return value; } - + /** * Get the known status + * * @return the known status */ TskData.FileKnown getKnown() { return known; } - + /** * Get the comment + * * @return the comment */ String getComment() { return comment; } - + /** - * Get the backing abstract file. - * Should only be called if isCentralRepoNode() is false + * Get the backing abstract file. Should only be called if + * isCentralRepoNode() is false + * * @return the original abstract file */ AbstractFile getAbstractFile() throws EamDbException { @@ -199,12 +217,14 @@ class OtherOccurrenceNodeInstanceData implements OtherOccurrenceNodeData { } return originalAbstractFile; } - + /** - * Get the backing CorrelationAttributeInstance. - * Should only be called if isCentralRepoNode() is true + * Get the backing CorrelationAttributeInstance. Should only be called if + * isCentralRepoNode() is true + * * @return the original CorrelationAttributeInstance - * @throws EamDbException + * + * @throws EamDbException */ CorrelationAttributeInstance getCorrelationAttributeInstance() throws EamDbException { if (originalCorrelationInstance == null) { @@ -212,4 +232,33 @@ class OtherOccurrenceNodeInstanceData implements OtherOccurrenceNodeData { } return originalCorrelationInstance; } + + /** + * Get the string to append between elements when writing the node instance + * data to a CSV + * + * @return the CSV_ITEM_SEPARATOR string + */ + static String getCsvItemSeparator() { + return CSV_ITEM_SEPARATOR; + } + + /** + * Create a string representation of the node's data comma separated with a + * line separator ending + * + * @return a comma separated string representation of the node's data + */ + String toCsvString() { + StringBuilder line = new StringBuilder("\""); + line.append(getCaseName()).append(CSV_ITEM_SEPARATOR) + .append(getDataSourceName()).append(CSV_ITEM_SEPARATOR) + .append(getType()).append(CSV_ITEM_SEPARATOR) + .append(getValue()).append(CSV_ITEM_SEPARATOR) + .append(getKnown().toString()).append(CSV_ITEM_SEPARATOR) + .append(getFilePath()).append(CSV_ITEM_SEPARATOR) + .append(getComment()).append('"') + .append(System.getProperty("line.separator")); + return line.toString(); + } } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesCasesTableModel.java b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesCasesTableModel.java index 07ecc0878a..d72e8edb57 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesCasesTableModel.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesCasesTableModel.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.List; import javax.swing.table.AbstractTableModel; import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationCase; /** * Model for cells in the cases section of the other occurrences data content @@ -32,26 +33,16 @@ public class OtherOccurrencesCasesTableModel extends AbstractTableModel { private static final long serialVersionUID = 1L; private final List correlationCaseList = new ArrayList<>(); + /** + * Create a table model for displaying case names + */ OtherOccurrencesCasesTableModel() { + // This constructor is intentionally empty. } @Override public int getColumnCount() { - return TableColumns.values().length; - } - - /** - * Get the preferred width that has been configured for this column. - * - * A value of 0 means that no preferred width has been defined for this - * column. - * - * @param colIdx Column index - * - * @return preferred column width >= 0 - */ - public int getColumnPreferredWidth(int colIdx) { - return TableColumns.values()[colIdx].columnWidth(); + return 1; } @Override @@ -59,45 +50,43 @@ public class OtherOccurrencesCasesTableModel extends AbstractTableModel { return correlationCaseList.size(); } + @Messages({"OtherOccurrencesCasesTableModel.case=Case",}) @Override public String getColumnName(int colIdx) { - return TableColumns.values()[colIdx].columnName(); + return Bundle.OtherOccurrencesCasesTableModel_case(); } + @Messages({"OtherOccurrencesCasesTableModel.noData=No Data."}) @Override public Object getValueAt(int rowIdx, int colIdx) { - if (0 == correlationCaseList.size()) { + //if anything would prevent this from working we will consider it no data for the sake of simplicity + if (correlationCaseList.isEmpty() || rowIdx < 0 + || rowIdx >= correlationCaseList.size() + || correlationCaseList.get(rowIdx) == null + || correlationCaseList.get(rowIdx).getMessage() == null + || correlationCaseList.get(rowIdx).getMessage().isEmpty()) { return Bundle.OtherOccurrencesCasesTableModel_noData(); } - - CorrelationCaseWrapper caseWrapper = correlationCaseList.get(rowIdx); - TableColumns columnId = TableColumns.values()[colIdx]; - return mapCorrelationCase(caseWrapper, columnId); + return correlationCaseList.get(rowIdx).getMessage(); } /** - * Map a column ID to the value in that cell for correlation case wrapper. + * Get a correlation case for the selected index. Does not query the Central + * Repository so CorrelationCase will be partial missing CR case ID and + * other information that is stored in the CR. * - * @param correlationCaseWrapper The correlation case wrapper - * @param columnId The ID of the cell column. + * @param rowIdx the row from the table model which corresponds to the case * - * @return The value in the cell. + * @return CorrelationCase for the table item specified or null if no + * correlation could be found for any reason */ - @Messages({"OtherOccurrencesCasesTableModel.noData=No Data."}) - private Object mapCorrelationCase(CorrelationCaseWrapper correlationCaseWrapper, TableColumns columnId) { - String value = Bundle.OtherOccurrencesCasesTableModel_noData(); - - switch (columnId) { - case CASE_NAME: - value = correlationCaseWrapper.getMessage(); - break; - default: //Use default "No data" value. - break; + CorrelationCase getCorrelationCase(int rowIdx) { + //if anything would prevent this from working we will return null + if (correlationCaseList.isEmpty() || rowIdx < 0 + || rowIdx >= correlationCaseList.size() + || correlationCaseList.get(rowIdx) == null) { + return null; } - return value; - } - - Object getCorrelationCase(int rowIdx) { return correlationCaseList.get(rowIdx).getCorrelationCase(); } @@ -107,7 +96,7 @@ public class OtherOccurrencesCasesTableModel extends AbstractTableModel { } /** - * Add one correlated instance object to the table + * Add one correlation case wrapper object to the table * * @param newCorrelationCaseWrapper data to add to the table */ @@ -123,27 +112,4 @@ public class OtherOccurrencesCasesTableModel extends AbstractTableModel { correlationCaseList.clear(); fireTableDataChanged(); } - - @Messages({"OtherOccurrencesCasesTableModel.case=Case",}) - enum TableColumns { - // Ordering here determines displayed column order in Content Viewer. - // If order is changed, update the CellRenderer to ensure correct row coloring. - CASE_NAME(Bundle.OtherOccurrencesCasesTableModel_case(), 100); - - private final String columnName; - private final int columnWidth; - - TableColumns(String columnName, int columnWidth) { - this.columnName = columnName; - this.columnWidth = columnWidth; - } - - public String columnName() { - return columnName; - } - - public int columnWidth() { - return columnWidth; - } - } } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesDataSourcesTableModel.java b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesDataSourcesTableModel.java new file mode 100644 index 0000000000..30b44ea5a6 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesDataSourcesTableModel.java @@ -0,0 +1,206 @@ +/* + * Central Repository + * + * 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.centralrepository.contentviewer; + +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Set; +import javax.swing.table.AbstractTableModel; +import org.openide.util.NbBundle; + +/** + * Model for cells in the data sources section of the other occurrences data + * content viewer + */ +final class OtherOccurrencesDataSourcesTableModel extends AbstractTableModel { + + private static final long serialVersionUID = 1L; + private final Set dataSourceSet = new LinkedHashSet<>(); + + /** + * Create a table model for displaying data source names + */ + OtherOccurrencesDataSourcesTableModel() { + // This constructor is intentionally empty. + } + + @Override + public int getColumnCount() { + return 1; + } + + @Override + public int getRowCount() { + return dataSourceSet.size(); + } + + @NbBundle.Messages({"OtherOccurrencesDataSourcesTableModel.dataSourceName=Data Source Name", + "OtherOccurrencesDataSourcesTableModel.noData=No Data."}) + @Override + public String getColumnName(int colIdx) { + return Bundle.OtherOccurrencesDataSourcesTableModel_dataSourceName(); + } + + @Override + public Object getValueAt(int rowIdx, int colIdx) { + //if anything would prevent this from working we will consider it no data for the sake of simplicity + if (dataSourceSet.isEmpty() || rowIdx < 0 + || rowIdx >= dataSourceSet.size() + || !(dataSourceSet.toArray()[rowIdx] instanceof DataSourceColumnItem)) { + return Bundle.OtherOccurrencesDataSourcesTableModel_noData(); + } + return ((DataSourceColumnItem) dataSourceSet.toArray()[rowIdx]).getDataSourceName(); + } + + /** + * Get the device id of the data source shown at the specified row index + * + * @param rowIdx the row index of the data source you want the device id for + * + * @return the device id of the specified data source or an empty string if + * a device id could not be retrieved + */ + String getDeviceIdForRow(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]).getDeviceId(); + } + + /** + * Get the case name of the data source shown at the specified row index + * + * @param rowIdx the row index of the data source you want the case name for + * + * @return the case name of the specified data source or an empty string if + * a case name could not be retrieved + */ + String getCaseNameForRow(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]).getCaseName(); + } + + @Override + public Class getColumnClass(int colIdx) { + return String.class; + } + + /** + * Add data source information to the table of unique data sources + * + * @param newNodeData data to add to the table + */ + void addNodeData(OtherOccurrenceNodeData newNodeData) { + dataSourceSet.add(new DataSourceColumnItem((OtherOccurrenceNodeInstanceData) newNodeData)); + fireTableDataChanged(); + } + + /** + * Clear the node data table. + */ + void clearTable() { + dataSourceSet.clear(); + fireTableDataChanged(); + } + + /** + * Private class for storing data source information in a way that + * facilitates de-duping. + */ + private final class DataSourceColumnItem { + + 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()); + } + + /** + * 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 dataSourceName the name of the data source + */ + private DataSourceColumnItem(String caseName, String deviceId, String dataSourceName) { + this.caseName = caseName; + this.deviceId = deviceId; + this.dataSourceName = dataSourceName; + } + + /** + * Get the device id + * + * @return the data source's device id + */ + private String getDeviceId() { + return deviceId; + } + + /** + * Get the data source name + * + * @return the data source's name + */ + private String getDataSourceName() { + return dataSourceName; + } + + /** + * Get the name of the case the data source exists in + * + * @return the name of the case the data source is in + */ + private String getCaseName() { + return caseName; + } + + @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()); + } + + @Override + public int hashCode() { + return Objects.hash(caseName, deviceId, dataSourceName); + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesFilesTableCellRenderer.java b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesFilesTableCellRenderer.java deleted file mode 100644 index 2f258d3e7b..0000000000 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesFilesTableCellRenderer.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Central Repository - * - * Copyright 2015-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.centralrepository.contentviewer; - -import java.awt.Color; -import java.awt.Component; -import javax.swing.JComponent; -import javax.swing.JTable; -import javax.swing.table.DefaultTableCellRenderer; -import javax.swing.table.TableCellRenderer; -import org.sleuthkit.datamodel.TskData; - -/** - * Renderer for cells in the files section of the other occurrences data content viewer - */ -public class OtherOccurrencesFilesTableCellRenderer implements TableCellRenderer { - - public static final DefaultTableCellRenderer DEFAULT_RENDERER = new DefaultTableCellRenderer(); - - @Override - public Component getTableCellRendererComponent( - JTable table, - Object value, - boolean isSelected, - boolean hasFocus, - int row, - int column) { - Component renderer = DEFAULT_RENDERER.getTableCellRendererComponent( - table, value, isSelected, hasFocus, row, column); - ((JComponent) renderer).setOpaque(true); - Color foreground, background; - if (isSelected) { - foreground = Color.WHITE; - background = new Color(51,153,255); - } else { - String known_status = (String) table.getModel().getValueAt(table.convertRowIndexToModel(row), - table.getColumn(OtherOccurrencesFilesTableModel.TableColumns.KNOWN.columnName()).getModelIndex()); - if (known_status.equals(TskData.FileKnown.BAD.getName())) { - foreground = Color.WHITE; - background = Color.RED; - } else if (known_status.equals(TskData.FileKnown.UNKNOWN.getName())) { - foreground = Color.BLACK; - background = Color.WHITE; - } else { - foreground = Color.BLACK; - background = Color.WHITE; - } - } - renderer.setForeground(foreground); - renderer.setBackground(background); - return renderer; - } -} diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesFilesTableModel.java b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesFilesTableModel.java index e6c0a4215f..dd797a19d3 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesFilesTableModel.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesFilesTableModel.java @@ -19,150 +19,76 @@ package org.sleuthkit.autopsy.centralrepository.contentviewer; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import javax.swing.table.AbstractTableModel; import org.openide.util.NbBundle.Messages; +import org.apache.commons.io.FilenameUtils; /** - * Model for cells in the files section of the other occurrences data content viewer + * Model for cells in the files section of the other occurrences data content + * viewer */ public class OtherOccurrencesFilesTableModel extends AbstractTableModel { private static final long serialVersionUID = 1L; + private final List nodeKeys = new ArrayList<>(); + private final Map> nodeMap = new HashMap<>(); - @Messages({"OtherOccurrencesFilesTableModel.device=Device", - "OtherOccurrencesFilesTableModel.dataSource=Data Source", - "OtherOccurrencesFilesTableModel.path=Path", - "OtherOccurrencesFilesTableModel.attribute=Matched Attribute", - "OtherOccurrencesFilesTableModel.value=Attribute Value", - "OtherOccurrencesFilesTableModel.known=Known", - "OtherOccurrencesFilesTableModel.comment=Comment", - "OtherOccurrencesFilesTableModel.noData=No Data.",}) - enum TableColumns { - // Ordering here determines displayed column order in Content Viewer. - // If order is changed, update the CellRenderer to ensure correct row coloring. - ATTRIBUTE(Bundle.OtherOccurrencesFilesTableModel_attribute(), 75), - VALUE(Bundle.OtherOccurrencesFilesTableModel_value(), 190), - KNOWN(Bundle.OtherOccurrencesFilesTableModel_known(), 25), - FILE_PATH(Bundle.OtherOccurrencesFilesTableModel_path(), 470), - COMMENT(Bundle.OtherOccurrencesFilesTableModel_comment(), 190); - - private final String columnName; - private final int columnWidth; - - TableColumns(String columnName, int columnWidth) { - this.columnName = columnName; - this.columnWidth = columnWidth; - } - - public String columnName() { - return columnName; - } - - public int columnWidth() { - return columnWidth; - } - }; - - private final List nodeDataList = new ArrayList<>(); - + /** + * Create a table model for displaying file names + */ OtherOccurrencesFilesTableModel() { - + // This constructor is intentionally empty. } @Override public int getColumnCount() { - return TableColumns.values().length; - } - - /** - * Get the preferred width that has been configured for this column. - * - * A value of 0 means that no preferred width has been defined for this - * column. - * - * @param colIdx Column index - * - * @return preferred column width >= 0 - */ - public int getColumnPreferredWidth(int colIdx) { - return TableColumns.values()[colIdx].columnWidth(); + return 1; } @Override public int getRowCount() { - return nodeDataList.size(); + return nodeKeys.size(); } + @Messages({"OtherOccurrencesFilesTableModel.fileName=File Name", + "OtherOccurrencesFilesTableModel.noData=No Data."}) @Override public String getColumnName(int colIdx) { - return TableColumns.values()[colIdx].columnName(); + return Bundle.OtherOccurrencesFilesTableModel_fileName(); } @Override public Object getValueAt(int rowIdx, int colIdx) { - if (0 == nodeDataList.size()) { + //if anything would prevent this from working we will consider it no data for the sake of simplicity + if (nodeMap.isEmpty() || nodeKeys.isEmpty() || rowIdx < 0 + || rowIdx >= nodeKeys.size() || nodeKeys.get(rowIdx) == null + || nodeMap.get(nodeKeys.get(rowIdx)) == null + || nodeMap.get(nodeKeys.get(rowIdx)).isEmpty()) { return Bundle.OtherOccurrencesFilesTableModel_noData(); } - - OtherOccurrenceNodeData nodeData = nodeDataList.get(rowIdx); - TableColumns columnId = TableColumns.values()[colIdx]; - if (nodeData instanceof OtherOccurrenceNodeMessageData) { - return mapNodeMessageData((OtherOccurrenceNodeMessageData) nodeData, columnId); - } - return mapNodeInstanceData((OtherOccurrenceNodeInstanceData) nodeData, columnId); + return FilenameUtils.getName(((OtherOccurrenceNodeInstanceData) nodeMap.get(nodeKeys.get(rowIdx)).get(0)).getFilePath()); } /** - * Map a column ID to the value in that cell for node message data. + * Get a list of OtherOccurrenceNodeData that exist for the file which + * corresponds to the Index * - * @param nodeData The node message data. - * @param columnId The ID of the cell column. + * @param rowIdx the index of the file to get data for * - * @return The value in the cell. + * @return a list of OtherOccurrenceNodeData for the specified index or an + * empty list if no data was found */ - private Object mapNodeMessageData(OtherOccurrenceNodeMessageData nodeData, TableColumns columnId) { - if (columnId == TableColumns.ATTRIBUTE) { - return nodeData.getDisplayMessage(); + List getListOfNodesForFile(int rowIdx) { + //if anything would prevent this from working return an empty list + if (nodeMap.isEmpty() || nodeKeys.isEmpty() || rowIdx < 0 + || rowIdx >= nodeKeys.size() || nodeKeys.get(rowIdx) == null + || nodeMap.get(nodeKeys.get(rowIdx)) == null) { + return new ArrayList<>(); } - return ""; - } - - /** - * Map a column ID to the value in that cell for node instance data. - * - * @param nodeData The node instance data. - * @param columnId The ID of the cell column. - * - * @return The value in the cell. - */ - private Object mapNodeInstanceData(OtherOccurrenceNodeInstanceData nodeData, TableColumns columnId) { - String value = Bundle.OtherOccurrencesFilesTableModel_noData(); - - switch (columnId) { - case FILE_PATH: - value = nodeData.getFilePath(); - break; - case ATTRIBUTE: - value = nodeData.getType(); - break; - case VALUE: - value = nodeData.getValue(); - break; - case KNOWN: - value = nodeData.getKnown().getName(); - break; - case COMMENT: - value = nodeData.getComment(); - break; - default: //Use default "No data" value. - break; - } - return value; - } - - Object getRow(int rowIdx) { - return nodeDataList.get(rowIdx); + return nodeMap.get(nodeKeys.get(rowIdx)); } @Override @@ -176,15 +102,27 @@ public class OtherOccurrencesFilesTableModel extends AbstractTableModel { * @param newNodeData data to add to the table */ void addNodeData(OtherOccurrenceNodeData newNodeData) { - nodeDataList.add(newNodeData); + String newNodeKey = createNodeKey((OtherOccurrenceNodeInstanceData) newNodeData);//FilenameUtils.getName(((OtherOccurrenceNodeInstanceData)newNodeData).getFilePath()); + List nodeList = nodeMap.get(newNodeKey); + if (nodeList == null) { + nodeKeys.add(newNodeKey); + nodeList = new ArrayList<>(); + } + nodeList.add(newNodeData); + nodeMap.put(newNodeKey, nodeList); fireTableDataChanged(); } + private String createNodeKey(OtherOccurrenceNodeInstanceData nodeData) { + return nodeData.getCaseName() + nodeData.getDataSourceName() + nodeData.getDeviceID() + nodeData.getFilePath(); + } + /** * Clear the node data table. */ void clearTable() { - nodeDataList.clear(); + nodeKeys.clear(); + nodeMap.clear(); fireTableDataChanged(); } diff --git a/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineIngestManager.java b/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineIngestManager.java index 8039aa0479..bbe14c0020 100755 --- a/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineIngestManager.java +++ b/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineIngestManager.java @@ -149,7 +149,7 @@ public class CommandLineIngestManager { // read options panel configuration String rootOutputDir = UserPreferences.getCommandLineModeResultsFolder(); LOGGER.log(Level.INFO, "Output directory = {0}", rootOutputDir); //NON-NLS - System.out.println("Output directoryh = " + rootOutputDir); + System.out.println("Output directory = " + rootOutputDir); if (rootOutputDir.isEmpty()) { LOGGER.log(Level.SEVERE, "Output directory not specified, please configure Command Line Options Panel (in Tools -> Options)"); @@ -264,6 +264,7 @@ public class CommandLineIngestManager { * Passes the data source for the current job through a data source * processor that adds it to the case database. * + * @param caseForJob The case * @param dataSource The data source. * * @throws diff --git a/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineOptionProcessor.java b/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineOptionProcessor.java index 36249ba497..8b94961fa6 100755 --- a/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineOptionProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineOptionProcessor.java @@ -56,22 +56,22 @@ public class CommandLineOptionProcessor extends OptionProcessor { @Override protected void process(Env env, Map values) throws CommandException { logger.log(Level.INFO, "Processing Autopsy command line options"); //NON-NLS - System.out.println("Processing Autopsy command line options using CommandLineOptionProcessor"); + System.out.println("Processing Autopsy command line options"); if (values.containsKey(pathToDataSourceOption) && values.containsKey(caseNameOption) && values.containsKey(runFromCommandLineOption)) { // parse input parameters String inputPath; String inputCaseName; String modeString; if (values.size() < 3) { - logger.log(Level.SEVERE, "Insufficient number of input arguments. Exiting"); - System.out.println("Insufficient number of input arguments. Exiting"); + logger.log(Level.SEVERE, "Insufficient number of input arguments to run command line ingest"); + System.out.println("Insufficient number of input arguments to run command line ingest"); this.runFromCommandLine = false; return; } else { String[] argDirs = values.get(pathToDataSourceOption); if (argDirs.length < 1) { - logger.log(Level.SEVERE, "Missing argument 'inputPath'. Exiting"); - System.out.println("Missing argument 'inputPath'. Exiting"); + logger.log(Level.SEVERE, "Missing argument 'inputPath'"); + System.out.println("Missing argument 'inputPath'"); this.runFromCommandLine = false; return; } @@ -79,8 +79,8 @@ public class CommandLineOptionProcessor extends OptionProcessor { argDirs = values.get(caseNameOption); if (argDirs.length < 1) { - logger.log(Level.SEVERE, "Missing argument 'caseName'. Exiting"); - System.out.println("Missing argument 'caseName'. Exiting"); + logger.log(Level.SEVERE, "Missing argument 'caseName'"); + System.out.println("Missing argument 'caseName'"); this.runFromCommandLine = false; return; } @@ -88,8 +88,8 @@ public class CommandLineOptionProcessor extends OptionProcessor { argDirs = values.get(runFromCommandLineOption); if (argDirs.length < 1) { - logger.log(Level.SEVERE, "Missing argument 'runFromCommandLine'. Exiting"); - System.out.println("Missing argument 'runFromCommandLine'. Exiting"); + logger.log(Level.SEVERE, "Missing argument 'runFromCommandLine'"); + System.out.println("Missing argument 'runFromCommandLine'"); this.runFromCommandLine = false; return; } @@ -111,15 +111,15 @@ public class CommandLineOptionProcessor extends OptionProcessor { // verify inputs if (inputPath == null || inputPath.isEmpty() || !(new File(inputPath).exists())) { - logger.log(Level.SEVERE, "Input file {0} doesn''t exist. Exiting", inputPath); - System.out.println("Input file " + inputPath + " doesn't exist. Exiting"); + logger.log(Level.SEVERE, "Input file {0} doesn''t exist", inputPath); + System.out.println("Input file " + inputPath + " doesn't exist"); this.runFromCommandLine = false; return; } if (inputCaseName == null || inputCaseName.isEmpty()) { - logger.log(Level.SEVERE, "Case name argument is empty. Exiting"); - System.out.println("Case name argument is empty. Exiting"); + logger.log(Level.SEVERE, "Case name argument is empty"); + System.out.println("Case name argument is empty"); this.runFromCommandLine = false; return; } @@ -134,8 +134,8 @@ public class CommandLineOptionProcessor extends OptionProcessor { System.out.println("Case name = " + this.baseCaseName); System.out.println("runFromCommandLine = " + this.runFromCommandLine); } else { - System.out.println("Missing input arguments for CommandLineOptionProcessor. Exiting"); - logger.log(Level.SEVERE, "Missing input arguments. Exiting"); + System.out.println("Missing input arguments to run command line ingest"); + logger.log(Level.SEVERE, "Missing input arguments to run command line ingest"); } } diff --git a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/CommonAttributeValueNode.java b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/CommonAttributeValueNode.java index ed8c395b5f..b9f88321f0 100644 --- a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/CommonAttributeValueNode.java +++ b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/CommonAttributeValueNode.java @@ -50,6 +50,7 @@ public class CommonAttributeValueNode extends DisplayableItemNode { * Create a Match node whose children will all have this object in common. * * @param data the common feature, and the children + * @param type the data type */ public CommonAttributeValueNode(CommonAttributeValue data, CorrelationAttributeInstance.Type type) { super(Children.create( diff --git a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/InstanceCountNode.java b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/InstanceCountNode.java index 973d7711c4..d3d1de26b8 100644 --- a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/InstanceCountNode.java +++ b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/InstanceCountNode.java @@ -52,8 +52,9 @@ public final class InstanceCountNode extends DisplayableItemNode { * Create a node with the given number of instances, and the given selection * of metadata. * - * @param instanceCount - * @param attributeValues + * @param instanceCount the number of instances + * @param attributeValues the attribute list + * @param type the data type */ @NbBundle.Messages({ "InstanceCountNode.displayName=Exists in %s data sources (%s)" diff --git a/Core/src/org/sleuthkit/autopsy/communications/snapshot/CommSnapShotReportWriter.java b/Core/src/org/sleuthkit/autopsy/communications/snapshot/CommSnapShotReportWriter.java index 7b4dee6d19..16945d324d 100755 --- a/Core/src/org/sleuthkit/autopsy/communications/snapshot/CommSnapShotReportWriter.java +++ b/Core/src/org/sleuthkit/autopsy/communications/snapshot/CommSnapShotReportWriter.java @@ -59,6 +59,7 @@ public class CommSnapShotReportWriter extends UiSnapShotReportWriter { * @param reportName The name of the report. * @param generationDate The generation Date of the report. * @param snapshot A snapshot of the view to include in the report. + * @param filter The communications filter */ public CommSnapShotReportWriter(Case currentCase, Path reportFolderPath, String reportName, Date generationDate, BufferedImage snapshot, CommunicationsFilter filter) { diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties index e10eb1b67c..652f8781ba 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties @@ -8,7 +8,7 @@ FXVideoPanel.progress.bufferingFile=Buffering {0} FXVideoPanel.progressLabel.buffering=Buffering... FXVideoPanel.media.unsupportedFormat=Unsupported Format. GstVideoPanel.cannotProcFile.err=The media player cannot process this file. -GstVideoPanel.initGst.gstException.msg=Error initializing gstreamer for audio/video viewing and frame extraction capabilities. Video and audio viewing will be disabled. +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. GstVideoPanel.exception.problemFile.msg=Cannot capture frames from this file ({0}). GstVideoPanel.exception.problemPlay.msg=Problem with video file; problem when attempting to play while obtaining duration. @@ -84,3 +84,8 @@ MediaViewImagePanel.zoomTextField.text= MediaViewImagePanel.rotationTextField.text= MediaViewImagePanel.rotateLeftButton.toolTipText= HtmlPanel.showImagesToggleButton.text=Show 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 diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED index 63850f7d66..236fddfada 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED @@ -19,8 +19,12 @@ FXVideoPanel.progress.bufferingFile=Buffering {0} FXVideoPanel.progressLabel.buffering=Buffering... FXVideoPanel.media.unsupportedFormat=Unsupported Format. GstVideoPanel.cannotProcFile.err=The media player cannot process this file. -GstVideoPanel.initGst.gstException.msg=Error initializing gstreamer for audio/video viewing and frame extraction capabilities. Video and audio viewing will be disabled. 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 +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. GstVideoPanel.exception.problemFile.msg=Cannot capture frames from this file ({0}). GstVideoPanel.exception.problemPlay.msg=Problem with video file; problem when attempting to play while obtaining duration. @@ -32,13 +36,12 @@ GstVideoPanel.progress.buffering=Buffering... GstVideoPanel.progressLabel.bufferingErr=Error buffering file GstVideoPanel.progress.infoLabel.updateErr=Error updating video progress: {0} GstVideoPanel.ExtractMedia.progress.buffering=Buffering {0} -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 -HtmlViewer_file_error=This file is missing or unreadable. MediaFileViewer.AccessibleContext.accessibleDescription= MediaFileViewer.title=Media MediaFileViewer.toolTip=Displays supported multimedia files (images, videos, audio) +MediaPlayerPanel.noSupport=File not supported. +MediaPlayerPanel.timeFormat=%02d:%02d:%02d +MediaPlayerPanel.unknownTime=Unknown MediaViewImagePanel.errorLabel.OOMText=Could not load file into Media View: insufficent memory. MediaViewImagePanel.errorLabel.text=Could not load file into Media View. MediaViewImagePanel.externalViewerButton.text=Open in External Viewer Ctrl+E @@ -143,6 +146,11 @@ MediaViewImagePanel.zoomTextField.text= MediaViewImagePanel.rotationTextField.text= MediaViewImagePanel.rotateLeftButton.toolTipText= HtmlPanel.showImagesToggleButton.text=Show 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 # {0} - tableName SQLiteViewer.readTable.errorText=Error getting rows for table: {0} # {0} - tableName diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/GstVideoRendererPanel.java b/Core/src/org/sleuthkit/autopsy/contentviewers/GstVideoRendererPanel.java deleted file mode 100755 index 185d19cc0d..0000000000 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/GstVideoRendererPanel.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * 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; - -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import javafx.application.Platform; -import javafx.embed.swing.JFXPanel; -import javafx.scene.Scene; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; -import javafx.scene.image.PixelFormat; -import javafx.scene.image.PixelWriter; -import javafx.scene.image.WritableImage; -import javafx.scene.layout.BorderPane; -import org.freedesktop.gstreamer.Buffer; -import org.freedesktop.gstreamer.Caps; -import org.freedesktop.gstreamer.FlowReturn; -import org.freedesktop.gstreamer.Sample; -import org.freedesktop.gstreamer.Structure; -import org.freedesktop.gstreamer.elements.AppSink; - -/** - * This is a video renderer for GStreamer. - */ -final class GstVideoRendererPanel extends JFXPanel { - - private static final String CAP_MIME_TYPE = "video/x-raw"; - private static final String CAP_BYTE_ORDER = (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN ? "format=BGRx" : "format=xRGB"); - private static final int PROP_MAX_BUFFERS = 5000; - private AppSink videoSink; - private ImageView fxImageView; - - /** - * Create an instance. - */ - GstVideoRendererPanel() { - initImageView(); - initVideoSink(); - } - - /** - * Initialize the ImageView to show the current frame. - */ - private void initImageView() { - fxImageView = new ImageView(); // Will hold the current video frame. - BorderPane borderpane = new BorderPane(fxImageView); // Center and size ImageView. - Scene scene = new Scene(borderpane); // Root of the JavaFX tree. - setScene(scene); - - // Bind size of image to that of scene, while keeping proportions - fxImageView.fitWidthProperty().bind(scene.widthProperty()); - fxImageView.fitHeightProperty().bind(scene.heightProperty()); - fxImageView.setPreserveRatio(true); - fxImageView.setSmooth(true); - fxImageView.setCache(true); - } - - /** - * Initialize the video sink. - */ - private void initVideoSink() { - videoSink = new AppSink("GstVideoComponent"); - videoSink.set("emit-signals", true); - AppSinkListener gstListener = new AppSinkListener(); - videoSink.connect(gstListener); - videoSink.setCaps(new Caps( - String.format("%s, %s", CAP_MIME_TYPE, CAP_BYTE_ORDER))); - videoSink.set("max-buffers", PROP_MAX_BUFFERS); - videoSink.set("drop", true); - } - - /** - * Get the video sink. - * - * @return The video sink. - */ - AppSink getVideoSink() { - return videoSink; - } - - /** - * Listen for NEW_SAMPLE events to update the ImageView with the newest - * video frame. - */ - class AppSinkListener implements AppSink.NEW_SAMPLE { - - private Image videoFrame; - private int lastWidth = 0; - private int lastHeight = 0; - private byte[] byteArray; - - @Override - public FlowReturn newSample(AppSink appSink) { - Sample sample = appSink.pullSample(); - Buffer buffer = sample.getBuffer(); - ByteBuffer byteBuffer = buffer.map(false); - if (byteBuffer != null) { - Structure capsStruct = sample.getCaps().getStructure(0); - int width = capsStruct.getInteger("width"); - int height = capsStruct.getInteger("height"); - if (width != lastWidth || height != lastHeight) { - lastWidth = width; - lastHeight = height; - byteArray = new byte[width * height * 4]; - } - byteBuffer.get(byteArray); - videoFrame = convertBytesToImage(byteArray, width, height); - Platform.runLater(() -> { - fxImageView.setImage(videoFrame); - }); - buffer.unmap(); - } - sample.dispose(); - - return FlowReturn.OK; - } - - /** - * Create an image from a byte array of pixels. - * - * @param pixels The byte array of pixels. - * @param width The width of the image. - * @param height The height of the image. - * - * @return The image. - */ - private Image convertBytesToImage(byte[] pixels, int width, int height) { - WritableImage image = new WritableImage(width, height); - PixelWriter pixelWriter = image.getPixelWriter(); - pixelWriter.setPixels(0, 0, width, height, PixelFormat.getByteBgraInstance(), pixels, 0, width * 4); - return image; - } - } -} diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/JavaFxAppSink.java b/Core/src/org/sleuthkit/autopsy/contentviewers/JavaFxAppSink.java new file mode 100755 index 0000000000..a76b7970c3 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/JavaFxAppSink.java @@ -0,0 +1,178 @@ +/* + * 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; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import javafx.embed.swing.JFXPanel; +import javafx.scene.Scene; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.image.PixelFormat; +import javafx.scene.image.PixelWriter; +import javafx.scene.image.WritableImage; +import javafx.scene.layout.BorderPane; +import org.freedesktop.gstreamer.Buffer; +import org.freedesktop.gstreamer.Caps; +import org.freedesktop.gstreamer.FlowReturn; +import org.freedesktop.gstreamer.Sample; +import org.freedesktop.gstreamer.Structure; +import org.freedesktop.gstreamer.elements.AppSink; + +/** + * This is a JavaFX Video renderer for GStreamer + */ +final class JavaFxAppSink extends AppSink { + + private static final String CAP_MIME_TYPE = "video/x-raw"; + private static final String CAP_BYTE_ORDER = (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN ? "format=BGRx" : "format=xRGB"); + private static final int PROP_MAX_BUFFERS = 5000; + + private final JavaFxFrameUpdater updater; + + /** + * Creates a new AppSink that hooks an ImageView into a JFXPanel. This AppSink + * comes prepackaged with an AppSink listener to accomplish the previous statement. + * + * @param name AppSink internal name + * @param target JFXPanel to display video playback in + */ + public JavaFxAppSink(String name, JFXPanel target) { + super(name); + set("emit-signals", true); + updater = new JavaFxFrameUpdater(target); + connect((AppSink.NEW_SAMPLE) updater); + connect((AppSink.NEW_PREROLL) updater); + setCaps(new Caps( + String.format("%s, %s", CAP_MIME_TYPE, CAP_BYTE_ORDER))); + set("max-buffers", PROP_MAX_BUFFERS); + set("drop", true); + } + + /** + * Clear the current frame in the JFXPanel + */ + public void clear() { + disconnect((AppSink.NEW_SAMPLE) updater); + disconnect((AppSink.NEW_PREROLL) updater); + updater.clear(); + } + + /** + * Responsible for keeping the ImageView that is hooked into the JFXPanel up-to-date + * with the most current or available frame from GStreamer. + */ + static class JavaFxFrameUpdater implements AppSink.NEW_SAMPLE, AppSink.NEW_PREROLL { + private final ImageView fxImageView; + + public JavaFxFrameUpdater(JFXPanel target) { + //We should probably pass an ImageView instead of a JFXPanel to make + //it more reuseable + fxImageView = new ImageView(); // Will hold the current video frame. + BorderPane borderpane = new BorderPane(fxImageView); // Center and size ImageView. + Scene scene = new Scene(borderpane); // Root of the JavaFX tree. + target.setScene(scene); + + // Bind size of image to that of scene, while keeping proportions + fxImageView.fitWidthProperty().bind(scene.widthProperty()); + fxImageView.fitHeightProperty().bind(scene.heightProperty()); + fxImageView.setPreserveRatio(true); + fxImageView.setSmooth(true); + fxImageView.setCache(true); + } + + /** + * Updates the ImageView when a brand new frame is in the pipeline. + * + * @param appSink Pipeline containing the new frame + * @return Result of update + */ + @Override + public FlowReturn newSample(AppSink appSink) { + return setSample(appSink.pullSample()); + } + + /** + * Set the ImageView to the input sample. Sample here is synonymous with + * frame. + * + * @param input Frame + * @return Result of update + */ + public FlowReturn setSample(Sample input) { + Buffer buffer = input.getBuffer(); + ByteBuffer byteBuffer = buffer.map(false); + if (byteBuffer != null) { + Structure capsStruct = input.getCaps().getStructure(0); + int width = capsStruct.getInteger("width"); + int height = capsStruct.getInteger("height"); + byte[] byteArray = new byte[width * height * 4]; + byteBuffer.get(byteArray); + Image videoFrame = convertBytesToImage(byteArray, width, height); + fxImageView.setImage(videoFrame); + buffer.unmap(); + } + input.dispose(); + + //Keep frames rolling + return FlowReturn.OK; + } + + /** + * Updates the ImageView with the next frame in the pipeline, without + * removing it. This function is invoked when Gstreamer is not in a + * PLAYING state, but we can peek at what's to come. + * + * It's essential for displaying the initial frame when a video is first + * selected. + * + * @param sink Pipeline containing video data + * @return + */ + @Override + public FlowReturn newPreroll(AppSink sink) { + //Grab the next frame without removing it from the pipeline + Sample sample = sink.pullPreroll(); + return setSample(sample); + } + + /** + * Create an image from a byte array of pixels. + * + * @param pixels The byte array of pixels. + * @param width The width of the image. + * @param height The height of the image. + * + * @return The image. + */ + private Image convertBytesToImage(byte[] pixels, int width, int height) { + WritableImage image = new WritableImage(width, height); + PixelWriter pixelWriter = image.getPixelWriter(); + pixelWriter.setPixels(0, 0, width, height, PixelFormat.getByteBgraInstance(), pixels, 0, width * 4); + return image; + } + + /** + * Remove the current frame from the display + */ + void clear() { + fxImageView.setImage(null); + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaFileViewer.java b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaFileViewer.java index eb86b052be..201f3f1f6f 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaFileViewer.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaFileViewer.java @@ -20,11 +20,13 @@ package org.sleuthkit.autopsy.contentviewers; import java.awt.CardLayout; import java.awt.Component; -import java.awt.Dimension; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; +import org.freedesktop.gstreamer.GstException; +import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.datamodel.AbstractFile; /** @@ -36,8 +38,7 @@ class MediaFileViewer extends javax.swing.JPanel implements FileTypeViewer { private static final Logger LOGGER = Logger.getLogger(MediaFileViewer.class.getName()); private AbstractFile lastFile; //UI - private final MediaPlayerPanel mediaPlayerPanel; - private final boolean mediaPlayerPanelInited; + private MediaPlayerPanel mediaPlayerPanel; private final MediaViewImagePanel imagePanel; private final boolean imagePanelInited; @@ -51,10 +52,14 @@ class MediaFileViewer extends javax.swing.JPanel implements FileTypeViewer { initComponents(); - // get the right panel for our platform - mediaPlayerPanel = new MediaPlayerPanel(); - mediaPlayerPanelInited = mediaPlayerPanel.isInited(); - + try { + mediaPlayerPanel = new MediaPlayerPanel(); + } catch (GstException | UnsatisfiedLinkError ex) { + LOGGER.log(Level.SEVERE, "Error initializing gstreamer for audio/video viewing and frame extraction capabilities", ex); //NON-NLS + MessageNotifyUtil.Notify.error( + NbBundle.getMessage(this.getClass(), "MediaFileViewer.initGst.gstException.msg"), + ex.getMessage()); + } imagePanel = new MediaViewImagePanel(); imagePanelInited = imagePanel.isInited(); @@ -64,7 +69,10 @@ class MediaFileViewer extends javax.swing.JPanel implements FileTypeViewer { private void customizeComponents() { add(imagePanel, IMAGE_VIEWER_LAYER); - add(mediaPlayerPanel, MEDIA_PLAYER_LAYER); + + if(mediaPlayerPanel != null) { + add(mediaPlayerPanel, MEDIA_PLAYER_LAYER); + } showImagePanel(); } @@ -86,30 +94,31 @@ class MediaFileViewer extends javax.swing.JPanel implements FileTypeViewer { /** * Returns a list of mimetypes supported by this viewer - * + * * @return list of supported mimetypes */ @Override public List getSupportedMIMETypes() { - + List mimeTypes = new ArrayList<>(); - + mimeTypes.addAll(this.imagePanel.getSupportedMimeTypes()); - mimeTypes.addAll(this.mediaPlayerPanel.getSupportedMimeTypes()); + if(mediaPlayerPanel != null) { + mimeTypes.addAll(this.mediaPlayerPanel.getSupportedMimeTypes()); + } return mimeTypes; } - - + /** * Set up the view to display the given file. - * + * * @param file file to display */ @Override public void setFile(AbstractFile file) { try { - + if (file == null) { resetComponent(); return; @@ -120,14 +129,11 @@ class MediaFileViewer extends javax.swing.JPanel implements FileTypeViewer { } lastFile = file; - - final Dimension dims = MediaFileViewer.this.getSize(); - //logger.info("setting node on media viewer"); //NON-NLS - if (mediaPlayerPanelInited && mediaPlayerPanel.isSupported(file)) { - mediaPlayerPanel.loadFile(file, dims); + if (mediaPlayerPanel != null && mediaPlayerPanel.isSupported(file)) { + mediaPlayerPanel.loadFile(file); this.showVideoPanel(); } else if (imagePanelInited && imagePanel.isSupported(file)) { - imagePanel.showImageFx(file, dims); + imagePanel.showImageFx(file); this.showImagePanel(); } } catch (Exception e) { @@ -142,7 +148,7 @@ class MediaFileViewer extends javax.swing.JPanel implements FileTypeViewer { CardLayout layout = (CardLayout) this.getLayout(); layout.show(this, MEDIA_PLAYER_LAYER); } - + /** * Show the image panel. */ @@ -158,7 +164,9 @@ class MediaFileViewer extends javax.swing.JPanel implements FileTypeViewer { @Override public void resetComponent() { - mediaPlayerPanel.reset(); + if (mediaPlayerPanel != null) { + mediaPlayerPanel.reset(); + } imagePanel.reset(); lastFile = null; } diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.form b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.form old mode 100644 new mode 100755 index 890e369b0d..880528e787 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.form +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.form @@ -1,6 +1,6 @@ -
+ @@ -16,8 +16,8 @@ - + @@ -41,7 +41,7 @@ - + @@ -51,66 +51,116 @@ - + - - - + + + + + - - - - - - + + + + + + - - - - + + + + + - - + + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java old mode 100644 new mode 100755 index 9fe38f0c69..5e0c85e807 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaPlayerPanel.java @@ -19,8 +19,8 @@ package org.sleuthkit.autopsy.contentviewers; import com.google.common.io.Files; -import java.awt.Dimension; -import java.awt.EventQueue; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; import java.io.File; import java.io.IOException; import java.util.Arrays; @@ -32,32 +32,28 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import javax.swing.BoxLayout; -import javax.swing.JButton; -import javax.swing.JLabel; import javax.swing.JPanel; -import javax.swing.JSlider; -import javax.swing.SwingUtilities; import javax.swing.SwingWorker; import javax.swing.Timer; import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; +import org.freedesktop.gstreamer.Bus; import org.freedesktop.gstreamer.ClockTime; import org.freedesktop.gstreamer.Gst; -import org.freedesktop.gstreamer.GstException; +import org.freedesktop.gstreamer.GstObject; import org.freedesktop.gstreamer.State; -import org.freedesktop.gstreamer.StateChangeReturn; import org.freedesktop.gstreamer.elements.PlayBin; import org.netbeans.api.progress.ProgressHandle; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.coreutils.VideoUtils; import org.sleuthkit.autopsy.datamodel.ContentUtils; import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector; import org.sleuthkit.datamodel.AbstractFile; -import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData; +import javafx.embed.swing.JFXPanel; +import javax.swing.event.ChangeListener; +import org.freedesktop.gstreamer.GstException; /** * This is a video player that is part of the Media View layered pane. It uses @@ -66,7 +62,8 @@ import org.sleuthkit.datamodel.TskData; @SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaViewPanel { - private static final String[] FILE_EXTENSIONS = new String[] { + //Enumerate the accepted file extensions and mimetypes + private static final String[] FILE_EXTENSIONS = new String[]{ ".3g2", ".3gp", ".3gpp", @@ -94,538 +91,238 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie ".wav", ".webm", ".wma", - ".wmv", - }; //NON-NLS + ".wmv",}; //NON-NLS private static final List MIME_TYPES = Arrays.asList( - "video/3gpp", - "video/3gpp2", - "audio/aiff", - "audio/amr-wb", - "audio/basic", - "audio/mp4", - "video/mp4", - "audio/mpeg", - "video/mpeg", - "audio/mpeg3", - "application/mxf", - "application/ogg", - "video/quicktime", - "audio/vorbis", - "audio/vnd.wave", - "video/webm", - "video/x-3ivx", - "audio/x-aac", - "audio/x-adpcm", - "audio/x-alaw", - "audio/x-cinepak", - "video/x-divx", - "audio/x-dv", - "video/x-dv", - "video/x-ffv", - "audio/x-flac", - "video/x-flv", - "audio/x-gsm", - "video/x-h263", - "video/x-h264", - "video/x-huffyuv", - "video/x-indeo", - "video/x-intel-h263", - "audio/x-ircam", - "video/x-jpeg", - "audio/x-m4a", - "video/x-m4v", - "audio/x-mace", - "audio/x-matroska", - "video/x-matroska", - "audio/x-mpeg", - "video/x-mpeg", - "audio/x-mpeg-3", - "video/x-ms-asf", - "audio/x-ms-wma", - "video/x-ms-wmv", - "video/x-msmpeg", - "video/x-msvideo", - "video/x-msvideocodec", - "audio/x-mulaw", - "audio/x-nist", - "audio/x-oggflac", - "audio/x-paris", - "audio/x-qdm2", - "audio/x-raw", - "video/x-raw", - "video/x-rle", - "audio/x-speex", - "video/x-svq", - "audio/x-svx", - "video/x-tarkin", - "video/x-theora", - "audio/x-voc", - "audio/x-vorbis", - "video/x-vp3", - "audio/x-w64", - "audio/x-wav", - "audio/x-wma", - "video/x-wmv", - "video/x-xvid" + "video/3gpp", + "video/3gpp2", + "audio/aiff", + "audio/amr-wb", + "audio/basic", + "audio/mp4", + "video/mp4", + "audio/mpeg", + "video/mpeg", + "audio/mpeg3", + "application/mxf", + "application/ogg", + "video/quicktime", + "audio/vorbis", + "audio/vnd.wave", + "video/webm", + "video/x-3ivx", + "audio/x-aac", + "audio/x-adpcm", + "audio/x-alaw", + "audio/x-cinepak", + "video/x-divx", + "audio/x-dv", + "video/x-dv", + "video/x-ffv", + "audio/x-flac", + "video/x-flv", + "audio/x-gsm", + "video/x-h263", + "video/x-h264", + "video/x-huffyuv", + "video/x-indeo", + "video/x-intel-h263", + "audio/x-ircam", + "video/x-jpeg", + "audio/x-m4a", + "video/x-m4v", + "audio/x-mace", + "audio/x-matroska", + "video/x-matroska", + "audio/x-mpeg", + "video/x-mpeg", + "audio/x-mpeg-3", + "video/x-ms-asf", + "audio/x-ms-wma", + "video/x-ms-wmv", + "video/x-msmpeg", + "video/x-msvideo", + "video/x-msvideocodec", + "audio/x-mulaw", + "audio/x-nist", + "audio/x-oggflac", + "audio/x-paris", + "audio/x-qdm2", + "audio/x-raw", + "video/x-raw", + "video/x-rle", + "audio/x-speex", + "video/x-svq", + "audio/x-svx", + "video/x-tarkin", + "video/x-theora", + "audio/x-voc", + "audio/x-vorbis", + "video/x-vp3", + "audio/x-w64", + "audio/x-wav", + "audio/x-wma", + "video/x-wmv", + "video/x-xvid" ); //NON-NLS private static final Logger logger = Logger.getLogger(MediaPlayerPanel.class.getName()); - private boolean gstInited; - private static final String MEDIA_PLAYER_ERROR_STRING = NbBundle.getMessage(MediaPlayerPanel.class, "GstVideoPanel.cannotProcFile.err"); - //playback - private long durationMillis = 0; - private int totalHours, totalMinutes, totalSeconds; + private static final String MEDIA_PLAYER_ERROR_STRING = NbBundle.getMessage(MediaPlayerPanel.class, + "GstVideoPanel.cannotProcFile.err"); + + //Video playback components private volatile PlayBin gstPlayBin; - private GstVideoRendererPanel gstVideoRenderer; - private final Object playbinLock = new Object(); // lock for synchronization of gstPlayBin player - private AbstractFile currentFile; - - private Timer timer; + private JavaFxAppSink fxAppSink; + private Bus.ERROR errorListener; + private Bus.STATE_CHANGED stateChangeListener; + private Bus.EOS endOfStreamListener; + + //Update progress bar and time label during video playback + private final Timer timer = new Timer(75, new VideoPanelUpdater()); + private static final int PROGRESS_SLIDER_SIZE = 2000; + private ExtractMedia extractMediaWorker; - - private static final long END_TIME_MARGIN_NS = 50000000; - private static final int PLAYER_STATUS_UPDATE_INTERVAL_MS = 50; /** * Creates new form MediaViewVideoPanel */ - public MediaPlayerPanel() { + public MediaPlayerPanel() throws GstException, UnsatisfiedLinkError { initComponents(); + initGst(); customizeComponents(); } - public JButton getPauseButton() { - return pauseButton; - } - - public JLabel getProgressLabel() { - return progressLabel; - } - - public JSlider getProgressSlider() { - return progressSlider; - } - - public JPanel getVideoPanel() { - return videoPanel; - } - - /** - * Has this MediaPlayerPanel been initialized correctly? - * - * @return - */ - public boolean isInited() { - return gstInited; - } - private void customizeComponents() { - if (!initGst()) { - return; - } - progressSlider.setEnabled(false); // disable slider; enable after user plays vid progressSlider.setMinimum(0); - progressSlider.setMaximum(2000); + progressSlider.setMaximum(PROGRESS_SLIDER_SIZE); progressSlider.setValue(0); + //Manage the gstreamer video position when a user is dragging the slider in the panel. progressSlider.addChangeListener(new ChangeListener() { @Override - public void stateChanged(ChangeEvent event) { - if (gstPlayBin == null) { - return; - } + public void stateChanged(ChangeEvent e) { if (progressSlider.getValueIsAdjusting()) { - synchronized (playbinLock) { - long duration = gstPlayBin.queryDuration(TimeUnit.NANOSECONDS); - long position = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS); - if (duration > 0) { - double relativePosition = progressSlider.getValue() / 2000.0; - gstPlayBin.seek((long) (relativePosition * duration), TimeUnit.NANOSECONDS); - } else if (position > 0 || progressSlider.getValue() > 0) { - gstPlayBin.seek(ClockTime.ZERO); - progressSlider.setValue(0); - } - } + long duration = gstPlayBin.queryDuration(TimeUnit.NANOSECONDS); + double relativePosition = progressSlider.getValue() * 1.0 / PROGRESS_SLIDER_SIZE; + long newPos = (long) (relativePosition * duration); + gstPlayBin.seek(newPos, TimeUnit.NANOSECONDS); + //Keep constantly updating the time label so users have a sense of + //where the slider they are dragging is in relation to the video time + updateTimeLabel(newPos, duration); } } }); + //Manage the audio level when the user is adjusting the volumn slider + audioSlider.addChangeListener((ChangeEvent event) -> { + if (audioSlider.getValueIsAdjusting()) { + double audioPercent = (audioSlider.getValue() * 2.0) / 100.0; + gstPlayBin.setVolume(audioPercent); + } + }); + errorListener = new Bus.ERROR() { + @Override + public void errorMessage(GstObject go, int i, String string) { + enableComponents(false); + infoLabel.setText(String.format( + "%s", + MEDIA_PLAYER_ERROR_STRING)); + timer.stop(); + } + }; + stateChangeListener = new Bus.STATE_CHANGED() { + @Override + public void stateChanged(GstObject go, State oldState, State currentState, State pendingState) { + if (State.PLAYING.equals(currentState)) { + playButton.setText("||"); + } else { + playButton.setText("â–º"); + } + } + }; + endOfStreamListener = new Bus.EOS() { + @Override + public void endOfStream(GstObject go) { + gstPlayBin.seek(ClockTime.ZERO); + progressSlider.setValue(0); + /** + * Keep the video from automatically playing + */ + Gst.getExecutor().submit(() -> gstPlayBin.pause()); + } + }; } - private boolean initGst() { - try { - logger.log(Level.INFO, "Initializing gstreamer for video/audio viewing"); //NON-NLS - Gst.init(); - gstInited = true; - } catch (GstException | UnsatisfiedLinkError ex) { - gstInited = false; - logger.log(Level.SEVERE, "Error initializing gstreamer for audio/video viewing and frame extraction capabilities", ex); //NON-NLS - MessageNotifyUtil.Notify.error( - NbBundle.getMessage(this.getClass(), "GstVideoPanel.initGst.gstException.msg"), - ex.getMessage()); - return false; - } - - return true; + private void initGst() throws GstException, UnsatisfiedLinkError { + logger.log(Level.INFO, "Attempting initializing of gstreamer for video/audio viewing"); //NON-NLS + Gst.init(); } /** - * Initialize all the necessary variables to play an audio/video file. + * Loads the file by spawning off a background task to handle file copying + * and video component initializations. * * @param file Media file to play. - * @param dims Dimension of the parent window. */ - @NbBundle.Messages ({"GstVideoPanel.noOpenCase.errMsg=No open case available."}) - void loadFile(final AbstractFile file, final Dimension dims) { - EventQueue.invokeLater(() -> { - reset(); - infoLabel.setText(""); - currentFile = file; - final boolean deleted = file.isDirNameFlagSet(TskData.TSK_FS_NAME_FLAG_ENUM.UNALLOC); - if (deleted) { - infoLabel.setText(NbBundle.getMessage(this.getClass(), "GstVideoPanel.setupVideo.infoLabel.text")); - videoPanel.removeAll(); - pauseButton.setEnabled(false); - progressSlider.setEnabled(false); - return; - } - - java.io.File ioFile; - try { - ioFile = VideoUtils.getVideoFileInTempDir(file); - } catch (NoCurrentCaseException ex) { - logger.log(Level.SEVERE, "Exception while getting open case.", ex); //NON-NLS - infoLabel.setText(Bundle.GstVideoPanel_noOpenCase_errMsg()); - pauseButton.setEnabled(false); - progressSlider.setEnabled(false); - - return; - } - - String path = ""; - try { - path = file.getUniquePath(); - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Cannot get unique path of video file.", ex); //NON-NLS - } - infoLabel.setText(path); - infoLabel.setToolTipText(path); - pauseButton.setEnabled(true); - progressSlider.setEnabled(true); - timer = new Timer(PLAYER_STATUS_UPDATE_INTERVAL_MS, event -> { - if (!progressSlider.getValueIsAdjusting()) { - long duration; - long position; - synchronized (playbinLock) { - duration = gstPlayBin.queryDuration(TimeUnit.NANOSECONDS); - position = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS); - if (duration > 0) { - long positionDelta = duration - position; - if (positionDelta <= END_TIME_MARGIN_NS && gstPlayBin.isPlaying()) { - gstPlayBin.pause(); - if (gstPlayBin.seek(ClockTime.ZERO) == false) { - logger.log(Level.WARNING, "Attempt to call PlayBin.seek() failed."); //NON-NLS - infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); - return; - } - progressSlider.setValue(0); - pauseButton.setText("â–º"); - } else { - double relativePosition = (double) position / duration; - progressSlider.setValue((int) (relativePosition * 2000)); - } - } - } - - durationMillis = duration / 1000000; - // pick out the total hours, minutes, seconds - long durationSeconds = (int) durationMillis / 1000; - totalHours = (int) durationSeconds / 3600; - durationSeconds -= totalHours * 3600; - totalMinutes = (int) durationSeconds / 60; - durationSeconds -= totalMinutes * 60; - totalSeconds = (int) durationSeconds; - - long millisElapsed = position / 1000000; - // pick out the elapsed hours, minutes, seconds - long secondsElapsed = millisElapsed / 1000; - int elapsedHours = (int) secondsElapsed / 3600; - secondsElapsed -= elapsedHours * 3600; - int elapsedMinutes = (int) secondsElapsed / 60; - secondsElapsed -= elapsedMinutes * 60; - int elapsedSeconds = (int) secondsElapsed; - - String durationFormat = "%02d:%02d:%02d/%02d:%02d:%02d "; //NON-NLS - String durationStr = String.format(durationFormat, - elapsedHours, elapsedMinutes, elapsedSeconds, - totalHours, totalMinutes, totalSeconds); - progressLabel.setText(durationStr); - } - }); - timer.start(); - - gstVideoRenderer = new GstVideoRendererPanel(); - synchronized (playbinLock) { - if (gstPlayBin != null) { - gstPlayBin.dispose(); - } - gstPlayBin = new PlayBin("VideoPlayer"); //NON-NLS - gstPlayBin.setVideoSink(gstVideoRenderer.getVideoSink()); - - videoPanel.removeAll(); - - videoPanel.setLayout(new BoxLayout(videoPanel, BoxLayout.Y_AXIS)); - - videoPanel.add(gstVideoRenderer);//add jfx ui to JPanel - - videoPanel.setVisible(true); - - gstPlayBin.setInputFile(ioFile); - } - }); - } - - /** - * Prepare this MediaViewVideoPanel to accept a different media file. - */ - void reset() { - if (timer != null) { - timer.stop(); - } - - // reset the progress label text on the event dispatch thread - SwingUtilities.invokeLater(() -> { - progressLabel.setText(""); - }); - - if (!isInited()) { + @NbBundle.Messages({"GstVideoPanel.noOpenCase.errMsg=No open case available."}) + void loadFile(final AbstractFile file) { + //Ensure everything is back in the initial state + infoLabel.setText(""); + if (file.isDirNameFlagSet(TskData.TSK_FS_NAME_FLAG_ENUM.UNALLOC)) { + infoLabel.setText(NbBundle.getMessage(this.getClass(), "GstVideoPanel.setupVideo.infoLabel.text")); return; } - synchronized (playbinLock) { - if (gstPlayBin != null) { - if (gstPlayBin.isPlaying() && gstPlayBin.stop() == StateChangeReturn.FAILURE) { - logger.log(Level.WARNING, "Attempt to call PlayBin.stop() failed."); //NON-NLS - infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); - return; - } - gstPlayBin.dispose(); - gstPlayBin = null; - } - gstVideoRenderer = null; + try { + //Pushing off initialization to the background + extractMediaWorker = new ExtractMedia(file, VideoUtils.getVideoFileInTempDir(file)); + extractMediaWorker.execute(); + } catch (NoCurrentCaseException ex) { + logger.log(Level.SEVERE, "Exception while getting open case.", ex); //NON-NLS + infoLabel.setText(String.format("%s", Bundle.GstVideoPanel_noOpenCase_errMsg())); + enableComponents(false); } - - progressSlider.setValue(0); - pauseButton.setText("â–º"); - - currentFile = null; } /** - * 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. + * Assume no support on a fresh reset until we begin loading the file for + * play. */ - @SuppressWarnings("unchecked") - // - private void initComponents() { - - videoPanel = new javax.swing.JPanel(); - controlPanel = new javax.swing.JPanel(); - pauseButton = new javax.swing.JButton(); - progressSlider = new javax.swing.JSlider(); - progressLabel = new javax.swing.JLabel(); - infoLabel = new javax.swing.JLabel(); - - javax.swing.GroupLayout videoPanelLayout = new javax.swing.GroupLayout(videoPanel); - videoPanel.setLayout(videoPanelLayout); - videoPanelLayout.setHorizontalGroup( - videoPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGap(0, 0, Short.MAX_VALUE) - ); - videoPanelLayout.setVerticalGroup( - videoPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGap(0, 231, Short.MAX_VALUE) - ); - - org.openide.awt.Mnemonics.setLocalizedText(pauseButton, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaViewVideoPanel.pauseButton.text")); // NOI18N - pauseButton.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - pauseButtonActionPerformed(evt); - } - }); - - org.openide.awt.Mnemonics.setLocalizedText(progressLabel, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaViewVideoPanel.progressLabel.text")); // NOI18N - - org.openide.awt.Mnemonics.setLocalizedText(infoLabel, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaViewVideoPanel.infoLabel.text")); // NOI18N - - javax.swing.GroupLayout controlPanelLayout = new javax.swing.GroupLayout(controlPanel); - controlPanel.setLayout(controlPanelLayout); - controlPanelLayout.setHorizontalGroup( - controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(controlPanelLayout.createSequentialGroup() - .addContainerGap() - .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(controlPanelLayout.createSequentialGroup() - .addGap(6, 6, 6) - .addComponent(infoLabel) - .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) - .addGroup(controlPanelLayout.createSequentialGroup() - .addComponent(pauseButton) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(progressSlider, javax.swing.GroupLayout.DEFAULT_SIZE, 265, Short.MAX_VALUE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(progressLabel) - .addContainerGap()))) - ); - controlPanelLayout.setVerticalGroup( - controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(controlPanelLayout.createSequentialGroup() - .addContainerGap() - .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(progressSlider, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(pauseButton) - .addComponent(progressLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 29, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(infoLabel) - .addContainerGap()) - ); - - javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); - this.setLayout(layout); - layout.setHorizontalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(controlPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(videoPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - ); - layout.setVerticalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addComponent(videoPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(controlPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) - ); - }// - - private void pauseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_pauseButtonActionPerformed - synchronized (playbinLock) { - if (gstPlayBin == null) { - return; - } - State state = gstPlayBin.getState(); - if (state.equals(State.PLAYING)) { - if (gstPlayBin.pause() == StateChangeReturn.FAILURE) { - logger.log(Level.WARNING, "Attempt to call PlayBin.pause() failed."); //NON-NLS - infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); - return; - } - pauseButton.setText("â–º"); - } else if (state.equals(State.PAUSED)) { - if (gstPlayBin.play() == StateChangeReturn.FAILURE) { - logger.log(Level.WARNING, "Attempt to call PlayBin.play() failed."); //NON-NLS - infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); - return; - } - pauseButton.setText("||"); - } else if (state.equals(State.READY) || state.equals(State.NULL)) { - final File tempVideoFile; - try { - tempVideoFile = VideoUtils.getVideoFileInTempDir(currentFile); - } catch (NoCurrentCaseException ex) { - logger.log(Level.WARNING, "Exception while getting open case."); //NON-NLS - infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); - return; - } - - if (extractMediaWorker != null) { - extractMediaWorker.cancel(true); - extractMediaWorker = null; - } - extractMediaWorker = new ExtractMedia(currentFile, tempVideoFile); - extractMediaWorker.execute(); - - } - } - }//GEN-LAST:event_pauseButtonActionPerformed - - // Variables declaration - do not modify//GEN-BEGIN:variables - private javax.swing.JPanel controlPanel; - private javax.swing.JLabel infoLabel; - private javax.swing.JButton pauseButton; - private javax.swing.JLabel progressLabel; - private javax.swing.JSlider progressSlider; - private javax.swing.JPanel videoPanel; - // End of variables declaration//GEN-END:variables + @NbBundle.Messages({ + "MediaPlayerPanel.noSupport=File not supported." + }) + void resetComponents() { + progressLabel.setText(String.format("%s/%s", Bundle.MediaPlayerPanel_unknownTime(), + Bundle.MediaPlayerPanel_unknownTime())); + infoLabel.setText(Bundle.MediaPlayerPanel_noSupport()); + progressSlider.setValue(0); + } /** - * Thread that extracts and plays a file + * Return this panel to its initial state. */ - private class ExtractMedia extends SwingWorker { - - private ProgressHandle progress; - private final AbstractFile sourceFile; - private final java.io.File tempFile; - - ExtractMedia(AbstractFile sFile, java.io.File jFile) { - this.sourceFile = sFile; - this.tempFile = jFile; + void reset() { + if (extractMediaWorker != null) { + extractMediaWorker.cancel(true); } - - @Override - protected Long doInBackground() throws Exception { - if (tempFile.exists() == false || tempFile.length() < sourceFile.getSize()) { - progress = ProgressHandle.createHandle(NbBundle.getMessage(MediaPlayerPanel.class, "GstVideoPanel.ExtractMedia.progress.buffering", sourceFile.getName()), () -> this.cancel(true)); - progressLabel.setText(NbBundle.getMessage(this.getClass(), "GstVideoPanel.progress.buffering")); - progress.start(100); - try { - Files.createParentDirs(tempFile); - return ContentUtils.writeToFile(sourceFile, tempFile, progress, this, true); - } catch (IOException ex) { - logger.log(Level.WARNING, "Error buffering file", ex); //NON-NLS - return 0L; - } - } - return 0L; + timer.stop(); + if (gstPlayBin != null) { + gstPlayBin.stop(); + gstPlayBin.getBus().disconnect(endOfStreamListener); + gstPlayBin.getBus().disconnect(endOfStreamListener); + gstPlayBin.getBus().disconnect(endOfStreamListener); + gstPlayBin.dispose(); + fxAppSink.clear(); + gstPlayBin = null; } + videoPanel.removeAll(); + resetComponents(); + enableComponents(false); + } - /* - * clean up or start the worker threads - */ - @Override - protected void done() { - try { - super.get(); //block and get all exceptions thrown while doInBackground() - } catch (CancellationException ex) { - logger.log(Level.INFO, "Media buffering was canceled."); //NON-NLS - } catch (InterruptedException ex) { - logger.log(Level.INFO, "Media buffering was interrupted."); //NON-NLS - } catch (ExecutionException ex) { - logger.log(Level.SEVERE, "Fatal error during media buffering.", ex); //NON-NLS - } finally { - if (progress != null) { - progress.finish(); - } - if (!this.isCancelled()) { - playMedia(); - } - } - } - - void playMedia() { - if (tempFile == null || !tempFile.exists()) { - progressLabel.setText(NbBundle.getMessage(this.getClass(), "GstVideoPanel.progressLabel.bufferingErr")); - return; - } - synchronized (playbinLock) { - gstPlayBin.seek(ClockTime.ZERO); - // must play, then pause and get state to get duration. - if (gstPlayBin.play() == StateChangeReturn.FAILURE) { - logger.log(Level.WARNING, "Attempt to call PlayBin.play() failed."); //NON-NLS - infoLabel.setText(MEDIA_PLAYER_ERROR_STRING); - return; - } - pauseButton.setText("||"); - } - } + private void enableComponents(boolean isEnabled) { + playButton.setEnabled(isEnabled); + progressSlider.setEnabled(isEnabled); + videoPanel.setEnabled(isEnabled); + audioSlider.setEnabled(isEnabled); } @Override @@ -674,4 +371,280 @@ public class MediaPlayerPanel extends JPanel implements MediaFileViewer.MediaVie return false; } + /** + * Formats current time and total time as the following ratio: HH:MM:SS / + * HH:MM:SS + * + * @param start + * @param total + */ + private void updateTimeLabel(long start, long total) { + progressLabel.setText(formatTime(start, false) + "/" + formatTime(total, true)); + } + + /** + * Convert nanoseconds into an HH:MM:SS format. + */ + @NbBundle.Messages({ + "MediaPlayerPanel.unknownTime=Unknown", + "MediaPlayerPanel.timeFormat=%02d:%02d:%02d" + }) + private String formatTime(long ns, boolean ceiling) { + if (ns == -1) { + return Bundle.MediaPlayerPanel_unknownTime(); + } + + double millis = ns / 1000000.0; + double seconds; + if (ceiling) { + seconds = Math.ceil(millis / 1000); + } else { + seconds = millis / 1000; + } + double hours = seconds / 3600; + seconds -= (int) hours * 3600; + double minutes = seconds / 60; + seconds -= (int) minutes * 60; + + return String.format(Bundle.MediaPlayerPanel_timeFormat(), (int) hours, (int) minutes, (int) seconds); + } + + /** + * Thread that extracts a file and initializes all of the playback + * components. + */ + private class ExtractMedia extends SwingWorker { + + private ProgressHandle progress; + private final AbstractFile sourceFile; + private final java.io.File tempFile; + + ExtractMedia(AbstractFile sFile, File jFile) { + this.sourceFile = sFile; + this.tempFile = jFile; + } + + @Override + protected Void doInBackground() throws Exception { + if (!tempFile.exists() || tempFile.length() < sourceFile.getSize()) { + progress = ProgressHandle.createHandle(NbBundle.getMessage(MediaPlayerPanel.class, "GstVideoPanel.ExtractMedia.progress.buffering", sourceFile.getName()), () -> this.cancel(true)); + progressLabel.setText(NbBundle.getMessage(this.getClass(), "GstVideoPanel.progress.buffering")); + progress.start(100); + try { + Files.createParentDirs(tempFile); + ContentUtils.writeToFile(sourceFile, tempFile, progress, this, true); + } catch (IOException ex) { + logger.log(Level.WARNING, "Error creating parent directory for copying video/audio in temp directory", ex); //NON-NLS + } finally { + progress.finish(); + } + } + return null; + } + + /* + * Initialize the playback components if the extraction was successful. + */ + @Override + protected void done() { + try { + super.get(); + + if(this.isCancelled()) { + return; + } + //Video is ready for playback. Create new components + gstPlayBin = new PlayBin("VideoPlayer", tempFile.toURI()); + //Configure event handling + Bus playBinBus = gstPlayBin.getBus(); + playBinBus.connect(endOfStreamListener); + playBinBus.connect(stateChangeListener); + playBinBus.connect(errorListener); + + if(this.isCancelled()) { + return; + } + + JFXPanel fxPanel = new JFXPanel(); + videoPanel.removeAll(); + videoPanel.setLayout(new BoxLayout(videoPanel, BoxLayout.Y_AXIS)); + videoPanel.add(fxPanel); + fxAppSink = new JavaFxAppSink("JavaFxAppSink", fxPanel); + gstPlayBin.setVideoSink(fxAppSink); + + if(this.isCancelled()) { + return; + } + + gstPlayBin.setVolume((audioSlider.getValue() * 2.0) / 100.0); + gstPlayBin.pause(); + + timer.start(); + enableComponents(true); + } catch (CancellationException ex) { + logger.log(Level.INFO, "Media buffering was canceled."); //NON-NLS + } catch (InterruptedException ex) { + logger.log(Level.INFO, "Media buffering was interrupted."); //NON-NLS + } catch (ExecutionException ex) { + logger.log(Level.SEVERE, "Fatal error during media buffering.", ex); //NON-NLS + } + } + } + + /** + * Updates the video time bar and the time label when a video is playing. + */ + private class VideoPanelUpdater implements ActionListener { + + @Override + public void actionPerformed(ActionEvent e) { + if (!progressSlider.getValueIsAdjusting()) { + long position = gstPlayBin.queryPosition(TimeUnit.NANOSECONDS); + long duration = gstPlayBin.queryDuration(TimeUnit.NANOSECONDS); + /** + * Duration may not be known until there is video data in the + * pipeline. We start this updater when data-flow has just been + * initiated so buffering may still be in progress. + */ + if (duration != -1) { + double relativePosition = (double) position / duration; + progressSlider.setValue((int) (relativePosition * PROGRESS_SLIDER_SIZE)); + } + + updateTimeLabel(position, duration); + } + } + } + + /** + * 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() { + + videoPanel = new javax.swing.JPanel(); + controlPanel = new javax.swing.JPanel(); + progressSlider = new javax.swing.JSlider(); + infoLabel = new javax.swing.JLabel(); + playButton = new javax.swing.JButton(); + progressLabel = new javax.swing.JLabel(); + VolumeIcon = new javax.swing.JLabel(); + audioSlider = new javax.swing.JSlider(); + + javax.swing.GroupLayout videoPanelLayout = new javax.swing.GroupLayout(videoPanel); + videoPanel.setLayout(videoPanelLayout); + videoPanelLayout.setHorizontalGroup( + videoPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGap(0, 0, Short.MAX_VALUE) + ); + videoPanelLayout.setVerticalGroup( + videoPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGap(0, 259, Short.MAX_VALUE) + ); + + progressSlider.setValue(0); + progressSlider.setCursor(new java.awt.Cursor(java.awt.Cursor.DEFAULT_CURSOR)); + progressSlider.setDoubleBuffered(true); + progressSlider.setMinimumSize(new java.awt.Dimension(36, 21)); + progressSlider.setPreferredSize(new java.awt.Dimension(200, 21)); + + org.openide.awt.Mnemonics.setLocalizedText(infoLabel, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.infoLabel.text")); // NOI18N + infoLabel.setCursor(new java.awt.Cursor(java.awt.Cursor.DEFAULT_CURSOR)); + + org.openide.awt.Mnemonics.setLocalizedText(playButton, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.playButton.text")); // NOI18N + playButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + playButtonActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(progressLabel, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.progressLabel.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(VolumeIcon, org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.VolumeIcon.text")); // NOI18N + + audioSlider.setMajorTickSpacing(10); + audioSlider.setMaximum(50); + audioSlider.setMinorTickSpacing(5); + audioSlider.setPaintTicks(true); + audioSlider.setToolTipText(org.openide.util.NbBundle.getMessage(MediaPlayerPanel.class, "MediaPlayerPanel.audioSlider.toolTipText")); // NOI18N + audioSlider.setValue(25); + audioSlider.setMinimumSize(new java.awt.Dimension(200, 21)); + audioSlider.setPreferredSize(new java.awt.Dimension(200, 21)); + + javax.swing.GroupLayout controlPanelLayout = new javax.swing.GroupLayout(controlPanel); + controlPanel.setLayout(controlPanelLayout); + controlPanelLayout.setHorizontalGroup( + controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, controlPanelLayout.createSequentialGroup() + .addContainerGap() + .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(controlPanelLayout.createSequentialGroup() + .addComponent(playButton, javax.swing.GroupLayout.PREFERRED_SIZE, 64, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(progressSlider, javax.swing.GroupLayout.DEFAULT_SIZE, 680, Short.MAX_VALUE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(progressLabel)) + .addGroup(controlPanelLayout.createSequentialGroup() + .addComponent(infoLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGap(18, 18, 18) + .addComponent(VolumeIcon, javax.swing.GroupLayout.PREFERRED_SIZE, 64, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(2, 2, 2) + .addComponent(audioSlider, javax.swing.GroupLayout.PREFERRED_SIZE, 229, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addContainerGap()) + ); + controlPanelLayout.setVerticalGroup( + controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(controlPanelLayout.createSequentialGroup() + .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addComponent(progressLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(progressSlider, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addComponent(playButton)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(audioSlider, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGroup(controlPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(VolumeIcon, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(infoLabel))) + .addGap(13, 13, 13)) + ); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(videoPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(controlPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(videoPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(controlPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + ); + }// //GEN-END:initComponents + + private void playButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_playButtonActionPerformed + if (gstPlayBin.isPlaying()) { + gstPlayBin.pause(); + } else { + gstPlayBin.play(); + } + }//GEN-LAST:event_playButtonActionPerformed + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JLabel VolumeIcon; + private javax.swing.JSlider audioSlider; + private javax.swing.JPanel controlPanel; + private javax.swing.JLabel infoLabel; + private javax.swing.JButton playButton; + private javax.swing.JLabel progressLabel; + private javax.swing.JSlider progressSlider; + private javax.swing.JPanel videoPanel; + // End of variables declaration//GEN-END:variables } diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaViewImagePanel.java b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaViewImagePanel.java index b96e61e462..936716fc78 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaViewImagePanel.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaViewImagePanel.java @@ -18,7 +18,6 @@ */ package org.sleuthkit.autopsy.contentviewers; -import java.awt.Dimension; import java.awt.EventQueue; import java.awt.event.ActionEvent; import java.util.Collections; @@ -172,9 +171,8 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan * Show the contents of the given AbstractFile as a visual image. * * @param file image file to show - * @param dims dimension of the parent window (ignored) */ - void showImageFx(final AbstractFile file, final Dimension dims) { + void showImageFx(final AbstractFile file) { if (!fxInited) { return; } diff --git a/Core/src/org/sleuthkit/autopsy/coordinationservice/CoordinationService.java b/Core/src/org/sleuthkit/autopsy/coordinationservice/CoordinationService.java index ea087664b2..9bd5710980 100644 --- a/Core/src/org/sleuthkit/autopsy/coordinationservice/CoordinationService.java +++ b/Core/src/org/sleuthkit/autopsy/coordinationservice/CoordinationService.java @@ -114,7 +114,16 @@ public final class CoordinationService { } try { instance = new CoordinationService(rootNode); - } catch (IOException | InterruptedException | KeeperException | CoordinationServiceException ex) { + } catch (IOException | KeeperException | CoordinationServiceException ex) { + throw new CoordinationServiceException("Failed to create coordination service", ex); + } catch (InterruptedException ex) { + /* + * The interrupted exception should be propagated to support + * task cancellation. To avoid a public API change here, restore + * the interrupted flag and then throw the InterruptedException + * in its wrapper. + */ + Thread.currentThread().interrupt(); throw new CoordinationServiceException("Failed to create coordination service", ex); } } @@ -363,15 +372,22 @@ public final class CoordinationService { * @param category The desired category in the namespace. * @param nodePath The node to be deleted. * - * @throws CoordinationServiceException If there is an error deleting the - * node. + * @throws CoordinationServiceException If there is an error deleting the + * node. + * @throws java.lang.InterruptedException If a thread interrupt occurs while + * blocked waiting for the operation + * to complete. */ - public void deleteNode(CategoryNode category, String nodePath) throws CoordinationServiceException { + public void deleteNode(CategoryNode category, String nodePath) throws CoordinationServiceException, InterruptedException { String fullNodePath = getFullyQualifiedNodePath(category, nodePath); try { curator.delete().forPath(fullNodePath); } catch (Exception ex) { - throw new CoordinationServiceException(String.format("Failed to delete node %s", fullNodePath), ex); + if (ex instanceof InterruptedException) { + throw (InterruptedException) ex; + } else { + throw new CoordinationServiceException(String.format("Failed to delete node %s", fullNodePath), ex); + } } } @@ -382,15 +398,22 @@ public final class CoordinationService { * * @return A list of child node names. * - * @throws CoordinationServiceException If there is an error getting the - * node list. + * @throws CoordinationServiceException If there is an error getting the + * node list. + * @throws java.lang.InterruptedException If a thread interrupt occurs while + * blocked waiting for the operation + * to complete. */ - public List getNodeList(CategoryNode category) throws CoordinationServiceException { + public List getNodeList(CategoryNode category) throws CoordinationServiceException, InterruptedException { try { List list = curator.getChildren().forPath(categoryNodeToPath.get(category.getDisplayName())); return list; } catch (Exception ex) { - throw new CoordinationServiceException(String.format("Failed to get node list for %s", category.getDisplayName()), ex); + if (ex instanceof InterruptedException) { + throw (InterruptedException) ex; + } else { + throw new CoordinationServiceException(String.format("Failed to get node list for %s", category.getDisplayName()), ex); + } } } @@ -404,9 +427,9 @@ public final class CoordinationService { */ private String getFullyQualifiedNodePath(CategoryNode category, String nodePath) { // nodePath on Unix systems starts with a "/" and ZooKeeper doesn't like two slashes in a row - if(nodePath.startsWith("/")){ + if (nodePath.startsWith("/")) { return categoryNodeToPath.get(category.getDisplayName()) + nodePath.toUpperCase(); - }else{ + } else { return categoryNodeToPath.get(category.getDisplayName()) + "/" + nodePath.toUpperCase(); } } diff --git a/Core/src/org/sleuthkit/autopsy/core/Installer.java b/Core/src/org/sleuthkit/autopsy/core/Installer.java index 2f65b6099b..a9a4131ea4 100644 --- a/Core/src/org/sleuthkit/autopsy/core/Installer.java +++ b/Core/src/org/sleuthkit/autopsy/core/Installer.java @@ -18,9 +18,12 @@ */ package org.sleuthkit.autopsy.core; +import com.sun.jna.platform.win32.Kernel32; 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.ArrayList; import java.util.List; import java.util.concurrent.Callable; @@ -31,6 +34,7 @@ import java.util.logging.Level; import javafx.application.Platform; import javafx.embed.swing.JFXPanel; import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; import org.openide.modules.InstalledFileLocator; import org.openide.modules.ModuleInstall; import org.openide.util.NbBundle; @@ -74,6 +78,8 @@ public class Installer extends ModuleInstall { */ if (PlatformUtil.isWindowsOS()) { try { + addGstreamerPathsToEnv(); + //Note: if shipping with a different CRT version, this will only print a warning //and try to use linker mechanism to find the correct versions of libs. //We should update this if we officially switch to a new version of CRT/compiler @@ -271,6 +277,43 @@ public class Installer extends ModuleInstall { } } + /** + * Add the Gstreamer bin and lib paths to the PATH environment variable so + * that the correct plugins and libraries are found when Gstreamer is + * initialized later. + */ + private static void addGstreamerPathsToEnv() { + Path gstreamerPath = InstalledFileLocator.getDefault().locate("gstreamer", Installer.class.getPackage().getName(), false).toPath(); + + if (gstreamerPath == null) { + logger.log(Level.SEVERE, "Failed to find GStreamer."); + } else { + String arch = "x86_64"; + if (!PlatformUtil.is64BitJVM()) { + arch = "x86"; + } + + Path gstreamerBasePath = Paths.get(gstreamerPath.toString(), "1.0", arch); + Path gstreamerBinPath = Paths.get(gstreamerBasePath.toString(), "bin"); + Path gstreamerLibPath = Paths.get(gstreamerBasePath.toString(), "lib", "gstreamer-1.0"); + + // Update the PATH environment variable to contain the GStreamer + // lib and bin paths. + Kernel32 k32 = Kernel32.INSTANCE; + String path = System.getenv("PATH"); + if (StringUtils.isBlank(path)) { + k32.SetEnvironmentVariable("PATH", gstreamerLibPath.toString()); + } else { + /* + * Note that we *prepend* the paths so that the Gstreamer + * binaries associated with the current release are found rather + * than binaries associated with an earlier version of Autopsy. + */ + k32.SetEnvironmentVariable("PATH", gstreamerBinPath.toString() + File.pathSeparator + gstreamerLibPath.toString() + path); + } + } + } + /** * Make a folder in the config directory for object detection classifiers if one does not * exist. @@ -288,7 +331,7 @@ public class Installer extends ModuleInstall { File pythonModulesDir = new File(PlatformUtil.getUserPythonModulesPath()); pythonModulesDir.mkdir(); } - + /** * Make a folder in the config directory for Ocr Language Packs if one does * not exist. @@ -296,10 +339,10 @@ public class Installer extends ModuleInstall { private static void ensureOcrLanguagePacksFolderExists() { File ocrLanguagePacksDir = new File(PlatformUtil.getOcrLanguagePacksPath()); boolean createDirectory = ocrLanguagePacksDir.mkdir(); - + //If the directory did not exist, copy the tessdata folder over so we //support english. - if(createDirectory) { + if (createDirectory) { File tessdataDir = InstalledFileLocator.getDefault().locate( "Tesseract-OCR/tessdata", Installer.class.getPackage().getName(), false); try { diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/ExecUtil.java b/Core/src/org/sleuthkit/autopsy/coreutils/ExecUtil.java index 4c7c022871..0a76b2a7fa 100644 --- a/Core/src/org/sleuthkit/autopsy/coreutils/ExecUtil.java +++ b/Core/src/org/sleuthkit/autopsy/coreutils/ExecUtil.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2017 Basis Technology Corp. + * Copyright 2013-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -59,7 +59,19 @@ public final class ExecUtil { } /** - * Process terminator that can be used to kill a processes after it exceeds + * A process terminator that can be used to kill a process spawned by a + * thread that has been interrupted. + */ + public static class InterruptedThreadProcessTerminator implements ProcessTerminator { + + @Override + public boolean shouldTerminateProcess() { + return Thread.currentThread().isInterrupted(); + } + } + + /** + * A process terminator that can be used to kill a process after it exceeds * a maximum allowable run time. */ public static class TimedProcessTerminator implements ProcessTerminator { @@ -212,9 +224,6 @@ public final class ExecUtil { } } - /** - * EVERYTHING FOLLOWING THIS LINE IS DEPRECATED AND SLATED FOR REMOVAL - */ private static final Logger logger = Logger.getLogger(ExecUtil.class.getName()); private Process proc = null; private ExecUtil.StreamToStringRedirect errorStringRedirect = null; diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/AddDataSourceCallback.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/AddDataSourceCallback.java index 3ee02b9026..314673d1d7 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/AddDataSourceCallback.java +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/AddDataSourceCallback.java @@ -47,6 +47,7 @@ public class AddDataSourceCallback extends DataSourceProcessorCallback { * @param caseForJob The case for the current job. * @param dataSourceInfo The data source * @param taskId The task id to associate with ingest job events. + * @param lock The DSP lock */ public AddDataSourceCallback(Case caseForJob, AutoIngestDataSource dataSourceInfo, UUID taskId, Object lock) { this.caseForJob = caseForJob; diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/DataSourceProcessorUtility.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/DataSourceProcessorUtility.java index 8b27ff6c19..b95625dc67 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/DataSourceProcessorUtility.java +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/DataSourceProcessorUtility.java @@ -41,6 +41,8 @@ public class DataSourceProcessorUtility { * AutoIngestDataSourceProcessor interface are used. * * @param dataSourcePath Full path to the data source + * @param processorCandidates Possible DSPs that can handle the data source + * * @return Hash map of all DSPs that can process the data source along with * their confidence score * @throws diff --git a/Core/src/org/sleuthkit/autopsy/healthmonitor/HealthMonitorDashboard.java b/Core/src/org/sleuthkit/autopsy/healthmonitor/HealthMonitorDashboard.java index 5e4f9ce03b..477fa3b957 100644 --- a/Core/src/org/sleuthkit/autopsy/healthmonitor/HealthMonitorDashboard.java +++ b/Core/src/org/sleuthkit/autopsy/healthmonitor/HealthMonitorDashboard.java @@ -58,7 +58,7 @@ public class HealthMonitorDashboard { private final static Logger logger = Logger.getLogger(HealthMonitorDashboard.class.getName()); - private final static String ADMIN_ACCESS_FILE_NAME = "_aiaa"; // NON-NLS + private final static String ADMIN_ACCESS_FILE_NAME = "admin"; // NON-NLS private final static String ADMIN_ACCESS_FILE_PATH = Paths.get(PlatformUtil.getUserConfigDirectory(), ADMIN_ACCESS_FILE_NAME).toString(); Map> timingData; diff --git a/Core/src/org/sleuthkit/autopsy/modules/encryptiondetection/EncryptionDetectionDataSourceIngestModule.java b/Core/src/org/sleuthkit/autopsy/modules/encryptiondetection/EncryptionDetectionDataSourceIngestModule.java index f7806db5a7..01b97b6254 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/encryptiondetection/EncryptionDetectionDataSourceIngestModule.java +++ b/Core/src/org/sleuthkit/autopsy/modules/encryptiondetection/EncryptionDetectionDataSourceIngestModule.java @@ -52,6 +52,7 @@ final class EncryptionDetectionDataSourceIngestModule implements DataSourceInges private Blackboard blackboard; private double calculatedEntropy; private final double minimumEntropy; + private IngestJobContext context; /** * Create an EncryptionDetectionDataSourceIngestModule object that will @@ -67,6 +68,7 @@ final class EncryptionDetectionDataSourceIngestModule implements DataSourceInges public void startUp(IngestJobContext context) throws IngestModule.IngestModuleException { validateSettings(); blackboard = Case.getCurrentCase().getServices().getBlackboard(); + this.context = context; } @Messages({ @@ -77,8 +79,6 @@ final class EncryptionDetectionDataSourceIngestModule implements DataSourceInges @Override public ProcessResult process(Content dataSource, DataSourceIngestModuleProgress progressBar) { - - try { if (dataSource instanceof Image) { @@ -92,10 +92,23 @@ final class EncryptionDetectionDataSourceIngestModule implements DataSourceInges int numVolSystemsChecked = 0; progressBar.progress(Bundle.EncryptionDetectionDataSourceIngestModule_processing_message(), 0); for (VolumeSystem volumeSystem : volumeSystems) { + + if (context.dataSourceIngestIsCancelled()) { + return ProcessResult.OK; + } + for (Volume volume : volumeSystem.getVolumes()) { + + if (context.dataSourceIngestIsCancelled()) { + return ProcessResult.OK; + } if (BitlockerDetection.isBitlockerVolume(volume)) { return flagVolume(volume, BlackboardArtifact.ARTIFACT_TYPE.TSK_ENCRYPTION_DETECTED, Bundle.EncryptionDetectionDataSourceIngestModule_artifactComment_bitlocker()); } + + if (context.dataSourceIngestIsCancelled()) { + return ProcessResult.OK; + } if (isVolumeEncrypted(volume)) { return flagVolume(volume, BlackboardArtifact.ARTIFACT_TYPE.TSK_ENCRYPTION_SUSPECTED, String.format(Bundle.EncryptionDetectionDataSourceIngestModule_artifactComment_suspected(), calculatedEntropy)); } @@ -139,6 +152,11 @@ final class EncryptionDetectionDataSourceIngestModule implements DataSourceInges * there was a problem. */ private IngestModule.ProcessResult flagVolume(Volume volume, BlackboardArtifact.ARTIFACT_TYPE artifactType, String comment) { + + if (context.dataSourceIngestIsCancelled()) { + return ProcessResult.OK; + } + try { BlackboardArtifact artifact = volume.newArtifact(artifactType); artifact.addAttribute(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_COMMENT, EncryptionDetectionModuleFactory.getModuleName(), comment)); @@ -198,7 +216,7 @@ final class EncryptionDetectionDataSourceIngestModule implements DataSourceInges * http://www.forensicswiki.org/wiki/TrueCrypt#Detection */ if (volume.getFileSystems().isEmpty()) { - calculatedEntropy = EncryptionDetectionTools.calculateEntropy(volume); + calculatedEntropy = EncryptionDetectionTools.calculateEntropy(volume, context); if (calculatedEntropy >= minimumEntropy) { return true; } diff --git a/Core/src/org/sleuthkit/autopsy/modules/encryptiondetection/EncryptionDetectionFileIngestModule.java b/Core/src/org/sleuthkit/autopsy/modules/encryptiondetection/EncryptionDetectionFileIngestModule.java index 3e454daff8..e884baa55c 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/encryptiondetection/EncryptionDetectionFileIngestModule.java +++ b/Core/src/org/sleuthkit/autopsy/modules/encryptiondetection/EncryptionDetectionFileIngestModule.java @@ -82,6 +82,7 @@ final class EncryptionDetectionFileIngestModule extends FileIngestModuleAdapter private final Logger logger = services.getLogger(EncryptionDetectionModuleFactory.getModuleName()); private FileTypeDetector fileTypeDetector; private Blackboard blackboard; + private IngestJobContext context; private double calculatedEntropy; private final double minimumEntropy; @@ -107,6 +108,7 @@ final class EncryptionDetectionFileIngestModule extends FileIngestModuleAdapter public void startUp(IngestJobContext context) throws IngestModule.IngestModuleException { try { validateSettings(); + this.context = context; blackboard = Case.getCurrentCaseThrows().getServices().getBlackboard(); fileTypeDetector = new FileTypeDetector(); } catch (FileTypeDetector.FileTypeDetectorInitException ex) { @@ -194,6 +196,10 @@ final class EncryptionDetectionFileIngestModule extends FileIngestModuleAdapter */ private IngestModule.ProcessResult flagFile(AbstractFile file, BlackboardArtifact.ARTIFACT_TYPE artifactType, String comment) { try { + if (context.fileIngestIsCancelled()) { + return IngestModule.ProcessResult.OK; + } + BlackboardArtifact artifact = file.newArtifact(artifactType); artifact.addAttribute(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_COMMENT, EncryptionDetectionModuleFactory.getModuleName(), comment)); @@ -397,7 +403,7 @@ final class EncryptionDetectionFileIngestModule extends FileIngestModuleAdapter /* * Qualify the entropy. */ - calculatedEntropy = EncryptionDetectionTools.calculateEntropy(file); + calculatedEntropy = EncryptionDetectionTools.calculateEntropy(file, context); if (calculatedEntropy >= minimumEntropy) { possiblyEncrypted = true; } diff --git a/Core/src/org/sleuthkit/autopsy/modules/encryptiondetection/EncryptionDetectionTools.java b/Core/src/org/sleuthkit/autopsy/modules/encryptiondetection/EncryptionDetectionTools.java index 99dbc0aaeb..af3a54a379 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/encryptiondetection/EncryptionDetectionTools.java +++ b/Core/src/org/sleuthkit/autopsy/modules/encryptiondetection/EncryptionDetectionTools.java @@ -22,6 +22,7 @@ import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.ingest.IngestJobContext; import org.sleuthkit.autopsy.ingest.IngestModule; import org.sleuthkit.datamodel.ReadContentInputStream; import org.sleuthkit.datamodel.Content; @@ -69,6 +70,7 @@ final class EncryptionDetectionTools { * content as possibly encrypted. * * @param content The content to be calculated against. + * @param context The ingest job context for cancellation checks * * @return The entropy of the content. * @@ -77,7 +79,7 @@ final class EncryptionDetectionTools { * @throws IOException If there is a failure closing or * reading from the InputStream. */ - static double calculateEntropy(Content content) throws ReadContentInputStream.ReadContentInputStreamException, IOException { + static double calculateEntropy(Content content, IngestJobContext context) throws ReadContentInputStream.ReadContentInputStreamException, IOException { /* * Logic in this method is based on * https://github.com/willjasen/entropy/blob/master/entropy.java @@ -95,8 +97,17 @@ final class EncryptionDetectionTools { */ int[] byteOccurences = new int[BYTE_OCCURENCES_BUFFER_SIZE]; int readByte; + long bytesRead = 0; while ((readByte = bin.read()) != -1) { byteOccurences[readByte]++; + + // Do a cancellation check every 10,000 bytes + bytesRead++; + if (bytesRead % 10000 == 0) { + if (context.dataSourceIngestIsCancelled() || context.fileIngestIsCancelled()) { + return 0; + } + } } /* diff --git a/Core/src/org/sleuthkit/autopsy/progress/AppFrameProgressBar.java b/Core/src/org/sleuthkit/autopsy/progress/AppFrameProgressBar.java new file mode 100755 index 0000000000..8f1de8a6e9 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/progress/AppFrameProgressBar.java @@ -0,0 +1,124 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019-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 + * 9 + * 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.progress; + +import org.netbeans.api.progress.ProgressHandle; +import org.openide.util.Cancellable; + +/** + * A progress indicator that displays progress using a progress bar in the lower + * right hand corner of the application main frame, i.e., a NetBeans + * ProgressHandle. + */ +public final class AppFrameProgressBar implements ProgressIndicator { + + private final String displayName; + private Cancellable cancellationBehavior; + private ProgressHandle progressHandle; + private volatile boolean cancelling; + + /** + * Constructs a progress indicator that displays progress using a progress + * bar in the lower right hand corner of the application main frame, i.e., a + * NetBeans ProgressHandle. + * + * @param displayName The display name for the progress bar (a fixed name + * that appears above the current progress message). + */ + public AppFrameProgressBar(String displayName) { + this.displayName = displayName; + } + + /** + * Sets the cancellation behavior that should happen when a user clicks on + * the "x" button of the progress bar. + * + * @param cancellationBehavior A org.openide.util.Cancellable that + * implements the desired cancellation behavior. + */ + public void setCancellationBehavior(Cancellable cancellationBehavior) { + this.cancellationBehavior = cancellationBehavior; + } + + @Override + public void start(String message, int totalWorkUnits) { + cancelling = false; + this.progressHandle = ProgressHandle.createHandle(displayName, cancellationBehavior); + progressHandle.start(totalWorkUnits); + progressHandle.progress(message); + } + + @Override + public void start(String message) { + cancelling = false; + this.progressHandle = ProgressHandle.createHandle(displayName, cancellationBehavior); + progressHandle.start(); + progressHandle.progress(message); + } + + @Override + public void switchToIndeterminate(String message) { + if (!cancelling) { + progressHandle.switchToIndeterminate(); + progressHandle.progress(message); + } + } + + @Override + public void switchToDeterminate(String message, int workUnitsCompleted, int totalWorkUnits) { + if (!cancelling) { + progressHandle.switchToDeterminate(totalWorkUnits); + progressHandle.progress(message, workUnitsCompleted); + } + } + + @Override + public void progress(String message) { + if (!cancelling) { + progressHandle.progress(message); + } + } + + @Override + public void progress(int workUnitsCompleted) { + if (!cancelling) { + progressHandle.progress(workUnitsCompleted); + } + } + + @Override + public void progress(String message, int workUnitsCompleted) { + if (!cancelling) { + progressHandle.progress(message, workUnitsCompleted); + } + } + + @Override + public void setCancelling(String cancellingMessage) { + cancelling = true; + progressHandle.switchToIndeterminate(); + progressHandle.progress(cancellingMessage); + } + + @Override + public void finish() { + progressHandle.finish(); + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/progress/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/progress/Bundle.properties-MERGED index 7fcb87097c..7fc3a591e1 100755 --- a/Core/src/org/sleuthkit/autopsy/progress/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/progress/Bundle.properties-MERGED @@ -3,3 +3,4 @@ # and open the template in the editor. ProgressPanel.progressMessage.text=Message +TaskCanceller.progress.cancellingMessage=Cancelling... diff --git a/Core/src/org/sleuthkit/autopsy/progress/ModalDialogProgressIndicator.java b/Core/src/org/sleuthkit/autopsy/progress/ModalDialogProgressIndicator.java index 909c08d29e..8d92e40d70 100644 --- a/Core/src/org/sleuthkit/autopsy/progress/ModalDialogProgressIndicator.java +++ b/Core/src/org/sleuthkit/autopsy/progress/ModalDialogProgressIndicator.java @@ -128,6 +128,7 @@ public final class ModalDialogProgressIndicator implements ProgressIndicator { * * @param cancellingMessage */ + @Override public synchronized void setCancelling(String cancellingMessage) { cancelling = true; SwingUtilities.invokeLater(() -> { diff --git a/Core/src/org/sleuthkit/autopsy/progress/ProgressIndicator.java b/Core/src/org/sleuthkit/autopsy/progress/ProgressIndicator.java index 8e5b881182..e62d940c08 100644 --- a/Core/src/org/sleuthkit/autopsy/progress/ProgressIndicator.java +++ b/Core/src/org/sleuthkit/autopsy/progress/ProgressIndicator.java @@ -49,7 +49,7 @@ public interface ProgressIndicator { * * @param message The initial progress message. */ - public void switchToIndeterminate(String message); + void switchToIndeterminate(String message); /** * Switches the progress indicator to determinate mode (the total number of @@ -59,14 +59,14 @@ public interface ProgressIndicator { * @param workUnitsCompleted The number of work units completed so far. * @param totalWorkUnits The total number of work units to be completed. */ - public void switchToDeterminate(String message, int workUnitsCompleted, int totalWorkUnits); + void switchToDeterminate(String message, int workUnitsCompleted, int totalWorkUnits); /** * Updates the progress indicator with a progress message. * * @param message The progress message. */ - public void progress(String message); + void progress(String message); /** * Updates the progress indicator with the number of work units completed so @@ -75,7 +75,7 @@ public interface ProgressIndicator { * * @param workUnitsCompleted Number of work units completed so far. */ - public void progress(int workUnitsCompleted); + void progress(int workUnitsCompleted); /** * Updates the progress indicator with a progress message and the number of @@ -85,7 +85,24 @@ public interface ProgressIndicator { * @param message The progress message. * @param workUnitsCompleted Number of work units completed so far. */ - public void progress(String message, int workUnitsCompleted); + void progress(String message, int workUnitsCompleted); + + /** + * If the progress indicator supports cancelling the underlying task, sets a + * cancelling message and causes the progress indicator to no longer accept + * updates unless start is called again. + * + * The default implementation assumes that cancelling the underlying task is + * not supported. + * + * @param cancellingMessage The cancelling messages. + */ + default void setCancelling(String cancellingMessage) { + /* + * The default implementation assumes that cancelling the underlying + * task is not supported. + */ + } /** * Finishes the progress indicator when the task is completed. diff --git a/Core/src/org/sleuthkit/autopsy/progress/TaskCancellable.java b/Core/src/org/sleuthkit/autopsy/progress/TaskCancellable.java new file mode 100755 index 0000000000..1ce473e9b0 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/progress/TaskCancellable.java @@ -0,0 +1,68 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019-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.progress; + +import java.util.concurrent.Future; +import org.openide.util.Cancellable; +import org.openide.util.NbBundle; + +/** + * Pluggable cancellation behavior for use in progress indicators (such as the + * application frame progress indicator) that support cancelling a task using an + * implementation of org.openide.util.Cancellable. Encapsulates a Future to + * be cancelled and sets the cancelling flag and message of the progress + * indicator. + */ +public class TaskCancellable implements Cancellable { + + private final ProgressIndicator progress; + private Future future; + + /** + * Constructs a pluggable cancellation behavior for use in progress + * indicators (such as the application frame progress indicator) that + * support cancelling a task using an implementation of + * org.openide.util.Cancellable. Encapsulates a Future to be cancelled + * and sets the cancelling flag and message of the progress indicator. + * + * @param progress + */ + public TaskCancellable(ProgressIndicator progress) { + this.progress = progress; + } + + /** + * Sets the Future used to cancel the associated task. + * + * @param future The future for the associated task. + */ + public synchronized void setFuture(Future future) { + this.future = future; + } + + @Override + @NbBundle.Messages({ + "TaskCanceller.progress.cancellingMessage=Cancelling..." + }) + public synchronized boolean cancel() { + progress.setCancelling(Bundle.TaskCanceller_progress_cancellingMessage()); + return future.cancel(true); + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/report/Bundle.properties b/Core/src/org/sleuthkit/autopsy/report/Bundle.properties index 8d6e81e3ef..20f16e4ac6 100644 --- a/Core/src/org/sleuthkit/autopsy/report/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/report/Bundle.properties @@ -37,7 +37,7 @@ FileReportDataTypes.knownStatus.text=Known Status FileReportDataTypes.perms.text=Permissions FileReportDataTypes.path.text=Full Path FileReportText.getName.text=Files - Text -FileReportText.getDesc.text=A tab delimited text file containing information about individual files in the case. +FileReportText.getDesc.text=A delimited text file containing information about individual files in the case. ReportBodyFile.progress.querying=Querying files... ReportBodyFile.ingestWarning.text=Warning, this report was run before ingest services completed\! ReportBodyFile.progress.loading=Loading files... @@ -258,3 +258,5 @@ CreatePortableCasePanel.outputFolderTextField.text=jTextField1 CreatePortableCasePanel.chooseOutputFolderButton.text=Choose folder CreatePortableCasePanel.jLabel1.text=Export files tagged as: CreatePortableCasePanel.jLabel2.text=Select output folder: +ReportFileTextConfigurationPanel.tabDelimitedButton.text=Tab delimited +ReportFileTextConfigurationPanel.commaDelimitedButton.text=Comma delimited diff --git a/Core/src/org/sleuthkit/autopsy/report/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/report/Bundle.properties-MERGED index 3111ffbe6d..1187fe8e81 100755 --- a/Core/src/org/sleuthkit/autopsy/report/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/report/Bundle.properties-MERGED @@ -85,7 +85,7 @@ FileReportDataTypes.knownStatus.text=Known Status FileReportDataTypes.perms.text=Permissions FileReportDataTypes.path.text=Full Path FileReportText.getName.text=Files - Text -FileReportText.getDesc.text=A tab delimited text file containing information about individual files in the case. +FileReportText.getDesc.text=A delimited text file containing information about individual files in the case. ReportBodyFile.progress.querying=Querying files... ReportBodyFile.ingestWarning.text=Warning, this report was run before ingest services completed\! ReportBodyFile.progress.loading=Loading files... @@ -306,4 +306,6 @@ CreatePortableCasePanel.outputFolderTextField.text=jTextField1 CreatePortableCasePanel.chooseOutputFolderButton.text=Choose folder CreatePortableCasePanel.jLabel1.text=Export files tagged as: CreatePortableCasePanel.jLabel2.text=Select output folder: +ReportFileTextConfigurationPanel.tabDelimitedButton.text=Tab delimited +ReportFileTextConfigurationPanel.commaDelimitedButton.text=Comma delimited TableReportGenerator.StatusColumn.Header=Review Status diff --git a/Core/src/org/sleuthkit/autopsy/report/CreatePortableCaseModule.java b/Core/src/org/sleuthkit/autopsy/report/CreatePortableCaseModule.java index b0db1d0b46..4e2d5c5fa0 100644 --- a/Core/src/org/sleuthkit/autopsy/report/CreatePortableCaseModule.java +++ b/Core/src/org/sleuthkit/autopsy/report/CreatePortableCaseModule.java @@ -230,6 +230,20 @@ public class CreatePortableCaseModule implements GeneralReportModule { return; } + // Set up tracking to support any custom artifact or attribute types + for (BlackboardArtifact.ARTIFACT_TYPE type:BlackboardArtifact.ARTIFACT_TYPE.values()) { + oldArtTypeIdToNewArtTypeId.put(type.getTypeID(), type.getTypeID()); + } + for (BlackboardAttribute.ATTRIBUTE_TYPE type:BlackboardAttribute.ATTRIBUTE_TYPE.values()) { + try { + oldAttrTypeIdToNewAttrType.put(type.getTypeID(), portableSkCase.getAttributeType(type.getLabel())); + } catch (TskCoreException ex) { + handleError("Error looking up attribute name " + type.getLabel(), + Bundle.CreatePortableCaseModule_generateReport_errorLookingUpAttrType(type.getLabel()), + ex, progressPanel); + } + } + // Copy the tagged files try { for(TagName tagName:tagNames) { @@ -245,20 +259,6 @@ public class CreatePortableCaseModule implements GeneralReportModule { return; } - // Set up tracking to support any custom artifact or attribute types - for (BlackboardArtifact.ARTIFACT_TYPE type:BlackboardArtifact.ARTIFACT_TYPE.values()) { - oldArtTypeIdToNewArtTypeId.put(type.getTypeID(), type.getTypeID()); - } - for (BlackboardAttribute.ATTRIBUTE_TYPE type:BlackboardAttribute.ATTRIBUTE_TYPE.values()) { - try { - oldAttrTypeIdToNewAttrType.put(type.getTypeID(), portableSkCase.getAttributeType(type.getLabel())); - } catch (TskCoreException ex) { - handleError("Error looking up attribute name " + type.getLabel(), - Bundle.CreatePortableCaseModule_generateReport_errorLookingUpAttrType(type.getLabel()), - ex, progressPanel); - } - } - // Copy the tagged artifacts and associated files try { for(TagName tagName:tagNames) { @@ -518,7 +518,7 @@ public class CreatePortableCaseModule implements GeneralReportModule { * Get the artifact type ID in the portable case and create new artifact type if needed. * For built-in artifacts this will be the same as the original. * - * @param oldArtifactTypeId The artifact type ID in the current case + * @param oldArtifact The artifact in the current case * * @return The corresponding artifact type ID in the portable case */ @@ -541,7 +541,7 @@ public class CreatePortableCaseModule implements GeneralReportModule { * Get the attribute type ID in the portable case and create new attribute type if needed. * For built-in attributes this will be the same as the original. * - * @param oldAttributeTypeId The attribute type ID in the current case + * @param oldAttribute The attribute in the current case * * @return The corresponding attribute type in the portable case */ @@ -577,30 +577,19 @@ public class CreatePortableCaseModule implements GeneralReportModule { }) private long copyContentToPortableCase(Content content, ReportProgressPanel progressPanel) throws TskCoreException { progressPanel.updateStatusLabel(Bundle.CreatePortableCaseModule_copyContentToPortableCase_copyingFile(content.getUniquePath())); - - long newFileId; - CaseDbTransaction trans = portableSkCase.beginTransaction(); - try { - newFileId = copyContent(content, trans); - trans.commit(); - return newFileId; - } catch (TskCoreException ex) { - trans.rollback(); - throw(ex); - } + return copyContent(content); } /** * Returns the object ID for the given content object in the portable case. * * @param content The content object to copy into the portable case - * @param trans The current transaction * * @return the new object ID for this content * * @throws TskCoreException */ - private long copyContent(Content content, CaseDbTransaction trans) throws TskCoreException { + private long copyContent(Content content) throws TskCoreException { // Check if we've already copied this content if (oldIdToNewContent.containsKey(content.getId())) { @@ -612,67 +601,82 @@ public class CreatePortableCaseModule implements GeneralReportModule { // - Copy this content long parentId = 0; if (content.getParent() != null) { - parentId = copyContent(content.getParent(), trans); + parentId = copyContent(content.getParent()); } Content newContent; - if (content instanceof Image) { - Image image = (Image)content; - newContent = portableSkCase.addImage(image.getType(), image.getSsize(), image.getSize(), image.getName(), - new ArrayList<>(), image.getTimeZone(), image.getMd5(), image.getSha1(), image.getSha256(), image.getDeviceId(), trans); - } else if (content instanceof VolumeSystem) { - VolumeSystem vs = (VolumeSystem)content; - newContent = portableSkCase.addVolumeSystem(parentId, vs.getType(), vs.getOffset(), vs.getBlockSize(), trans); - } else if (content instanceof Volume) { - Volume vs = (Volume)content; - newContent = portableSkCase.addVolume(parentId, vs.getAddr(), vs.getStart(), vs.getLength(), - vs.getDescription(), vs.getFlags(), trans); - } else if (content instanceof FileSystem) { - FileSystem fs = (FileSystem)content; - newContent = portableSkCase.addFileSystem(parentId, fs.getImageOffset(), fs.getFsType(), fs.getBlock_size(), - fs.getBlock_count(), fs.getRoot_inum(), fs.getFirst_inum(), fs.getLastInum(), - fs.getName(), trans); - } else if (content instanceof AbstractFile) { - AbstractFile abstractFile = (AbstractFile)content; - - if (abstractFile instanceof LocalFilesDataSource) { - LocalFilesDataSource localFilesDS = (LocalFilesDataSource)abstractFile; - newContent = portableSkCase.addLocalFilesDataSource(localFilesDS.getDeviceId(), localFilesDS.getName(), localFilesDS.getTimeZone(), trans); - } else { - if (abstractFile.isDir()) { - newContent = portableSkCase.addLocalDirectory(parentId, abstractFile.getName(), trans); - } else { - try { - // Copy the file - String fileName = abstractFile.getId() + "-" + FileUtil.escapeFileName(abstractFile.getName()); - String exportSubFolder = getExportSubfolder(abstractFile); - File exportFolder = Paths.get(copiedFilesFolder.toString(), exportSubFolder).toFile(); - File localFile = new File(exportFolder, fileName); - ContentUtils.writeToFile(abstractFile, localFile); - - // Get the new parent object in the portable case database - Content oldParent = abstractFile.getParent(); - if (! oldIdToNewContent.containsKey(oldParent.getId())) { - throw new TskCoreException("Parent of file with ID " + abstractFile.getId() + " has not been created"); - } - Content newParent = oldIdToNewContent.get(oldParent.getId()); - - // Construct the relative path to the copied file - String relativePath = FILE_FOLDER_NAME + File.separator + exportSubFolder + File.separator + fileName; - - newContent = portableSkCase.addLocalFile(abstractFile.getName(), relativePath, abstractFile.getSize(), - abstractFile.getCtime(), abstractFile.getCrtime(), abstractFile.getAtime(), abstractFile.getMtime(), - abstractFile.getMd5Hash(), abstractFile.getKnown(), abstractFile.getMIMEType(), - true, TskData.EncodingType.NONE, - newParent, trans); - } catch (IOException ex) { - throw new TskCoreException("Error copying file " + abstractFile.getName() + " with original obj ID " - + abstractFile.getId(), ex); - } - } - } + if (content instanceof BlackboardArtifact) { + BlackboardArtifact artifactToCopy = (BlackboardArtifact)content; + newContent = copyArtifact(parentId, artifactToCopy); } else { - throw new TskCoreException("Trying to copy unexpected Content type " + content.getClass().getName()); + CaseDbTransaction trans = portableSkCase.beginTransaction(); + try { + if (content instanceof Image) { + Image image = (Image)content; + newContent = portableSkCase.addImage(image.getType(), image.getSsize(), image.getSize(), image.getName(), + new ArrayList<>(), image.getTimeZone(), image.getMd5(), image.getSha1(), image.getSha256(), image.getDeviceId(), trans); + } else if (content instanceof VolumeSystem) { + VolumeSystem vs = (VolumeSystem)content; + newContent = portableSkCase.addVolumeSystem(parentId, vs.getType(), vs.getOffset(), vs.getBlockSize(), trans); + } else if (content instanceof Volume) { + Volume vs = (Volume)content; + newContent = portableSkCase.addVolume(parentId, vs.getAddr(), vs.getStart(), vs.getLength(), + vs.getDescription(), vs.getFlags(), trans); + } else if (content instanceof FileSystem) { + FileSystem fs = (FileSystem)content; + newContent = portableSkCase.addFileSystem(parentId, fs.getImageOffset(), fs.getFsType(), fs.getBlock_size(), + fs.getBlock_count(), fs.getRoot_inum(), fs.getFirst_inum(), fs.getLastInum(), + fs.getName(), trans); + } else if (content instanceof BlackboardArtifact) { + BlackboardArtifact artifactToCopy = (BlackboardArtifact)content; + newContent = copyArtifact(parentId, artifactToCopy); + } else if (content instanceof AbstractFile) { + AbstractFile abstractFile = (AbstractFile)content; + + if (abstractFile instanceof LocalFilesDataSource) { + LocalFilesDataSource localFilesDS = (LocalFilesDataSource)abstractFile; + newContent = portableSkCase.addLocalFilesDataSource(localFilesDS.getDeviceId(), localFilesDS.getName(), localFilesDS.getTimeZone(), trans); + } else { + if (abstractFile.isDir()) { + newContent = portableSkCase.addLocalDirectory(parentId, abstractFile.getName(), trans); + } else { + try { + // Copy the file + String fileName = abstractFile.getId() + "-" + FileUtil.escapeFileName(abstractFile.getName()); + String exportSubFolder = getExportSubfolder(abstractFile); + File exportFolder = Paths.get(copiedFilesFolder.toString(), exportSubFolder).toFile(); + File localFile = new File(exportFolder, fileName); + ContentUtils.writeToFile(abstractFile, localFile); + + // Get the new parent object in the portable case database + Content oldParent = abstractFile.getParent(); + if (! oldIdToNewContent.containsKey(oldParent.getId())) { + throw new TskCoreException("Parent of file with ID " + abstractFile.getId() + " has not been created"); + } + Content newParent = oldIdToNewContent.get(oldParent.getId()); + + // Construct the relative path to the copied file + String relativePath = FILE_FOLDER_NAME + File.separator + exportSubFolder + File.separator + fileName; + + newContent = portableSkCase.addLocalFile(abstractFile.getName(), relativePath, abstractFile.getSize(), + abstractFile.getCtime(), abstractFile.getCrtime(), abstractFile.getAtime(), abstractFile.getMtime(), + abstractFile.getMd5Hash(), abstractFile.getKnown(), abstractFile.getMIMEType(), + true, TskData.EncodingType.NONE, + newParent, trans); + } catch (IOException ex) { + throw new TskCoreException("Error copying file " + abstractFile.getName() + " with original obj ID " + + abstractFile.getId(), ex); + } + } + } + } else { + throw new TskCoreException("Trying to copy unexpected Content type " + content.getClass().getName()); + } + trans.commit(); + } catch (TskCoreException ex) { + trans.rollback(); + throw(ex); + } } // Save the new object diff --git a/Core/src/org/sleuthkit/autopsy/report/FileReportText.java b/Core/src/org/sleuthkit/autopsy/report/FileReportText.java index f96bad446b..9d3de2c945 100644 --- a/Core/src/org/sleuthkit/autopsy/report/FileReportText.java +++ b/Core/src/org/sleuthkit/autopsy/report/FileReportText.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013 - 2018 Basis Technology Corp. + * Copyright 2013 - 2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,6 +19,7 @@ package org.sleuthkit.autopsy.report; import java.io.BufferedWriter; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; @@ -27,6 +28,7 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.logging.Level; +import javax.swing.JPanel; import org.sleuthkit.autopsy.coreutils.Logger; import org.openide.util.NbBundle; @@ -36,18 +38,18 @@ import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.TskCoreException; /** - * A Tab-delimited text report of the files in the case. + * A delimited text report of the files in the case. * * @author jwallace */ class FileReportText implements FileReportModule { private static final Logger logger = Logger.getLogger(FileReportText.class.getName()); + private static final String FILE_NAME = "file-report.txt"; //NON-NLS + private static FileReportText instance; private String reportPath; private Writer out; - private static final String FILE_NAME = "file-report.txt"; //NON-NLS - - private static FileReportText instance; + private ReportFileTextConfigurationPanel configPanel; // Get the default implementation of this report public static synchronized FileReportText getDefault() { @@ -62,7 +64,7 @@ class FileReportText implements FileReportModule { this.reportPath = baseReportDir + FILE_NAME; try { out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(this.reportPath))); - } catch (IOException ex) { + } catch (FileNotFoundException ex) { logger.log(Level.WARNING, "Failed to create report text file", ex); //NON-NLS } } @@ -85,11 +87,12 @@ class FileReportText implements FileReportModule { } } - private String getTabDelimitedList(List list) { - StringBuilder output = new StringBuilder(); + private String getDelimitedList(List list, String delimiter) { + StringBuilder output; + output = new StringBuilder(); Iterator it = list.iterator(); while (it.hasNext()) { - output.append(it.next()).append((it.hasNext() ? "\t" : System.lineSeparator())); + output.append('"').append(it.next()).append('"').append((it.hasNext() ? delimiter : System.lineSeparator())); } return output.toString(); } @@ -101,7 +104,7 @@ class FileReportText implements FileReportModule { titles.add(col.getName()); } try { - out.write(getTabDelimitedList(titles)); + out.write(getDelimitedList(titles, configPanel.getDelimiter())); } catch (IOException ex) { logger.log(Level.WARNING, "Error when writing headers to report file: {0}", ex); //NON-NLS } @@ -114,7 +117,7 @@ class FileReportText implements FileReportModule { cells.add(type.getValue(toAdd)); } try { - out.write(getTabDelimitedList(cells)); + out.write(getDelimitedList(cells, configPanel.getDelimiter())); } catch (IOException ex) { logger.log(Level.WARNING, "Error when writing row to report file: {0}", ex); //NON-NLS } @@ -143,4 +146,12 @@ class FileReportText implements FileReportModule { public String getRelativeFilePath() { return FILE_NAME; } + + @Override + public JPanel getConfigurationPanel() { + if (configPanel == null) { + configPanel = new ReportFileTextConfigurationPanel(); + } + return configPanel; + } } diff --git a/Core/src/org/sleuthkit/autopsy/report/ReportFileTextConfigurationPanel.form b/Core/src/org/sleuthkit/autopsy/report/ReportFileTextConfigurationPanel.form new file mode 100644 index 0000000000..d0c59f7bdd --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/report/ReportFileTextConfigurationPanel.form @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/report/ReportFileTextConfigurationPanel.java b/Core/src/org/sleuthkit/autopsy/report/ReportFileTextConfigurationPanel.java new file mode 100644 index 0000000000..e1b9de2df2 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/report/ReportFileTextConfigurationPanel.java @@ -0,0 +1,99 @@ +/* + * 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.report; + +/** + * Panel for configuring settings for the file text report + */ +class ReportFileTextConfigurationPanel extends javax.swing.JPanel { + + private static final long serialVersionUID = 1L; + private static final String TAB_DELIMITER = "\t"; //NON-NLS + private static final String COMMA_DELIMITER = ","; //NON-NLS + + /** + * Creates new form ReportFileTextConfigurationPanel + */ + ReportFileTextConfigurationPanel() { + initComponents(); + } + + /** + * Get the delimiter that was selected on this panel + * + * @return the selected delimiter + */ + String getDelimiter() { + if (commaDelimitedButton.isSelected()) { + return COMMA_DELIMITER; + } else { + //if the comma button is not selected default to tab since it was previously the only option + return TAB_DELIMITER; + } + } + + /** + * 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() { + + delimiterGroup = new javax.swing.ButtonGroup(); + tabDelimitedButton = new javax.swing.JRadioButton(); + commaDelimitedButton = new javax.swing.JRadioButton(); + + delimiterGroup.add(tabDelimitedButton); + tabDelimitedButton.setSelected(true); + org.openide.awt.Mnemonics.setLocalizedText(tabDelimitedButton, org.openide.util.NbBundle.getMessage(ReportFileTextConfigurationPanel.class, "ReportFileTextConfigurationPanel.tabDelimitedButton.text")); // NOI18N + + delimiterGroup.add(commaDelimitedButton); + org.openide.awt.Mnemonics.setLocalizedText(commaDelimitedButton, org.openide.util.NbBundle.getMessage(ReportFileTextConfigurationPanel.class, "ReportFileTextConfigurationPanel.commaDelimitedButton.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() + .addComponent(tabDelimitedButton, javax.swing.GroupLayout.PREFERRED_SIZE, 116, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(commaDelimitedButton, javax.swing.GroupLayout.PREFERRED_SIZE, 133, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap(166, Short.MAX_VALUE)) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(tabDelimitedButton) + .addComponent(commaDelimitedButton)) + .addContainerGap(78, Short.MAX_VALUE)) + ); + }// //GEN-END:initComponents + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JRadioButton commaDelimitedButton; + private javax.swing.ButtonGroup delimiterGroup; + private javax.swing.JRadioButton tabDelimitedButton; + // End of variables declaration//GEN-END:variables +} diff --git a/Core/src/org/sleuthkit/autopsy/report/TableReportGenerator.java b/Core/src/org/sleuthkit/autopsy/report/TableReportGenerator.java index fd1084dfc7..8e7ac04d05 100644 --- a/Core/src/org/sleuthkit/autopsy/report/TableReportGenerator.java +++ b/Core/src/org/sleuthkit/autopsy/report/TableReportGenerator.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-2018 Basis Technology Corp. + * Copyright 2013-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -59,7 +59,7 @@ import org.sleuthkit.datamodel.TagName; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData; -class TableReportGenerator { +class TableReportGenerator { private final List artifactTypes = new ArrayList<>(); private final HashSet tagNamesFilter = new HashSet<>(); @@ -288,7 +288,7 @@ class TableReportGenerator { ArrayList columnHeaders = new ArrayList<>(Arrays.asList( NbBundle.getMessage(this.getClass(), "ReportGenerator.htmlOutput.header.tag"), NbBundle.getMessage(this.getClass(), "ReportGenerator.htmlOutput.header.file"), - NbBundle.getMessage(this.getClass(), "ReportGenerator.htmlOutput.header.comment"), + NbBundle.getMessage(this.getClass(), "ReportGenerator.htmlOutput.header.comment"), NbBundle.getMessage(this.getClass(), "ReportGenerator.tagTable.header.userName"), NbBundle.getMessage(this.getClass(), "ReportGenerator.htmlOutput.header.timeModified"), NbBundle.getMessage(this.getClass(), "ReportGenerator.htmlOutput.header.timeChanged"), @@ -389,7 +389,7 @@ class TableReportGenerator { NbBundle.getMessage(this.getClass(), "ReportGenerator.tagTable.header.resultType"), NbBundle.getMessage(this.getClass(), "ReportGenerator.tagTable.header.tag"), NbBundle.getMessage(this.getClass(), "ReportGenerator.tagTable.header.comment"), - NbBundle.getMessage(this.getClass(), "ReportGenerator.tagTable.header.srcFile"), + NbBundle.getMessage(this.getClass(), "ReportGenerator.tagTable.header.srcFile"), NbBundle.getMessage(this.getClass(), "ReportGenerator.tagTable.header.userName")))); // Give the modules the rows for the content tags. @@ -400,7 +400,7 @@ class TableReportGenerator { } List row; - row = new ArrayList<>(Arrays.asList(tag.getArtifact().getArtifactTypeName(), tag.getName().getDisplayName() + notableString, + row = new ArrayList<>(Arrays.asList(tag.getArtifact().getArtifactTypeName(), tag.getName().getDisplayName() + notableString, tag.getComment(), tag.getContent().getName(), tag.getUserName())); tableReport.addRow(row); @@ -528,7 +528,7 @@ class TableReportGenerator { * @param tableModule module to report on */ @SuppressWarnings("deprecation") - @NbBundle.Messages ({"ReportGenerator.errList.noOpenCase=No open case available."}) + @NbBundle.Messages({"ReportGenerator.errList.noOpenCase=No open case available."}) private void writeKeywordHits(TableReportModule tableModule, String comment, HashSet tagNamesFilter) { // Query for keyword lists-only so that we can tell modules what lists @@ -545,24 +545,24 @@ class TableReportGenerator { logger.log(Level.SEVERE, "Exception while getting open case: ", ex); //NON-NLS return; } - + // Get a list of all selected tag IDs String tagIDList = ""; - if( ! tagNamesFilter.isEmpty()) { + if (!tagNamesFilter.isEmpty()) { try { Map tagNamesMap = Case.getCurrentCaseThrows().getServices().getTagsManager().getDisplayNamesToTagNamesMap(); - for(String tagDisplayName : tagNamesFilter) { - if(tagNamesMap.containsKey(tagDisplayName)) { - if (! tagIDList.isEmpty()) { + for (String tagDisplayName : tagNamesFilter) { + if (tagNamesMap.containsKey(tagDisplayName)) { + if (!tagIDList.isEmpty()) { tagIDList += ","; } tagIDList += tagNamesMap.get(tagDisplayName).getId(); } else { // If the tag name ends with "(Notable)", try stripping that off - if(tagDisplayName.endsWith(getNotableTagLabel())) { + if (tagDisplayName.endsWith(getNotableTagLabel())) { String editedDisplayName = tagDisplayName.substring(0, tagDisplayName.length() - getNotableTagLabel().length()); - if(tagNamesMap.containsKey(editedDisplayName)) { - if (! tagIDList.isEmpty()) { + if (tagNamesMap.containsKey(editedDisplayName)) { + if (!tagIDList.isEmpty()) { tagIDList += ","; } tagIDList += tagNamesMap.get(editedDisplayName).getId(); @@ -575,9 +575,10 @@ class TableReportGenerator { tagIDList = ""; } } - + // Check if there are any ad-hoc results - String adHocCountQuery = "SELECT COUNT(*) FROM " + //NON-NLS + String adHocCountQuery = "SELECT COUNT(*) FROM " + + //NON-NLS "(SELECT art.artifact_id FROM blackboard_artifacts AS art, blackboard_attributes AS att1 ";//NON-NLS if (!tagIDList.isEmpty()) { adHocCountQuery += ", blackboard_artifact_tags as tag "; //NON-NLS @@ -586,7 +587,8 @@ class TableReportGenerator { if (!tagIDList.isEmpty()) { adHocCountQuery += " AND (art.artifact_id = tag.artifact_id) AND (tag.tag_name_id IN (" + tagIDList + ")) "; //NON-NLS } - adHocCountQuery += "EXCEPT " + // NON-NLS + adHocCountQuery += "EXCEPT " + + // NON-NLS "SELECT art.artifact_id FROM blackboard_artifacts AS art, blackboard_attributes AS att1 WHERE (att1.artifact_id = art.artifact_id) AND (art.artifact_type_id = " + BlackboardArtifact.ARTIFACT_TYPE.TSK_KEYWORD_HIT.getTypeID() + ") AND (att1.attribute_type_id = " + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID() + ")) AS adHocHits"; //NON-NLS int adHocCount = 0; @@ -602,7 +604,7 @@ class TableReportGenerator { logger.log(Level.SEVERE, "Failed to count ad hoc searches with query " + adHocCountQuery, ex); //NON-NLS return; } - + // Create the query to get the keyword list names if (openCase.getCaseType() == Case.CaseType.MULTI_USER_CASE) { orderByClause = "ORDER BY convert_to(list, 'SQL_ASCII') ASC NULLS FIRST"; //NON-NLS @@ -613,7 +615,7 @@ class TableReportGenerator { = "SELECT att.value_text AS list " + //NON-NLS "FROM blackboard_attributes AS att, blackboard_artifacts AS art "; // NON-NLS - if(! tagIDList.isEmpty()) { + if (!tagIDList.isEmpty()) { keywordListQuery += ", blackboard_artifact_tags as tag "; //NON-NLS } keywordListQuery += "WHERE att.attribute_type_id = " + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID() + " " @@ -621,8 +623,9 @@ class TableReportGenerator { "AND art.artifact_type_id = " + BlackboardArtifact.ARTIFACT_TYPE.TSK_KEYWORD_HIT.getTypeID() + " " + //NON-NLS "AND att.artifact_id = art.artifact_id "; - if (! tagIDList.isEmpty()) { - keywordListQuery += "AND (art.artifact_id = tag.artifact_id) " + //NON-NLS + if (!tagIDList.isEmpty()) { + keywordListQuery += "AND (art.artifact_id = tag.artifact_id) " + + //NON-NLS "AND (tag.tag_name_id IN (" + tagIDList + ")) "; //NON-NLS } if (adHocCount > 0) { @@ -665,7 +668,7 @@ class TableReportGenerator { } else { orderByClause = "ORDER BY list ASC, keyword ASC, parent_path ASC, name ASC, preview ASC"; //NON-NLS } - + // Query for keywords that are part of a list String keywordListsQuery = "SELECT art.artifact_id AS artifact_id, art.obj_id AS obj_id, att1.value_text AS keyword, att2.value_text AS preview, att3.value_text AS list, f.name AS name, f.parent_path AS parent_path " @@ -687,22 +690,33 @@ class TableReportGenerator { "AND (att3.attribute_type_id = " + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID() + ") " + //NON-NLS "AND (art.artifact_type_id = " + BlackboardArtifact.ARTIFACT_TYPE.TSK_KEYWORD_HIT.getTypeID() + ") "; - + // Query for keywords that are not part of a list - String keywordAdHocQuery = - "SELECT art.artifact_id AS artifact_id, art.obj_id AS obj_id, att1.value_text AS keyword, att2.value_text AS preview, \'\' AS list, f.name AS name, f.parent_path AS parent_path " + // NON-NLS - "FROM blackboard_artifacts AS art, blackboard_attributes AS att1, blackboard_attributes AS att2, tsk_files AS f " + // NON-NLS - "WHERE " + // NON-NLS - " (art.artifact_id IN (SELECT art.artifact_id FROM blackboard_artifacts AS art, blackboard_attributes AS att1 WHERE (att1.artifact_id = art.artifact_id) AND (art.artifact_type_id = " + BlackboardArtifact.ARTIFACT_TYPE.TSK_KEYWORD_HIT.getTypeID() + ") " + // NON-NLS - "EXCEPT " + // NON-NLS - "SELECT art.artifact_id FROM blackboard_artifacts AS art, blackboard_attributes AS att1 WHERE (att1.artifact_id = art.artifact_id) AND (art.artifact_type_id = " + BlackboardArtifact.ARTIFACT_TYPE.TSK_KEYWORD_HIT.getTypeID() + ") AND (att1.attribute_type_id = " + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID() + "))) " + //NON-NLS - "AND (att1.artifact_id = art.artifact_id) " + //NON-NLS - "AND (att2.artifact_id = art.artifact_id) " + //NON-NLS - "AND (f.obj_id = art.obj_id) " + //NON-NLS - "AND (att1.attribute_type_id = " + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD.getTypeID() + ") " + // NON-NLS - "AND (att2.attribute_type_id = " + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD_PREVIEW.getTypeID() + ") " + // NON-NLS + String keywordAdHocQuery + = "SELECT art.artifact_id AS artifact_id, art.obj_id AS obj_id, att1.value_text AS keyword, att2.value_text AS preview, \'\' AS list, f.name AS name, f.parent_path AS parent_path " + + // NON-NLS + "FROM blackboard_artifacts AS art, blackboard_attributes AS att1, blackboard_attributes AS att2, tsk_files AS f " + + // NON-NLS + "WHERE " + + // NON-NLS + " (art.artifact_id IN (SELECT art.artifact_id FROM blackboard_artifacts AS art, blackboard_attributes AS att1 WHERE (att1.artifact_id = art.artifact_id) AND (art.artifact_type_id = " + BlackboardArtifact.ARTIFACT_TYPE.TSK_KEYWORD_HIT.getTypeID() + ") " + + // NON-NLS + "EXCEPT " + + // NON-NLS + "SELECT art.artifact_id FROM blackboard_artifacts AS art, blackboard_attributes AS att1 WHERE (att1.artifact_id = art.artifact_id) AND (art.artifact_type_id = " + BlackboardArtifact.ARTIFACT_TYPE.TSK_KEYWORD_HIT.getTypeID() + ") AND (att1.attribute_type_id = " + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID() + "))) " + + //NON-NLS + "AND (att1.artifact_id = art.artifact_id) " + + //NON-NLS + "AND (att2.artifact_id = art.artifact_id) " + + //NON-NLS + "AND (f.obj_id = art.obj_id) " + + //NON-NLS + "AND (att1.attribute_type_id = " + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD.getTypeID() + ") " + + // NON-NLS + "AND (att2.attribute_type_id = " + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD_PREVIEW.getTypeID() + ") " + + // NON-NLS "AND (art.artifact_type_id = " + BlackboardArtifact.ARTIFACT_TYPE.TSK_KEYWORD_HIT.getTypeID() + ") "; // NON-NLS - + String keywordsQuery = "SELECT * FROM ( " + keywordListsQuery + " UNION " + keywordAdHocQuery + " ) kwHits " + orderByClause; try (SleuthkitCase.CaseDbQuery dbQuery = openCase.getSleuthkitCase().executeQuery(keywordsQuery)) { @@ -760,7 +774,7 @@ class TableReportGenerator { if (!currentKeyword.equals("")) { tableModule.endTable(); } - + // Prepare for a new table. currentKeyword = keyword; tableModule.addSetElement(currentKeyword); @@ -773,7 +787,7 @@ class TableReportGenerator { tableModule.addRow(Arrays.asList(new String[]{preview, uniquePath, tagsList})); } - + // End the previous table if one exists. if (!currentKeyword.isEmpty()) { tableModule.endTable(); @@ -1242,6 +1256,7 @@ class TableReportGenerator { columns.add(new AttributeColumn(NbBundle.getMessage(this.getClass(), "ReportGenerator.artTableColHdr.program"), new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME))); + attributeTypeSet.remove(new Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH_ID)); } else if (BlackboardArtifact.ARTIFACT_TYPE.TSK_RECENT_OBJECT.getTypeID() == artifactTypeId) { columns.add(new AttributeColumn(NbBundle.getMessage(this.getClass(), "ReportGenerator.artTableColHdr.path"), new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH))); @@ -1249,6 +1264,7 @@ class TableReportGenerator { columns.add(new AttributeColumn(NbBundle.getMessage(this.getClass(), "ReportGenerator.artTableColHdr.dateTime"), new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME))); + attributeTypeSet.remove(new Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH_ID)); } else if (BlackboardArtifact.ARTIFACT_TYPE.TSK_INSTALLED_PROG.getTypeID() == artifactTypeId) { columns.add(new AttributeColumn(NbBundle.getMessage(this.getClass(), "ReportGenerator.artTableColHdr.progName"), new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME))); @@ -1509,8 +1525,8 @@ class TableReportGenerator { columns.add(new AttributeColumn(NbBundle.getMessage(this.getClass(), "ReportGenerator.artTableColHdr.mailServer"), new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SERVER_NAME))); - } else if (BlackboardArtifact.ARTIFACT_TYPE.TSK_ENCRYPTION_DETECTED.getTypeID() == artifactTypeId || - BlackboardArtifact.ARTIFACT_TYPE.TSK_ENCRYPTION_SUSPECTED.getTypeID() == artifactTypeId) { + } else if (BlackboardArtifact.ARTIFACT_TYPE.TSK_ENCRYPTION_DETECTED.getTypeID() == artifactTypeId + || BlackboardArtifact.ARTIFACT_TYPE.TSK_ENCRYPTION_SUSPECTED.getTypeID() == artifactTypeId) { columns.add(new AttributeColumn(NbBundle.getMessage(this.getClass(), "ReportGenerator.artTableColHdr.name"), new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME))); @@ -1570,7 +1586,7 @@ class TableReportGenerator { columns.add(new AttributeColumn(NbBundle.getMessage(this.getClass(), "ReportGenerator.artTableColHdr.tskPath"), new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH))); - + columns.add(new AttributeColumn(NbBundle.getMessage(this.getClass(), "ReportGenerator.artTableColHdr.comment"), new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_COMMENT))); @@ -1647,6 +1663,8 @@ class TableReportGenerator { attributeTypeSet.remove(new Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT)); attributeTypeSet.remove(new Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME)); attributeTypeSet.remove(new Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD_SEARCH_DOCUMENT_ID)); + } else if (artifactTypeId == BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_CACHE.getTypeID()) { + attributeTypeSet.remove(new Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH_ID)); } else { // This is the case that it is a custom type. The reason an else is // necessary is to make sure that the source file column is added diff --git a/Core/src/org/sleuthkit/autopsy/report/caseuco/CaseUcoFormatExporter.java b/Core/src/org/sleuthkit/autopsy/report/caseuco/CaseUcoFormatExporter.java index efa7a0b325..7c09ef3cb5 100755 --- a/Core/src/org/sleuthkit/autopsy/report/caseuco/CaseUcoFormatExporter.java +++ b/Core/src/org/sleuthkit/autopsy/report/caseuco/CaseUcoFormatExporter.java @@ -248,7 +248,7 @@ public final class CaseUcoFormatExporter { * @param selectedDataSourceId Object ID of the data source * @param caseTraceId CASE-UCO trace ID object for the Autopsy case entry * @param skCase SleuthkitCase object - * @param catalog JsonGenerator object + * @param jsonGenerator JsonGenerator object * @return * @throws TskCoreException * @throws SQLException diff --git a/Core/src/org/sleuthkit/autopsy/report/uisnapshot/UiSnapShotReportWriter.java b/Core/src/org/sleuthkit/autopsy/report/uisnapshot/UiSnapShotReportWriter.java index 1017e2e743..0242bae3d5 100755 --- a/Core/src/org/sleuthkit/autopsy/report/uisnapshot/UiSnapShotReportWriter.java +++ b/Core/src/org/sleuthkit/autopsy/report/uisnapshot/UiSnapShotReportWriter.java @@ -62,8 +62,6 @@ public abstract class UiSnapShotReportWriter { * report. * @param reportName The name of the report. * @param generationDate The generation Date of the report. - * @param snapshot A snapshot of the view to include in the - * report. */ protected UiSnapShotReportWriter(Case currentCase, Path reportFolderPath, String reportName, Date generationDate) { this.currentCase = currentCase; diff --git a/Core/src/org/sleuthkit/autopsy/textextractors/TextExtractorFactory.java b/Core/src/org/sleuthkit/autopsy/textextractors/TextExtractorFactory.java index 22d4aa5040..86b96194c0 100755 --- a/Core/src/org/sleuthkit/autopsy/textextractors/TextExtractorFactory.java +++ b/Core/src/org/sleuthkit/autopsy/textextractors/TextExtractorFactory.java @@ -29,7 +29,7 @@ import org.sleuthkit.datamodel.Report; /** * Factory for creating TextExtractors given a Content instance * - * See {@link org.sleuthkit.autopsy.textextractors.textextractorconfigs} for + * See {@link org.sleuthkit.autopsy.textextractors.configs} for * available extractor configuration options. * * @see org.openide.util.Lookup @@ -40,7 +40,7 @@ public class TextExtractorFactory { * Returns a TextExtractor containing the Content text. Configuration files * can be added to the Lookup. * - * See {@link org.sleuthkit.autopsy.textextractors.textextractorconfigs} for + * See {@link org.sleuthkit.autopsy.textextractors.configs} for * available extractor configuration options. * * @param content Content source that will be read from @@ -124,7 +124,7 @@ public class TextExtractorFactory { * getExtractor(Content, Lookup). * * Configure this extractor with the StringsConfig in - * {@link org.sleuthkit.autopsy.textextractors.textextractorconfigs} + * {@link org.sleuthkit.autopsy.textextractors.configs} * * @param content Content source to read from * @param context Contains extraction configurations for certain file types diff --git a/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java b/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java index 78b15fc2af..76c04ff029 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java @@ -72,6 +72,7 @@ import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardArtifactTag; +import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.ContentTag; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.Tag; @@ -711,10 +712,10 @@ public class EventsRepository { // if the time is legitimate ( greater than zero ) insert it into the db if (eventDescription != null && eventDescription.getTime() > 0) { long objectID = bbart.getObjectID(); - AbstractFile f = skCase.getAbstractFileById(objectID); - long datasourceID = f.getDataSource().getId(); + Content content = skCase.getContentById(objectID); + long datasourceID = content.getDataSource().getId(); long artifactID = bbart.getArtifactID(); - Set hashSets = f.getHashSetNames(); + Set hashSets = content.getHashSetNames(); List tags = tagsManager.getBlackboardArtifactTagsByArtifact(bbart); String fullDescription = eventDescription.getFullDescription(); String medDescription = eventDescription.getMedDescription(); diff --git a/CoreLibs/ivy.xml b/CoreLibs/ivy.xml index 1ad880a363..994e82c694 100644 --- a/CoreLibs/ivy.xml +++ b/CoreLibs/ivy.xml @@ -13,7 +13,7 @@ - + diff --git a/CoreLibs/nbproject/project.properties b/CoreLibs/nbproject/project.properties index 647666e3cd..7a154a4cf6 100644 --- a/CoreLibs/nbproject/project.properties +++ b/CoreLibs/nbproject/project.properties @@ -23,8 +23,7 @@ file.reference.controlsfx-8.40.11.jar=release/modules/ext/controlsfx-8.40.11.jar file.reference.dom4j-1.6.1.jar=release/modules/ext/dom4j-1.6.1.jar file.reference.geronimo-jms_1.1_spec-1.0.jar=release/modules/ext/geronimo-jms_1.1_spec-1.0.jar file.reference.gson-2.8.1.jar=release/modules/ext/gson-2.8.1.jar -file.reference.gstreamer-java-1.5.jar=release/modules/ext/gstreamer-java-1.5.jar -file.reference.gst1-java-core-0.9.3.jar=release/modules/ext/gst1-java-core-0.9.3.jar +file.reference.gst1-java-core-1.0.0.jar=release\\modules\\ext\\gst1-java-core-1.0.0.jar file.reference.jna-3.4.0.jar=release/modules/ext/jna-3.4.0.jar file.reference.guava-19.0.jar=release/modules/ext/guava-19.0.jar file.reference.imageio-bmp-3.2.jar=release/modules/ext/imageio-bmp-3.2.jar diff --git a/CoreLibs/nbproject/project.xml b/CoreLibs/nbproject/project.xml index 32ad41947a..f7ebf84361 100644 --- a/CoreLibs/nbproject/project.xml +++ b/CoreLibs/nbproject/project.xml @@ -243,6 +243,7 @@ org.apache.commons.io.input org.apache.commons.io.monitor org.apache.commons.io.output + org.apache.commons.io.serialization org.apache.commons.lang org.apache.commons.lang.builder org.apache.commons.lang.enums @@ -589,23 +590,16 @@ org.dom4j.xpath org.dom4j.xpp org.freedesktop.gstreamer - org.freedesktop.gstreamer.controller + org.freedesktop.gstreamer.device org.freedesktop.gstreamer.elements - org.freedesktop.gstreamer.elements.good org.freedesktop.gstreamer.event - org.freedesktop.gstreamer.example org.freedesktop.gstreamer.glib org.freedesktop.gstreamer.interfaces - org.freedesktop.gstreamer.io org.freedesktop.gstreamer.lowlevel org.freedesktop.gstreamer.lowlevel.annotations - org.freedesktop.gstreamer.media - org.freedesktop.gstreamer.media.event org.freedesktop.gstreamer.message org.freedesktop.gstreamer.query - org.freedesktop.gstreamer.swing - org.freedesktop.gstreamer.swt - org.freedesktop.gstreamer.swt.overlay + org.freedesktop.gstreamer.webrtc org.hyperic.jni org.hyperic.sigar org.hyperic.sigar.cmd @@ -707,6 +701,10 @@ ext/logkit-1.0.1.jar release/modules/ext/logkit-1.0.1.jar + + ext/gst1-java-core-1.0.0.jar + release\modules\ext\gst1-java-core-1.0.0.jar + ext/imageio-jpeg-3.2.jar release/modules/ext/imageio-jpeg-3.2.jar @@ -967,10 +965,6 @@ ext/common-lang-3.2.jar release/modules/ext/common-lang-3.2.jar - - ext/gst1-java-core-0.9.3.jar - release/modules/ext/gst1-java-core-0.9.3.jar - ext/dd-plist-1.20.jar release/modules/ext/dd-plist-1.20.jar diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AinStatusPanel.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AinStatusPanel.java index d744abe8b4..45af5c3025 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AinStatusPanel.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AinStatusPanel.java @@ -26,7 +26,6 @@ import org.netbeans.swing.outline.DefaultOutlineModel; import org.netbeans.swing.outline.Outline; import org.openide.explorer.ExplorerManager; import org.openide.nodes.Node; -import org.sleuthkit.autopsy.experimental.autoingest.AutoIngestJobsNode.JobNode; /** * A panel which displays an outline view with all auto ingest nodes and their diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestAdminActions.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestAdminActions.java index 1e1b82d8e7..7a31ab6573 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestAdminActions.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestAdminActions.java @@ -293,75 +293,6 @@ final class AutoIngestAdminActions { } } - @NbBundle.Messages({"AutoIngestAdminActions.deleteCaseAction.title=Delete Case", - "AutoIngestAdminActions.deleteCaseAction.error=Failed to delete case."}) - static final class DeleteCaseAction extends AbstractAction { - - private static final long serialVersionUID = 1L; - private final AutoIngestJob job; - - DeleteCaseAction(AutoIngestJob selectedJob) { - super(Bundle.AutoIngestAdminActions_deleteCaseAction_title()); - this.job = selectedJob; - } - - @Override - public void actionPerformed(ActionEvent e) { - if (job == null) { - return; - } - - final AutoIngestDashboardTopComponent tc = (AutoIngestDashboardTopComponent) WindowManager.getDefault().findTopComponent(AutoIngestDashboardTopComponent.PREFERRED_ID); - if (tc == null) { - return; - } - - AutoIngestDashboard dashboard = tc.getAutoIngestDashboard(); - if (dashboard != null) { - String caseName = job.getManifest().getCaseName(); - - Object[] options = { - org.openide.util.NbBundle.getMessage(AutoIngestControlPanel.class, "ConfirmationDialog.Delete"), - org.openide.util.NbBundle.getMessage(AutoIngestControlPanel.class, "ConfirmationDialog.DoNotDelete") - }; - Object[] msgContent = {org.openide.util.NbBundle.getMessage(AutoIngestControlPanel.class, "ConfirmationDialog.DeleteAreYouSure") + "\"" + caseName + "\"?"}; - int reply = JOptionPane.showOptionDialog(dashboard, - msgContent, - org.openide.util.NbBundle.getMessage(AutoIngestControlPanel.class, "ConfirmationDialog.ConfirmDeletionHeader"), - JOptionPane.DEFAULT_OPTION, - JOptionPane.WARNING_MESSAGE, - null, - options, - options[JOptionPane.NO_OPTION]); - if (reply == JOptionPane.YES_OPTION) { - EventQueue.invokeLater(() -> { - dashboard.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); - AutoIngestManager.CaseDeletionResult result = dashboard.getMonitor().deleteCase(job); - - dashboard.getCompletedJobsPanel().refresh(new AutoIngestNodeRefreshEvents.RefreshChildrenEvent(dashboard.getMonitor())); - dashboard.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); - if (AutoIngestManager.CaseDeletionResult.FAILED == result) { - JOptionPane.showMessageDialog(dashboard, - String.format("Could not delete case %s. It may be in use.", caseName), - org.openide.util.NbBundle.getMessage(AutoIngestControlPanel.class, "AutoIngestControlPanel.DeletionFailed"), - JOptionPane.INFORMATION_MESSAGE); - } else if (AutoIngestManager.CaseDeletionResult.PARTIALLY_DELETED == result) { - JOptionPane.showMessageDialog(dashboard, - String.format("Could not fully delete case %s. See log for details.", caseName), - org.openide.util.NbBundle.getMessage(AutoIngestControlPanel.class, "AutoIngestControlPanel.DeletionFailed"), - JOptionPane.INFORMATION_MESSAGE); - } - }); - } - } - } - - @Override - public Object clone() throws CloneNotSupportedException { - return super.clone(); //To change body of generated methods, choose Tools | Templates. - } - } - @NbBundle.Messages({"AutoIngestAdminActions.showCaseLogAction.title=Show Case Log", "AutoIngestAdminActions.showCaseLogActionFailed.title=Unable to display case log", "AutoIngestAdminActions.showCaseLogActionFailed.message=Case log file does not exist", diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestControlPanel.form b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestControlPanel.form index 1e1cc73c43..62855a7bdb 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestControlPanel.form +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestControlPanel.form @@ -57,14 +57,15 @@ - - - - - - - - + + + + + + + + + @@ -135,8 +136,6 @@ - - @@ -255,28 +254,6 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestControlPanel.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestControlPanel.java index db8e25a592..78d8853d7b 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestControlPanel.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestControlPanel.java @@ -63,7 +63,6 @@ import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.coreutils.NetworkUtils; import org.sleuthkit.autopsy.coreutils.PlatformUtil; -import org.sleuthkit.autopsy.experimental.autoingest.AutoIngestManager.CaseDeletionResult; import org.sleuthkit.autopsy.experimental.autoingest.AutoIngestManager.JobsSnapshot; import org.sleuthkit.autopsy.guiutils.DurationCellRenderer; import org.sleuthkit.autopsy.guiutils.LongDateCellRenderer; @@ -621,7 +620,6 @@ public final class AutoIngestControlPanel extends JPanel implements Observer { } int row = completedTable.getSelectedRow(); boolean enabled = row >= 0 && row < completedTable.getRowCount(); - bnDeleteCase.setEnabled(enabled); bnShowCaseLog.setEnabled(enabled); bnReprocessJob.setEnabled(enabled); }); @@ -632,7 +630,6 @@ public final class AutoIngestControlPanel extends JPanel implements Observer { */ private void initButtons() { bnOptions.setEnabled(true); - bnDeleteCase.setEnabled(false); enablePrioritizeButtons(false); enableDeprioritizeButtons(false); bnShowCaseLog.setEnabled(false); @@ -1229,7 +1226,6 @@ public final class AutoIngestControlPanel extends JPanel implements Observer { completedScrollPane = new javax.swing.JScrollPane(); completedTable = new javax.swing.JTable(); bnCancelJob = new javax.swing.JButton(); - bnDeleteCase = new javax.swing.JButton(); lbPending = new javax.swing.JLabel(); lbRunning = new javax.swing.JLabel(); lbCompleted = new javax.swing.JLabel(); @@ -1317,17 +1313,6 @@ public final class AutoIngestControlPanel extends JPanel implements Observer { } }); - org.openide.awt.Mnemonics.setLocalizedText(bnDeleteCase, org.openide.util.NbBundle.getMessage(AutoIngestControlPanel.class, "AutoIngestControlPanel.bnDeleteCase.text")); // NOI18N - bnDeleteCase.setToolTipText(org.openide.util.NbBundle.getMessage(AutoIngestControlPanel.class, "AutoIngestControlPanel.bnDeleteCase.toolTipText")); // NOI18N - bnDeleteCase.setMaximumSize(new java.awt.Dimension(162, 23)); - bnDeleteCase.setMinimumSize(new java.awt.Dimension(162, 23)); - bnDeleteCase.setPreferredSize(new java.awt.Dimension(162, 23)); - bnDeleteCase.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - bnDeleteCaseActionPerformed(evt); - } - }); - lbPending.setFont(new java.awt.Font("Tahoma", 0, 14)); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(lbPending, org.openide.util.NbBundle.getMessage(AutoIngestControlPanel.class, "AutoIngestControlPanel.lbPending.text")); // NOI18N @@ -1547,13 +1532,13 @@ public final class AutoIngestControlPanel extends JPanel implements Observer { .addComponent(runningScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 1021, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(completedScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 1021, javax.swing.GroupLayout.PREFERRED_SIZE)) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) - .addComponent(bnCancelJob, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(bnShowProgress, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(bnCancelModule, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(bnDeleteCase, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(bnShowCaseLog, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(bnReprocessJob, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addComponent(bnCancelJob, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(bnShowProgress, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(bnCancelModule, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(bnReprocessJob, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addComponent(bnShowCaseLog, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))) .addGroup(layout.createSequentialGroup() .addComponent(pendingScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 1021, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) @@ -1565,7 +1550,7 @@ public final class AutoIngestControlPanel extends JPanel implements Observer { .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)))) ); - layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {bnCancelJob, bnCancelModule, bnDeleteCase, bnShowProgress}); + layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {bnCancelJob, bnCancelModule, bnShowProgress}); layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {bnClusterMetrics, bnExit, bnOpenLogDir, bnOptions, bnPause, bnRefresh}); @@ -1612,8 +1597,6 @@ public final class AutoIngestControlPanel extends JPanel implements Observer { .addGap(68, 68, 68) .addComponent(bnReprocessJob, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(bnDeleteCase, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(bnShowCaseLog, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) .addGroup(layout.createSequentialGroup() .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) @@ -1631,7 +1614,7 @@ public final class AutoIngestControlPanel extends JPanel implements Observer { .addContainerGap()) ); - layout.linkSize(javax.swing.SwingConstants.VERTICAL, new java.awt.Component[] {bnCancelJob, bnCancelModule, bnClusterMetrics, bnDeleteCase, bnExit, bnOpenLogDir, bnOptions, bnPrioritizeCase, bnPrioritizeJob, bnRefresh, bnShowProgress}); + layout.linkSize(javax.swing.SwingConstants.VERTICAL, new java.awt.Component[] {bnCancelJob, bnCancelModule, bnClusterMetrics, bnExit, bnOpenLogDir, bnOptions, bnPrioritizeCase, bnPrioritizeJob, bnRefresh, bnShowProgress}); }// //GEN-END:initComponents @@ -1649,60 +1632,6 @@ public final class AutoIngestControlPanel extends JPanel implements Observer { this.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); }//GEN-LAST:event_bnRefreshActionPerformed - /** - * Handles a click on the delete case button. If an entry is selected that - * can be deleted, pops up a confirmation dialog. Upon confirmation, asks - * AutoIngestManager to delete the entry and asks for an updated view. - * - * @param evt The button click event. - */ - @Messages({ - "AutoIngestControlPanel.DeletionFailed=Deletion failed for job" - }) - private void bnDeleteCaseActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnDeleteCaseActionPerformed - if (completedTable.getModel().getRowCount() < 0 || completedTable.getSelectedRow() < 0) { - return; - } - - String caseName = (String) completedTable.getModel().getValueAt(completedTable.convertRowIndexToModel(completedTable.getSelectedRow()), JobsTableModelColumns.CASE.ordinal()); - Object[] options = { - org.openide.util.NbBundle.getMessage(AutoIngestControlPanel.class, "ConfirmationDialog.Delete"), - org.openide.util.NbBundle.getMessage(AutoIngestControlPanel.class, "ConfirmationDialog.DoNotDelete") - }; - Object[] msgContent = {org.openide.util.NbBundle.getMessage(AutoIngestControlPanel.class, "ConfirmationDialog.DeleteAreYouSure") + "\"" + caseName + "\"?"}; - int reply = JOptionPane.showOptionDialog(this, - msgContent, - org.openide.util.NbBundle.getMessage(AutoIngestControlPanel.class, "ConfirmationDialog.ConfirmDeletionHeader"), - JOptionPane.DEFAULT_OPTION, - JOptionPane.WARNING_MESSAGE, - null, - options, - options[JOptionPane.NO_OPTION]); - if (reply == JOptionPane.YES_OPTION) { - bnDeleteCase.setEnabled(false); - bnShowCaseLog.setEnabled(false); - if (completedTable.getModel().getRowCount() > 0 && completedTable.getSelectedRow() >= 0) { - Path caseDirectoryPath = (Path) completedTable.getModel().getValueAt(completedTable.convertRowIndexToModel(completedTable.getSelectedRow()), JobsTableModelColumns.CASE_DIRECTORY_PATH.ordinal()); - completedTable.clearSelection(); - this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); - CaseDeletionResult result = manager.deleteCase(caseName, caseDirectoryPath); - refreshTables(); - this.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); - if (CaseDeletionResult.FAILED == result) { - JOptionPane.showMessageDialog(this, - String.format("Could not delete case %s. It may be in use.", caseName), - org.openide.util.NbBundle.getMessage(AutoIngestControlPanel.class, "AutoIngestControlPanel.DeletionFailed"), - JOptionPane.INFORMATION_MESSAGE); - } else if (CaseDeletionResult.PARTIALLY_DELETED == result) { - JOptionPane.showMessageDialog(this, - String.format("Could not fully delete case %s. See system log for details.", caseName), - org.openide.util.NbBundle.getMessage(AutoIngestControlPanel.class, "AutoIngestControlPanel.DeletionFailed"), - JOptionPane.INFORMATION_MESSAGE); - } - } - } - }//GEN-LAST:event_bnDeleteCaseActionPerformed - /** * Handles a click on the cancel auto ingest job button. Cancels the * selected job. @@ -1976,7 +1905,6 @@ public final class AutoIngestControlPanel extends JPanel implements Observer { private javax.swing.JButton bnCancelJob; private javax.swing.JButton bnCancelModule; private javax.swing.JButton bnClusterMetrics; - private javax.swing.JButton bnDeleteCase; private javax.swing.JButton bnDeprioritizeCase; private javax.swing.JButton bnDeprioritizeJob; private javax.swing.JButton bnExit; diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestDashboard.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestDashboard.java index 754a9c3ad7..febcf4fb7a 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestDashboard.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestDashboard.java @@ -51,8 +51,10 @@ import org.sleuthkit.autopsy.experimental.autoingest.AutoIngestNodeRefreshEvents @SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives final class AutoIngestDashboard extends JPanel implements Observer { - private final static String ADMIN_ACCESS_FILE_NAME = "_aiaa"; // NON-NLS + private final static String ADMIN_ACCESS_FILE_NAME = "admin"; // NON-NLS private final static String ADMIN_ACCESS_FILE_PATH = Paths.get(PlatformUtil.getUserConfigDirectory(), ADMIN_ACCESS_FILE_NAME).toString(); + private final static String ADMIN_EXT_ACCESS_FILE_NAME = "adminext"; // NON-NLS + private final static String ADMIN_EXT_ACCESS_FILE_PATH = Paths.get(PlatformUtil.getUserConfigDirectory(), ADMIN_EXT_ACCESS_FILE_NAME).toString(); private final static String AID_REFRESH_THREAD_NAME = "AID-refresh-jobs-%d"; private final static int AID_REFRESH_INTERVAL_SECS = 30; private final static int AID_DELAY_BEFORE_FIRST_REFRESH = 0; @@ -277,8 +279,8 @@ final class AutoIngestDashboard extends JPanel implements Observer { } /** - * Reloads the table models using a RefreshChildrenEvent and refreshes the JTables - * that use the models. + * Reloads the table models using a RefreshChildrenEvent and refreshes the + * JTables that use the models. * */ void refreshTables() { @@ -318,9 +320,24 @@ final class AutoIngestDashboard extends JPanel implements Observer { } + /** + * Determines whether or not system adminstrator features of the dashboard + * are enabled. + * + * @return True or false. + */ static boolean isAdminAutoIngestDashboard() { - File f = new File(ADMIN_ACCESS_FILE_PATH); - return f.exists(); + return new File(ADMIN_ACCESS_FILE_PATH).exists() || new File(ADMIN_EXT_ACCESS_FILE_PATH).exists(); + } + + /** + * Determines whether the extended system administrator features of the + * cases dashboard are enabled. + * + * @return True or false. + */ + static boolean extendedFeaturesAreEnabled() { + return new File(ADMIN_EXT_ACCESS_FILE_PATH).exists(); } /** diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestDashboardLogger.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestDashboardLogger.java new file mode 100755 index 0000000000..61d6032048 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestDashboardLogger.java @@ -0,0 +1,92 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019-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.experimental.autoingest; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.sql.Timestamp; +import java.util.logging.FileHandler; +import java.util.logging.Formatter; +import java.util.logging.LogRecord; +import javax.annotation.concurrent.GuardedBy; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.PlatformUtil; + +/** + * A logger for the auto ingest dashboard log. + */ +final class AutoIngestDashboardLogger { + + private static final int LOG_SIZE = 50000000; // In bytes, zero is unlimited. + private static final int LOG_FILE_COUNT = 10; + private static final Logger logger = Logger.getLogger("AutoIngestDashboardLogger"); //NON-NLS + private static final String NEWLINE = System.lineSeparator(); + @GuardedBy("AutoIngestDashboardLogger") + private static boolean configured; + + /** + * Gets a logger for the auto ingest dashboard log. + * + * @return The logger. + */ + synchronized static Logger getLogger() { + if (!configured) { + Path logFilePath = Paths.get(PlatformUtil.getUserDirectory().getAbsolutePath(), "var", "log", "auto_ingest_dashboard.log"); //NON-NLS + try { + FileHandler fileHandler = new FileHandler(logFilePath.toString(), LOG_SIZE, LOG_FILE_COUNT); + fileHandler.setEncoding(PlatformUtil.getLogFileEncoding()); + fileHandler.setFormatter(new Formatter() { + @Override + public String format(LogRecord record) { + Throwable thrown = record.getThrown(); + String stackTrace = ""; //NON-NLS + while (thrown != null) { + stackTrace += thrown.toString() + NEWLINE; + for (StackTraceElement traceElem : record.getThrown().getStackTrace()) { + stackTrace += "\t" + traceElem.toString() + NEWLINE; //NON-NLS + } + thrown = thrown.getCause(); + } + return (new Timestamp(record.getMillis())).toString() + " " //NON-NLS + + record.getSourceClassName() + " " //NON-NLS + + record.getSourceMethodName() + NEWLINE + + record.getLevel() + ": " //NON-NLS + + this.formatMessage(record) + NEWLINE + + stackTrace; + } + }); + logger.addHandler(fileHandler); + logger.setUseParentHandlers(false); + } catch (IOException ex) { + throw new UncheckedIOException(String.format("Error initializing file handler for %s", logFilePath), ex); //NON-NLS + } + configured = true; + } + return logger; + } + + /** + * Prevents instantiation of this utility class. + */ + private AutoIngestDashboardLogger() { + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestDashboardTopComponent.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestDashboardTopComponent.java index 5bf3cd47fb..f21d80fb8d 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestDashboardTopComponent.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestDashboardTopComponent.java @@ -77,7 +77,7 @@ public final class AutoIngestDashboardTopComponent extends TopComponent { AutoIngestDashboard dashboard = AutoIngestDashboard.createDashboard(); tc.add(dashboard); dashboard.setSize(dashboard.getPreferredSize()); - //if the user has administrator access enabled open the Node Status top component as well + //if the user has administrator access enabled open the Node Status and cases top components as well if (AutoIngestDashboard.isAdminAutoIngestDashboard()) { EventQueue.invokeLater(() -> { AinStatusDashboardTopComponent.openTopComponent(dashboard.getMonitor()); diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJob.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJob.java index 4083d448f7..7a9a446ae6 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJob.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJob.java @@ -193,6 +193,7 @@ final class AutoIngestJob implements Comparable, IngestProgressSn this.ingestThreadsSnapshot = Collections.emptyList(); this.ingestJobsSnapshot = Collections.emptyList(); this.moduleRunTimesSnapshot = Collections.emptyMap(); + } catch (Exception ex) { throw new AutoIngestJobException(String.format("Error creating automated ingest job"), ex); } @@ -651,7 +652,7 @@ final class AutoIngestJob implements Comparable, IngestProgressSn PENDING, PROCESSING, COMPLETED, - DELETED + DELETED // No longer used, retained for legacy jobs only. } /** diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJobLogger.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJobLogger.java index dfff014556..708ce7ee14 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJobLogger.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJobLogger.java @@ -47,8 +47,8 @@ import org.sleuthkit.autopsy.coreutils.NetworkUtils; * of the error. */ @Immutable -final class AutoIngestJobLogger { - +final class AutoIngestJobLogger { + private static final String LOG_FILE_NAME = "auto_ingest_log.txt"; private static final int LOCK_TIME_OUT = 15; private static final TimeUnit LOCK_TIME_OUT_UNIT = TimeUnit.MINUTES; diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJobNodeDataCollector.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJobNodeDataCollector.java new file mode 100755 index 0000000000..0f9554aeee --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJobNodeDataCollector.java @@ -0,0 +1,58 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019-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.experimental.autoingest; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import org.sleuthkit.autopsy.coordinationservice.CoordinationService; +import org.sleuthkit.autopsy.coordinationservice.CoordinationService.CoordinationServiceException; +import org.sleuthkit.autopsy.coreutils.Logger; + +/** + * Collects the auto ingest job node data stored in the manifest file + * coordination service nodes. + */ +final class AutoIngestJobNodeDataCollector { + + private static final Logger logger = Logger.getLogger(AutoIngestJobNodeDataCollector.class.getName()); + + static List getNodeData() throws CoordinationServiceException, InterruptedException { + final CoordinationService coordinationService = CoordinationService.getInstance(); + final List nodePaths = coordinationService.getNodeList(CoordinationService.CategoryNode.MANIFESTS); + final List nodeDataList = new ArrayList<>(); + for (String nodePath : nodePaths) { + try { + final byte[] nodeBytes = coordinationService.getNodeData(CoordinationService.CategoryNode.MANIFESTS, nodePath); + AutoIngestJobNodeData nodeData = new AutoIngestJobNodeData(nodeBytes); + nodeDataList.add(nodeData); + } catch (AutoIngestJobNodeData.InvalidDataException ex) { + logger.log(Level.WARNING, String.format("Error reading node data from manifest file coordination service node %s", nodePath), ex); // NON-NLS + } + } + return nodeDataList; + } + + /** + * Prevents instantiation of this utility class. + */ + private AutoIngestJobNodeDataCollector() { + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJobsNode.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJobsNode.java index 00a2bc4d37..c37b834348 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJobsNode.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJobsNode.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2018 Basis Technology Corp. + * Copyright 2018-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -372,7 +372,6 @@ final class AutoIngestJobsNode extends AbstractNode { break; case COMPLETED_JOB: actions.add(new AutoIngestAdminActions.ReprocessJobAction(jobWrapper.getJob())); - actions.add(new AutoIngestAdminActions.DeleteCaseAction(jobWrapper.getJob())); actions.add(new AutoIngestAdminActions.ShowCaseLogAction(jobWrapper.getJob())); break; default: diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java index cf0e5c615d..4bade61c1c 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.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.util.concurrent.ThreadFactoryBuilder; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; +import java.io.FileWriter; import java.io.IOException; import static java.nio.file.FileVisitOption.FOLLOW_LINKS; import java.nio.file.FileVisitResult; @@ -39,11 +40,9 @@ import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.EnumSet; -import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.Observable; import java.util.Set; import java.util.UUID; @@ -63,6 +62,7 @@ import org.sleuthkit.autopsy.casemodule.CaseActionException; import org.sleuthkit.autopsy.casemodule.CaseDetails; import org.sleuthkit.autopsy.casemodule.CaseMetadata; import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; +import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData.CaseNodeDataException; import org.sleuthkit.autopsy.coordinationservice.CoordinationService; import org.sleuthkit.autopsy.coordinationservice.CoordinationService.CoordinationServiceException; import org.sleuthkit.autopsy.coordinationservice.CoordinationService.Lock; @@ -146,6 +146,7 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen private static final String JOB_STATUS_PUBLISHING_THREAD_NAME = "AIM-job-status-event-publisher-%d"; private static final long MAX_MISSED_JOB_STATUS_UPDATES = 10; private static final int DEFAULT_PRIORITY = 0; + private static String CASE_MANIFESTS_LIST_FILE_NAME = "auto-ingest-job-manifests.txt"; private static final Logger sysLogger = AutoIngestSystemLogger.getLogger(); private static AutoIngestManager instance; private final AutopsyEventPublisher eventPublisher; @@ -158,8 +159,6 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen private final ConcurrentHashMap hostNamesToRunningJobs; private final Object jobsLock; @GuardedBy("jobsLock") - private final Map> casesToManifests; - @GuardedBy("jobsLock") private List pendingJobs; @GuardedBy("jobsLock") private AutoIngestJob currentJob; @@ -175,6 +174,16 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen private volatile AutoIngestNodeStateEvent lastPublishedStateEvent; + /** + * Gets the name of the file in a case directory that is used to record the + * manifest file paths for the auto ingest jobs for the case. + * + * @return The file name. + */ + static String getCaseManifestsListFileName() { + return CASE_MANIFESTS_LIST_FILE_NAME; + } + /** * Gets a singleton auto ingest manager responsible for processing auto * ingest jobs defined by manifest files that can be added to any level of a @@ -206,7 +215,6 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen hostNamesToRunningJobs = new ConcurrentHashMap<>(); hostNamesToLastMsgTime = new ConcurrentHashMap<>(); jobsLock = new Object(); - casesToManifests = new HashMap<>(); pendingJobs = new ArrayList<>(); completedJobs = new ArrayList<>(); try { @@ -695,7 +703,7 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen int oldPriority = job.getPriority(); job.setPriority(DEFAULT_PRIORITY); try { - this.updateCoordinationServiceManifestNode(job); + this.updateAutoIngestJobData(job); } catch (CoordinationServiceException | InterruptedException ex) { job.setPriority(oldPriority); throw new AutoIngestManagerException("Error updating case priority", ex); @@ -745,7 +753,7 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen int oldPriority = job.getPriority(); job.setPriority(maxPriority); try { - this.updateCoordinationServiceManifestNode(job); + this.updateAutoIngestJobData(job); } catch (CoordinationServiceException | InterruptedException ex) { job.setPriority(oldPriority); throw new AutoIngestManagerException("Error updating case priority", ex); @@ -797,7 +805,7 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen int oldPriority = jobToDeprioritize.getPriority(); jobToDeprioritize.setPriority(DEFAULT_PRIORITY); try { - this.updateCoordinationServiceManifestNode(jobToDeprioritize); + this.updateAutoIngestJobData(jobToDeprioritize); } catch (CoordinationServiceException | InterruptedException ex) { jobToDeprioritize.setPriority(oldPriority); throw new AutoIngestManagerException("Error updating job priority", ex); @@ -855,7 +863,7 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen int oldPriority = jobToPrioritize.getPriority(); jobToPrioritize.setPriority(maxPriority); try { - this.updateCoordinationServiceManifestNode(jobToPrioritize); + this.updateAutoIngestJobData(jobToPrioritize); } catch (CoordinationServiceException | InterruptedException ex) { jobToPrioritize.setPriority(oldPriority); throw new AutoIngestManagerException("Error updating job priority", ex); @@ -910,7 +918,7 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen completedJob.setCompletedDate(new Date(0)); completedJob.setProcessingStatus(PENDING); completedJob.setProcessingStage(AutoIngestJob.Stage.PENDING, Date.from(Instant.now())); - updateCoordinationServiceManifestNode(completedJob); + updateAutoIngestJobData(completedJob); pendingJobs.add(completedJob); } catch (CoordinationServiceException ex) { sysLogger.log(Level.SEVERE, String.format("Coordination service error while reprocessing %s", manifestPath), ex); @@ -925,123 +933,6 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen } } - /** - * Deletes a case. This includes deleting the case directory, the text - * index, and the case database. This does not include the directories - * containing the data sources and their manifests. - * - * @param caseName The name of the case. - * @param caseDirectoryPath The path to the case directory. - * - * @return A result code indicating success, partial success, or failure. - */ - CaseDeletionResult deleteCase(String caseName, Path caseDirectoryPath) { - if (state != State.RUNNING) { - return CaseDeletionResult.FAILED; - } - - CaseDeletionResult result = CaseDeletionResult.FULLY_DELETED; - List manifestFileLocks = new ArrayList<>(); - try { - synchronized (jobsLock) { - /* - * Get the case metadata. - */ - CaseMetadata metaData; - Path caseMetaDataFilePath = Paths.get(caseDirectoryPath.toString(), caseName + CaseMetadata.getFileExtension()); - try { - metaData = new CaseMetadata(caseMetaDataFilePath); - } catch (CaseMetadata.CaseMetadataException ex) { - sysLogger.log(Level.SEVERE, String.format("Failed to get case metadata file %s for case %s at %s", caseMetaDataFilePath, caseName, caseDirectoryPath), ex); - return CaseDeletionResult.FAILED; - } - - /* - * Do a fresh input directory scan. - */ - InputDirScanner scanner = new InputDirScanner(); - scanner.scan(); - Set manifestPaths = casesToManifests.get(caseName); - if (null == manifestPaths) { - sysLogger.log(Level.SEVERE, String.format("No manifest paths found for case %s at %s", caseName, caseDirectoryPath)); - return CaseDeletionResult.FAILED; - } - - /* - * Get exclusive locks on all of the manifests for the case. - * This will exclude other auot ingest nodes from doing anything - * with the case. - */ - for (Path manifestPath : manifestPaths) { - try { - Lock lock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.MANIFESTS, manifestPath.toString()); - if (null != lock) { - manifestFileLocks.add(lock); - } else { - return CaseDeletionResult.FAILED; - } - } catch (CoordinationServiceException ex) { - sysLogger.log(Level.SEVERE, String.format("Error attempting to acquire manifest lock for %s for case %s at %s", manifestPath, caseName, caseDirectoryPath), ex); - return CaseDeletionResult.FAILED; - } - } - - try { - /* - * Physically delete the case. - */ - Case.deleteCase(metaData); - } catch (CaseActionException ex) { - sysLogger.log(Level.SEVERE, String.format("Failed to physically delete case %s at %s", caseName, caseDirectoryPath), ex); - return CaseDeletionResult.FAILED; - } - - /* - * Mark each job (manifest file) as deleted - */ - for (Path manifestPath : manifestPaths) { - try { - AutoIngestJobNodeData nodeData = new AutoIngestJobNodeData(coordinationService.getNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestPath.toString())); - AutoIngestJob deletedJob = new AutoIngestJob(nodeData); - deletedJob.setProcessingStatus(AutoIngestJob.ProcessingStatus.DELETED); - this.updateCoordinationServiceManifestNode(deletedJob); - } catch (AutoIngestJobNodeData.InvalidDataException | AutoIngestJobException ex) { - sysLogger.log(Level.WARNING, String.format("Invalid auto ingest job node data for %s", manifestPath), ex); - return CaseDeletionResult.PARTIALLY_DELETED; - } catch (InterruptedException | CoordinationServiceException ex) { - sysLogger.log(Level.SEVERE, String.format("Error attempting to set delete flag on manifest data for %s for case %s at %s", manifestPath, caseName, caseDirectoryPath), ex); - return CaseDeletionResult.PARTIALLY_DELETED; - } - } - - /* - * Remove the jobs for the case from the pending jobs queue and - * completed jobs list. - */ - removeJobs(manifestPaths, pendingJobs); - removeJobs(manifestPaths, completedJobs); - casesToManifests.remove(caseName); - } - - eventPublisher.publishRemotely(new AutoIngestCaseDeletedEvent(caseName, LOCAL_HOST_NAME, getSystemUserNameProperty())); - setChanged(); - notifyObservers(Event.CASE_DELETED); - return result; - - } finally { - /* - * Always release the manifest locks, regardless of the outcome. - */ - for (Lock lock : manifestFileLocks) { - try { - lock.release(); - } catch (CoordinationServiceException ex) { - sysLogger.log(Level.SEVERE, String.format("Failed to release manifest file lock when deleting case %s at %s", caseName, caseDirectoryPath), ex); - } - } - } - } - /** * Get the current snapshot of the job lists. * @@ -1114,15 +1005,12 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen } /** - * Sets the coordination service manifest node. - * - * Note that a new auto ingest job node data object will be created from the - * job passed in. Thus, if the data version of the node has changed, the - * node will be "upgraded" as well as updated. + * Writes the node data for an auto ingest job to the job's manifest file + * lock coordination service node. * * @param job The auto ingest job. */ - void updateCoordinationServiceManifestNode(AutoIngestJob job) throws CoordinationServiceException, InterruptedException { + void updateAutoIngestJobData(AutoIngestJob job) throws CoordinationServiceException, InterruptedException { AutoIngestJobNodeData nodeData = new AutoIngestJobNodeData(job); String manifestNodePath = job.getManifest().getFilePath().toString(); byte[] rawData = nodeData.toArray(); @@ -1130,19 +1018,24 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen } /** - * Sets the error flag for case node data given a case directory path. + * Sets the error flag in the case node data stored in a case directory + * coordination service node. * * @param caseDirectoryPath The case directory path. * - * @throws CoordinationService.CoordinationServiceException - * @throws InterruptedException - * @throws IOException + * @throws InterruptedException If the thread running the input directory + * scan task is interrupted while blocked, + * i.e., if auto ingest is shutting down. */ - private void setCaseNodeDataErrorsOccurred(Path caseDirectoryPath) throws CoordinationServiceException, InterruptedException, IOException { - CaseNodeData caseNodeData = new CaseNodeData(coordinationService.getNodeData(CoordinationService.CategoryNode.CASES, caseDirectoryPath.toString())); - caseNodeData.setErrorsOccurred(true); - byte[] rawData = caseNodeData.toArray(); - coordinationService.setNodeData(CoordinationService.CategoryNode.CASES, caseDirectoryPath.toString(), rawData); + private void setErrorsOccurredFlagForCase(Path caseDirectoryPath) throws InterruptedException { + try { + CaseNodeData caseNodeData = CaseNodeData.readCaseNodeData(caseDirectoryPath.toString()); + caseNodeData.setErrorsOccurred(true); + CaseNodeData.writeCaseNodeData(caseNodeData); + } catch (CaseNodeDataException ex) { + sysLogger.log(Level.WARNING, String.format("Error attempting to set error flag in case node data for %s", caseDirectoryPath), ex); + } + } /** @@ -1206,6 +1099,7 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen private final List newPendingJobsList = new ArrayList<>(); private final List newCompletedJobsList = new ArrayList<>(); + private Lock currentDirLock; /** * Searches the input directories for manifest files. The search results @@ -1227,9 +1121,9 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen } catch (Exception ex) { /* - * NOTE: Need to catch all exceptions here. Otherwise - * uncaught exceptions will propagate up to the calling - * thread and may stop it from running. + * NOTE: Need to catch all unhandled exceptions here. + * Otherwise uncaught exceptions will propagate up to the + * calling thread and may stop it from running. */ sysLogger.log(Level.SEVERE, String.format("Error scanning the input directory %s", rootInputDirectory), ex); } @@ -1263,20 +1157,15 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen } /** - * Invoked for a file in a directory. If the file is a manifest file, - * creates a pending pending or completed auto ingest job for the - * manifest, based on the data stored in the coordination service node - * for the manifest. - *

- * Note that the mapping of case names to manifest paths that is used - * for case deletion is updated as well. + * Creates a pending or completed auto ingest job if the file visited is + * a manifest file, based on the data stored in the coordination service + * node for the manifest. * * @param filePath The path of the file. * @param attrs The file system attributes of the file. * - * @return TERMINATE if auto ingest is shutting down, CONTINUE if it has - * not. - * + * @return TERMINATE if auto ingest is shutting down, CONTINUE + * otherwise. */ @Override public FileVisitResult visitFile(Path filePath, BasicFileAttributes attrs) { @@ -1285,6 +1174,10 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen } try { + /* + * Determine whether or not the file is a manifest file. If it + * is, then parse it. + */ Manifest manifest = null; for (ManifestFileParser parser : Lookup.getDefault().lookupAll(ManifestFileParser.class)) { if (parser.fileIsManifest(filePath)) { @@ -1304,125 +1197,111 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen return TERMINATE; } - if (null != manifest) { - /* - * Update the mapping of case names to manifest paths that - * is used for case deletion. - */ - String caseName = manifest.getCaseName(); - Path manifestPath = manifest.getFilePath(); - if (casesToManifests.containsKey(caseName)) { - Set manifestPaths = casesToManifests.get(caseName); - manifestPaths.add(manifestPath); - } else { - Set manifestPaths = new HashSet<>(); - manifestPaths.add(manifestPath); - casesToManifests.put(caseName, manifestPaths); - } - - /* - * Add a job to the pending jobs queue, the completed jobs - * list, or do crashed job recovery, as required. - */ - try { - byte[] rawData = coordinationService.getNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestPath.toString()); - if (null != rawData && rawData.length > 0) { - try { - AutoIngestJobNodeData nodeData = new AutoIngestJobNodeData(rawData); - AutoIngestJob.ProcessingStatus processingStatus = nodeData.getProcessingStatus(); - switch (processingStatus) { - case PENDING: - addPendingJob(manifest, nodeData); - break; - case PROCESSING: - doRecoveryIfCrashed(manifest, nodeData); - break; - case COMPLETED: - addCompletedJob(manifest, nodeData); - break; - case DELETED: - /* - * Ignore jobs marked as "deleted." - */ - break; - default: - sysLogger.log(Level.SEVERE, "Unknown ManifestNodeData.ProcessingStatus"); - break; - } - } catch (AutoIngestJobNodeData.InvalidDataException | AutoIngestJobException ex) { - sysLogger.log(Level.SEVERE, String.format("Invalid auto ingest job node data for %s", manifestPath), ex); - } - } else { - try { - addNewPendingJob(manifest); - } catch (AutoIngestJobException ex) { - sysLogger.log(Level.SEVERE, String.format("Invalid manifest data for %s", manifestPath), ex); - } - } - } catch (CoordinationServiceException ex) { - sysLogger.log(Level.SEVERE, String.format("Error transmitting node data for %s", manifestPath), ex); - return CONTINUE; - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - return TERMINATE; + if (manifest == null) { + return CONTINUE; + } + /* + * If a manifest file has been found, get the corresponding auto + * ingest job state from the manifest file coordination service + * node and put the job in the appropriate jobs list. + * + * There can be a race condition between queuing jobs and case + * deletion. However, in practice eliminating the race condition + * by acquiring a manifest file coordination service lock when + * analyzing job state here appears to have a significant + * performance cost for both input directory scanning and + * dequeuing jobs. Therefore, job state must be checked again + * during job dequeuing, while actually holding the lock, before + * executing the job. + */ + String manifestFilePath = manifest.getFilePath().toString(); + byte[] rawData = coordinationService.getNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestFilePath); + if (null != rawData && rawData.length > 0) { + AutoIngestJobNodeData nodeData = new AutoIngestJobNodeData(rawData); + AutoIngestJob.ProcessingStatus processingStatus = nodeData.getProcessingStatus(); + switch (processingStatus) { + case PENDING: + addPendingJob(manifest, nodeData); + break; + case PROCESSING: + doRecoveryIfCrashed(manifest, nodeData); + break; + case COMPLETED: + addCompletedJob(manifest, nodeData); + break; + case DELETED: + break; + default: + sysLogger.log(Level.SEVERE, "Unknown ManifestNodeData.ProcessingStatus"); + break; } + } else { + addNewPendingJob(manifest); } + } catch (CoordinationServiceException | AutoIngestJobException | AutoIngestJobNodeData.InvalidDataException ex) { + sysLogger.log(Level.SEVERE, String.format("Error visiting %s", filePath), ex); + + } catch (InterruptedException ex) { + return TERMINATE; + } catch (Exception ex) { - // Catch all unhandled and unexpected exceptions. Otherwise one bad file - // can stop the entire input folder scanning. Given that the exception is unexpected, - // I'm hesitant to add logging which requires accessing or de-referencing data. - sysLogger.log(Level.SEVERE, "Unexpected exception in file visitor", ex); - return CONTINUE; + /* + * This is an exception firewall so that an unexpected runtime + * exception from the handling of a single file does not stop + * the input directory scan. + */ + sysLogger.log(Level.SEVERE, String.format("Unexpected exception visiting %s", filePath), ex); } if (!Thread.currentThread().isInterrupted()) { return CONTINUE; } else { + sysLogger.log(Level.WARNING, String.format("Auto ingest shut down while visiting %s", filePath)); return TERMINATE; } } /** - * Adds an existing job to the pending jobs queue. + * Adds an existing auto ingest job to the pending jobs queue. If the + * version of the coordination service node data is out of date, it is + * upgraded to the current version. * * @param manifest The manifest for the job. - * @param nodeData The data stored in the coordination service node for - * the job. + * @param nodeData The data stored in the manifest file coordination + * service node for the job. * - * @throws InterruptedException if the thread running the input - * directory scan task is interrupted while - * blocked, i.e., if auto ingest is - * shutting down. + * @throws AutoIngestJobException If there was an error working with the + * node data. + * @throws InterruptedException If the thread running the input + * directory scan task is interrupted + * while blocked, i.e., if auto ingest is + * shutting down. */ - private void addPendingJob(Manifest manifest, AutoIngestJobNodeData nodeData) throws InterruptedException, AutoIngestJobException { + private void addPendingJob(Manifest manifest, AutoIngestJobNodeData nodeData) throws AutoIngestJobException, InterruptedException { AutoIngestJob job; if (nodeData.getVersion() == AutoIngestJobNodeData.getCurrentVersion()) { job = new AutoIngestJob(nodeData); } else { + /* + * Upgrade the auto ingest node data to the current version. + */ job = new AutoIngestJob(manifest); - job.setPriority(nodeData.getPriority()); // Retain priority, present in all versions of the node data. + job.setPriority(nodeData.getPriority()); Path caseDirectory = PathUtils.findCaseDirectory(rootOutputDirectory, manifest.getCaseName()); if (null != caseDirectory) { job.setCaseDirectoryPath(caseDirectory); } /* - * Try to upgrade/update the coordination service manifest node - * data for the job. - * - * An exclusive lock is obtained before doing so because another - * host may have already found the job, obtained an exclusive - * lock, and started processing it. However, this locking does - * make it possible that two processing hosts will both try to - * obtain the lock to do the upgrade operation at the same time. - * If this happens, the host that is holding the lock will - * complete the upgrade operation, so there is nothing more for - * this host to do. + * Try to write the upgraded node data to coordination service + * manifest node data for the job. If the lock cannot be + * obtained, assume that the auto ingest node holding the lock + * is taking care of this. */ try (Lock manifestLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.MANIFESTS, manifest.getFilePath().toString())) { if (null != manifestLock) { - updateCoordinationServiceManifestNode(job); + updateAutoIngestJobData(job); } } catch (CoordinationServiceException ex) { sysLogger.log(Level.SEVERE, String.format("Error attempting to set node data for %s", manifest.getFilePath()), ex); @@ -1432,154 +1311,145 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen } /** - * Adds a new job to the pending jobs queue. + * Adds a new auto ingest job to the pending jobs queue. * * @param manifest The manifest for the job. * - * @throws InterruptedException if the thread running the input - * directory scan task is interrupted while - * blocked, i.e., if auto ingest is - * shutting down. + * @throws AutoIngestJobException If there was an error creating + * the node data. + * @throws CoordinationServiceException If there was an error writing + * the node data by the + * coordination service. + * @throws InterruptedException If the thread running the input + * directory scan task is + * interrupted while blocked, i.e., + * if auto ingest is shutting down. */ - private void addNewPendingJob(Manifest manifest) throws InterruptedException, AutoIngestJobException { + private void addNewPendingJob(Manifest manifest) throws AutoIngestJobException, CoordinationServiceException, InterruptedException { /* - * Create the coordination service manifest node data for the job. - * Note that getting the lock will create the node for the job (with - * no data) if it does not already exist. - * - * An exclusive lock is obtained before creating the node data - * because another host may have already found the job, obtained an - * exclusive lock, and started processing it. However, this locking - * does make it possible that two hosts will both try to obtain the - * lock to do the create operation at the same time. If this - * happens, the host that is locked out will not add the job to its - * pending queue for this scan of the input directory, but it will - * be picked up on the next scan. + * Create the coordination service manifest file node data for the + * job. Getting the lock both guards the writing of the new node + * data and creates the coordination service node if it does not + * already exist. Note that if this auto ingest node cannot get the + * lock, it is assumed that the auto ingest node holding the lock is + * taking care of this. In this case, this auto ingest node will not + * add the new job to its pending queue during this scan of the + * input directory, but it will be picked up during the next scan. */ try (Lock manifestLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.MANIFESTS, manifest.getFilePath().toString())) { if (null != manifestLock) { AutoIngestJob job = new AutoIngestJob(manifest); - updateCoordinationServiceManifestNode(job); + updateAutoIngestJobData(job); newPendingJobsList.add(job); } - } catch (CoordinationServiceException ex) { - sysLogger.log(Level.SEVERE, String.format("Error attempting to set node data for %s", manifest.getFilePath()), ex); } } /** - * Does crash recovery for a manifest, if required. The criterion for - * crash recovery is a manifest with coordination service node data - * indicating it is being processed for which an exclusive lock on the - * node can be acquired. If this condition is true, it is probable that - * the node that was processing the job crashed and the processing - * status was not updated. + * If required, does recovery for an auto ingest job that was left in + * the processing state by an auto ingest node (AIN) that crashed. * - * @param manifest The manifest for upgrading the node. - * @param jobNodeData The auto ingest job node data. + * @param manifest The manifest for the job. + * @param nodeData The data stored in the manifest file lock + * coordination service node for the job. * - * @throws InterruptedException if the thread running the input - * directory scan task is interrupted - * while blocked, i.e., if auto ingest is - * shutting down. - * @throws AutoIngestJobException if there is an issue creating a new - * AutoIngestJob object. + * @throws AutoIngestJobException If there was an error working + * with the node data. + * @throws CoordinationServiceException If there was an error writing + * updated node data by the + * coordination service. + * @throws InterruptedException If the thread running the input + * directory scan task is + * interrupted while blocked, i.e., + * if auto ingest is shutting down. */ - private void doRecoveryIfCrashed(Manifest manifest, AutoIngestJobNodeData jobNodeData) throws InterruptedException, AutoIngestJobException { - /* - * Try to get an exclusive lock on the coordination service node for - * the job. If the lock cannot be obtained, another host in the auto - * ingest cluster is already doing the recovery, so there is nothing - * to do. - */ + private void doRecoveryIfCrashed(Manifest manifest, AutoIngestJobNodeData jobNodeData) throws AutoIngestJobException, CoordinationServiceException, InterruptedException { String manifestPath = manifest.getFilePath().toString(); try (Lock manifestLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.MANIFESTS, manifestPath)) { if (null != manifestLock) { - sysLogger.log(Level.SEVERE, "Attempting crash recovery for {0}", manifestPath); - Path caseDirectoryPath = PathUtils.findCaseDirectory(rootOutputDirectory, manifest.getCaseName()); - - /* - * Create the recovery job. - */ AutoIngestJob job = new AutoIngestJob(jobNodeData); - int numberOfCrashes = job.getNumberOfCrashes(); - if (numberOfCrashes <= AutoIngestUserPreferences.getMaxNumTimesToProcessImage()) { + if (job.getProcessingStatus() == AutoIngestJob.ProcessingStatus.PROCESSING) { + /* + * If the lock can be obtained with the job status set + * to processing, then an auto ingest node crashed while + * executing the job and was unable to update the job + * status. + */ + sysLogger.log(Level.SEVERE, "Attempting crash recovery for {0}", manifestPath); + + /* + * First, try to set the case node data error flag that + * indicates there was an auto ingest job error. If the + * auto ingest node that was executing the job crashed + * before the case directory was created, the job was a + * no-op, so the error flag does not need to be set. + * However, note that if another auto ingest job + * subsequently completed, the failed job may still have + * been a no-op, but in this case the flag will be set + * anyway, because a case directory will be found. + */ + Path caseDirectoryPath = PathUtils.findCaseDirectory(rootOutputDirectory, manifest.getCaseName()); + if (null != caseDirectoryPath) { + job.setCaseDirectoryPath(caseDirectoryPath); + job.setErrorsOccurred(true); + setErrorsOccurredFlagForCase(caseDirectoryPath); + } else { + job.setErrorsOccurred(false); + } + + /* + * Update the crash count for the job, determine whether + * or not to retry processing its data source, and deal + * with the job accordingly. + */ + int numberOfCrashes = job.getNumberOfCrashes(); ++numberOfCrashes; job.setNumberOfCrashes(numberOfCrashes); - if (numberOfCrashes <= AutoIngestUserPreferences.getMaxNumTimesToProcessImage()) { + if (numberOfCrashes < AutoIngestUserPreferences.getMaxNumTimesToProcessImage()) { + job.setProcessingStatus(AutoIngestJob.ProcessingStatus.PENDING); job.setCompletedDate(new Date(0)); + if (null != caseDirectoryPath) { + try { + new AutoIngestJobLogger(manifest.getFilePath(), manifest.getDataSourceFileName(), caseDirectoryPath).logCrashRecoveryWithRetry(); + } catch (AutoIngestJobLoggerException ex) { + sysLogger.log(Level.SEVERE, String.format("Error writing case auto ingest log entry for crashed job for %s", manifestPath), ex); + } + } + updateAutoIngestJobData(job); + newPendingJobsList.add(job); } else { + job.setProcessingStatus(AutoIngestJob.ProcessingStatus.COMPLETED); job.setCompletedDate(Date.from(Instant.now())); - } - } - - if (null != caseDirectoryPath) { - job.setCaseDirectoryPath(caseDirectoryPath); - job.setErrorsOccurred(true); - try { - setCaseNodeDataErrorsOccurred(caseDirectoryPath); - } catch (IOException ex) { - sysLogger.log(Level.SEVERE, String.format("Error attempting to set error flag in case node data for %s", caseDirectoryPath), ex); - } - } else { - job.setErrorsOccurred(false); - } - - if (numberOfCrashes <= AutoIngestUserPreferences.getMaxNumTimesToProcessImage()) { - job.setProcessingStatus(AutoIngestJob.ProcessingStatus.PENDING); - if (null != caseDirectoryPath) { - try { - new AutoIngestJobLogger(manifest.getFilePath(), manifest.getDataSourceFileName(), caseDirectoryPath).logCrashRecoveryWithRetry(); - } catch (AutoIngestJobLoggerException ex) { - sysLogger.log(Level.SEVERE, String.format("Error creating case auto ingest log entry for crashed job for %s", manifestPath), ex); + if (null != caseDirectoryPath) { + try { + new AutoIngestJobLogger(manifest.getFilePath(), manifest.getDataSourceFileName(), caseDirectoryPath).logCrashRecoveryNoRetry(); + } catch (AutoIngestJobLoggerException ex) { + sysLogger.log(Level.SEVERE, String.format("Error writing case auto ingest log entry for crashed job for %s", manifestPath), ex); + } } + updateAutoIngestJobData(job); + newCompletedJobsList.add(job); } - } else { - job.setProcessingStatus(AutoIngestJob.ProcessingStatus.COMPLETED); - if (null != caseDirectoryPath) { - try { - new AutoIngestJobLogger(manifest.getFilePath(), manifest.getDataSourceFileName(), caseDirectoryPath).logCrashRecoveryNoRetry(); - } catch (AutoIngestJobLoggerException ex) { - sysLogger.log(Level.SEVERE, String.format("Error creating case auto ingest log entry for crashed job for %s", manifestPath), ex); - } - } - } - - /* - * Update the coordination service node for the job. If this - * fails, leave the recovery to another host. - */ - try { - updateCoordinationServiceManifestNode(job); - } catch (CoordinationServiceException ex) { - sysLogger.log(Level.SEVERE, String.format("Error attempting to set node data for %s", manifestPath), ex); - return; - } - - jobNodeData = new AutoIngestJobNodeData(job); - - if (numberOfCrashes <= AutoIngestUserPreferences.getMaxNumTimesToProcessImage()) { - newPendingJobsList.add(job); - } else { - newCompletedJobsList.add(new AutoIngestJob(jobNodeData)); } } - } catch (CoordinationServiceException ex) { - sysLogger.log(Level.SEVERE, String.format("Error attempting to get exclusive lock for %s", manifestPath), ex); } } /** * Adds a job to process a manifest to the completed jobs list. * - * @param nodeData The data stored in the coordination service node for - * the manifest. - * @param manifest The manifest for upgrading the node. + * @param manifest The manifest for the job. + * @param nodeData The data stored in the manifest file lock + * coordination service node for the job. * - * @throws CoordinationServiceException - * @throws InterruptedException + * @throws AutoIngestJobException If there was an error working with the + * node data. + * @throws InterruptedException If the thread running the input + * directory scan task is interrupted + * while blocked, i.e., if auto ingest is + * shutting down. */ - private void addCompletedJob(Manifest manifest, AutoIngestJobNodeData nodeData) throws CoordinationServiceException, InterruptedException, AutoIngestJobException { + private void addCompletedJob(Manifest manifest, AutoIngestJobNodeData nodeData) throws AutoIngestJobException, InterruptedException { Path caseDirectoryPath = nodeData.getCaseDirectoryPath(); if (!caseDirectoryPath.toFile().exists()) { sysLogger.log(Level.WARNING, String.format("Job completed for %s, but cannot find case directory %s, ignoring job", nodeData.getManifestFilePath(), caseDirectoryPath.toString())); @@ -1591,19 +1461,11 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen job = new AutoIngestJob(nodeData); job.setCaseDirectoryPath(caseDirectoryPath); } else { - /** - * Use the manifest rather than the node data here to create a - * new AutoIngestJob instance because the AutoIngestJob - * constructor that takes a node data object expects the node - * data to have fields that do not exist in earlier versions. + /* + * Upgrade the auto ingest node data to the current version. */ job = new AutoIngestJob(manifest); job.setCaseDirectoryPath(caseDirectoryPath); - - /** - * Update the job with the fields that exist in all versions of - * the nodeData. - */ job.setCompletedDate(nodeData.getCompletedDate()); job.setErrorsOccurred(nodeData.getErrorsOccurred()); job.setPriority(nodeData.getPriority()); @@ -1612,20 +1474,20 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen job.setProcessingStatus(AutoIngestJob.ProcessingStatus.COMPLETED); /* - * Try to upgrade/update the coordination service manifest node - * data for the job. It is possible that two hosts will both try - * to obtain the lock to do the upgrade operation at the same - * time. If this happens, the host that is holding the lock will - * complete the upgrade operation. + * Try to write the upgraded node data to coordination service + * manifest node data for the job. If the lock cannot be + * obtained, assume that the auto ingest node holding the lock + * is taking care of this. */ try (Lock manifestLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.MANIFESTS, manifest.getFilePath().toString())) { if (null != manifestLock) { - updateCoordinationServiceManifestNode(job); + updateAutoIngestJobData(job); } } catch (CoordinationServiceException ex) { sysLogger.log(Level.SEVERE, String.format("Error attempting to set node data for %s", manifest.getFilePath()), ex); } } + newCompletedJobsList.add(job); } @@ -1654,17 +1516,17 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen } /** - * Invoked for an input directory after entries in the directory are + * Invoked for an input directory after the files in the directory are * visited. Checks if the task thread has been interrupted because auto * ingest is shutting down and terminates the scan if that is the case. * * @param dirPath The directory about to be visited. * @param unused Unused. * - * @return TERMINATE if the task thread has been interrupted, CONTINUE - * if it has not. + * @return FileVisitResult.TERMINATE if the task thread has been + * interrupted, FileVisitResult.CONTINUE if it has not. * - * @throws IOException if an I/O error occurs, but this implementation + * @throws IOException If an I/O error occurs, but this implementation * does not throw. */ @Override @@ -2121,17 +1983,40 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen } try { + /* + * There can be a race condition between queuing jobs + * and case deletion. However, in practice eliminating + * the race condition by acquiring a manifest file + * coordination service lock when analyzing job state + * during the input directory scan appears to have a + * significant performance cost for both input directory + * scanning and dequeuing jobs. Therefore, job state + * must be checked again here, while actually holding + * the lock, before executing the job. + */ AutoIngestJobNodeData nodeData = new AutoIngestJobNodeData(coordinationService.getNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestPath.toString())); if (!nodeData.getProcessingStatus().equals(PENDING)) { - /* - * Due to a timing issue or a missed event, a - * non-pending job has ended up on the pending - * queue. Skip the job and remove it from the queue. - */ iterator.remove(); + manifestLock.release(); + manifestLock = null; continue; } + /* + * Ditto for the presence of the manifest file. + */ + File manifestFile = nodeData.getManifestFilePath().toFile(); + if (!manifestFile.exists()) { + iterator.remove(); + manifestLock.release(); + manifestLock = null; + continue; + } + + /* + * Finally, check for devoting too many resources to a + * single case, if the check is enabled. + */ if (enforceMaxJobsPerCase) { int currentJobsForCase = 0; for (AutoIngestJob runningJob : hostNamesToRunningJobs.values()) { @@ -2145,11 +2030,13 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen continue; } } + iterator.remove(); currentJob = job; break; + } catch (AutoIngestJobNodeData.InvalidDataException ex) { - sysLogger.log(Level.WARNING, String.format("Unable to use node data for %s", manifestPath), ex); // JCTODO: Is this right? + sysLogger.log(Level.WARNING, String.format("Unable to use node data for %s", manifestPath), ex); } } } @@ -2220,7 +2107,7 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen currentJob.setProcessingStatus(AutoIngestJob.ProcessingStatus.PROCESSING); currentJob.setProcessingStage(AutoIngestJob.Stage.STARTING, Date.from(Instant.now())); currentJob.setProcessingHostName(AutoIngestManager.LOCAL_HOST_NAME); - updateCoordinationServiceManifestNode(currentJob); + updateAutoIngestJobData(currentJob); setChanged(); notifyObservers(Event.JOB_STARTED); eventPublisher.publishRemotely(new AutoIngestJobStartedEvent(currentJob)); @@ -2244,14 +2131,14 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen currentJob.setProcessingStatus(AutoIngestJob.ProcessingStatus.PENDING); } currentJob.setProcessingHostName(""); - updateCoordinationServiceManifestNode(currentJob); + updateAutoIngestJobData(currentJob); boolean retry = (!currentJob.isCanceled() && !currentJob.isCompleted()); sysLogger.log(Level.INFO, "Completed processing of {0}, retry = {1}", new Object[]{manifestPath, retry}); if (currentJob.isCanceled()) { Path caseDirectoryPath = currentJob.getCaseDirectoryPath(); if (null != caseDirectoryPath) { - setCaseNodeDataErrorsOccurred(caseDirectoryPath); + setErrorsOccurredFlagForCase(caseDirectoryPath); AutoIngestJobLogger jobLogger = new AutoIngestJobLogger(manifestPath, currentJob.getManifest().getDataSourceFileName(), caseDirectoryPath); jobLogger.logJobCancelled(); } @@ -2410,9 +2297,9 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen sysLogger.log(Level.INFO, "Opening case {0} for {1}", new Object[]{caseName, manifest.getFilePath()}); currentJob.setProcessingStage(AutoIngestJob.Stage.OPENING_CASE, Date.from(Instant.now())); /* - * Acquire and hold a case name lock so that only one node at as - * time can scan the output directory at a time. This prevents - * making duplicate cases for the saem auto ingest case. + * Acquire and hold a case name lock so that only one node at a time + * can search the output directory for an existing case. This + * prevents making duplicate cases for the same auto ingest case. */ try (Lock caseLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.CASES, caseName, 30, TimeUnit.MINUTES)) { if (null != caseLock) { @@ -2440,13 +2327,16 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen Thread.sleep(AutoIngestUserPreferences.getSecondsToSleepBetweenCases() * 1000); } currentJob.setCaseDirectoryPath(caseDirectoryPath); - updateCoordinationServiceManifestNode(currentJob); // update case directory path + updateAutoIngestJobData(currentJob); + recordManifest(caseDirectoryPath, manifest.getFilePath()); Case caseForJob = Case.getCurrentCase(); sysLogger.log(Level.INFO, "Opened case {0} for {1}", new Object[]{caseForJob.getName(), manifest.getFilePath()}); return caseForJob; } catch (KeywordSearchModuleException ex) { throw new CaseManagementException(String.format("Error creating solr settings file for case %s for %s", caseName, manifest.getFilePath()), ex); + } catch (IOException ex) { + throw new CaseManagementException(String.format("Error recording manifest file path for case %s for %s", caseName, manifest.getFilePath()), ex); } catch (CaseActionException ex) { throw new CaseManagementException(String.format("Error creating or opening case %s for %s", caseName, manifest.getFilePath()), ex); } @@ -2456,6 +2346,22 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen } } + /** + * Writes the path of the manifest file for the current job to a list of + * manifest file paths for the case in file in the case directory. + * + * @param caseDirectoryPath The case directory path. + * + * @throws IOException If the file cannot be created or opened and + * updated. + */ + private void recordManifest(Path caseDirectoryPath, Path manifestFilePath) throws IOException { + final Path manifestsListFilePath = Paths.get(caseDirectoryPath.toString(), AutoIngestManager.getCaseManifestsListFileName()); + try (FileWriter fileWriter = new FileWriter(manifestsListFilePath.toString(), true)) { + fileWriter.write(manifestFilePath.toString() + "\n"); + } + } + /** * Runs the ingest process for the current job. * @@ -2590,7 +2496,7 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen if (!dataSource.exists()) { sysLogger.log(Level.SEVERE, "Missing data source for {0}", manifestPath); currentJob.setErrorsOccurred(true); - setCaseNodeDataErrorsOccurred(caseDirectoryPath); + setErrorsOccurredFlagForCase(caseDirectoryPath); jobLogger.logMissingDataSource(); return null; } @@ -2635,7 +2541,7 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen // did we find a data source processor that can process the data source if (validDataSourceProcessors.isEmpty()) { // This should never happen. We should add all unsupported data sources as logical files. - setCaseNodeDataErrorsOccurred(caseDirectoryPath); + setErrorsOccurredFlagForCase(caseDirectoryPath); currentJob.setErrorsOccurred(true); jobLogger.logFailedToIdentifyDataSource(); sysLogger.log(Level.WARNING, "Unsupported data source {0} for {1}", new Object[]{dataSource.getPath(), manifestPath}); // NON-NLS @@ -2670,7 +2576,7 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen // If we get to this point, none of the processors were successful sysLogger.log(Level.SEVERE, "All data source processors failed to process {0}", dataSource.getPath()); jobLogger.logFailedToAddDataSource(); - setCaseNodeDataErrorsOccurred(caseDirectoryPath); + setErrorsOccurredFlagForCase(caseDirectoryPath); currentJob.setErrorsOccurred(true); // Throw an exception. It will get caught & handled upstream and will result in AIM auto-pause. throw new AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException("Failed to process " + dataSource.getPath() + " with all data source processors"); @@ -2789,7 +2695,7 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen if (!cancelledModules.isEmpty()) { sysLogger.log(Level.WARNING, String.format("Ingest module(s) cancelled for %s", manifestPath)); currentJob.setErrorsOccurred(true); - setCaseNodeDataErrorsOccurred(caseDirectoryPath); + setErrorsOccurredFlagForCase(caseDirectoryPath); for (String module : snapshot.getCancelledDataSourceIngestModules()) { sysLogger.log(Level.WARNING, String.format("%s ingest module cancelled for %s", module, manifestPath)); nestedJobLogger.logIngestModuleCancelled(module); @@ -2799,7 +2705,7 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen } else { currentJob.setProcessingStage(AutoIngestJob.Stage.CANCELLING, Date.from(Instant.now())); currentJob.setErrorsOccurred(true); - setCaseNodeDataErrorsOccurred(caseDirectoryPath); + setErrorsOccurredFlagForCase(caseDirectoryPath); nestedJobLogger.logAnalysisCancelled(); CancellationReason cancellationReason = snapshot.getCancellationReason(); if (CancellationReason.NOT_CANCELLED != cancellationReason && CancellationReason.USER_CANCELLED != cancellationReason) { @@ -2812,13 +2718,13 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen sysLogger.log(Level.SEVERE, String.format("%s ingest module startup error for %s", error.getModuleDisplayName(), manifestPath), error.getThrowable()); } currentJob.setErrorsOccurred(true); - setCaseNodeDataErrorsOccurred(caseDirectoryPath); + setErrorsOccurredFlagForCase(caseDirectoryPath); jobLogger.logIngestModuleStartupErrors(); throw new AnalysisStartupException(String.format("Error(s) during ingest module startup for %s", manifestPath)); } else { sysLogger.log(Level.SEVERE, String.format("Ingest manager ingest job start error for %s", manifestPath), ingestJobStartResult.getStartupException()); currentJob.setErrorsOccurred(true); - setCaseNodeDataErrorsOccurred(caseDirectoryPath); + setErrorsOccurredFlagForCase(caseDirectoryPath); jobLogger.logAnalysisStartupError(); throw new AnalysisStartupException("Ingest manager error starting job", ingestJobStartResult.getStartupException()); } @@ -2827,7 +2733,7 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen sysLogger.log(Level.SEVERE, "Ingest job settings error for {0}: {1}", new Object[]{manifestPath, warning}); } currentJob.setErrorsOccurred(true); - setCaseNodeDataErrorsOccurred(caseDirectoryPath); + setErrorsOccurredFlagForCase(caseDirectoryPath); jobLogger.logIngestJobSettingsErrors(); throw new AnalysisStartupException("Error(s) in ingest job settings"); } @@ -2910,7 +2816,7 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen } catch (FileExportException ex) { sysLogger.log(Level.SEVERE, String.format("Error doing file export for %s", manifestPath), ex); currentJob.setErrorsOccurred(true); - setCaseNodeDataErrorsOccurred(caseDirectoryPath); + setErrorsOccurredFlagForCase(caseDirectoryPath); jobLogger.logFileExportError(); } } @@ -3096,7 +3002,7 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen currentJob.setModuleRuntimesSnapshot(IngestManager.getInstance().getModuleRunTimes()); setChanged(); notifyObservers(Event.JOB_STATUS_UPDATED); - updateCoordinationServiceManifestNode(currentJob); + updateAutoIngestJobData(currentJob); eventPublisher.publishRemotely(new AutoIngestJobStatusEvent(currentJob)); } } @@ -3256,12 +3162,6 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen } - enum CaseDeletionResult { - FAILED, - PARTIALLY_DELETED, - FULLY_DELETED - } - static final class AutoIngestManagerException extends Exception { private static final long serialVersionUID = 1L; diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestMetricsCollector.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestMetricsCollector.java index 7b07a15aec..3192e55b7a 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestMetricsCollector.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestMetricsCollector.java @@ -72,13 +72,13 @@ final class AutoIngestMetricsCollector { switch (processingStatus) { case PENDING: case PROCESSING: - case DELETED: /* * These are not jobs we care about for metrics, so * we will ignore them. */ break; case COMPLETED: + case DELETED: // Assuming deleted jobs were completed before they were deleted. newMetricsSnapshot.addCompletedJobMetric(job.getCompletedDate(), job.getDataSourceSize()); break; default: @@ -96,7 +96,7 @@ final class AutoIngestMetricsCollector { return newMetricsSnapshot; - } catch (CoordinationService.CoordinationServiceException ex) { + } catch (CoordinationService.CoordinationServiceException | InterruptedException ex) { LOGGER.log(Level.SEVERE, "Failed to get node list from coordination service", ex); return new MetricsSnapshot(); } diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestMonitor.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestMonitor.java index 8f7a9c0696..b3d3fb21a8 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestMonitor.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestMonitor.java @@ -21,7 +21,6 @@ package org.sleuthkit.autopsy.experimental.autoingest; import com.google.common.util.concurrent.ThreadFactoryBuilder; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; -import java.nio.file.Path; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; @@ -38,20 +37,15 @@ import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.stream.Collectors; import javax.annotation.concurrent.GuardedBy; -import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.casemodule.CaseActionException; -import org.sleuthkit.autopsy.casemodule.CaseMetadata; import org.sleuthkit.autopsy.coordinationservice.CoordinationService; import org.sleuthkit.autopsy.coordinationservice.CoordinationService.CoordinationServiceException; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.NetworkUtils; -import org.sleuthkit.autopsy.coreutils.StopWatch; import org.sleuthkit.autopsy.events.AutopsyEventException; import org.sleuthkit.autopsy.events.AutopsyEventPublisher; import org.sleuthkit.autopsy.experimental.autoingest.AutoIngestJob.ProcessingStatus; import static org.sleuthkit.autopsy.experimental.autoingest.AutoIngestJob.ProcessingStatus.DELETED; import static org.sleuthkit.autopsy.experimental.autoingest.AutoIngestJob.ProcessingStatus.PENDING; -import org.sleuthkit.autopsy.experimental.autoingest.AutoIngestManager.CaseDeletionResult; import org.sleuthkit.autopsy.experimental.autoingest.AutoIngestManager.Event; import org.sleuthkit.autopsy.experimental.autoingest.AutoIngestNodeControlEvent.ControlEventType; @@ -361,6 +355,9 @@ final class AutoIngestMonitor extends Observable implements PropertyChangeListen newJobsSnapshot.addOrReplaceCompletedJob(job); break; case DELETED: + /* + * Ignore jobs marked as deleted. + */ break; default: LOGGER.log(Level.SEVERE, "Unknown AutoIngestJobData.ProcessingStatus"); @@ -378,7 +375,7 @@ final class AutoIngestMonitor extends Observable implements PropertyChangeListen return newJobsSnapshot; - } catch (CoordinationServiceException ex) { + } catch (CoordinationServiceException | InterruptedException ex) { LOGGER.log(Level.SEVERE, "Failed to get node list from coordination service", ex); return new JobsSnapshot(); } @@ -659,88 +656,6 @@ final class AutoIngestMonitor extends Observable implements PropertyChangeListen } } - /** - * Deletes a case. This includes deleting the case directory, the text - * index, and the case database. This does not include the directories - * containing the data sources and their manifests. - * - * @param job The job whose case you want to delete - * - * @return A result code indicating success, partial success, or failure. - */ - CaseDeletionResult deleteCase(AutoIngestJob job) { - String caseName = job.getManifest().getCaseName(); - Path caseDirectoryPath = job.getCaseDirectoryPath(); - Path metadataFilePath = caseDirectoryPath.resolve(caseName + CaseMetadata.getFileExtension()); - StopWatch stopWatch = new StopWatch(); - stopWatch.start(); - synchronized (jobsLock) { - stopWatch.stop(); - LOGGER.log(Level.INFO, String.format("Used %d s to acquire jobsLock (Java monitor in AutoIngestMonitor class) for case %s at %s", stopWatch.getElapsedTimeSecs(), caseName, caseDirectoryPath)); - stopWatch.reset(); - stopWatch.start(); - try { - CaseMetadata metadata = new CaseMetadata(metadataFilePath); - stopWatch.stop(); - LOGGER.log(Level.INFO, String.format("Used %d s to read case metadata for case %s at %s", stopWatch.getElapsedTimeSecs(), caseName, caseDirectoryPath)); - stopWatch.reset(); - stopWatch.start(); - Case.deleteCase(metadata); - } catch (CaseMetadata.CaseMetadataException ex) { - LOGGER.log(Level.SEVERE, String.format("Failed to read case metadata file %s for case %s at %s", metadataFilePath, caseName, caseDirectoryPath), ex); - stopWatch.stop(); - LOGGER.log(Level.INFO, String.format("Used %d s to fail to read case metadata file %s for case %s at %s", stopWatch.getElapsedTimeSecs(), metadataFilePath, caseName, caseDirectoryPath)); - return CaseDeletionResult.FAILED; - } catch (CaseActionException ex) { - LOGGER.log(Level.SEVERE, String.format("Failed to delete case %s at %s", caseName, caseDirectoryPath), ex); - return CaseDeletionResult.FAILED; - } - - // Update the state of completed jobs associated with this case to indicate - // that the case has been deleted - stopWatch.reset(); - stopWatch.start(); - List completedJobs = getCompletedJobs(); - stopWatch.stop(); - LOGGER.log(Level.INFO, String.format("Used %d s to get completed jobs listing for case %s at %s", stopWatch.getElapsedTimeSecs(), caseName, caseDirectoryPath)); - stopWatch.reset(); - stopWatch.start(); - for (AutoIngestJob completedJob : completedJobs) { - if (caseName.equals(completedJob.getManifest().getCaseName())) { - try { - completedJob.setProcessingStatus(DELETED); - AutoIngestJobNodeData nodeData = new AutoIngestJobNodeData(completedJob); - coordinationService.setNodeData(CoordinationService.CategoryNode.MANIFESTS, completedJob.getManifest().getFilePath().toString(), nodeData.toArray()); - } catch (CoordinationServiceException | InterruptedException ex) { - LOGGER.log(Level.SEVERE, String.format("Failed to update completed job node data for %s when deleting case %s at %s", completedJob.getManifest().getFilePath(), caseName, caseDirectoryPath), ex); - stopWatch.stop(); - LOGGER.log(Level.INFO, String.format("Used %d s to fail to update job node data for completed jobs for case %s at %s", stopWatch.getElapsedTimeSecs(), caseName, caseDirectoryPath)); - return CaseDeletionResult.PARTIALLY_DELETED; - } - } - } - stopWatch.stop(); - LOGGER.log(Level.INFO, String.format("Used %d s to update job node data for completed jobs for case %s at %s", stopWatch.getElapsedTimeSecs(), caseName, caseDirectoryPath)); - - // Remove jobs associated with this case from the completed jobs collection. - stopWatch.reset(); - stopWatch.start(); - completedJobs.removeIf((AutoIngestJob completedJob) - -> completedJob.getManifest().getCaseName().equals(caseName)); - stopWatch.stop(); - LOGGER.log(Level.INFO, String.format("Used %d s to remove completed jobs for case %s at %s from current jobs snapshot", stopWatch.getElapsedTimeSecs(), caseName, caseDirectoryPath)); - - // Publish a message to update auto ingest nodes. - stopWatch.reset(); - stopWatch.start(); - eventPublisher.publishRemotely(new AutoIngestCaseDeletedEvent(caseName, LOCAL_HOST_NAME, AutoIngestManager.getSystemUserNameProperty())); - stopWatch.stop(); - LOGGER.log(Level.INFO, String.format("Used %d s to publish job deletion event for case %s at %s", stopWatch.getElapsedTimeSecs(), caseName, caseDirectoryPath)); - } - - return CaseDeletionResult.FULLY_DELETED; - } - /** * Send the given control event to the given node. * diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/BackgroundTaskAction.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/BackgroundTaskAction.java new file mode 100755 index 0000000000..1140b49cf1 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/BackgroundTaskAction.java @@ -0,0 +1,80 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019-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.experimental.autoingest; + +import java.awt.event.ActionEvent; +import java.util.concurrent.FutureTask; +import javax.swing.AbstractAction; +import org.sleuthkit.autopsy.progress.AppFrameProgressBar; +import org.sleuthkit.autopsy.progress.ProgressIndicator; +import org.sleuthkit.autopsy.progress.TaskCancellable; + +/** + * A base class for action classes that kick off a cancellable task that runs in + * a background thread and reports progress using an application frame progress + * bar. + */ +abstract class BackgroundTaskAction extends AbstractAction { + + private static final long serialVersionUID = 1L; + private final String progressDisplayName; + + /** + * Constructs the base class part of action classes that kick off a + * cancellable task that runs in a background thread and reports progress + * using an application frame progress bar. + * + * @param actionName The name of the action. + * @param progressDisplayName The display name for the progress bar. + */ + BackgroundTaskAction(String actionName, String progressDisplayName) { + super(actionName); + this.progressDisplayName = progressDisplayName; + } + + @Override + public void actionPerformed(ActionEvent event) { + final AppFrameProgressBar progress = new AppFrameProgressBar(progressDisplayName); + final TaskCancellable taskCanceller = new TaskCancellable(progress); + progress.setCancellationBehavior(taskCanceller); + final Runnable task = getTask(progress); + final FutureTask future = new FutureTask<>(task, null); + taskCanceller.setFuture(future); + new Thread(future).start(); + } + + /** + * Gets the background task to be executed. The task is expected to report + * its progress using the supplied progress indicator and to check for + * cancellation by checking to see if the thread it is running in has been + * interrupted. + * + * @param progress A progress indicator for the task. + * + * @return The Runnnable task. + */ + abstract Runnable getTask(ProgressIndicator progress); + + @Override + public BackgroundTaskAction clone() throws CloneNotSupportedException { + super.clone(); + throw new CloneNotSupportedException(); + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties index 0f074ca11c..f3081bef89 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties @@ -228,7 +228,6 @@ AutoIngestControlPanel.bnShowProgress.text=Ingest Progress AutoIngestControlPanel.bnCancelJob.text=&Cancel Job AutoIngestControlPanel.bnCancelModule.text=Cancel &Module AutoIngestControlPanel.bnReprocessJob.text=Reprocess Job -AutoIngestControlPanel.bnDeleteCase.text=&Delete Case AutoIngestControlPanel.bnShowCaseLog.text=Show Case &Log AutoIngestControlPanel.bnPause.text=Pause AutoIngestControlPanel.bnRefresh.text=&Refresh @@ -255,3 +254,6 @@ AinStatusDashboard.clusterMetricsButton.text=Auto Ingest &Metrics AinStatusDashboard.nodeStatusTableTitle.text=Auto Ingest Nodes AinStatusDashboard.healthMonitorButton.text=Health Monitor CasesDashboardTopComponent.refreshButton.text=Refresh +AutoIngestCasesDeletionDialog.jLabel1.text=Progress +CasesDashboardTopComponent.deleteOrphanCaseNodesButton.text=Delete Orphan Case Znodes +CasesDashboardTopComponent.deleteOrphanManifestNodesButton.text=Delete Orphan Manifest Znodes diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties-MERGED b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties-MERGED index a7336e26a6..38d8ee47a1 100755 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties-MERGED +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties-MERGED @@ -10,8 +10,6 @@ AinStatusNode.status.title=Status AinStatusNode.status.unknown=Unknown AutoIngestAdminActions.cancelJobAction.title=Cancel Job AutoIngestAdminActions.cancelModuleAction.title=Cancel Module -AutoIngestAdminActions.deleteCaseAction.error=Failed to delete case. -AutoIngestAdminActions.deleteCaseAction.title=Delete Case AutoIngestAdminActions.pause.title=Pause Node AutoIngestAdminActions.progressDialogAction.title=Ingest Progress AutoIngestAdminActions.reprocessJobAction.error=Failed to reprocess job @@ -56,7 +54,6 @@ AutoIngestControlPanel.Cancelling=Cancelling... AutoIngestControlPanel.completedTable.toolTipText=The Completed table shows all Jobs that have been processed already AutoIngestControlPanel.ConfigLocked=The shared configuration directory is locked because upload from another node is in progress. \nIf this is an error, you can unlock the directory and then retry the upload. AutoIngestControlPanel.ConfigLockedTitle=Configuration directory locked -AutoIngestControlPanel.DeletionFailed=Deletion failed for job AutoIngestControlPanel.EnableConfigurationSettings=Enable shared configuration from the options panel before uploading AutoIngestControlPanel.errorMessage.caseDeprioritization=An error occurred when deprioritizing the case. Some or all jobs may not have been deprioritized. AutoIngestControlPanel.errorMessage.casePrioritization=An error occurred when prioritizing the case. Some or all jobs may not have been prioritized. @@ -167,9 +164,58 @@ CTL_AutoIngestDashboardOpenAction=Auto Ingest Dashboard CTL_AutoIngestDashboardTopComponent=Auto Ingest Jobs CTL_CasesDashboardAction=Multi-User Cases Dashboard CTL_CasesDashboardTopComponent=Cases -DeleteCaseInputDirectoriesAction.menuItemText=Delete Input Directories -DeleteCasesAction.menuItemText=Delete Case and Jobs -DeleteCasesForReprocessingAction.menuItemText=Delete for Reprocessing +DeleteCaseAction.confirmationText=Are you sure you want to delete the following for the case(s):\n\tManifest file znodes\n\tCase database\n\tCore.properties file\n\tCase directory\n\tCase znodes +DeleteCaseAction.menuItemText=Delete Case(s) +DeleteCaseAction.progressDisplayName=Delete Case(s) +DeleteCaseAction.taskName=app-input-and-output +DeleteCaseInputAction.confirmationText=Are you sure you want to delete the following for the case(s):\n\tManifest files\n\tData sources\n +DeleteCaseInputAction.menuItemText=Delete Input +DeleteCaseInputAction.progressDisplayName=Delete Input +DeleteCaseInputAction.taskName=input +DeleteCaseInputAndOutputAction.confirmationText=Are you sure you want to delete the following for the case(s):\n\tManifest files\n\tData sources\n\tManifest file znodes\n\tCase database\n\tCore.properties file\n\tCase directory\n\tCase znodes +DeleteCaseInputAndOutputAction.menuItemText=Delete Input and Output +DeleteCaseInputAndOutputAction.progressDisplayName=Delete Input and Output +DeleteCaseInputAndOutputAction.taskName=input-and-output +DeleteCaseOutputAction.confirmationText=Are you sure you want to delete the following for the case(s):\n\tManifest file znodes\n\tCase database\n\tCore.properties file\n\tCase directory\n\tCase znodes +DeleteCaseOutputAction.menuItemText=Delete Output +DeleteCaseOutputAction.progressDisplayName=Delete Output +DeleteCaseOutputAction.taskName=output +DeleteCaseTask.progress.acquiringCaseDirLock=Acquiring exclusive case directory lock... +DeleteCaseTask.progress.acquiringCaseNameLock=Acquiring exclusive case name lock... +DeleteCaseTask.progress.acquiringManifestLocks=Acquiring exclusive manifest file locks... +DeleteCaseTask.progress.connectingToCoordSvc=Connecting to the coordination service... +DeleteCaseTask.progress.deletingCaseDirCoordSvcNode=Deleting case directory znode... +DeleteCaseTask.progress.deletingCaseNameCoordSvcNode=Deleting case name znode... +# {0} - data source path +DeleteCaseTask.progress.deletingDataSource=Deleting data source {0}... +DeleteCaseTask.progress.deletingJobLogLockNode=Deleting case auto ingest log znode... +# {0} - manifest file path +DeleteCaseTask.progress.deletingManifest=Deleting manifest file {0}... +# {0} - manifest file path +DeleteCaseTask.progress.deletingManifestFileNode=Deleting the manifest file znode for {0}... +DeleteCaseTask.progress.deletingResourcesLockNode=Deleting case resources znode... +DeleteCaseTask.progress.gettingManifestPaths=Getting manifest file paths... +# {0} - manifest file path +DeleteCaseTask.progress.lockingManifest=Locking manifest file {0}... +DeleteCaseTask.progress.openingCaseDatabase=Opening the case database... +DeleteCaseTask.progress.openingCaseMetadataFile=Opening case metadata file... +# {0} - manifest file path +DeleteCaseTask.progress.parsingManifest=Parsing manifest file {0}... +# {0} - manifest file path +DeleteCaseTask.progress.releasingManifestLock=Releasing lock on the manifest file {0}... +DeleteCaseTask.progress.startMessage=Starting deletion... +DeleteOrphanCaseNodesAction.progressDisplayName=Cleanup Case Znodes +DeleteOrphanCaseNodesTask.progress.connectingToCoordSvc=Connecting to the coordination service +# {0} - node path +DeleteOrphanCaseNodesTask.progress.deletingOrphanedCaseNode=Deleting orphaned case znode {0} +DeleteOrphanCaseNodesTask.progress.gettingCaseNodesListing=Querying coordination service for case znodes +DeleteOrphanCaseNodesTask.progress.startMessage=Starting orphaned case znode cleanup +DeleteOrphanManifestNodesAction.progressDisplayName=Cleanup Manifest File Znodes +DeleteOrphanManifestNodesTask.progress.connectingToCoordSvc=Connecting to the coordination service +# {0} - node path +DeleteOrphanManifestNodesTask.progress.deletingOrphanedManifestNode=Deleting orphaned manifest file znode {0} +DeleteOrphanManifestNodesTask.progress.gettingManifestNodes=Querying the coordination service for manifest file znodes +DeleteOrphanManifestNodesTask.progress.startMessage=Starting orphaned manifest file znode cleanup HINT_CasesDashboardTopComponent=This is an adminstrative dashboard for multi-user cases OpenAutoIngestLogAction.deletedLogErrorMsg=The case auto ingest log has been deleted. OpenAutoIngestLogAction.logOpenFailedErrorMsg=Failed to open case auto ingest log. See application log for details. @@ -286,7 +332,6 @@ PrioritizationAction.prioritizeJobAction.error=Failed to prioritize job "%s". PrioritizationAction.prioritizeJobAction.title=Prioritize Job PrioritizedIconCellRenderer.notPrioritized.tooltiptext=This job has not been prioritized. PrioritizedIconCellRenderer.prioritized.tooltiptext=This job has been prioritized. The most recently prioritized job should be processed next. -ShowCaseDeletionStatusAction.menuItemText=Show Deletion Status SingleUserCaseImporter.NonUniqueOutputFolder=Output folder not unique. Skipping SingleUserCaseImporter.WillImport=Will import: SingleUserCaseImporter.None=None @@ -380,7 +425,6 @@ AutoIngestControlPanel.bnShowProgress.text=Ingest Progress AutoIngestControlPanel.bnCancelJob.text=&Cancel Job AutoIngestControlPanel.bnCancelModule.text=Cancel &Module AutoIngestControlPanel.bnReprocessJob.text=Reprocess Job -AutoIngestControlPanel.bnDeleteCase.text=&Delete Case AutoIngestControlPanel.bnShowCaseLog.text=Show Case &Log AutoIngestControlPanel.bnPause.text=Pause AutoIngestControlPanel.bnRefresh.text=&Refresh @@ -407,3 +451,6 @@ AinStatusDashboard.clusterMetricsButton.text=Auto Ingest &Metrics AinStatusDashboard.nodeStatusTableTitle.text=Auto Ingest Nodes AinStatusDashboard.healthMonitorButton.text=Health Monitor CasesDashboardTopComponent.refreshButton.text=Refresh +AutoIngestCasesDeletionDialog.jLabel1.text=Progress +CasesDashboardTopComponent.deleteOrphanCaseNodesButton.text=Delete Orphan Case Znodes +CasesDashboardTopComponent.deleteOrphanManifestNodesButton.text=Delete Orphan Manifest Znodes diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CasesDashboardCustomizer.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CasesDashboardCustomizer.java index 498a4e67ad..3126becbfe 100755 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CasesDashboardCustomizer.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CasesDashboardCustomizer.java @@ -31,23 +31,28 @@ import org.sleuthkit.autopsy.casemodule.multiusercasesbrowser.MultiUserCaseBrows */ final class CasesDashboardCustomizer implements MultiUserCaseBrowserCustomizer { - private final DeleteCaseInputDirectoriesAction deleteCaseInputAction; - private final DeleteCasesForReprocessingAction deleteCaseOutputAction; - private final DeleteCasesAction deleteCaseAction; + private final DeleteCaseAction deleteCaseAction; + private final DeleteCaseInputAction deleteCaseInputAction; + private final DeleteCaseOutputAction deleteCaseOutputAction; + private final DeleteCaseInputAndOutputAction deleteCaseInputAndOutputAction; /** * Constructs a customizer for the multi-user case browser panel used in the * administrative dashboard for auto ingest cases to present a tabular view * of the multi-user cases known to the coordination service. + * + * @param executor An executor for tasks for actions that do work in the + * background. */ CasesDashboardCustomizer() { /* * These actions are shared by all nodes in order to support multiple * selection. */ - deleteCaseInputAction = new DeleteCaseInputDirectoriesAction(); - deleteCaseOutputAction = new DeleteCasesForReprocessingAction(); - deleteCaseAction = new DeleteCasesAction(); + deleteCaseAction = new DeleteCaseAction(); + deleteCaseInputAction = new DeleteCaseInputAction(); + deleteCaseOutputAction = new DeleteCaseOutputAction(); + deleteCaseInputAndOutputAction = new DeleteCaseInputAndOutputAction(); } @Override @@ -56,6 +61,13 @@ final class CasesDashboardCustomizer implements MultiUserCaseBrowserCustomizer { properties.add(Column.CREATE_DATE); properties.add(Column.LAST_ACCESS_DATE); properties.add(Column.DIRECTORY); + properties.add(Column.MANIFEST_FILE_ZNODES_DELETE_STATUS); + if (AutoIngestDashboard.extendedFeaturesAreEnabled()) { + properties.add(Column.DATA_SOURCES_DELETE_STATUS); + } + properties.add(Column.TEXT_INDEX_DELETE_STATUS); + properties.add(Column.CASE_DB_DELETE_STATUS); + properties.add(Column.CASE_DIR_DELETE_STATUS); return properties; } @@ -76,10 +88,13 @@ final class CasesDashboardCustomizer implements MultiUserCaseBrowserCustomizer { List actions = new ArrayList<>(); actions.add(new OpenCaseAction(nodeData)); actions.add(new OpenAutoIngestLogAction(nodeData)); - actions.add(deleteCaseInputAction); - actions.add(deleteCaseOutputAction); - actions.add(deleteCaseAction); - actions.add(new ShowCaseDeletionStatusAction(nodeData)); + if (AutoIngestDashboard.extendedFeaturesAreEnabled()) { + actions.add(deleteCaseInputAction); + actions.add(deleteCaseOutputAction); + actions.add(deleteCaseInputAndOutputAction); + } else { + actions.add(deleteCaseAction); + } return actions; } diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CasesDashboardTopComponent.form b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CasesDashboardTopComponent.form index 8625b7f645..3ade741f25 100755 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CasesDashboardTopComponent.form +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CasesDashboardTopComponent.form @@ -20,8 +20,12 @@ - - + + + + + + @@ -37,7 +41,11 @@ - + + + + + @@ -58,5 +66,25 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CasesDashboardTopComponent.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CasesDashboardTopComponent.java index 5df5dba929..dcd7fa1fab 100755 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CasesDashboardTopComponent.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CasesDashboardTopComponent.java @@ -34,7 +34,7 @@ import org.sleuthkit.autopsy.coreutils.Logger; */ @TopComponent.Description( preferredID = "CasesDashboardTopComponent", - persistenceType = TopComponent.PERSISTENCE_ALWAYS + persistenceType = TopComponent.PERSISTENCE_NEVER ) @TopComponent.Registration( mode = "dashboard", @@ -62,8 +62,6 @@ public final class CasesDashboardTopComponent extends TopComponent implements Ex * for multi-user cases. The top component is docked into the "dashboard * mode" defined by the auto ingest jobs top component. */ - // RJCTODO: Consider moving all of the dashboard code into its own - // admindashboards or dashboards package. public static void openTopComponent() { CasesDashboardTopComponent topComponent = (CasesDashboardTopComponent) WindowManager.getDefault().findTopComponent("CasesDashboardTopComponent"); // NON-NLS if (topComponent == null) { @@ -121,6 +119,8 @@ public final class CasesDashboardTopComponent extends TopComponent implements Ex refreshButton = new javax.swing.JButton(); caseBrowserScrollPane = new javax.swing.JScrollPane(); + deleteOrphanCaseNodesButton = new javax.swing.JButton(); + deleteOrphanManifestNodesButton = new javax.swing.JButton(); org.openide.awt.Mnemonics.setLocalizedText(refreshButton, org.openide.util.NbBundle.getMessage(CasesDashboardTopComponent.class, "CasesDashboardTopComponent.refreshButton.text")); // NOI18N refreshButton.addActionListener(new java.awt.event.ActionListener() { @@ -129,6 +129,20 @@ public final class CasesDashboardTopComponent extends TopComponent implements Ex } }); + org.openide.awt.Mnemonics.setLocalizedText(deleteOrphanCaseNodesButton, org.openide.util.NbBundle.getMessage(CasesDashboardTopComponent.class, "CasesDashboardTopComponent.deleteOrphanCaseNodesButton.text")); // NOI18N + deleteOrphanCaseNodesButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + deleteOrphanCaseNodesButtonActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(deleteOrphanManifestNodesButton, org.openide.util.NbBundle.getMessage(CasesDashboardTopComponent.class, "CasesDashboardTopComponent.deleteOrphanManifestNodesButton.text")); // NOI18N + deleteOrphanManifestNodesButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + deleteOrphanManifestNodesButtonActionPerformed(evt); + } + }); + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( @@ -138,28 +152,51 @@ public final class CasesDashboardTopComponent extends TopComponent implements Ex .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() .addComponent(refreshButton) - .addGap(0, 458, Short.MAX_VALUE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(deleteOrphanCaseNodesButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(deleteOrphanManifestNodesButton) + .addGap(0, 0, Short.MAX_VALUE)) .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() .addComponent(caseBrowserScrollPane) .addContainerGap()))) ); + + layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {deleteOrphanCaseNodesButton, deleteOrphanManifestNodesButton, refreshButton}); + layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() .addContainerGap() .addComponent(caseBrowserScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 246, Short.MAX_VALUE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addComponent(refreshButton) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(refreshButton) + .addComponent(deleteOrphanCaseNodesButton) + .addComponent(deleteOrphanManifestNodesButton)) .addContainerGap()) ); + + layout.linkSize(javax.swing.SwingConstants.VERTICAL, new java.awt.Component[] {deleteOrphanCaseNodesButton, deleteOrphanManifestNodesButton, refreshButton}); + }// //GEN-END:initComponents private void refreshButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_refreshButtonActionPerformed caseBrowserPanel.displayCases(); }//GEN-LAST:event_refreshButtonActionPerformed + private void deleteOrphanCaseNodesButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_deleteOrphanCaseNodesButtonActionPerformed + new DeleteOrphanCaseNodesAction().actionPerformed(evt); + }//GEN-LAST:event_deleteOrphanCaseNodesButtonActionPerformed + + private void deleteOrphanManifestNodesButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_deleteOrphanManifestNodesButtonActionPerformed + new DeleteOrphanManifestNodesAction().actionPerformed(evt); + }//GEN-LAST:event_deleteOrphanManifestNodesButtonActionPerformed + // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JScrollPane caseBrowserScrollPane; + private javax.swing.JButton deleteOrphanCaseNodesButton; + private javax.swing.JButton deleteOrphanManifestNodesButton; private javax.swing.JButton refreshButton; // End of variables declaration//GEN-END:variables diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseAction.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseAction.java new file mode 100755 index 0000000000..d51e03d350 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseAction.java @@ -0,0 +1,74 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019-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.experimental.autoingest; + +import java.awt.event.ActionEvent; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import org.sleuthkit.autopsy.progress.ProgressIndicator; + +/** + * An action that completely deletes one or more multi-user cases. Only the + * components created by the application are deleted: the case output and the + * coordination service nodes. Note that manifest file coordination service + * nodes are only marked as deleted by setting the processing status field for + * the corresponding auto ingest job to DELETED. This is done to avoid imposing + * the requirement that the manifests be deleted before deleting the cases, + * since at this time manifests are not considered to be case components created + * by the application. + */ +final class DeleteCaseAction extends DeleteCaseComponentsAction { + + private static final long serialVersionUID = 1L; + + /** + * Constructs action that completely deletes one or more multi-user cases. + * Only the components created by the application are deleted: the case + * output and the coordination service nodes. Note that manifest file + * coordination service nodes are only marked as deleted by setting the + * processing status field for the corresponding auto ingest job to DELETED. + * This is done to avoid imposing the requirement that the manifests be + * deleted before deleting the cases, since at this time manifests are not + * considered to be case components created by the application. + */ + @NbBundle.Messages({ + "DeleteCaseAction.menuItemText=Delete Case(s)", + "DeleteCaseAction.progressDisplayName=Delete Case(s)", + "DeleteCaseAction.taskName=app-input-and-output" + }) + DeleteCaseAction() { + super(Bundle.DeleteCaseAction_menuItemText(), Bundle.DeleteCaseAction_progressDisplayName(), Bundle.DeleteCaseAction_taskName()); + } + + @NbBundle.Messages({ + "DeleteCaseAction.confirmationText=Are you sure you want to delete the following for the case(s):\n\tManifest file znodes\n\tCase database\n\tCore.properties file\n\tCase directory\n\tCase znodes" + }) + @Override + public void actionPerformed(ActionEvent event) { + if (MessageNotifyUtil.Message.confirm(Bundle.DeleteCaseAction_confirmationText())) { + super.actionPerformed(event); + } + } + + @Override + DeleteCaseTask getTask(CaseNodeData caseNodeData, ProgressIndicator progress) { + return new DeleteCaseTask(caseNodeData, DeleteCaseTask.DeleteOptions.DELETE_CASE, progress); + } +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseComponentsAction.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseComponentsAction.java new file mode 100755 index 0000000000..b84921deab --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseComponentsAction.java @@ -0,0 +1,107 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019-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.experimental.autoingest; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import java.awt.event.ActionEvent; +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import javax.swing.AbstractAction; +import org.openide.util.Utilities; +import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; +import org.sleuthkit.autopsy.progress.AppFrameProgressBar; +import org.sleuthkit.autopsy.progress.TaskCancellable; +import org.sleuthkit.autopsy.progress.ProgressIndicator; + +/** + * An abstract class for an action that deletes components one or more + * multi-user cases using a thread pool, one task per case. Uses the Template + * Method design pattern to allow subclasses to specify the deletion task to be + * performed. + * + * This cases to delete are discovered by querying the actions global context + * lookup for CaseNodeData objects. See + * https://platform.netbeans.org/tutorials/nbm-selection-1.html and + * https://platform.netbeans.org/tutorials/nbm-selection-2.html for details. + */ +abstract class DeleteCaseComponentsAction extends AbstractAction { + + private static final long serialVersionUID = 1L; + private static final int NUMBER_OF_THREADS = 4; + private static final String THREAD_NAME_SUFFIX = "-task-%d"; //NON-NLS + private static final String PROGRESS_DISPLAY_NAME = "%s for %s"; //NON-NLS + private final String taskDisplayName; + private final ExecutorService executor; + + /** + * Constructs an abstract class for an action that deletes components of one + * or more multi-user cases using a thread pool, one task per case. Uses the + * Template Method design pattern to allow subclasses to specify the + * deletion task to be performed. + * + * @param menuItemText The menu item text for the action. + * @param taskDisplayName The task display name for the progress indicator + * for the task, to be inserted in the first position + * of "%s for %s", where the second substitution is + * the case name. + * @param taskName The task name, to be inserted in the first + * position of "%s-task-%d", where the second + * substitution is the pool thread number. + */ + DeleteCaseComponentsAction(String menuItemText, String taskDisplayName, String taskName) { + super(menuItemText); + this.taskDisplayName = taskDisplayName; + String threadNameFormat = taskName + THREAD_NAME_SUFFIX; + executor = Executors.newFixedThreadPool(NUMBER_OF_THREADS, new ThreadFactoryBuilder().setNameFormat(threadNameFormat).build()); + } + + @Override + public void actionPerformed(ActionEvent event) { + Collection selectedNodeData = new ArrayList<>(Utilities.actionsGlobalContext().lookupAll(CaseNodeData.class)); + for (CaseNodeData nodeData : selectedNodeData) { + AppFrameProgressBar progress = new AppFrameProgressBar(String.format(PROGRESS_DISPLAY_NAME, taskDisplayName, nodeData.getDisplayName())); + TaskCancellable taskCanceller = new TaskCancellable(progress); + progress.setCancellationBehavior(taskCanceller); + Future future = executor.submit(getTask(nodeData, progress)); + taskCanceller.setFuture(future); + } + } + + /** + * Uses the Template Method design pattern to allow subclasses to specify + * the deletion task to be performed in a worker thread by this action. + * + * @param caseNodeData The case directory lock coordination service node + * data for the case to be deleted. + * @param progress A progress indicator for the task. + * + * @return A case deletion task, ready to be executed. + */ + abstract DeleteCaseTask getTask(CaseNodeData caseNodeData, ProgressIndicator progress); + + @Override + public DeleteCaseComponentsAction clone() throws CloneNotSupportedException { + super.clone(); + throw new CloneNotSupportedException(); + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseInputDirectoriesAction.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseInputAction.java similarity index 60% rename from Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseInputDirectoriesAction.java rename to Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseInputAction.java index aab830be8f..a3148fbf52 100755 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseInputDirectoriesAction.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseInputAction.java @@ -19,12 +19,11 @@ package org.sleuthkit.autopsy.experimental.autoingest; import java.awt.event.ActionEvent; -import java.util.ArrayList; -import java.util.Collection; -import javax.swing.AbstractAction; import org.openide.util.NbBundle; -import org.openide.util.Utilities; import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import org.sleuthkit.autopsy.experimental.autoingest.DeleteCaseTask.DeleteOptions; +import org.sleuthkit.autopsy.progress.ProgressIndicator; /** * An action that deletes the auto ingest job input directories associated with @@ -32,13 +31,8 @@ import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; * ingest jobs are not deleted. This supports the use case where the directories * may need to be directed to reclaim space, but the option to restore the * directories without having the jobs be reprocessed is retained. - * - * This cases to delete are discovered by querying the actions global context - * lookup for CaseNodeData objects. See - * https://platform.netbeans.org/tutorials/nbm-selection-1.html and - * https://platform.netbeans.org/tutorials/nbm-selection-2.html for details. */ -final class DeleteCaseInputDirectoriesAction extends AbstractAction { +final class DeleteCaseInputAction extends DeleteCaseComponentsAction { private static final long serialVersionUID = 1L; @@ -51,27 +45,27 @@ final class DeleteCaseInputDirectoriesAction extends AbstractAction { * reprocessed is retained. */ @NbBundle.Messages({ - "DeleteCaseInputDirectoriesAction.menuItemText=Delete Input Directories" + "DeleteCaseInputAction.menuItemText=Delete Input", + "DeleteCaseInputAction.progressDisplayName=Delete Input", + "DeleteCaseInputAction.taskName=input" }) - DeleteCaseInputDirectoriesAction() { - super(Bundle.DeleteCaseInputDirectoriesAction_menuItemText()); - setEnabled(false); // RJCTODO: Enable when implemented + DeleteCaseInputAction() { + super(Bundle.DeleteCaseInputAction_menuItemText(), Bundle.DeleteCaseInputAction_progressDisplayName(), Bundle.DeleteCaseInputAction_taskName()); } + @NbBundle.Messages({ + "DeleteCaseInputAction.confirmationText=Are you sure you want to delete the following for the case(s):\n\tManifest files\n\tData sources\n" + }) @Override public void actionPerformed(ActionEvent event) { - final Collection selectedNodeData = new ArrayList<>(Utilities.actionsGlobalContext().lookupAll(CaseNodeData.class)); -// if (!selectedNodeData.isEmpty()) { -// /* -// * RJCTODO: Create a background task that does the deletion and -// * displays results in a dialog with a scrolling text pane. -// */ -// } + if (MessageNotifyUtil.Message.confirm(Bundle.DeleteCaseInputAction_confirmationText())) { + super.actionPerformed(event); + } } @Override - public DeleteCaseInputDirectoriesAction clone() throws CloneNotSupportedException { - throw new CloneNotSupportedException(); + DeleteCaseTask getTask(CaseNodeData caseNodeData, ProgressIndicator progress) { + return new DeleteCaseTask(caseNodeData, DeleteOptions.DELETE_INPUT, progress); } } diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseInputAndOutputAction.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseInputAndOutputAction.java new file mode 100755 index 0000000000..c773c465e0 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseInputAndOutputAction.java @@ -0,0 +1,67 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019-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.experimental.autoingest; + +import java.awt.event.ActionEvent; +import org.openide.util.NbBundle; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import org.sleuthkit.autopsy.experimental.autoingest.DeleteCaseTask.DeleteOptions; +import org.sleuthkit.autopsy.progress.ProgressIndicator; + +/** + * An action that completely deletes one or more multi-user cases, including any + * associated auto ingest job input directories and all coordination service + * nodes. + */ +final class DeleteCaseInputAndOutputAction extends DeleteCaseComponentsAction { + + private static final long serialVersionUID = 1L; + + /** + * Constructs an action that completely deletes one or more multi-user + * cases, including any associated auto ingest job input directories and + * coordination service nodes. + */ + @Messages({ + "DeleteCaseInputAndOutputAction.menuItemText=Delete Input and Output", + "DeleteCaseInputAndOutputAction.progressDisplayName=Delete Input and Output", + "DeleteCaseInputAndOutputAction.taskName=input-and-output" + }) + DeleteCaseInputAndOutputAction() { + super(Bundle.DeleteCaseInputAndOutputAction_menuItemText(), Bundle.DeleteCaseInputAndOutputAction_progressDisplayName(), Bundle.DeleteCaseInputAndOutputAction_taskName()); + } + + @NbBundle.Messages({ + "DeleteCaseInputAndOutputAction.confirmationText=Are you sure you want to delete the following for the case(s):\n\tManifest files\n\tData sources\n\tManifest file znodes\n\tCase database\n\tCore.properties file\n\tCase directory\n\tCase znodes" + }) + @Override + public void actionPerformed(ActionEvent event) { + if (MessageNotifyUtil.Message.confirm(Bundle.DeleteCaseInputAndOutputAction_confirmationText())) { + super.actionPerformed(event); + } + } + + @Override + DeleteCaseTask getTask(CaseNodeData caseNodeData, ProgressIndicator progress) { + return new DeleteCaseTask(caseNodeData, DeleteOptions.DELETE_INPUT_AND_OUTPUT, progress); + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCasesForReprocessingAction.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseOutputAction.java similarity index 58% rename from Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCasesForReprocessingAction.java rename to Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseOutputAction.java index 9a0c4d50fb..cd6f6f9418 100755 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCasesForReprocessingAction.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseOutputAction.java @@ -19,12 +19,11 @@ package org.sleuthkit.autopsy.experimental.autoingest; import java.awt.event.ActionEvent; -import java.util.ArrayList; -import java.util.Collection; -import javax.swing.AbstractAction; import org.openide.util.NbBundle; -import org.openide.util.Utilities; import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import org.sleuthkit.autopsy.experimental.autoingest.DeleteCaseTask.DeleteOptions; +import org.sleuthkit.autopsy.progress.ProgressIndicator; /** * An action that deletes everything except the auto ingest job input @@ -32,13 +31,8 @@ import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; * where a case needs to be reprocessed, so the input directories are not * deleted even though the coordination service nodes for the auto ingest jobs * are deleted. - * - * This cases to delete are discovered by querying the actions global context - * lookup for CaseNodeData objects. See - * https://platform.netbeans.org/tutorials/nbm-selection-1.html and - * https://platform.netbeans.org/tutorials/nbm-selection-2.html for details. */ -final class DeleteCasesForReprocessingAction extends AbstractAction { +final class DeleteCaseOutputAction extends DeleteCaseComponentsAction { private static final long serialVersionUID = 1L; @@ -50,27 +44,27 @@ final class DeleteCasesForReprocessingAction extends AbstractAction { * ingest jobs are deleted. */ @NbBundle.Messages({ - "DeleteCasesForReprocessingAction.menuItemText=Delete for Reprocessing" + "DeleteCaseOutputAction.menuItemText=Delete Output", + "DeleteCaseOutputAction.progressDisplayName=Delete Output", + "DeleteCaseOutputAction.taskName=output" }) - DeleteCasesForReprocessingAction() { - super(Bundle.DeleteCasesForReprocessingAction_menuItemText()); - setEnabled(false); // RJCTODO: Enable when implemented + DeleteCaseOutputAction() { + super(Bundle.DeleteCaseOutputAction_menuItemText(), Bundle.DeleteCaseOutputAction_progressDisplayName(), Bundle.DeleteCaseOutputAction_taskName()); } + @NbBundle.Messages({ + "DeleteCaseOutputAction.confirmationText=Are you sure you want to delete the following for the case(s):\n\tManifest file znodes\n\tCase database\n\tCore.properties file\n\tCase directory\n\tCase znodes" + }) @Override public void actionPerformed(ActionEvent event) { - final Collection selectedNodeData = new ArrayList<>(Utilities.actionsGlobalContext().lookupAll(CaseNodeData.class)); -// if (!selectedNodeData.isEmpty()) { -// /* -// * RJCTODO: Create a background task that does the deletion and -// * displays results in a dialog with a scrolling text pane. -// */ -// } - } - + if (MessageNotifyUtil.Message.confirm(Bundle.DeleteCaseOutputAction_confirmationText())) { + super.actionPerformed(event); + } + } + @Override - public DeleteCasesForReprocessingAction clone() throws CloneNotSupportedException { - throw new CloneNotSupportedException(); + DeleteCaseTask getTask(CaseNodeData caseNodeData, ProgressIndicator progress) { + return new DeleteCaseTask(caseNodeData, DeleteOptions.DELETE_OUTPUT, progress); } } diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseTask.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseTask.java new file mode 100755 index 0000000000..e96d2cc5fd --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseTask.java @@ -0,0 +1,945 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019-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.experimental.autoingest; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Scanner; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import org.openide.util.Lookup; +import org.openide.util.NbBundle; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.CaseMetadata; +import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; +import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData.CaseNodeDataException; +import org.sleuthkit.autopsy.casemodule.multiusercases.CoordinationServiceUtils; +import org.sleuthkit.autopsy.coordinationservice.CoordinationService; +import org.sleuthkit.autopsy.coordinationservice.CoordinationService.CategoryNode; +import org.sleuthkit.autopsy.coordinationservice.CoordinationService.CoordinationServiceException; +import org.sleuthkit.autopsy.coordinationservice.CoordinationService.Lock; +import org.sleuthkit.autopsy.core.UserPreferences; +import org.sleuthkit.autopsy.core.UserPreferencesException; +import org.sleuthkit.autopsy.coreutils.FileUtil; +import org.sleuthkit.autopsy.progress.ProgressIndicator; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.experimental.autoingest.AutoIngestJobNodeData.InvalidDataException; +import org.sleuthkit.datamodel.DataSource; +import org.sleuthkit.datamodel.Image; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * A task that deletes part or all of a given case. Note that all logging is + * directed to the dedicated auto ingest dashboard log instead of to the general + * application log. + */ +final class DeleteCaseTask implements Runnable { + + private static final int MANIFEST_FILE_LOCKING_TIMEOUT_MINS = 5; + private static final int MANIFEST_DELETE_TRIES = 3; + private static final Logger logger = AutoIngestDashboardLogger.getLogger(); + private final CaseNodeData caseNodeData; + private final DeleteOptions deleteOption; + private final ProgressIndicator progress; + private final List manifestFileLocks; + private CoordinationService coordinationService; + private CaseMetadata caseMetadata; + + /** + * Options to support implementing different case deletion use cases. + */ + enum DeleteOptions { + /** + * Delete the auto ingest job manifests and corresponding data sources, + * while leaving the manifest file coordination service nodes and the + * rest of the case intact. The use case is freeing auto ingest input + * directory space while retaining the option to restore the data + * sources, effectively restoring the case. + */ + DELETE_INPUT, + /** + * Delete the manifest file coordination service nodes and the output + * for a case, while leaving the auto ingest job manifests and + * corresponding data sources intact. The use case is auto ingest + * reprocessing of a case with a clean slate without having to restore + * the manifests and data sources. + */ + DELETE_OUTPUT, + /** + * Delete everything. + */ + DELETE_INPUT_AND_OUTPUT, + /** + * Delete only the case components that the application created. This is + * DELETE_OUTPUT with the additional feature that manifest file + * coordination service nodes are marked as deleted, rather than + * actually deleted. This eliminates the requirement that manifests and + * data sources have to be deleted before deleting the case to avoid an + * unwanted, automatic reprocessing of the case. + */ + DELETE_CASE + } + + /** + * Constructs a task that deletes part or all of a given case. Note that all + * logging is directed to the dedicated auto ingest dashboard log instead of + * to the general application log. + * + * @param caseNodeData The case directory coordination service node data for + * the case. + * @param deleteOption The deletion option for the task. + * @param progress A progress indicator. + */ + DeleteCaseTask(CaseNodeData caseNodeData, DeleteOptions deleteOption, ProgressIndicator progress) { + this.caseNodeData = caseNodeData; + this.deleteOption = deleteOption; + this.progress = progress; + manifestFileLocks = new ArrayList<>(); + } + + @Override + @NbBundle.Messages({ + "DeleteCaseTask.progress.startMessage=Starting deletion..." + }) + public void run() { + try { + progress.start(Bundle.DeleteCaseTask_progress_startMessage()); + logger.log(Level.INFO, String.format("Starting deletion of %s (%s)", caseNodeData.getDisplayName(), deleteOption)); + deleteCase(); + logger.log(Level.INFO, String.format("Finished deletion of %s (%s)", caseNodeData.getDisplayName(), deleteOption)); + + } catch (CoordinationServiceException | IOException ex) { + logger.log(Level.SEVERE, String.format("Error deleting %s (%s) in %s", caseNodeData.getDisplayName(), caseNodeData.getName(), caseNodeData.getDirectory()), ex); + + } catch (InterruptedException ex) { + logger.log(Level.WARNING, String.format("Deletion of %s cancelled while incomplete", caseNodeData.getDisplayName()), ex); + Thread.currentThread().interrupt(); + + } catch (Exception ex) { + /* + * This is an unexpected runtime exceptions firewall. It is here + * because this task is designed to be able to be run in scenarios + * where there is no call to get() on a Future associated with + * the task, so this ensures that any such errors get logged. + */ + logger.log(Level.SEVERE, String.format("Unexpected error deleting %s", caseNodeData.getDisplayName()), ex); + throw ex; + + } finally { + releaseManifestFileLocks(); + progress.finish(); + } + } + + /** + * Deletes part or all of the given case. + * + * @throws InterruptedException If the thread in which this task is running + * is interrupted while blocked waiting for a + * coordination service operation to complete. + */ + @NbBundle.Messages({ + "DeleteCaseTask.progress.connectingToCoordSvc=Connecting to the coordination service...", + "DeleteCaseTask.progress.acquiringCaseNameLock=Acquiring exclusive case name lock...", + "DeleteCaseTask.progress.acquiringCaseDirLock=Acquiring exclusive case directory lock...", + "DeleteCaseTask.progress.gettingManifestPaths=Getting manifest file paths...", + "DeleteCaseTask.progress.acquiringManifestLocks=Acquiring exclusive manifest file locks...", + "DeleteCaseTask.progress.openingCaseMetadataFile=Opening case metadata file...", + "DeleteCaseTask.progress.deletingResourcesLockNode=Deleting case resources znode...", + "DeleteCaseTask.progress.deletingJobLogLockNode=Deleting case auto ingest log znode...", + "DeleteCaseTask.progress.deletingCaseDirCoordSvcNode=Deleting case directory znode...", + "DeleteCaseTask.progress.deletingCaseNameCoordSvcNode=Deleting case name znode..." + }) + private void deleteCase() throws CoordinationServiceException, IOException, InterruptedException { + progress.progress(Bundle.DeleteCaseTask_progress_connectingToCoordSvc()); + logger.log(Level.INFO, String.format("Connecting to the coordination service for deletion of %s", caseNodeData.getDisplayName())); + coordinationService = CoordinationService.getInstance(); + checkForCancellation(); + + /* + * Acquire an exclusive case name lock. The case name lock is the lock + * that auto ingest node (AIN) job processing tasks acquire exclusively + * when creating or opening a case specified in an auto ingest job + * manifest file. The reason AINs do this is to ensure that only one of + * them at a time can search the auto ingest output directory for an + * existing case matching the one in the manifest file. If a matching + * case is found, it is opened, otherwise the case is created. Acquiring + * this lock effectively disables this AIN job processing task behavior + * while the case is being deleted. + */ + progress.progress(Bundle.DeleteCaseTask_progress_acquiringCaseNameLock()); + logger.log(Level.INFO, String.format("Acquiring an exclusive case name lock for %s", caseNodeData.getDisplayName())); + String caseNameLockName = CoordinationServiceUtils.getCaseNameNodePath(caseNodeData.getDirectory()); + try (CoordinationService.Lock nameLock = coordinationService.tryGetExclusiveLock(CategoryNode.CASES, caseNameLockName)) { + if (nameLock == null) { + logger.log(Level.INFO, String.format("Could not delete %s because a case name lock was already held by another host", caseNodeData.getDisplayName())); + return; + } + checkForCancellation(); + + /* + * Acquire an exclusive case directory lock. A shared case directory + * lock is acquired by each auto ingest node (AIN) and examiner node + * (EIN) when it opens a case. The shared locks are held by the AINs + * and EINs for as long as they have the case open. Acquiring this + * lock exclusively ensures that no AIN or EIN has the case to be + * deleted open and prevents another node from trying to open the + * case while it is being deleted. + */ + progress.progress(Bundle.DeleteCaseTask_progress_acquiringCaseDirLock()); + logger.log(Level.INFO, String.format("Acquiring an exclusive case directory lock for %s", caseNodeData.getDisplayName())); + String caseDirLockName = CoordinationServiceUtils.getCaseDirectoryNodePath(caseNodeData.getDirectory()); + try (CoordinationService.Lock caseDirLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.CASES, caseDirLockName)) { + if (caseDirLock == null) { + logger.log(Level.INFO, String.format("Could not delete %s because a case directory lock was already held by another host", caseNodeData.getDisplayName())); + return; + } + checkForCancellation(); + + /* + * Acquire exclusive locks for the auto ingest job manifest + * files for the case, if any. Manifest file locks are acquired + * by the auto ingest node (AIN) input directory scanning tasks + * when they look for auto ingest jobs to enqueue, and by the + * AIN job execution tasks when they do a job. Acquiring these + * locks here ensures that the scanning tasks and job execution + * tasks cannot do anything with the auto ingest jobs for a case + * during case deletion. + */ + if (!acquireManifestFileLocks()) { + logger.log(Level.INFO, String.format("Could not delete %s because at least one manifest file lock was already held by another host", caseNodeData.getDisplayName())); + return; + } + checkForCancellation(); + + deleteCaseContents(); + checkForCancellation(); + + deleteCaseResourcesNode(); + checkForCancellation(); + + deleteCaseAutoIngestLogNode(); + checkForCancellation(); + + deleteManifestFileNodes(); + checkForCancellation(); + } + + deleteCaseDirectoryNode(); + checkForCancellation(); + } + + deleteCaseNameNode(); + } + + /** + * Gets the manifest file paths for the case, if there are any. + * + * @throws CoordinationServiceException If there is an error completing a + * coordination service operation. + * @throws InterruptedException If the thread in which this task is + * running is interrupted while blocked + * waiting for a coordination service + * operation to complete. + * @throws IOException If there is an error reading the + * manifests list file. + */ + private List getManifestFilePaths() throws IOException, CoordinationServiceException, InterruptedException { + progress.progress(Bundle.DeleteCaseTask_progress_gettingManifestPaths()); + logger.log(Level.INFO, String.format("Getting manifest file paths for %s", caseNodeData.getDisplayName())); + final Path manifestsListFilePath = Paths.get(caseNodeData.getDirectory().toString(), AutoIngestManager.getCaseManifestsListFileName()); + final File manifestListsFile = manifestsListFilePath.toFile(); + if (manifestListsFile.exists()) { + return getManifestPathsFromFile(manifestsListFilePath); + } else { + return getManifestPathsFromNodes(); + } + } + + /** + * Gets a list of the manifest file paths for the case by reading them from + * the manifests list file for the case. + * + * @param manifestsListFilePath The path of the manifests list file. + * + * @return A list of manifest file paths, possibly empty. + * + * @throws IOException If there is an error reading the manifests + * list file. + * @throws InterruptedException If the thread in which this task is running + * is interrupted while blocked waiting for a + * coordination service operation to complete. + */ + private List getManifestPathsFromFile(Path manifestsListFilePath) throws IOException, InterruptedException { + final List manifestFilePaths = new ArrayList<>(); + try (final Scanner manifestsListFileScanner = new Scanner(manifestsListFilePath)) { + while (manifestsListFileScanner.hasNextLine()) { + checkForCancellation(); + final Path manifestFilePath = Paths.get(manifestsListFileScanner.nextLine()); + if (manifestFilePath.toFile().exists()) { + manifestFilePaths.add(manifestFilePath); + } + } + } + return manifestFilePaths; + } + + /** + * Gets a list of the manifest file paths for the case by sifting through + * the node data of the manifest file coordination service nodes and + * matching on case name. + * + * @return A list of manifest file paths, possibly empty. + * + * @throws CoordinationServiceException If there is an error completing a + * coordination service operation. + * @throws InterruptedException If the thread in which this task is + * running is interrupted while blocked + * waiting for a coordination service + * operation to complete. + */ + private List getManifestPathsFromNodes() throws CoordinationServiceException, InterruptedException { + /* + * Get the original, undecorated case name from the case directory. This + * is necessary because the case display name can be changed and the + * original case name may have a time stamp added to make it unique, + * depending on how the case was created. An alternative aproach would + * be to strip off any time stamp from the case name in the case node + * data. + */ + String caseName = CoordinationServiceUtils.getCaseNameNodePath(caseNodeData.getDirectory()); + final List manifestFilePaths = new ArrayList<>(); + final List nodeNames = coordinationService.getNodeList(CoordinationService.CategoryNode.MANIFESTS); + for (String manifestNodeName : nodeNames) { + checkForCancellation(); + try { + final byte[] nodeBytes = coordinationService.getNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestNodeName); + AutoIngestJobNodeData nodeData = new AutoIngestJobNodeData(nodeBytes); + if (caseName.equals(nodeData.getCaseName())) { + Path manifestFilePath = nodeData.getManifestFilePath(); + if (manifestFilePath.toFile().exists()) { + manifestFilePaths.add(manifestFilePath); + } + } + } catch (CoordinationServiceException | InvalidDataException ex) { + logger.log(Level.WARNING, String.format("Error getting coordination service node data from %s", manifestNodeName), ex); + } + } + return manifestFilePaths; + } + + /** + * Acquires either all or none of the auto ingest job manifest file locks + * for a case. + * + * @return True if all of the locks were acquired; false otherwise. + * + * @throws CoordinationServiceException If there is an error completing a + * coordination service operation. + * @throws InterruptedException If the thread in which this task is + * running is interrupted while blocked + * waiting for a coordination service + * operation to complete. + */ + @NbBundle.Messages({ + "# {0} - manifest file path", "DeleteCaseTask.progress.lockingManifest=Locking manifest file {0}..." + }) + private boolean acquireManifestFileLocks() throws IOException, CoordinationServiceException, InterruptedException { + boolean allLocksAcquired = true; + List manifestFilePaths = getManifestFilePaths(); + logger.log(Level.INFO, String.format("Found %d manifest file path(s) for %s", manifestFilePaths.size(), caseNodeData.getDisplayName())); + if (!manifestFilePaths.isEmpty()) { + progress.progress(Bundle.DeleteCaseTask_progress_acquiringManifestLocks()); + logger.log(Level.INFO, String.format("Acquiring exclusive manifest file locks for %s", caseNodeData.getDisplayName())); + /* + * When acquiring the locks, it is reasonable to block briefly, + * since the auto ingest node (AIN) input directory scanning tasks + * do a lot of short-term acquiring and releasing of the same locks. + * The assumption here is that the originator of this case deletion + * task is not asking for deletion of a case that has a job that an + * auto ingest node (AIN) job execution task is working on and that + * MANIFEST_FILE_LOCKING_TIMEOUT_MINS is not very long anyway, so + * waiting a bit should be fine. + */ + try { + for (Path manifestPath : manifestFilePaths) { + checkForCancellation(); + progress.progress(Bundle.DeleteCaseTask_progress_lockingManifest(manifestPath.toString())); + logger.log(Level.INFO, String.format("Exclusively locking the manifest %s for %s", manifestPath, caseNodeData.getDisplayName())); + CoordinationService.Lock manifestLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.MANIFESTS, manifestPath.toString(), MANIFEST_FILE_LOCKING_TIMEOUT_MINS, TimeUnit.MINUTES); + if (null != manifestLock) { + manifestFileLocks.add(new ManifestFileLock(manifestPath, manifestLock)); + } else { + logger.log(Level.INFO, String.format("Failed to exclusively lock the manifest %s because it was already held by another host", manifestPath, caseNodeData.getDisplayName())); + allLocksAcquired = false; + releaseManifestFileLocks(); + break; + } + } + } catch (CoordinationServiceException | InterruptedException ex) { + releaseManifestFileLocks(); + throw ex; + } + } else { + setDeletedItemFlag(CaseNodeData.DeletedFlags.MANIFEST_FILE_NODES); + } + return allLocksAcquired; + } + + /** + * Deletes case contents, based on the specified deletion option. + * + * @throws InterruptedException If the thread in which this task is running + * is interrupted while blocked waiting for a + * coordination service operation to complete. + */ + private void deleteCaseContents() throws InterruptedException { + final File caseDirectory = caseNodeData.getDirectory().toFile(); + if (caseDirectory.exists()) { + progress.progress(Bundle.DeleteCaseTask_progress_openingCaseMetadataFile()); + logger.log(Level.INFO, String.format("Opening case metadata file for %s", caseNodeData.getDisplayName())); + Path caseMetadataPath = CaseMetadata.getCaseMetadataFilePath(caseNodeData.getDirectory()); + if (caseMetadataPath != null) { + try { + caseMetadata = new CaseMetadata(caseMetadataPath); + checkForCancellation(); + if (!manifestFileLocks.isEmpty()) { + if (deleteOption == DeleteOptions.DELETE_INPUT || deleteOption == DeleteOptions.DELETE_INPUT_AND_OUTPUT) { + deleteAutoIngestInput(); + } else if (deleteOption == DeleteOptions.DELETE_CASE) { + markManifestFileNodesAsDeleted(); + } + } + checkForCancellation(); + if (deleteOption == DeleteOptions.DELETE_OUTPUT || deleteOption == DeleteOptions.DELETE_INPUT_AND_OUTPUT || deleteOption == DeleteOptions.DELETE_CASE) { + Case.deleteMultiUserCase(caseNodeData, caseMetadata, progress, logger); + } + + } catch (CaseMetadata.CaseMetadataException ex) { + logger.log(Level.SEVERE, String.format("Error reading metadata file for %s", caseNodeData.getDisplayName()), ex); + } + + } else { + logger.log(Level.WARNING, String.format("No case metadata file found for %s", caseNodeData.getDisplayName())); + } + + } else { + setDeletedItemFlag(CaseNodeData.DeletedFlags.CASE_DIR); + logger.log(Level.INFO, String.format("No case directory found for %s", caseNodeData.getDisplayName())); + } + } + + /** + * Deletes the auto ingest job input manifests for the case along with the + * corresponding data sources. + * + * @throws InterruptedException If the thread in which this task is running + * is interrupted while blocked waiting for a + * coordination service operation to complete. + */ + @NbBundle.Messages({ + "DeleteCaseTask.progress.openingCaseDatabase=Opening the case database...", + "# {0} - manifest file path", "DeleteCaseTask.progress.parsingManifest=Parsing manifest file {0}..." + }) + private void deleteAutoIngestInput() throws InterruptedException { + SleuthkitCase caseDb = null; + try { + progress.progress(Bundle.DeleteCaseTask_progress_openingCaseDatabase()); + logger.log(Level.INFO, String.format("Opening the case database for %s", caseNodeData.getDisplayName())); + caseDb = SleuthkitCase.openCase(caseMetadata.getCaseDatabaseName(), UserPreferences.getDatabaseConnectionInfo(), caseMetadata.getCaseDirectory()); + List dataSources = caseDb.getDataSources(); + checkForCancellation(); + + /* + * For every manifest file associated with the case, attempt to + * delete both the data source referenced by the manifest and the + * manifest. + */ + boolean allInputDeleted = true; + for (ManifestFileLock lock : manifestFileLocks) { + checkForCancellation(); + Path manifestFilePath = lock.getManifestFilePath(); + final File manifestFile = manifestFilePath.toFile(); + if (manifestFile.exists()) { + Manifest manifest = parseManifestFile(manifestFilePath); + if (manifest != null) { + if (deleteDataSources(manifest, dataSources) && deleteManifestFile(manifestFile)) { + lock.setInputDeleted(); + } else { + allInputDeleted = false; + } + } else { + logger.log(Level.WARNING, String.format("Failed to parse manifest file %s for %s", manifestFilePath, caseNodeData.getDisplayName())); + allInputDeleted = false; + } + } else { + logger.log(Level.WARNING, String.format("Did not find manifest file %s for %s", manifestFilePath, caseNodeData.getDisplayName())); + } + } + if (allInputDeleted) { + setDeletedItemFlag(CaseNodeData.DeletedFlags.DATA_SOURCES); + } + + } catch (TskCoreException | UserPreferencesException ex) { + logger.log(Level.INFO, String.format("Failed to open or query the case database for %s", caseNodeData.getDisplayName()), ex); + + } finally { + if (caseDb != null) { + caseDb.close(); + } + } + } + + /** + * Parses a manifest file. + * + * @param manifestFilePath The manifest file path. + * + * @return A manifest, if the parsing is successful, null otherwise. + */ + private Manifest parseManifestFile(Path manifestFilePath) { + progress.progress(Bundle.DeleteCaseTask_progress_parsingManifest(manifestFilePath)); + logger.log(Level.INFO, String.format("Parsing manifest file %s for %s", manifestFilePath, caseNodeData.getDisplayName())); + Manifest manifest = null; + for (ManifestFileParser parser : Lookup.getDefault().lookupAll(ManifestFileParser.class)) { + if (parser.fileIsManifest(manifestFilePath)) { + try { + manifest = parser.parse(manifestFilePath); + break; + } catch (ManifestFileParser.ManifestFileParserException ex) { + logger.log(Level.WARNING, String.format("Error parsing manifest file %s for %s", manifestFilePath, caseNodeData.getDisplayName()), ex); + } + } + } + return manifest; + } + + /** + * Deletes a manifest file. + * + * @param manifestFile The manifest file. + * + * @return True if the file was deleted, false otherwise. + * + * @throws InterruptedException If the thread in which this task is running + * is interrupted while blocked waiting for a + * coordination service operation to complete. + */ + @NbBundle.Messages({ + "# {0} - manifest file path", "DeleteCaseTask.progress.deletingManifest=Deleting manifest file {0}..." + }) + private boolean deleteManifestFile(File manifestFile) throws InterruptedException { + /* + * Delete the manifest file, allowing a few retries. This is a way to + * resolve the race condition between this task and auto ingest node + * (AIN) input directory scanning tasks, which parse manifests (actually + * all files) before getting a coordination service lock, without + * resorting to a protocol using locking of the input directory. + */ + Path manifestFilePath = manifestFile.toPath(); + progress.progress(Bundle.DeleteCaseTask_progress_deletingManifest(manifestFilePath)); + logger.log(Level.INFO, String.format("Deleting manifest file %s for %s", manifestFilePath, caseNodeData.getDisplayName())); + int tries = 0; + boolean deleted = false; + while (!deleted && tries < MANIFEST_DELETE_TRIES) { + deleted = manifestFile.delete(); + if (!deleted) { + ++tries; + Thread.sleep(1000); + } + } + if (!deleted) { + logger.log(Level.WARNING, String.format("Failed to delete manifest file %s for %s", manifestFilePath, caseNodeData.getDisplayName())); + } + return deleted; + } + + /** + * Locates and deletes the data source files referenced by a manifest. + * + * @param manifest A manifest. + * @param dataSources The data sources in the case as obtained from the case + * database. + * + * @return True if all of the data source files werre deleted, false + * otherwise. + */ + @NbBundle.Messages({ + "# {0} - data source path", "DeleteCaseTask.progress.deletingDataSource=Deleting data source {0}..." + }) + private boolean deleteDataSources(Manifest manifest, List dataSources) { + final Path dataSourcePath = manifest.getDataSourcePath(); + progress.progress(Bundle.DeleteCaseTask_progress_deletingDataSource(dataSourcePath)); + logger.log(Level.INFO, String.format("Deleting data source %s from %s", dataSourcePath, caseNodeData.getDisplayName())); + + /* + * There are two possibilities here. The data source may be an image, + * and if so, it may be split into multiple files. In this case, all of + * the files for the image need to be deleted. Otherwise, the data + * source is a single directory or file (a logical file, logical file + * set, report file, archive file, etc.). In this case, just the file + * referenced by the manifest will be deleted. + */ + Set filesToDelete = new HashSet<>(); + int index = 0; + while (index < dataSources.size() && filesToDelete.isEmpty()) { + DataSource dataSource = dataSources.get(index); + if (dataSource instanceof Image) { + Image image = (Image) dataSource; + String[] imageFilePaths = image.getPaths(); + /* + * Check for a match between one of the paths for the image + * files and the data source file path in the manifest. + */ + for (String imageFilePath : imageFilePaths) { + Path candidatePath = Paths.get(imageFilePath); + if (candidatePath.equals(dataSourcePath)) { + /* + * If a match is found, add all of the file paths for + * the image to the set of files to be deleted. + */ + for (String path : imageFilePaths) { + filesToDelete.add(Paths.get(path)); + } + break; + } + } + } + ++index; + } + + /* + * At a minimum, the data source at the file path given in the manifest + * should be deleted. If the data source is not a disk image, this will + * be the path of an archive, a logical file, or a logical directory. + * TODO-4933: Currently, the contents extracted from an archive are not + * deleted, nor are any additional files associated with a report data + * source. + */ + filesToDelete.add(dataSourcePath); + + /* + * Delete the file(s). + */ + boolean allFilesDeleted = true; + for (Path path : filesToDelete) { + File fileOrDir = path.toFile(); + if (fileOrDir.exists() && !FileUtil.deleteFileDir(fileOrDir)) { + allFilesDeleted = false; + logger.log(Level.WARNING, String.format("Failed to delete data source file at %s for %s", path, caseNodeData.getDisplayName())); + } + } + + return allFilesDeleted; + } + + /** + * Marks the manifest file coordination service nodes as deleted by setting + * the auto ingest job processing status field to deleted. + * + * @throws InterruptedException If the thread in which this task is running + * is interrupted while blocked waiting for a + * coordination service operation to complete. + */ + private void markManifestFileNodesAsDeleted() throws InterruptedException { + boolean allNodesMarked = true; + for (ManifestFileLock manifestFileLock : manifestFileLocks) { + String manifestFilePath = manifestFileLock.getManifestFilePath().toString(); + try { + progress.progress(Bundle.DeleteCaseTask_progress_deletingManifestFileNode(manifestFilePath)); + logger.log(Level.INFO, String.format("Marking as deleted the manifest file znode for %s for %s", manifestFilePath, caseNodeData.getDisplayName())); + final byte[] nodeBytes = coordinationService.getNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestFilePath); + AutoIngestJobNodeData nodeData = new AutoIngestJobNodeData(nodeBytes); + nodeData.setProcessingStatus(AutoIngestJob.ProcessingStatus.DELETED); + coordinationService.setNodeData(CategoryNode.MANIFESTS, manifestFilePath, nodeData.toArray()); + } catch (CoordinationServiceException | InvalidDataException ex) { + logger.log(Level.WARNING, String.format("Error marking as deleted the manifest file znode for %s for %s", manifestFilePath, caseNodeData.getDisplayName()), ex); + allNodesMarked = false; + } + } + if (allNodesMarked) { + setDeletedItemFlag(CaseNodeData.DeletedFlags.MANIFEST_FILE_NODES); + } + } + + /** + * Deletes the case resources coordination service node. + * + * @throws InterruptedException If the thread in which this task is running + * is interrupted while blocked waiting for a + * coordination service operation to complete. + */ + private void deleteCaseResourcesNode() throws InterruptedException { + if (deleteOption == DeleteOptions.DELETE_OUTPUT || deleteOption == DeleteOptions.DELETE_INPUT_AND_OUTPUT || deleteOption == DeleteOptions.DELETE_CASE) { + progress.progress(Bundle.DeleteCaseTask_progress_deletingResourcesLockNode()); + logger.log(Level.INFO, String.format("Deleting case resources log znode for %s", caseNodeData.getDisplayName())); + String resourcesNodePath = CoordinationServiceUtils.getCaseResourcesNodePath(caseNodeData.getDirectory()); + try { + coordinationService.deleteNode(CategoryNode.CASES, resourcesNodePath); + } catch (CoordinationServiceException ex) { + if (!DeleteCaseUtils.isNoNodeException(ex)) { + logger.log(Level.SEVERE, String.format("Error deleting case resources znode for %s", caseNodeData.getDisplayName()), ex); + } + } + } + } + + /** + * Deletes the case auto ingest log coordination service node. + * + * @throws InterruptedException If the thread in which this task is running + * is interrupted while blocked waiting for a + * coordination service operation to complete. + */ + private void deleteCaseAutoIngestLogNode() throws InterruptedException { + if (deleteOption == DeleteOptions.DELETE_OUTPUT || deleteOption == DeleteOptions.DELETE_INPUT_AND_OUTPUT || deleteOption == DeleteOptions.DELETE_CASE) { + progress.progress(Bundle.DeleteCaseTask_progress_deletingJobLogLockNode()); + logger.log(Level.INFO, String.format("Deleting case auto ingest job log znode for %s", caseNodeData.getDisplayName())); + String logFilePath = CoordinationServiceUtils.getCaseAutoIngestLogNodePath(caseNodeData.getDirectory()); + try { + coordinationService.deleteNode(CategoryNode.CASES, logFilePath); + } catch (CoordinationServiceException ex) { + if (!DeleteCaseUtils.isNoNodeException(ex)) { + logger.log(Level.SEVERE, String.format("Error deleting case auto ingest job log znode for %s", caseNodeData.getDisplayName()), ex); + } + } + } + } + + /** + * Deletes the case directory coordination service node if everything that + * was supposed to be deleted was deleted. Otherwise, leave the node so that + * what was and was not deleted can be inspected. + * + * @throws InterruptedException If the thread in which this task is running + * is interrupted while blocked waiting for a + * coordination service operation to complete. + */ + private void deleteCaseDirectoryNode() throws InterruptedException { + if (((deleteOption == DeleteOptions.DELETE_OUTPUT || deleteOption == DeleteOptions.DELETE_INPUT_AND_OUTPUT) + && caseNodeData.isDeletedFlagSet(CaseNodeData.DeletedFlags.DATA_SOURCES) + && caseNodeData.isDeletedFlagSet(CaseNodeData.DeletedFlags.CASE_DB) + && caseNodeData.isDeletedFlagSet(CaseNodeData.DeletedFlags.TEXT_INDEX) + && caseNodeData.isDeletedFlagSet(CaseNodeData.DeletedFlags.CASE_DIR) + && caseNodeData.isDeletedFlagSet(CaseNodeData.DeletedFlags.MANIFEST_FILE_NODES)) + || (deleteOption == DeleteOptions.DELETE_CASE + && caseNodeData.isDeletedFlagSet(CaseNodeData.DeletedFlags.CASE_DB) + && caseNodeData.isDeletedFlagSet(CaseNodeData.DeletedFlags.TEXT_INDEX) + && caseNodeData.isDeletedFlagSet(CaseNodeData.DeletedFlags.CASE_DIR) + && caseNodeData.isDeletedFlagSet(CaseNodeData.DeletedFlags.MANIFEST_FILE_NODES))) { + progress.progress(Bundle.DeleteCaseTask_progress_deletingCaseDirCoordSvcNode()); + logger.log(Level.INFO, String.format("Deleting case directory znode for %s", caseNodeData.getDisplayName())); + String caseDirNodePath = CoordinationServiceUtils.getCaseDirectoryNodePath(caseNodeData.getDirectory()); + try { + coordinationService.deleteNode(CategoryNode.CASES, caseDirNodePath); + } catch (CoordinationServiceException ex) { + logger.log(Level.SEVERE, String.format("Error deleting case directory lock node for %s", caseNodeData.getDisplayName()), ex); + } + } + } + + /** + * Deletes the case name coordiation service node. + * + * @throws InterruptedException If the thread in which this task is running + * is interrupted while blocked waiting for a + * coordination service operation to complete. + */ + private void deleteCaseNameNode() throws InterruptedException { + if (deleteOption == DeleteOptions.DELETE_OUTPUT || deleteOption == DeleteOptions.DELETE_INPUT_AND_OUTPUT || deleteOption == DeleteOptions.DELETE_CASE) { + progress.progress(Bundle.DeleteCaseTask_progress_deletingCaseNameCoordSvcNode()); + logger.log(Level.INFO, String.format("Deleting case name znode for %s", caseNodeData.getDisplayName())); + try { + String caseNameLockNodeName = CoordinationServiceUtils.getCaseNameNodePath(caseNodeData.getDirectory()); + coordinationService.deleteNode(CategoryNode.CASES, caseNameLockNodeName); + } catch (CoordinationServiceException ex) { + logger.log(Level.SEVERE, String.format("Error deleting case name lock node for %s", caseNodeData.getDisplayName()), ex); + } + } + } + + /** + * Releases all of the manifest file locks that have been acquired by this + * task. + */ + @NbBundle.Messages({ + "# {0} - manifest file path", "DeleteCaseTask.progress.releasingManifestLock=Releasing lock on the manifest file {0}..." + }) + private void releaseManifestFileLocks() { + for (ManifestFileLock manifestFileLock : manifestFileLocks) { + String manifestFilePath = manifestFileLock.getManifestFilePath().toString(); + try { + progress.progress(Bundle.DeleteCaseTask_progress_releasingManifestLock(manifestFilePath)); + logger.log(Level.INFO, String.format("Releasing the exclusive coordination service lock on the manifest file %s for %s", manifestFilePath, caseNodeData.getDisplayName())); + manifestFileLock.release(); + } catch (CoordinationServiceException ex) { + logger.log(Level.WARNING, String.format("Error releasing the exclusive coordination service lock on the manifest file %s for %s", manifestFilePath, caseNodeData.getDisplayName()), ex); + } + } + manifestFileLocks.clear(); + } + + /** + * Releases all of the manifest file locks that have been acquired by this + * task and attempts to delete the corresponding coordination service nodes. + * + * @return True if all of the manifest file coordianiton service nodes have + * been deleted, false otherwise. + * + * @throws InterruptedException If the thread in which this task is running + * is interrupted while blocked waiting for a + * coordination service operation to complete. + */ + @Messages({ + "# {0} - manifest file path", "DeleteCaseTask.progress.deletingManifestFileNode=Deleting the manifest file znode for {0}..." + }) + private void deleteManifestFileNodes() throws InterruptedException { + if (deleteOption == DeleteOptions.DELETE_OUTPUT || deleteOption == DeleteOptions.DELETE_INPUT_AND_OUTPUT) { + boolean allINodesDeleted = true; + Iterator iterator = manifestFileLocks.iterator(); + while (iterator.hasNext()) { + ManifestFileLock manifestFileLock = iterator.next(); + String manifestFilePath = manifestFileLock.getManifestFilePath().toString(); + try { + progress.progress(Bundle.DeleteCaseTask_progress_releasingManifestLock(manifestFilePath)); + logger.log(Level.INFO, String.format("Releasing the lock on the manifest file %s for %s", manifestFilePath, caseNodeData.getDisplayName())); + manifestFileLock.release(); + if (manifestFileLock.isInputDeleted()) { + progress.progress(Bundle.DeleteCaseTask_progress_deletingManifestFileNode(manifestFilePath)); + logger.log(Level.INFO, String.format("Deleting the manifest file znode for %s for %s", manifestFilePath, caseNodeData.getDisplayName())); + coordinationService.deleteNode(CoordinationService.CategoryNode.MANIFESTS, manifestFilePath); + } else { + allINodesDeleted = false; + } + } catch (CoordinationServiceException ex) { + allINodesDeleted = false; + logger.log(Level.WARNING, String.format("Error deleting the manifest file znode for %s for %s", manifestFilePath, caseNodeData.getDisplayName()), ex); + } + iterator.remove(); + } + if (allINodesDeleted) { + setDeletedItemFlag(CaseNodeData.DeletedFlags.MANIFEST_FILE_NODES); + } + } + } + + /** + * Sets a deleted item flag in the coordination service node data for the + * case. + * + * @param flag The flag to set. + * + * @throws InterruptedException If the interrupted flag is set. + */ + private void setDeletedItemFlag(CaseNodeData.DeletedFlags flag) throws InterruptedException { + try { + caseNodeData.setDeletedFlag(flag); + CaseNodeData.writeCaseNodeData(caseNodeData); + } catch (CaseNodeDataException ex) { + logger.log(Level.SEVERE, String.format("Error updating deleted item flag %s for %s", flag.name(), caseNodeData.getDisplayName()), ex); + } + } + + /** + * Checks whether the interrupted flag of the current thread is set. + * + * @throws InterruptedException If the interrupted flag is set. + */ + private void checkForCancellation() throws InterruptedException { + if (Thread.currentThread().isInterrupted()) { + throw new InterruptedException("Interrupt detected"); + } + } + + /** + * A wrapper class that bundles a manifest file coordination service lock + * with a manifest file path and a flag indicating whether or not the case + * input (manifest file and data source) associated with the lock has been + * deleted. + */ + private static class ManifestFileLock { + + private final Path manifestFilePath; + private final Lock lock; + private boolean inputDeleted; + + /** + * Constructs an instance of a wrapper class that bundles a manifest + * file coordination service lock with a manifest file path and a flag + * indicating whether or not the case input (manifest file and data + * source) associated with the lock has been deleted. + * + * @param manifestFilePath The manifest file path. + * @param lock The coordination service lock. + */ + private ManifestFileLock(Path manifestFilePath, Lock lock) { + this.manifestFilePath = manifestFilePath; + this.lock = lock; + this.inputDeleted = false; + } + + /** + * Gets the path of the manifest file associated with the lock. + * + * @return + */ + Path getManifestFilePath() { + return this.manifestFilePath; + } + + /** + * Sets the flag that indicates whether or not the case input (manifest + * file and data source) associated with the lock has been deleted. + */ + private void setInputDeleted() { + this.inputDeleted = true; + } + + /** + * Gets the value of the flag that indicates whether or not the case + * input (manifest file and data source) associated with the lock has + * been deleted. + * + * @return True or false. + */ + private boolean isInputDeleted() { + return this.inputDeleted; + } + + /** + * Releases the manifest file lock. + * + * @throws CoordinationServiceException If an error occurs while + * releasing the lock. + */ + private void release() throws CoordinationServiceException { + lock.release(); + } + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseUtils.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseUtils.java new file mode 100755 index 0000000000..848bc0a80d --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCaseUtils.java @@ -0,0 +1,55 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019-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.experimental.autoingest; + +import org.sleuthkit.autopsy.coordinationservice.CoordinationService; + +/** + * A utility class supplying helper methods for case deletion. + */ +final class DeleteCaseUtils { + + private static final String NO_NODE_ERROR_MSG_FRAGMENT = "KeeperErrorCode = NoNode"; + + /** + * Examines a coordination service exception to try to determine if it is a + * no node exception. + * + * @param ex A coordination service exception. + * + * @return True or false. + */ + static boolean isNoNodeException(CoordinationService.CoordinationServiceException ex) { + boolean isNodeNodeEx = false; + Throwable cause = ex.getCause(); + if (cause != null) { + String causeMessage = cause.getMessage(); + isNodeNodeEx = causeMessage.contains(NO_NODE_ERROR_MSG_FRAGMENT); + } + return isNodeNodeEx; + } + + /** + * A private constructor to prevent instantiation. + */ + private DeleteCaseUtils() { + + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCasesAction.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCasesAction.java deleted file mode 100755 index a1fcc632bb..0000000000 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteCasesAction.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2019-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.experimental.autoingest; - -import java.awt.event.ActionEvent; -import java.util.ArrayList; -import java.util.Collection; -import javax.swing.AbstractAction; -import org.openide.util.NbBundle; -import org.openide.util.Utilities; -import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; - -/** - * An action that completely deletes one or more multi-user cases, including any - * associated auto ingest job input directories and coordination service nodes. - * - * This cases to delete are discovered by querying the actions global context - * lookup for CaseNodeData objects. See - * https://platform.netbeans.org/tutorials/nbm-selection-1.html and - * https://platform.netbeans.org/tutorials/nbm-selection-2.html for details. - */ -final class DeleteCasesAction extends AbstractAction { - - private static final long serialVersionUID = 1L; - - /** - * Constructs an action that completely deletes one or more multi-user - * cases, including any associated auto ingest job input directories and - * coordination service nodes. - */ - @NbBundle.Messages({ - "DeleteCasesAction.menuItemText=Delete Case and Jobs" - }) - DeleteCasesAction() { - super(Bundle.DeleteCasesAction_menuItemText()); - setEnabled(false); // RJCTODO: Enable when implemented - } - - @Override - public void actionPerformed(ActionEvent event) { -// final Collection selectedNodeData = new ArrayList<>(Utilities.actionsGlobalContext().lookupAll(CaseNodeData.class)); -// if (!selectedNodeData.isEmpty()) { -// /* -// * RJCTODO: Create a background task that does the deletion and -// * displays results in a dialog with a scrolling text pane. -// */ -// } - } - - @Override - public DeleteCasesAction clone() throws CloneNotSupportedException { - throw new CloneNotSupportedException(); - } - -} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteOrphanCaseNodesAction.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteOrphanCaseNodesAction.java new file mode 100755 index 0000000000..dd4d461d43 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteOrphanCaseNodesAction.java @@ -0,0 +1,56 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019-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.experimental.autoingest; + +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.progress.ProgressIndicator; + +/** + * An action class that kicks off a cancellable orphaned case nodes deletion + * task that runs in a background thread and reports progress using an + * application frame progress bar. + */ +final class DeleteOrphanCaseNodesAction extends BackgroundTaskAction { + + private static final long serialVersionUID = 1L; + + /** + * Constructs an instance of an action class that kicks off a cancellable + * orphaned case nodes deletion task that runs in a background thread and + * reports progress using an application frame progress bar. + */ + @NbBundle.Messages({ + "DeleteOrphanCaseNodesAction.progressDisplayName=Cleanup Case Znodes" + }) + DeleteOrphanCaseNodesAction() { + super(Bundle.DeleteOrphanCaseNodesAction_progressDisplayName(), Bundle.DeleteOrphanCaseNodesAction_progressDisplayName()); + } + + @Override + Runnable getTask(ProgressIndicator progress) { + return new DeleteOrphanCaseNodesTask(progress); + } + + @Override + public DeleteOrphanCaseNodesAction clone() throws CloneNotSupportedException { + super.clone(); + throw new CloneNotSupportedException(); + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteOrphanCaseNodesTask.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteOrphanCaseNodesTask.java new file mode 100755 index 0000000000..e06427e500 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteOrphanCaseNodesTask.java @@ -0,0 +1,149 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019-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.experimental.autoingest; + +import java.io.File; +import java.nio.file.Path; +import java.util.List; +import java.util.logging.Level; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; +import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeDataCollector; +import org.sleuthkit.autopsy.casemodule.multiusercases.CoordinationServiceUtils; +import org.sleuthkit.autopsy.coordinationservice.CoordinationService; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.progress.ProgressIndicator; + +/** + * Task for deleting case coordination service nodes for which there is no + * longer a corresponding case. + */ +final class DeleteOrphanCaseNodesTask implements Runnable { + + private static final Logger logger = AutoIngestDashboardLogger.getLogger(); + private final ProgressIndicator progress; + + /** + * Constucts an instance of a task for deleting case coordination service + * nodes for which there is no longer a corresponding case. + * + * @param progress + */ + DeleteOrphanCaseNodesTask(ProgressIndicator progress) { + this.progress = progress; + } + + @Override + @NbBundle.Messages({ + "DeleteOrphanCaseNodesTask.progress.startMessage=Starting orphaned case znode cleanup", + "DeleteOrphanCaseNodesTask.progress.connectingToCoordSvc=Connecting to the coordination service", + "DeleteOrphanCaseNodesTask.progress.gettingCaseNodesListing=Querying coordination service for case znodes" + }) + public void run() { + progress.start(Bundle.DeleteOrphanCaseNodesTask_progress_startMessage()); + try { + progress.progress(Bundle.DeleteOrphanCaseNodesTask_progress_connectingToCoordSvc()); + logger.log(Level.INFO, Bundle.DeleteOrphanCaseNodesTask_progress_connectingToCoordSvc()); + CoordinationService coordinationService; + try { + coordinationService = CoordinationService.getInstance(); + } catch (CoordinationService.CoordinationServiceException ex) { + logger.log(Level.SEVERE, "Error connecting to the coordination service", ex); //NON-NLS + return; + } + + progress.progress(Bundle.DeleteOrphanCaseNodesTask_progress_gettingCaseNodesListing()); + logger.log(Level.INFO, Bundle.DeleteOrphanCaseNodesTask_progress_gettingCaseNodesListing()); + List nodeDataList; + try { + nodeDataList = CaseNodeDataCollector.getNodeData(); + } catch (CoordinationService.CoordinationServiceException ex) { + logger.log(Level.SEVERE, "Error collecting case node data", ex); //NON-NLS + return; + } catch (InterruptedException unused) { + logger.log(Level.WARNING, "Task cancelled while collecting case node data"); //NON-NLS + return; + } + + for (CaseNodeData nodeData : nodeDataList) { + final Path caseDirectoryPath = nodeData.getDirectory(); + final File caseDirectory = caseDirectoryPath.toFile(); + if (!caseDirectory.exists()) { + String caseName = nodeData.getDisplayName(); + String nodePath = ""; // NON-NLS + try { + nodePath = CoordinationServiceUtils.getCaseNameNodePath(caseDirectoryPath); + deleteNode(coordinationService, caseName, nodePath); + + nodePath = CoordinationServiceUtils.getCaseResourcesNodePath(caseDirectoryPath); + deleteNode(coordinationService, caseName, nodePath); + + nodePath = CoordinationServiceUtils.getCaseAutoIngestLogNodePath(caseDirectoryPath); + deleteNode(coordinationService, caseName, nodePath); + + nodePath = CoordinationServiceUtils.getCaseDirectoryNodePath(caseDirectoryPath); + deleteNode(coordinationService, caseName, nodePath); + + } catch (InterruptedException unused) { + logger.log(Level.WARNING, String.format("Task cancelled while deleting orphaned znode %s for %s", nodePath, caseName)); //NON-NLS + return; + } + } + } + } catch (Exception ex) { + /* + * This is an unexpected runtime exceptions firewall. It is here + * because this task is designed to be able to be run in scenarios + * where there is no call to get() on a Future associated with + * the task, so this ensures that any such errors get logged. + */ + logger.log(Level.SEVERE, "Unexpected error during orphan case znode cleanup", ex); //NON-NLS + throw ex; + + } finally { + progress.finish(); + } + } + + /** + * Attempts to delete a case coordination service node. + * + * @param coordinationService The ccordination service. + * @param caseName The case name. + * @param nodePath The path of the node to delete. + * + * @throws InterruptedException If the thread executing this task is + * interrupted during the delete operation. + */ + @NbBundle.Messages({ + "# {0} - node path", "DeleteOrphanCaseNodesTask.progress.deletingOrphanedCaseNode=Deleting orphaned case znode {0}" + }) + private void deleteNode(CoordinationService coordinationService, String caseName, String nodePath) throws InterruptedException { + try { + progress.progress(Bundle.DeleteOrphanCaseNodesTask_progress_deletingOrphanedCaseNode(nodePath)); + logger.log(Level.INFO, String.format("Deleting orphaned case node %s for %s", nodePath, caseName)); //NON-NLS + coordinationService.deleteNode(CoordinationService.CategoryNode.CASES, nodePath); + } catch (CoordinationService.CoordinationServiceException ex) { + if (!DeleteCaseUtils.isNoNodeException(ex)) { + logger.log(Level.SEVERE, String.format("Error deleting orphaned case node %s for %s", nodePath, caseName), ex); //NON-NLS + } + } + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteOrphanManifestNodesAction.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteOrphanManifestNodesAction.java new file mode 100755 index 0000000000..e106b7a5de --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteOrphanManifestNodesAction.java @@ -0,0 +1,56 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019-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.experimental.autoingest; + +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.progress.ProgressIndicator; + +/** + * An action class that kicks off a cancellable orphaned manifest file nodes + * deletion task that runs in a background thread and reports progress using an + * application frame progress bar. + */ +public class DeleteOrphanManifestNodesAction extends BackgroundTaskAction { + + private static final long serialVersionUID = 1L; + + /** + * Constructs an instance of an action class that kicks off a cancellable + * orphaned manifest file nodes deletion task that runs in a background + * thread and reports progress using an application frame progress bar. + */ + @NbBundle.Messages({ + "DeleteOrphanManifestNodesAction.progressDisplayName=Cleanup Manifest File Znodes" + }) + DeleteOrphanManifestNodesAction() { + super(Bundle.DeleteOrphanManifestNodesAction_progressDisplayName(), Bundle.DeleteOrphanManifestNodesAction_progressDisplayName()); + } + + @Override + Runnable getTask(ProgressIndicator progress) { + return new DeleteOrphanManifestNodesTask(progress); + } + + @Override + public DeleteOrphanManifestNodesAction clone() throws CloneNotSupportedException { + super.clone(); + throw new CloneNotSupportedException(); + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteOrphanManifestNodesTask.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteOrphanManifestNodesTask.java new file mode 100755 index 0000000000..158a092522 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DeleteOrphanManifestNodesTask.java @@ -0,0 +1,116 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019-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.experimental.autoingest; + +import java.io.File; +import java.nio.file.Path; +import java.util.List; +import java.util.logging.Level; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coordinationservice.CoordinationService; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.progress.ProgressIndicator; + +/** + * A task class for cleaning up auto ingest job coordination service nodes for + * which there is no longer a corresponding manifest file. + */ +final class DeleteOrphanManifestNodesTask implements Runnable { + + private static final Logger logger = Logger.getLogger(DeleteOrphanManifestNodesTask.class.getName()); + private final ProgressIndicator progress; + + /** + * Constucts an instance of a task for cleaning up case coordination service + * nodes for which there is no longer a corresponding case. + * + * @param progress + */ + DeleteOrphanManifestNodesTask(ProgressIndicator progress) { + this.progress = progress; + } + + @Override + @NbBundle.Messages({ + "DeleteOrphanManifestNodesTask.progress.startMessage=Starting orphaned manifest file znode cleanup", + "DeleteOrphanManifestNodesTask.progress.connectingToCoordSvc=Connecting to the coordination service", + "DeleteOrphanManifestNodesTask.progress.gettingManifestNodes=Querying the coordination service for manifest file znodes", + "# {0} - node path", "DeleteOrphanManifestNodesTask.progress.deletingOrphanedManifestNode=Deleting orphaned manifest file znode {0}" + }) + public void run() { + progress.start(Bundle.DeleteOrphanManifestNodesTask_progress_startMessage()); + try { + progress.progress(Bundle.DeleteOrphanManifestNodesTask_progress_connectingToCoordSvc()); + logger.log(Level.INFO, Bundle.DeleteOrphanManifestNodesTask_progress_connectingToCoordSvc()); + CoordinationService coordinationService; + try { + coordinationService = CoordinationService.getInstance(); + } catch (CoordinationService.CoordinationServiceException ex) { + logger.log(Level.SEVERE, "Error connecting to the coordination service", ex); // NON-NLS + return; + } + + progress.progress(Bundle.DeleteOrphanManifestNodesTask_progress_gettingManifestNodes()); + logger.log(Level.INFO, Bundle.DeleteOrphanManifestNodesTask_progress_gettingManifestNodes()); + List nodeDataList; + try { + nodeDataList = AutoIngestJobNodeDataCollector.getNodeData(); + } catch (CoordinationService.CoordinationServiceException ex) { + logger.log(Level.SEVERE, "Error collecting auto ingest job node data", ex); // NON-NLS + return; + } catch (InterruptedException unused) { + logger.log(Level.WARNING, "Task cancelled while collecting auto ingest job node data"); // NON-NLS + return; + } + + for (AutoIngestJobNodeData nodeData : nodeDataList) { + final String caseName = nodeData.getCaseName(); + final Path manifestFilePath = nodeData.getManifestFilePath(); + final File manifestFile = manifestFilePath.toFile(); + if (!manifestFile.exists()) { + try { + progress.progress(Bundle.DeleteOrphanManifestNodesTask_progress_deletingOrphanedManifestNode(manifestFilePath)); + logger.log(Level.INFO, String.format("Deleting orphaned manifest file znode %s for %s", manifestFilePath, caseName)); + coordinationService.deleteNode(CoordinationService.CategoryNode.MANIFESTS, manifestFilePath.toString()); + } catch (CoordinationService.CoordinationServiceException ex) { + if (!DeleteCaseUtils.isNoNodeException(ex)) { + logger.log(Level.SEVERE, String.format("Error deleting %s znode for %s", manifestFilePath, caseName), ex); // NON-NLS + } + } catch (InterruptedException unused) { + logger.log(Level.WARNING, String.format("Task cancelled while deleting %s znode for %s", manifestFilePath, caseName)); // NON-NLS + return; + } + } + } + } catch (Exception ex) { + /* + * This is an unexpected runtime exceptions firewall. It is here + * because this task is designed to be able to be run in scenarios + * where there is no call to get() on a Future associated with + * the task, so this ensures that any such errors get logged. + */ + logger.log(Level.SEVERE, "Unexpected error deleting orphan manifest file znodes", ex); // NON-NLS + throw ex; + + } finally { + progress.finish(); + } + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ShowCaseDeletionStatusAction.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ShowCaseDeletionStatusAction.java deleted file mode 100755 index c691cd2038..0000000000 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ShowCaseDeletionStatusAction.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * 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.experimental.autoingest; - -import java.awt.event.ActionEvent; -import javax.swing.AbstractAction; -import org.openide.util.NbBundle; -import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; - -/** - * An action that shows a popup that enumerates the deletion status of the - * various parts of a multi-user case known to the coordination service. - */ -final class ShowCaseDeletionStatusAction extends AbstractAction { - - private static final long serialVersionUID = 1L; - // private final CaseNodeData caseNodeData; - - /** - * Constructs an action that shows a popup that enumerates the deletion - * status of the various parts of a multi-user case known to the - * coordination service. - * - * @param caseNodeData The coordination service node data for the case. - */ - @NbBundle.Messages({ - "ShowCaseDeletionStatusAction.menuItemText=Show Deletion Status" - }) - ShowCaseDeletionStatusAction(CaseNodeData caseNodeData) { - super(Bundle.ShowCaseDeletionStatusAction_menuItemText()); - // this.caseNodeData = caseNodeData; // RJCTODO: Use - setEnabled(false); // RJCTODO: Enable when implemented - } - - @Override - public void actionPerformed(ActionEvent e) { - // RJCTODO: Implement - } - - @Override - public ShowCaseDeletionStatusAction clone() throws CloneNotSupportedException { - throw new CloneNotSupportedException(); - } - -} diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java index d25e8e8659..1881281475 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java @@ -514,7 +514,7 @@ public final class DrawableDB { private boolean initializeDBSchema() { dbWriteLock(); try { - boolean existingDB = true; + boolean drawableDbTablesExist = true; if (isClosed()) { logger.log(Level.SEVERE, "The drawables database is closed"); //NON-NLS @@ -533,11 +533,11 @@ public final class DrawableDB { */ try (Statement stmt = con.createStatement()) { - // Check if the database is a new or existing database - existingDB = doesTableExist("datasources"); + // Check if the database is new or an existing database + drawableDbTablesExist = doesTableExist("drawable_files"); if (false == doesTableExist(IG_DB_INFO_TABLE)) { try { - VersionNumber ig_creation_schema_version = existingDB + VersionNumber ig_creation_schema_version = drawableDbTablesExist ? IG_STARTING_SCHEMA_VERSION : IG_SCHEMA_VERSION; @@ -651,7 +651,8 @@ public final class DrawableDB { String autogenKeyType = (DbType.POSTGRESQL == tskCase.getDatabaseType()) ? "BIGSERIAL" : "INTEGER"; try { - VersionNumber ig_creation_schema_version = existingDB + boolean caseDbTablesExist = tskCase.getCaseDbAccessManager().tableExists(GROUPS_TABLENAME); + VersionNumber ig_creation_schema_version = caseDbTablesExist ? IG_STARTING_SCHEMA_VERSION : IG_SCHEMA_VERSION; @@ -935,13 +936,14 @@ public final class DrawableDB { */ private VersionNumber upgradeCaseDbIgSchema1dot0TO1dot1(VersionNumber currVersion, CaseDbTransaction caseDbTransaction ) throws TskCoreException { - if (currVersion.getMajor() != 1 || - currVersion.getMinor() != 0) { + // Upgrade if current version is 1.0 + // or 1.1 - a bug in versioning alllowed some databases to be versioned as 1.1 without the actual corresponding upgrade. This allows such databases to be fixed, if needed. + if (!(currVersion.getMajor() == 1 && + (currVersion.getMinor() == 0 || currVersion.getMinor() == 1))) { return currVersion; } - // 1.0 -> 1.1 upgrade - // Add a 'isAnalyzed' column to groups table in CaseDB + // Add a 'is_analyzed' column to groups table in CaseDB String alterSQL = " ADD COLUMN is_analyzed integer DEFAULT 1 "; //NON-NLS if (false == tskCase.getCaseDbAccessManager().columnExists(GROUPS_TABLENAME, "is_analyzed", caseDbTransaction )) { tskCase.getCaseDbAccessManager().alterTable(GROUPS_TABLENAME, alterSQL, caseDbTransaction); diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java index 67f02d69f1..d555f0569b 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java @@ -700,7 +700,7 @@ public class GroupManager { try { DrawableFile file = getDrawableDB().getFileFromID(fileId); String pathVal = file.getDrawablePath(); - GroupKey pathGroupKey = new GroupKey(DrawableAttribute.PATH,pathVal, file.getDataSource()); + GroupKey pathGroupKey = new GroupKey<>(DrawableAttribute.PATH,pathVal, file.getDataSource()); updateCurrentPathGroup(pathGroupKey); } catch (TskCoreException | TskDataException ex) { @@ -712,7 +712,6 @@ public class GroupManager { for (GroupKey gk : groupsForFile) { // see if a group has been created yet for the key DrawableGroup g = getGroupForKey(gk); - addFileToGroup(g, gk, fileId); } } @@ -816,8 +815,10 @@ public class GroupManager { controller.getCategoryManager().registerListener(group); groupMap.put(groupKey, group); } - - if (analyzedGroups.contains(group) == false) { + + // Add to analyzedGroups only if it's the same group type as the one in view + if ((analyzedGroups.contains(group) == false) && + (getGroupBy() == group.getGroupKey().getAttribute())) { // Add to analyzedGroups only if this is the grouping being viewed. if (getGroupBy() == group.getGroupKey().getAttribute()) { analyzedGroups.add(group); diff --git a/KeywordSearch/nbproject/project.properties b/KeywordSearch/nbproject/project.properties index b47080f282..42f016f8b7 100644 --- a/KeywordSearch/nbproject/project.properties +++ b/KeywordSearch/nbproject/project.properties @@ -131,6 +131,7 @@ file.reference.uimaj-tools-2.6.0.jar=release/modules/ext/uimaj-tools-2.6.0.jar file.reference.vorbis-java-core-0.8.jar=release/modules/ext/vorbis-java-core-0.8.jar file.reference.vorbis-java-tika-0.8.jar=release/modules/ext/vorbis-java-tika-0.8.jar file.reference.woodstox-core-asl-4.4.1.jar=release/modules/ext/woodstox-core-asl-4.4.1.jar +file.reference.wstx-asl-3.2.7.jar=release\\modules\\ext\\wstx-asl-3.2.7.jar file.reference.xmlbeans-2.6.0.jar=release/modules/ext/xmlbeans-2.6.0.jar file.reference.xmpcore-5.1.3.jar=release/modules/ext/xmpcore-5.1.3.jar file.reference.zookeeper-3.4.6.jar=release/modules/ext/zookeeper-3.4.6.jar diff --git a/KeywordSearch/nbproject/project.xml b/KeywordSearch/nbproject/project.xml index b87ba81e88..6011d2acdc 100644 --- a/KeywordSearch/nbproject/project.xml +++ b/KeywordSearch/nbproject/project.xml @@ -49,14 +49,6 @@ 1.31.1 - - org.netbeans.swing.outline - - - - 1.34.1 - - org.openide.awt @@ -141,9 +133,107 @@ + com.ctc.wstx.api + com.ctc.wstx.cfg + com.ctc.wstx.compat + com.ctc.wstx.dom + com.ctc.wstx.dtd + com.ctc.wstx.ent + com.ctc.wstx.evt + com.ctc.wstx.exc + com.ctc.wstx.io + com.ctc.wstx.msv + com.ctc.wstx.sax + com.ctc.wstx.sr + com.ctc.wstx.stax + com.ctc.wstx.sw + com.ctc.wstx.util org.apache.commons.logging.impl + org.apache.http + org.apache.http.annotation + org.apache.http.auth + org.apache.http.auth.params + org.apache.http.client + org.apache.http.client.config + org.apache.http.client.entity + org.apache.http.client.methods + org.apache.http.client.params + org.apache.http.client.protocol + org.apache.http.client.utils + org.apache.http.concurrent + org.apache.http.config + org.apache.http.conn + org.apache.http.conn.params + org.apache.http.conn.routing + org.apache.http.conn.scheme + org.apache.http.conn.socket + org.apache.http.conn.ssl + org.apache.http.conn.util + org.apache.http.cookie + org.apache.http.cookie.params + org.apache.http.entity + org.apache.http.entity.mime + org.apache.http.entity.mime.content + org.apache.http.impl + org.apache.http.impl.auth + org.apache.http.impl.bootstrap + org.apache.http.impl.client + org.apache.http.impl.conn + org.apache.http.impl.conn.tsccm + org.apache.http.impl.cookie + org.apache.http.impl.entity + org.apache.http.impl.execchain + org.apache.http.impl.io + org.apache.http.impl.pool + org.apache.http.io + org.apache.http.message + org.apache.http.params + org.apache.http.pool + org.apache.http.protocol + org.apache.http.ssl + org.apache.http.util + org.apache.jute + org.apache.jute.compiler + org.apache.jute.compiler.generated + org.apache.solr.client.solrj + org.apache.solr.client.solrj.beans + org.apache.solr.client.solrj.impl + org.apache.solr.client.solrj.request + org.apache.solr.client.solrj.response + org.apache.solr.client.solrj.util + org.apache.solr.common + org.apache.solr.common.cloud + org.apache.solr.common.luke + org.apache.solr.common.params + org.apache.solr.common.util org.apache.tika.parser.txt + org.apache.zookeeper + org.apache.zookeeper.client + org.apache.zookeeper.common + org.apache.zookeeper.data + org.apache.zookeeper.jmx + org.apache.zookeeper.proto + org.apache.zookeeper.server + org.apache.zookeeper.server.auth + org.apache.zookeeper.server.persistence + org.apache.zookeeper.server.quorum + org.apache.zookeeper.server.quorum.flexible + org.apache.zookeeper.server.upgrade + org.apache.zookeeper.server.util + org.apache.zookeeper.txn + org.apache.zookeeper.version + org.apache.zookeeper.version.util + org.codehaus.stax2 + org.codehaus.stax2.evt + org.codehaus.stax2.io + org.codehaus.stax2.ri + org.codehaus.stax2.validation + org.noggit org.sleuthkit.autopsy.keywordsearch + org.slf4j + org.slf4j.event + org.slf4j.helpers + org.slf4j.spi ext/commons-validator-1.5.1-sources.jar @@ -417,6 +507,10 @@ ext/jmatio-1.2.jar release/modules/ext/jmatio-1.2.jar + + ext/wstx-asl-3.2.7.jar + release\modules\ext\wstx-asl-3.2.7.jar + ext/commons-csv-1.0.jar release/modules/ext/commons-csv-1.0.jar diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Keyword.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Keyword.java index b56415094e..d7c7d7705f 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Keyword.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Keyword.java @@ -31,7 +31,7 @@ import org.sleuthkit.datamodel.BlackboardAttribute; * with a keyword. This feature was added to support an initial implementation * of account number search and may be removed in the future. */ -class Keyword { +public class Keyword { private String searchTerm; private boolean isLiteral; @@ -123,7 +123,7 @@ class Keyword { * * @return The search term. */ - String getSearchTerm() { + public String getSearchTerm() { return searchTerm; } @@ -133,7 +133,7 @@ class Keyword { * * @return True or false. */ - boolean searchTermIsLiteral() { + public boolean searchTermIsLiteral() { return isLiteral; } @@ -144,7 +144,7 @@ class Keyword { * * @return True or false. */ - boolean searchTermIsWholeWord() { + public boolean searchTermIsWholeWord() { return isWholeWord; } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordList.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordList.java index 13f094a89d..f2db250baa 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordList.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordList.java @@ -167,7 +167,7 @@ public class KeywordList { * * @return A colleciton of Keyword objects. */ - List getKeywords() { + public List getKeywords() { return keywords; } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordListsManager.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordListsManager.java index bdae07a971..0a83603bbb 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordListsManager.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordListsManager.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2014 Basis Technology Corp. + * Copyright 2014-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -52,6 +52,8 @@ public class KeywordListsManager extends Observable { /** * Gets the singleton instance of the keyword lists manager. + * + * @return an instance of KeywordListsManager. */ public static synchronized KeywordListsManager getInstance() { if (instance == null) { @@ -73,6 +75,17 @@ public class KeywordListsManager extends Observable { return names; } + /** + * Get keyword list by name. + * + * @param name id of the list + * + * @return keyword list representation, null if no list by that name + */ + public KeywordList getList(String name) { + return XmlKeywordSearchList.getCurrent().getList(name); + } + /** * Force reload of the keyword lists XML file. */ diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/Bundle.properties b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/Bundle.properties deleted file mode 100755 index 97b54b454f..0000000000 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/Bundle.properties +++ /dev/null @@ -1,21 +0,0 @@ -SelectMultiUserCasesPanel.selectAllButton.text=Select All -SelectMultiUserCasesPanel.deselectAllButton.text=Deselect All -SelectMultiUserCasesPanel.jLabel1.text=Select case(s) for keyword search or start typing to search by case name -SelectMultiUserCasesPanel.confirmSelections.text=OK -SelectMultiUserCasesPanel.cancelButton.text=Cancel -MultiCaseKeywordSearchErrorDialog.closeButton.text=Close -MultiCaseKeywordSearchPanel.exactRadioButton.text_1=Exact Match -MultiCaseKeywordSearchPanel.substringRadioButton.text_1=Substring Match -MultiCaseKeywordSearchPanel.regexRadioButton.text_1=Regular Expression -MultiCaseKeywordSearchPanel.keywordTextField.text_1= -MultiCaseKeywordSearchPanel.toolDescriptionTextArea.text=Perform a keyword search on the selected cases. The case can be opened to examine the results more closely. -MultiCaseKeywordSearchPanel.casesLabel.text_1=Cases -MultiCaseKeywordSearchPanel.resultsLabel.text=Results -MultiCaseKeywordSearchPanel.searchButton.text=Search -MultiCaseKeywordSearchPanel.viewErrorsButton.text=View Errors -MultiCaseKeywordSearchPanel.warningLabel.text= -MultiCaseKeywordSearchPanel.exportButton.text=Export Results -MultiCaseKeywordSearchPanel.cancelButton.text=Cancel -MultiCaseKeywordSearchPanel.resultsCountLabel.text= -MultiCaseKeywordSearchPanel.pickCasesButton.text_1=Add Cases -SelectMultiUserCasesPanel.refreshButton.text=Refresh diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/Bundle.properties-MERGED b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/Bundle.properties-MERGED deleted file mode 100755 index a42701dd53..0000000000 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/Bundle.properties-MERGED +++ /dev/null @@ -1,102 +0,0 @@ -CTL_MultiCaseKeywordSearchOpenAction=Multi-case Keyword Search -CTL_MultiCaseKeywordSearchTopComponent=Multi-case Keyword Search -CTL_MultiCaseKeywordSearchTopComponentAction=Multi-case Keyword Search -MultiCaseKeywordSearchErrorDialog.title.text=Error(s) While Searching -MultiCaseKeywordSearchNode.copyResultAction.text=Copy to clipboard -MultiCaseKeywordSearchNode.OpenCaseAction.text=Open Case -MultiCaseKeywordSearchNode.properties.case=Case -MultiCaseKeywordSearchNode.properties.caseDirectory=Case Directory -MultiCaseKeywordSearchNode.properties.dataSource=Data Source -MultiCaseKeywordSearchNode.properties.path=Keyword Hit Source Path -MultiCaseKeywordSearchNode.properties.source=Keyword Hit Source -MultiCaseKeywordSearchNode.properties.sourceType=Keyword Hit Source Type -MultiCaseKeywordSearchPanel.continueSearch.text=A search is currently being performed. Would you like the search to continue in the background while the search window is closed? -MultiCaseKeywordSearchPanel.continueSearch.title=Closing multi-case search -MultiCaseKeywordSearchPanel.countOfResults.label=Count: -MultiCaseKeywordSearchPanel.emptyNode.waitText=Please Wait... -# {0} - numberOfErrors -MultiCaseKeywordSearchPanel.errorsEncounter.text={0} Error(s) encountered while performing search -MultiCaseKeywordSearchPanel.searchResultsExport.csvExtensionFilterlbl=Comma Separated Values File (csv) -# {0} - file name -MultiCaseKeywordSearchPanel.searchResultsExport.exportMsg=Search results exported to {0} -MultiCaseKeywordSearchPanel.searchResultsExport.failedExportMsg=Export of search results failed -MultiCaseKeywordSearchPanel.searchResultsExport.featureName=Search Results Export -# {0} - file name -MultiCaseKeywordSearchPanel.searchResultsExport.fileExistPrompt=File {0} exists, overwrite? -MultiCaseKeywordSearchPanel.searchThread.cancellingText=Cancelling search -MultiCaseKeywordSearchPanel.warningText.emptySearch=You must enter something to search for in the text field. -MultiCaseKeywordSearchPanel.warningText.noCases=At least one case must be selected to perform a search. -MultiCaseKeywordSearchTopComponent.exceptionMessage.failedToCreatePanel=Failed to create Multi-case Keyword Search panel. -MultiCaseKeywordSearchTopComponent.name.text=Multi-case Keyword Search -MultiCaseSearcher.exceptionMessage.cancelledMessage=Search cancelled -# {0} - connection info -# {1} - case name -# {2} - case directory -MultiCaseSearcher.exceptionMessage.errorLoadingCore=Error connecting to Solr server and loading core (URL: {0}) for case {1} in {2} -# {0} - PostgreSQL server host -# {1} - PostgreSQL server port -# {2} - case database name -# {3} - case directory -MultiCaseSearcher.exceptionMessage.errorOpeningCaseDatabase=Error connecting to PostgreSQL server (Host/Port: [{0}:{1}] and opening case database {2} for case at {3} -# {0} - case directory -MultiCaseSearcher.exceptionMessage.failedToFindCaseMetadata=Failed to find case metadata file in {0} -# {0} - case_name -MultiCaseSearcher.exceptionMessage.failedToGetCaseDatabaseConnectionInfo=Failed to get case database connection info for case {0} -# {0} - case directory path -MultiCaseSearcher.exceptionMessage.failedToGetCaseDirReadlock=Failed to obtain read lock for case directory at {0} -# {0} - case directory -MultiCaseSearcher.exceptionMessage.failedToParseCaseMetadata=Failed to parse case file metadata in {0} -# {0} - Solr document id -# {1} - case database name -# {2} - case directory -MultiCaseSearcher.exceptionMessage.hitProcessingError=Failed to query case database for processing of Solr object id {0} of case {1} in {2} -# {0} - file name -# {1} - case directory -MultiCaseSearcher.exceptionMessage.missingSolrPropertiesFile=Missing {0} file in {1} -# {0} - file name -# {1} - case directory -MultiCaseSearcher.exceptionMessage.solrPropertiesFileParseError=Error parsing {0} file in {1} -# {0} - query -# {1} - case_name -MultiCaseSearcher.exceptionMessage.solrQueryError=Failed to execute query "{0}" on case {1} -# {0} - case name -# {1} - case counter -# {2} - total cases -MultiCaseSearcher.progressMessage.acquiringSharedLockForCase=Acquiring shared lock for "{0}" ({1} of {2} case(s)) -MultiCaseSearcher.progressMessage.creatingSolrQuery=Creating search query for Solr server -# {0} - case name -# {1} - case counter -# {2} - total cases -MultiCaseSearcher.progressMessage.executingSolrQueryForCase=Getting keyword hits for "{0}" ({1} of {2} case(s)) -MultiCaseSearcher.progressMessage.findingCases=Finding selected cases -# {0} - case name -# {1} - case counter -# {2} - total cases -MultiCaseSearcher.progressMessage.loadingSolrCoreForCase=Loading Solr core for "{0}" ({1} of {2} case(s)) -# {0} - case name -# {1} - case counter -# {2} - total cases -MultiCaseSearcher.progressMessage.openingCaseDbForCase=Opening case database for "{0}" ({1} of {2} case(s)) -# {0} - total cases -MultiCaseSearcher.progressMessage.startingCaseSearches=Searching {0} case(s) -SelectMultiUserCasesPanel.selectAllButton.text=Select All -SelectMultiUserCasesPanel.deselectAllButton.text=Deselect All -SelectMultiUserCasesPanel.jLabel1.text=Select case(s) for keyword search or start typing to search by case name -SelectMultiUserCasesPanel.confirmSelections.text=OK -SelectMultiUserCasesPanel.cancelButton.text=Cancel -MultiCaseKeywordSearchErrorDialog.closeButton.text=Close -MultiCaseKeywordSearchPanel.exactRadioButton.text_1=Exact Match -MultiCaseKeywordSearchPanel.substringRadioButton.text_1=Substring Match -MultiCaseKeywordSearchPanel.regexRadioButton.text_1=Regular Expression -MultiCaseKeywordSearchPanel.keywordTextField.text_1= -MultiCaseKeywordSearchPanel.toolDescriptionTextArea.text=Perform a keyword search on the selected cases. The case can be opened to examine the results more closely. -MultiCaseKeywordSearchPanel.casesLabel.text_1=Cases -MultiCaseKeywordSearchPanel.resultsLabel.text=Results -MultiCaseKeywordSearchPanel.searchButton.text=Search -MultiCaseKeywordSearchPanel.viewErrorsButton.text=View Errors -MultiCaseKeywordSearchPanel.warningLabel.text= -MultiCaseKeywordSearchPanel.exportButton.text=Export Results -MultiCaseKeywordSearchPanel.cancelButton.text=Cancel -MultiCaseKeywordSearchPanel.resultsCountLabel.text= -MultiCaseKeywordSearchPanel.pickCasesButton.text_1=Add Cases -SelectMultiUserCasesPanel.refreshButton.text=Refresh diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchErrorDialog.form b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchErrorDialog.form deleted file mode 100755 index c93c91817b..0000000000 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchErrorDialog.form +++ /dev/null @@ -1,82 +0,0 @@ - - -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchErrorDialog.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchErrorDialog.java deleted file mode 100755 index 69d5755d37..0000000000 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchErrorDialog.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * 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.keywordsearch.multicase; - -import javax.swing.JDialog; -import org.openide.util.NbBundle.Messages; -import org.openide.windows.WindowManager; - -/** - * Dialog to display the errors encounter while perfomring a multi-case keyword - * search. - */ -final class MultiCaseKeywordSearchErrorDialog extends JDialog { - - private static final long serialVersionUID = 1L; - - /** - * Creates new MultiCaseKeywordSearchErrorDialog - */ - @Messages({"MultiCaseKeywordSearchErrorDialog.title.text=Error(s) While Searching"}) - MultiCaseKeywordSearchErrorDialog(String contents) { - setTitle(Bundle.MultiCaseKeywordSearchErrorDialog_title_text()); - initComponents(); - errorsTextArea.setText(contents); - this.setLocationRelativeTo(WindowManager.getDefault().getMainWindow()); - pack(); - setModal(true); - setResizable(false); - setVisible(true); - } - - /** - * 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() { - - errorsScrollPane = new javax.swing.JScrollPane(); - errorsTextArea = new javax.swing.JTextArea(); - closeButton = new javax.swing.JButton(); - - errorsScrollPane.setPreferredSize(new java.awt.Dimension(470, 175)); - - errorsTextArea.setEditable(false); - errorsTextArea.setColumns(40); - errorsTextArea.setLineWrap(true); - errorsTextArea.setRows(5); - errorsTextArea.setWrapStyleWord(true); - errorsTextArea.setPreferredSize(new java.awt.Dimension(460, 160)); - errorsScrollPane.setViewportView(errorsTextArea); - - org.openide.awt.Mnemonics.setLocalizedText(closeButton, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchErrorDialog.class, "MultiCaseKeywordSearchErrorDialog.closeButton.text")); // NOI18N - closeButton.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - closeButtonActionPerformed(evt); - } - }); - - javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); - getContentPane().setLayout(layout); - layout.setHorizontalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) - .addGroup(layout.createSequentialGroup() - .addContainerGap() - .addComponent(errorsScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 480, Short.MAX_VALUE)) - .addGroup(layout.createSequentialGroup() - .addGap(0, 0, Short.MAX_VALUE) - .addComponent(closeButton))) - .addContainerGap()) - ); - layout.setVerticalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addContainerGap() - .addComponent(errorsScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 196, Short.MAX_VALUE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(closeButton) - .addGap(14, 14, 14)) - ); - }// //GEN-END:initComponents - - private void closeButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_closeButtonActionPerformed - dispose(); - }//GEN-LAST:event_closeButtonActionPerformed - - - // Variables declaration - do not modify//GEN-BEGIN:variables - private javax.swing.JButton closeButton; - private javax.swing.JScrollPane errorsScrollPane; - private javax.swing.JTextArea errorsTextArea; - // End of variables declaration//GEN-END:variables -} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchNode.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchNode.java deleted file mode 100755 index 04e66f1717..0000000000 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchNode.java +++ /dev/null @@ -1,292 +0,0 @@ -/* - * 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.keywordsearch.multicase; - -import java.awt.Toolkit; -import java.awt.datatransfer.StringSelection; -import java.awt.event.ActionEvent; -import java.io.File; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.logging.Level; -import javax.swing.AbstractAction; -import javax.swing.Action; -import org.openide.nodes.AbstractNode; -import org.openide.nodes.Children; -import org.openide.nodes.Node; -import org.openide.nodes.Sheet; -import org.openide.util.NbBundle; -import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.casemodule.CaseActionCancelledException; -import org.sleuthkit.autopsy.casemodule.CaseActionException; -import static org.sleuthkit.autopsy.casemodule.CaseMetadata.getFileExtension; -import org.sleuthkit.autopsy.casemodule.StartupWindowProvider; -import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; -import org.sleuthkit.autopsy.datamodel.NodeProperty; - -@NbBundle.Messages({ - "MultiCaseKeywordSearchNode.properties.case=Case", - "MultiCaseKeywordSearchNode.properties.caseDirectory=Case Directory", - "MultiCaseKeywordSearchNode.properties.dataSource=Data Source", - "MultiCaseKeywordSearchNode.properties.sourceType=Keyword Hit Source Type", - "MultiCaseKeywordSearchNode.properties.source=Keyword Hit Source", - "MultiCaseKeywordSearchNode.properties.path=Keyword Hit Source Path" -}) - -/** - * A root node containing child nodes of the results of a multi-case keyword - * Search. - */ -class MultiCaseKeywordSearchNode extends AbstractNode { - - private static final Logger LOGGER = Logger.getLogger(MultiCaseKeywordSearchNode.class.getName()); - - /** - * Construct a new MultiCaseKeywordSearchNode - * - * @param resultList the list of KeywordSearchHits which will be the - * children of this node. - */ - MultiCaseKeywordSearchNode(Collection resultList) { - super(new MultiCaseKeywordSearchChildren(resultList)); - } - - /** - * A factory for creating children of the MultiCaseKeywordSearchNode. - */ - static class MultiCaseKeywordSearchChildren extends Children.Keys { - - private final Collection resultList; - - /** - * Construct a new MultiCaseKeywordSearchChildren - * - * @param resultList the list of KeywordSearchHits which will be used to - * construct the children. - */ - MultiCaseKeywordSearchChildren(Collection resultList) { - this.resultList = resultList; - } - - @Override - protected void addNotify() { - super.addNotify(); - setKeys(resultList); - } - - @Override - protected void removeNotify() { - super.removeNotify(); - setKeys(Collections.emptyList()); - } - - @Override - protected Node[] createNodes(SearchHit t) { - return new Node[]{new SearchHitNode(t)}; - } - - @Override - public Object clone() { - return super.clone(); - } - - } - - /** - * A leaf node which represents a hit for the multi-case keyword search. - */ - static final class SearchHitNode extends AbstractNode { - - private final SearchHit searchHit; - - /** - * Construct a new SearchHitNode - * - * @param kwsHit the KeywordSearchHit which will be represented by this - * node. - */ - SearchHitNode(SearchHit kwsHit) { - super(Children.LEAF); - searchHit = kwsHit; - super.setName(searchHit.getCaseDisplayName()); - setDisplayName(searchHit.getCaseDisplayName()); - } - - @Override - public Action getPreferredAction() { - return new OpenCaseAction(getCasePath()); - } - - /** - * Get the path to the case directory - * - * @return the path to the case directory for the KeywordSearchHit - * represented by this node - */ - private String getCasePath() { - return searchHit.getCaseDirectoryPath(); - } - - @Override - protected Sheet createSheet() { - Sheet s = super.createSheet(); - Sheet.Set ss = s.get(Sheet.PROPERTIES); - if (ss == null) { - ss = Sheet.createPropertiesSet(); - s.put(ss); - } - ss.put(new NodeProperty<>(Bundle.MultiCaseKeywordSearchNode_properties_case(), Bundle.MultiCaseKeywordSearchNode_properties_case(), Bundle.MultiCaseKeywordSearchNode_properties_case(), - searchHit.getCaseDisplayName())); - ss.put(new NodeProperty<>(Bundle.MultiCaseKeywordSearchNode_properties_caseDirectory(), Bundle.MultiCaseKeywordSearchNode_properties_caseDirectory(), Bundle.MultiCaseKeywordSearchNode_properties_caseDirectory(), - searchHit.getCaseDirectoryPath())); - ss.put(new NodeProperty<>(Bundle.MultiCaseKeywordSearchNode_properties_dataSource(), Bundle.MultiCaseKeywordSearchNode_properties_dataSource(), Bundle.MultiCaseKeywordSearchNode_properties_dataSource(), - searchHit.getDataSourceName())); - ss.put(new NodeProperty<>(Bundle.MultiCaseKeywordSearchNode_properties_path(), Bundle.MultiCaseKeywordSearchNode_properties_path(), Bundle.MultiCaseKeywordSearchNode_properties_path(), - searchHit.getSourcePath())); - ss.put(new NodeProperty<>(Bundle.MultiCaseKeywordSearchNode_properties_sourceType(), Bundle.MultiCaseKeywordSearchNode_properties_sourceType(), Bundle.MultiCaseKeywordSearchNode_properties_sourceType(), - searchHit.getSourceType().getDisplayName())); - ss.put(new NodeProperty<>(Bundle.MultiCaseKeywordSearchNode_properties_source(), Bundle.MultiCaseKeywordSearchNode_properties_source(), Bundle.MultiCaseKeywordSearchNode_properties_source(), - searchHit.getSourceName())); - return s; - } - - @Override - public Action[] getActions(boolean context) { - List actions = new ArrayList<>(); - actions.add(new OpenCaseAction(getCasePath())); - actions.add(new CopyResultAction(searchHit)); - return actions.toArray(new Action[actions.size()]); - } - } - - @NbBundle.Messages({"MultiCaseKeywordSearchNode.copyResultAction.text=Copy to clipboard"}) - /** - * Put the contents of the selected row in the clipboard in the same tab - * seperated format as pressing ctrl+c. - */ - private static class CopyResultAction extends AbstractAction { - - private static final long serialVersionUID = 1L; - - SearchHit result; - - /** - * Construct a new CopyResultAction - */ - CopyResultAction(SearchHit selectedResult) { - super(Bundle.MultiCaseKeywordSearchNode_copyResultAction_text()); - result = selectedResult; - } - - @Override - public void actionPerformed(ActionEvent e) { - StringSelection resultSelection = new StringSelection(result.getCaseDisplayName() + "\t" - + result.getCaseDirectoryPath() + "\t" - + result.getDataSourceName() + "\t" - + result.getSourceType().getDisplayName() + "\t" - + result.getSourceName() + "\t" - + result.getSourcePath() + "\t"); - Toolkit.getDefaultToolkit().getSystemClipboard().setContents(resultSelection, resultSelection); - } - - @Override - public Object clone() throws CloneNotSupportedException { - return super.clone(); //To change body of generated methods, choose Tools | Templates. - } - - } - - @NbBundle.Messages({"MultiCaseKeywordSearchNode.OpenCaseAction.text=Open Case"}) - /** - * Action to open the case associated with the selected node. - */ - private static class OpenCaseAction extends AbstractAction { - - private static final long serialVersionUID = 1L; - private final String caseDirPath; - - /** - * Finds the path to the .aut file for the specified case directory. - * - * @param caseDirectory the directory to check for a .aut file - * - * @return the path to the first .aut file found in the directory - * - * @throws CaseActionException if there was an issue finding a .aut file - */ - private static String findAutFile(String caseDirectory) throws CaseActionException { - File caseFolder = Paths.get(caseDirectory).toFile(); - if (caseFolder.exists()) { - /* - * Search for '*.aut' files. - */ - File[] fileArray = caseFolder.listFiles(); - if (fileArray == null) { - throw new CaseActionException("No files found in case directory"); - } - String autFilePath = null; - for (File file : fileArray) { - String name = file.getName().toLowerCase(); - if (autFilePath == null && name.endsWith(getFileExtension())) { - return file.getAbsolutePath(); - } - } - throw new CaseActionException("No .aut files found in case directory"); - } - throw new CaseActionException("Case directory was not found"); - } - - /** - * Construct a new open case action - * - * @param path the path to the case directory for the case to open - */ - OpenCaseAction(String path) { - super(Bundle.MultiCaseKeywordSearchNode_OpenCaseAction_text()); - caseDirPath = path; - } - - @Override - public void actionPerformed(ActionEvent e) { - StartupWindowProvider.getInstance().close(); - new Thread( - () -> { - try { - Case.openAsCurrentCase(findAutFile(caseDirPath)); - } catch (CaseActionException ex) { - if (null != ex.getCause() && !(ex.getCause() instanceof CaseActionCancelledException)) { - LOGGER.log(Level.SEVERE, String.format("Error opening case with metadata file path %s", caseDirPath), ex); //NON-NLS - MessageNotifyUtil.Message.error(ex.getCause().getLocalizedMessage()); - } - } - } - ).start(); - } - - @Override - public Object clone() throws CloneNotSupportedException { - return super.clone(); //To change body of generated methods, choose Tools | Templates. - } - - } -} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchOpenAction.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchOpenAction.java deleted file mode 100755 index 2cc30ab381..0000000000 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchOpenAction.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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.keywordsearch.multicase; - -import org.openide.awt.ActionID; -import org.openide.awt.ActionReference; -import org.openide.awt.ActionRegistration; -import org.openide.util.HelpCtx; -import org.openide.util.NbBundle.Messages; -import org.openide.util.actions.CallableSystemAction; -import org.sleuthkit.autopsy.core.UserPreferences; - -@ActionID(category = "Tools", id = "org.sleuthkit.autopsy.experimental.autoingest.MultiCaseKeywordSearchOpenAction") -@ActionReference(path = "Menu/Tools", position = 202) -@ActionRegistration(displayName = "#CTL_MultiCaseKeywordSearchOpenAction", lazy = false) -@Messages({"CTL_MultiCaseKeywordSearchOpenAction=Multi-case Keyword Search"}) -/** - * Action to open the top level component for the multi-case keyword search. - */ -public final class MultiCaseKeywordSearchOpenAction extends CallableSystemAction { - - private static final String DISPLAY_NAME = Bundle.CTL_MultiCaseKeywordSearchOpenAction(); - private static final long serialVersionUID = 1L; - - @Override - public boolean isEnabled() { - return UserPreferences.getIsMultiUserModeEnabled(); - } - - @Override - public void performAction() { - MultiCaseKeywordSearchTopComponent.openTopComponent(); - } - - @Override - public String getName() { - return DISPLAY_NAME; - } - - @Override - public HelpCtx getHelpCtx() { - return HelpCtx.DEFAULT_HELP; - } - - @Override - public boolean asynchronous() { - return false; // run on edt - } - -} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchPanel.form b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchPanel.form deleted file mode 100755 index ee2bd31d85..0000000000 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchPanel.form +++ /dev/null @@ -1,374 +0,0 @@ - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchPanel.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchPanel.java deleted file mode 100755 index bc197c16a0..0000000000 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchPanel.java +++ /dev/null @@ -1,860 +0,0 @@ -/* - * 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.keywordsearch.multicase; - -import com.google.common.eventbus.Subscribe; -import com.google.common.eventbus.DeadEvent; -import java.awt.Color; -import java.awt.Component; -import java.awt.Dimension; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.logging.Level; -import java.util.stream.Collectors; -import javax.swing.AbstractButton; -import javax.swing.DefaultListModel; -import javax.swing.DefaultListSelectionModel; -import javax.swing.JCheckBox; -import javax.swing.JFileChooser; -import javax.swing.table.TableColumn; -import javax.swing.JOptionPane; -import javax.swing.JTable; -import javax.swing.ListModel; -import javax.swing.ListSelectionModel; -import javax.swing.SwingUtilities; -import javax.swing.filechooser.FileNameExtensionFilter; -import javax.swing.table.TableCellRenderer; -import org.netbeans.swing.outline.DefaultOutlineModel; -import org.openide.explorer.ExplorerManager; -import org.netbeans.swing.outline.Outline; -import org.openide.nodes.Children; -import org.openide.nodes.Node; -import org.openide.util.NbBundle.Messages; -import org.openide.windows.WindowManager; -import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; -import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.datamodel.EmptyNode; -import org.sleuthkit.autopsy.keywordsearch.multicase.MultiCaseSearcher.MultiCaseSearcherException; -import org.sleuthkit.autopsy.keywordsearch.multicase.SearchQuery.QueryType; - -/** - * Panel to display the controls and results for the multi-case search. - */ -final class MultiCaseKeywordSearchPanel extends javax.swing.JPanel implements ExplorerManager.Provider { - - @Messages({ - "MultiCaseKeywordSearchPanel.emptyNode.waitText=Please Wait..." - }) - private static final long serialVersionUID = 1L; - private volatile SearchThread searchThread = null; - private final Outline outline; - private final ExplorerManager em; - private final org.openide.explorer.view.OutlineView outlineView; - private static final Logger LOGGER = Logger.getLogger(MultiCaseKeywordSearchPanel.class.getName()); - private static final EmptyNode PLEASE_WAIT_NODE = new EmptyNode(Bundle.MultiCaseKeywordSearchPanel_emptyNode_waitText()); - private static final MultiCaseKeywordSearchNode NO_RESULTS_NODE = new MultiCaseKeywordSearchNode(new ArrayList<>()); - private Collection allSearchHits = new ArrayList<>(); - private Collection searchExceptions = new ArrayList<>(); - private final SelectMultiUserCasesDialog caseSelectionDialog = SelectMultiUserCasesDialog.getInstance(); - private final Map caseNameToCaseDataMap; - private Node[] currentConfirmedSelections; - - /** - * Creates new form MultiCaseKeywordSearchPanel - */ - MultiCaseKeywordSearchPanel() { - em = new ExplorerManager(); - outlineView = new org.openide.explorer.view.OutlineView(); - outline = outlineView.getOutline(); - outlineView.setPropertyColumns( - Bundle.MultiCaseKeywordSearchNode_properties_caseDirectory(), Bundle.MultiCaseKeywordSearchNode_properties_caseDirectory(), - Bundle.MultiCaseKeywordSearchNode_properties_dataSource(), Bundle.MultiCaseKeywordSearchNode_properties_dataSource(), - Bundle.MultiCaseKeywordSearchNode_properties_path(), Bundle.MultiCaseKeywordSearchNode_properties_path(), - Bundle.MultiCaseKeywordSearchNode_properties_sourceType(), Bundle.MultiCaseKeywordSearchNode_properties_sourceType(), - Bundle.MultiCaseKeywordSearchNode_properties_source(), Bundle.MultiCaseKeywordSearchNode_properties_source()); - ((DefaultOutlineModel) outline.getOutlineModel()).setNodesColumnLabel(Bundle.MultiCaseKeywordSearchNode_properties_case()); - initComponents(); - outline.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - outline.setRootVisible(false); - outlineView.setPreferredSize(resultsScrollPane.getPreferredSize()); - resultsScrollPane.setViewportView(outlineView); - caseSelectionDialog.subscribeToNewCaseSelections(new ChangeListener() { - @Override - public void nodeSelectionChanged(Node[] selections, List selectionCaseData) { - populateCasesList(selectionCaseData); - currentConfirmedSelections = selections; - revalidate(); - repaint(); - } - }); - searchEnabled(true); - outline.setRowSelectionAllowed(false); - searchProgressBar.setVisible(false); - exportButton.setEnabled(false); - outline.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); - caseNameToCaseDataMap = new HashMap<>(); - setColumnWidths(); - - //Disable selection in JList - caseSelectionList.setSelectionModel(new DefaultListSelectionModel() { - @Override - public void setSelectionInterval(int index0, int index1) { - super.setSelectionInterval(-1, -1); - } - }); - } - - /** - * Listener for new selections - */ - public interface ChangeListener { - public void nodeSelectionChanged(Node[] selections, List selectionCaseData); - } - - /** - * If a collection of SearchHits is received update the results shown on the - * panel to include them. - * - * @param hits the collection of SearchHits which was received. - */ - @Messages({"MultiCaseKeywordSearchPanel.countOfResults.label=Count: "}) - @Subscribe - void subscribeToResults(Collection hits) { - allSearchHits.addAll(hits); - if (allSearchHits.size() > 0) { - MultiCaseKeywordSearchNode resultsNode = new MultiCaseKeywordSearchNode(allSearchHits); - SwingUtilities.invokeLater(() -> { - em.setRootContext(resultsNode); - outline.setRowSelectionAllowed(true); - resultsCountLabel.setText(Bundle.MultiCaseKeywordSearchPanel_countOfResults_label() + Integer.toString(outline.getRowCount())); - }); - } else { - em.setRootContext(NO_RESULTS_NODE); - resultsCountLabel.setText(Bundle.MultiCaseKeywordSearchPanel_countOfResults_label() + 0); - } - } - - /** - * If a string is received and it matches the - * MultiCaseSearcher.SEARCH_COMPLETE_STRING reset elements of this panel to - * reflect that the search is done. - * - * @param stringRecived the String which was received - */ - @Subscribe - void subscribeToStrings(String stringReceived) { - if (stringReceived.equals(MultiCaseSearcher.getSearchCompleteMessage())) { - searchThread.unregisterWithSearcher(MultiCaseKeywordSearchPanel.this); - searchThread = null; - searchEnabled(true); - if (!searchExceptions.isEmpty()) { - warningLabel.setText(Bundle.MultiCaseKeywordSearchPanel_errorsEncounter_text(searchExceptions.size())); - } - if (!em.getRootContext().equals(PLEASE_WAIT_NODE) && !em.getRootContext().equals(NO_RESULTS_NODE)) { - exportButton.setEnabled(true); - SwingUtilities.invokeLater(() -> { - exportButton.setEnabled(true); - setColumnWidths(); - }); - } - } else { - //If it is not the SEARCH_COMPLETE_STRING log it. - LOGGER.log(Level.INFO, "String posted to MultiCaseKeywordSearchPanel EventBus with value of " + stringReceived); - } - } - - /** - * If a InterruptedException is received over the EventBus update the - * warning label. - * - * @param exception the InterruptedException which was received. - */ - @Subscribe - void subscribeToInterruptionExceptions(InterruptedException exception) { - warningLabel.setText(exception.getMessage()); - //if we are still displaying please wait force it to update to no results - if (em.getRootContext().equals(PLEASE_WAIT_NODE)) { - em.setRootContext(NO_RESULTS_NODE); - resultsCountLabel.setText(Bundle.MultiCaseKeywordSearchPanel_countOfResults_label() + 0); - } - } - - /** - * If a MultiCaseSearcherException is received over the EventBus cancel the - * current search and update the warning label. - * - * @param exception the MultiCaseSearcherException which was received. - */ - @Messages({"# {0} - numberOfErrors", - "MultiCaseKeywordSearchPanel.errorsEncounter.text={0} Error(s) encountered while performing search" - }) - @Subscribe - void subscribeToMultiCaseSearcherExceptions(MultiCaseSearcherException exception) { - searchExceptions.add(exception); - } - - /** - * Log all other events received over the event bus which are not - * specifically covered by another @Subscribe method - * - * @param deadEvent Any object received over the event bus which was not of - * a type otherwise subscribed to - */ - @Subscribe - void subscribeToDeadEvents(DeadEvent deadEvent) { - LOGGER.log(Level.INFO, "Dead Event posted to MultiCaseKeywordSearchPanel EventBus " + deadEvent.toString()); - } - - private void displaySearchErrors() { - if (!searchExceptions.isEmpty()) { - StringBuilder strBuilder = new StringBuilder(""); - searchExceptions.forEach((exception) -> { - strBuilder.append("- ").append(exception.getMessage()).append(System.lineSeparator()); - }); - SwingUtilities.invokeLater(() -> { - new MultiCaseKeywordSearchErrorDialog(strBuilder.toString()); - }); - } - - } - - /** - * Get the list of cases from the Multi user case browser - */ - private void populateCasesList(List selectedNodes) { - caseSelectionList.removeAll(); - caseSelectionList.revalidate(); - caseSelectionList.repaint(); - caseNameToCaseDataMap.clear(); - DefaultListModel listModel = new DefaultListModel<>(); - Collections.sort(selectedNodes, (CaseNodeData o1, CaseNodeData o2) -> { - return o1.getName().toLowerCase() - .compareTo(o2.getName().toLowerCase()); - }); - - for (int i = 0; i < selectedNodes.size(); i++) { - CaseNodeData data = selectedNodes.get(i); - String multiUserCaseName = data.getName(); - listModel.addElement(multiUserCaseName); - /** - * Map out the name to CaseNodeData so we can retrieve it later for - * search. - */ - caseNameToCaseDataMap.put(multiUserCaseName, data); - } - caseSelectionList.setModel(listModel); - } - - @Override - public ExplorerManager getExplorerManager() { - return em; - } - - /** - * 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() { - - searchTypeGroup = new javax.swing.ButtonGroup(); - searchButton = new javax.swing.JButton(); - substringRadioButton = new javax.swing.JRadioButton(); - keywordTextField = new javax.swing.JTextField(); - exactRadioButton = new javax.swing.JRadioButton(); - regexRadioButton = new javax.swing.JRadioButton(); - casesLabel = new javax.swing.JLabel(); - resultsLabel = new javax.swing.JLabel(); - toolDescriptionScrollPane = new javax.swing.JScrollPane(); - toolDescriptionTextArea = new javax.swing.JTextArea(); - resultsScrollPane = new javax.swing.JScrollPane(); - cancelButton = new javax.swing.JButton(); - searchProgressBar = new javax.swing.JProgressBar(); - warningLabel = new javax.swing.JLabel(); - exportButton = new javax.swing.JButton(); - resultsCountLabel = new javax.swing.JLabel(); - viewErrorsButton = new javax.swing.JButton(); - pickCasesButton = new javax.swing.JButton(); - jScrollPane1 = new javax.swing.JScrollPane(); - caseSelectionList = new javax.swing.JList<>(); - - setName(""); // NOI18N - setOpaque(false); - setPreferredSize(new java.awt.Dimension(1000, 442)); - - org.openide.awt.Mnemonics.setLocalizedText(searchButton, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.searchButton.text")); // NOI18N - searchButton.setMaximumSize(new java.awt.Dimension(84, 23)); - searchButton.setMinimumSize(new java.awt.Dimension(84, 23)); - searchButton.setPreferredSize(new java.awt.Dimension(84, 23)); - searchButton.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - searchButtonActionPerformed(evt); - } - }); - - searchTypeGroup.add(substringRadioButton); - org.openide.awt.Mnemonics.setLocalizedText(substringRadioButton, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.substringRadioButton.text_1")); // NOI18N - substringRadioButton.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - substringRadioButtonActionPerformed(evt); - } - }); - - keywordTextField.setFont(new java.awt.Font("Monospaced", 0, 14)); // NOI18N - keywordTextField.setText(org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.keywordTextField.text_1")); // NOI18N - keywordTextField.setBorder(new javax.swing.border.LineBorder(new java.awt.Color(192, 192, 192), 1, true)); - keywordTextField.setMinimumSize(new java.awt.Dimension(2, 25)); - keywordTextField.setPreferredSize(new java.awt.Dimension(2, 25)); - - searchTypeGroup.add(exactRadioButton); - exactRadioButton.setSelected(true); - org.openide.awt.Mnemonics.setLocalizedText(exactRadioButton, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.exactRadioButton.text_1")); // NOI18N - - searchTypeGroup.add(regexRadioButton); - org.openide.awt.Mnemonics.setLocalizedText(regexRadioButton, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.regexRadioButton.text_1")); // NOI18N - - org.openide.awt.Mnemonics.setLocalizedText(casesLabel, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.casesLabel.text_1")); // NOI18N - - org.openide.awt.Mnemonics.setLocalizedText(resultsLabel, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.resultsLabel.text")); // NOI18N - - toolDescriptionTextArea.setEditable(false); - toolDescriptionTextArea.setBackground(new java.awt.Color(240, 240, 240)); - toolDescriptionTextArea.setColumns(20); - toolDescriptionTextArea.setFont(new java.awt.Font("Tahoma", 0, 11)); // NOI18N - toolDescriptionTextArea.setLineWrap(true); - toolDescriptionTextArea.setRows(3); - toolDescriptionTextArea.setText(org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.toolDescriptionTextArea.text")); // NOI18N - toolDescriptionTextArea.setWrapStyleWord(true); - toolDescriptionTextArea.setFocusable(false); - toolDescriptionScrollPane.setViewportView(toolDescriptionTextArea); - - resultsScrollPane.setMinimumSize(new java.awt.Dimension(100, 40)); - resultsScrollPane.setPreferredSize(new java.awt.Dimension(200, 100)); - resultsScrollPane.setRequestFocusEnabled(false); - - org.openide.awt.Mnemonics.setLocalizedText(cancelButton, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.cancelButton.text")); // NOI18N - cancelButton.setEnabled(false); - cancelButton.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - cancelButtonActionPerformed(evt); - } - }); - - warningLabel.setForeground(new java.awt.Color(200, 0, 0)); - org.openide.awt.Mnemonics.setLocalizedText(warningLabel, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.warningLabel.text")); // NOI18N - warningLabel.setFocusable(false); - - org.openide.awt.Mnemonics.setLocalizedText(exportButton, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.exportButton.text")); // NOI18N - exportButton.setMargin(new java.awt.Insets(2, 2, 2, 2)); - exportButton.setMaximumSize(new java.awt.Dimension(84, 23)); - exportButton.setMinimumSize(new java.awt.Dimension(84, 23)); - exportButton.setPreferredSize(new java.awt.Dimension(84, 23)); - exportButton.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - exportButtonActionPerformed(evt); - } - }); - - resultsCountLabel.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING); - org.openide.awt.Mnemonics.setLocalizedText(resultsCountLabel, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.resultsCountLabel.text")); // NOI18N - - org.openide.awt.Mnemonics.setLocalizedText(viewErrorsButton, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.viewErrorsButton.text")); // NOI18N - viewErrorsButton.setEnabled(false); - viewErrorsButton.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - viewErrorsButtonActionPerformed(evt); - } - }); - - org.openide.awt.Mnemonics.setLocalizedText(pickCasesButton, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.pickCasesButton.text_1")); // NOI18N - pickCasesButton.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - pickCasesButtonActionPerformed(evt); - } - }); - - jScrollPane1.setViewportView(caseSelectionList); - - 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.LEADING) - .addGroup(layout.createSequentialGroup() - .addComponent(exactRadioButton) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addComponent(substringRadioButton) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(regexRadioButton)) - .addComponent(keywordTextField, javax.swing.GroupLayout.DEFAULT_SIZE, 591, Short.MAX_VALUE)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(toolDescriptionScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 295, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addGroup(layout.createSequentialGroup() - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) - .addComponent(casesLabel) - .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 174, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGroup(layout.createSequentialGroup() - .addComponent(pickCasesButton, javax.swing.GroupLayout.PREFERRED_SIZE, 84, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(searchButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addComponent(resultsLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 154, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(resultsCountLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 98, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addComponent(resultsScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(viewErrorsButton) - .addComponent(warningLabel, javax.swing.GroupLayout.DEFAULT_SIZE, 607, Short.MAX_VALUE)) - .addGap(14, 14, 14) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) - .addComponent(exportButton, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(cancelButton, javax.swing.GroupLayout.DEFAULT_SIZE, 87, Short.MAX_VALUE)))))) - .addContainerGap()) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addGap(196, 196, 196) - .addComponent(searchProgressBar, javax.swing.GroupLayout.DEFAULT_SIZE, 608, Short.MAX_VALUE) - .addGap(108, 108, 108))) - ); - layout.setVerticalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addContainerGap() - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) - .addGroup(layout.createSequentialGroup() - .addComponent(keywordTextField, 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) - .addComponent(regexRadioButton, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(exactRadioButton) - .addComponent(substringRadioButton)))) - .addComponent(toolDescriptionScrollPane)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(casesLabel) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(resultsLabel) - .addComponent(resultsCountLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 14, javax.swing.GroupLayout.PREFERRED_SIZE))) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(resultsScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 281, Short.MAX_VALUE) - .addComponent(jScrollPane1)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(warningLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 15, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(exportButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(pickCasesButton) - .addComponent(searchButton, 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) - .addComponent(viewErrorsButton) - .addComponent(cancelButton)) - .addContainerGap()) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() - .addContainerGap(433, Short.MAX_VALUE) - .addComponent(searchProgressBar, javax.swing.GroupLayout.PREFERRED_SIZE, 22, javax.swing.GroupLayout.PREFERRED_SIZE) - .addContainerGap())) - ); - }// //GEN-END:initComponents - - @Messages({ - "MultiCaseKeywordSearchPanel.warningText.noCases=At least one case must be selected to perform a search.", - "MultiCaseKeywordSearchPanel.warningText.emptySearch=You must enter something to search for in the text field." - }) - /** - * perform a search if the previous search is done or no previous search has - * occured - */ - private void searchButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_searchButtonActionPerformed - if (null == searchThread) { - Collection cases = getCases(); - String searchString = keywordTextField.getText(); - if (cases.isEmpty()) { - warningLabel.setText(Bundle.MultiCaseKeywordSearchPanel_warningText_noCases()); - } else if (searchString.isEmpty()) { - warningLabel.setText(Bundle.MultiCaseKeywordSearchPanel_warningText_emptySearch()); - } else { - //Map case names to CaseNodeData objects - Collection caseNodeData = cases.stream() - .map(c -> caseNameToCaseDataMap.get(c)) - .collect(Collectors.toList()); - - //perform the search - warningLabel.setText(""); - allSearchHits = new ArrayList<>(); - searchExceptions = new ArrayList<>(); - searchEnabled(false); - exportButton.setEnabled(false); - outline.setRowSelectionAllowed(false); - SearchQuery kwsQuery = new SearchQuery(getQueryType(), searchString); - em.setRootContext(PLEASE_WAIT_NODE); - resultsCountLabel.setText(""); - searchThread = new SearchThread(caseNodeData, kwsQuery); - searchThread.registerWithSearcher(MultiCaseKeywordSearchPanel.this); - searchThread.start(); - } - } - }//GEN-LAST:event_searchButtonActionPerformed - - /** - * Get the case names from the Case List - * - * @return cases the cases that match the selected status of isSelected - */ - private Collection getCases() { - Collection cases = new HashSet<>(); - ListModel listModel = caseSelectionList.getModel(); - for(int i = 0; i < listModel.getSize(); i++) { - String caseName = listModel.getElementAt(i); - cases.add(caseName); - } - return cases; - } - - /** - * Get the type of Query which was selected by the user. - * - * @return one of the values of the QueryType enum - */ - private QueryType getQueryType() { - String queryTypeText = ""; - Enumeration buttonGroup = searchTypeGroup.getElements(); - while (buttonGroup.hasMoreElements()) { - AbstractButton dspButton = buttonGroup.nextElement(); - if (dspButton.isSelected()) { - queryTypeText = dspButton.getText(); - break; - } - } - if (queryTypeText.equals(substringRadioButton.getText())) { - return QueryType.SUBSTRING; - } else if (queryTypeText.equals(regexRadioButton.getText())) { - return QueryType.REGEX; - } else { - //default to Exact match - return QueryType.EXACT_MATCH; - } - } - - /** - * Set the column widths to have their width influenced by the width of the - * content in them for up to the first hundred rows. - */ - private void setColumnWidths() { - int widthLimit = 1000; - int margin = 4; - int padding = 8; - for (int col = 0; col < outline.getColumnCount(); col++) { - int width = 115; //min initial width for columns - int rowsToResize = Math.min(outline.getRowCount(), 100); - for (int row = 0; row < rowsToResize; row++) { - if (outline.getValueAt(row, col) != null) { - TableCellRenderer renderer = outline.getCellRenderer(row, col); - Component comp = outline.prepareRenderer(renderer, row, col); - width = Math.max(comp.getPreferredSize().width, width); - } - - } - width += 2 * margin + padding; - width = Math.min(width, widthLimit); - TableColumn column = outline.getColumnModel().getColumn(outline.convertColumnIndexToModel(col)); - column.setPreferredWidth(width); - } - resultsScrollPane.setPreferredSize(new Dimension(outline.getPreferredSize().width, resultsScrollPane.getPreferredSize().height)); - } - - /** - * Cancel the current multi-case search which is being performed. - * - * @param evt ignored - */ - private void cancelButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cancelButtonActionPerformed - cancelSearch(); - }//GEN-LAST:event_cancelButtonActionPerformed - - /** - * Cancel the current multi-case search which is being performed. - */ - @Messages({ - "MultiCaseKeywordSearchPanel.searchThread.cancellingText=Cancelling search"}) - private void cancelSearch() { - if (null != searchThread) { - warningLabel.setText(Bundle.MultiCaseKeywordSearchPanel_searchThread_cancellingText()); - searchThread.interrupt(); - } - } - - @Messages({"MultiCaseKeywordSearchPanel.searchResultsExport.csvExtensionFilterlbl=Comma Separated Values File (csv)", - "MultiCaseKeywordSearchPanel.searchResultsExport.featureName=Search Results Export", - "MultiCaseKeywordSearchPanel.searchResultsExport.failedExportMsg=Export of search results failed" - }) - /** - * Export the currently displayed search results to a file specified by the - * user with data saved in comma seperated format. - */ - private void exportButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exportButtonActionPerformed - JFileChooser chooser = new JFileChooser(); - final String EXTENSION = "csv"; //NON-NLS - FileNameExtensionFilter csvFilter = new FileNameExtensionFilter( - Bundle.MultiCaseKeywordSearchPanel_searchResultsExport_csvExtensionFilterlbl(), EXTENSION); - chooser.setFileFilter(csvFilter); - chooser.setFileSelectionMode(JFileChooser.FILES_ONLY); - chooser.setName("Choose file to export results to"); - chooser.setMultiSelectionEnabled(false); - int returnVal = chooser.showSaveDialog(this); - if (returnVal == JFileChooser.APPROVE_OPTION) { - File selFile = chooser.getSelectedFile(); - if (selFile == null) { - JOptionPane.showMessageDialog(this, - Bundle.MultiCaseKeywordSearchPanel_searchResultsExport_failedExportMsg(), - Bundle.MultiCaseKeywordSearchPanel_searchResultsExport_featureName(), - JOptionPane.WARNING_MESSAGE); - LOGGER.warning("Selected file was null, when trying to export search results"); - return; - } - String fileAbs = selFile.getAbsolutePath(); - if (!fileAbs.endsWith("." + EXTENSION)) { - fileAbs = fileAbs + "." + EXTENSION; - selFile = new File(fileAbs); - } - saveResultsAsTextFile(selFile); - } - }//GEN-LAST:event_exportButtonActionPerformed - - private void viewErrorsButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_viewErrorsButtonActionPerformed - displaySearchErrors(); - }//GEN-LAST:event_viewErrorsButtonActionPerformed - - private void pickCasesButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_pickCasesButtonActionPerformed - caseSelectionDialog.setVisible(true); - if (currentConfirmedSelections != null) { - caseSelectionDialog.setNodeSelections(currentConfirmedSelections); - } - - }//GEN-LAST:event_pickCasesButtonActionPerformed - - private void substringRadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_substringRadioButtonActionPerformed - // TODO add your handling code here: - }//GEN-LAST:event_substringRadioButtonActionPerformed - - /** - * Set the user interface elements to reflect whether the search feature is - * currently enabled or disabled. - * - * @param canSearch True if the search feature should be enabled, false if - * it should be disabled. - */ - private void searchEnabled(boolean canSearch) { - searchButton.setEnabled(canSearch); - cancelButton.setEnabled(!canSearch); - viewErrorsButton.setEnabled(canSearch); - viewErrorsButton.setVisible(!searchExceptions.isEmpty()); - } - - @Messages({"# {0} - file name", - "MultiCaseKeywordSearchPanel.searchResultsExport.fileExistPrompt=File {0} exists, overwrite?", - "# {0} - file name", - "MultiCaseKeywordSearchPanel.searchResultsExport.exportMsg=Search results exported to {0}" - }) - /** - * Saves the results to the file specified - */ - private void saveResultsAsTextFile(File resultsFile) { - if (resultsFile.exists()) { - //if the file already exists ask the user how to proceed - boolean shouldWrite = JOptionPane.showConfirmDialog(null, - Bundle.MultiCaseKeywordSearchPanel_searchResultsExport_fileExistPrompt(resultsFile.getName()), - Bundle.MultiCaseKeywordSearchPanel_searchResultsExport_featureName(), - JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE) - == JOptionPane.YES_OPTION; - if (!shouldWrite) { - return; - } - } - try { - BufferedWriter resultsWriter; - resultsWriter = new BufferedWriter(new FileWriter(resultsFile)); - int col = 0; - //write headers - while (col < outline.getColumnCount()) { - - resultsWriter.write(outline.getColumnName(col)); - col++; - if (col < outline.getColumnCount()) { - resultsWriter.write(","); - } - } - resultsWriter.write(System.lineSeparator()); - //write data - Children resultsChildren = em.getRootContext().getChildren(); - for (int row = 0; row < resultsChildren.getNodesCount(); row++) { - col = 0; - while (col < outline.getColumnCount()) { - if (outline.getValueAt(row, col) instanceof Node.Property) { - resultsWriter.write(((Node.Property) outline.getValueAt(row, col)).getValue().toString()); - } else { - resultsWriter.write(outline.getValueAt(row, col).toString()); - } - col++; - if (col < outline.getColumnCount()) { - resultsWriter.write(","); - } - } - resultsWriter.write(System.lineSeparator()); - } - resultsWriter.flush(); - resultsWriter.close(); - setColumnWidths(); - JOptionPane.showMessageDialog( - WindowManager.getDefault().getMainWindow(), - Bundle.MultiCaseKeywordSearchPanel_searchResultsExport_exportMsg(resultsFile.getName()), - Bundle.MultiCaseKeywordSearchPanel_searchResultsExport_featureName(), - JOptionPane.INFORMATION_MESSAGE); - } catch (IllegalAccessException | IOException | InvocationTargetException ex) { - JOptionPane.showMessageDialog(WindowManager.getDefault().getMainWindow(), - Bundle.MultiCaseKeywordSearchPanel_searchResultsExport_failedExportMsg(), - Bundle.MultiCaseKeywordSearchPanel_searchResultsExport_featureName(), - JOptionPane.WARNING_MESSAGE); - LOGGER.log(Level.WARNING, "Export of search results failed unable to write results csv file", ex); - } - } - - /** - * Ask the user if they want to continue their search while this window is - * closed. Cancels the current search if they select no. - */ - @Messages({ - "MultiCaseKeywordSearchPanel.continueSearch.text=A search is currently being performed. " - + "Would you like the search to continue in the background while the search window is closed?", - "MultiCaseKeywordSearchPanel.continueSearch.title=Closing multi-case search" - }) - void closeSearchPanel() { - if (cancelButton.isEnabled()) { - boolean shouldContinueSearch = JOptionPane.showConfirmDialog(null, - Bundle.MultiCaseKeywordSearchPanel_continueSearch_text(), - Bundle.MultiCaseKeywordSearchPanel_continueSearch_title(), - JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE) - == JOptionPane.YES_OPTION; - if (!shouldContinueSearch) { - cancelSearch(); - } - } - } - - // Variables declaration - do not modify//GEN-BEGIN:variables - private javax.swing.JButton cancelButton; - private javax.swing.JList caseSelectionList; - private javax.swing.JLabel casesLabel; - private javax.swing.JRadioButton exactRadioButton; - private javax.swing.JButton exportButton; - private javax.swing.JScrollPane jScrollPane1; - private javax.swing.JTextField keywordTextField; - private javax.swing.JButton pickCasesButton; - private javax.swing.JRadioButton regexRadioButton; - private javax.swing.JLabel resultsCountLabel; - private javax.swing.JLabel resultsLabel; - private javax.swing.JScrollPane resultsScrollPane; - private javax.swing.JButton searchButton; - private javax.swing.JProgressBar searchProgressBar; - private javax.swing.ButtonGroup searchTypeGroup; - private javax.swing.JRadioButton substringRadioButton; - private javax.swing.JScrollPane toolDescriptionScrollPane; - private javax.swing.JTextArea toolDescriptionTextArea; - private javax.swing.JButton viewErrorsButton; - private javax.swing.JLabel warningLabel; - // End of variables declaration//GEN-END:variables - - /* - * A thread that performs a keyword search of cases - */ - private final class SearchThread extends Thread { - - private final Collection caseNodes; - private final SearchQuery searchQuery; - private final MultiCaseSearcher multiCaseSearcher = new MultiCaseSearcher(); - - /** - * Constructs a thread that performs a keyword search of cases - * - * @param caseNames The names of the cases to search. - * @param query The keyword search query to perform. - */ - private SearchThread(Collection caseNodes, SearchQuery searchQuery) { - this.caseNodes = caseNodes; - this.searchQuery = searchQuery; - } - - /** - * Register an object with the MultiCaseSearcher eventBus so that the - * object's subscribe methods can receive results. - * - * @param object the object to register with the MultiCaseSearcher - */ - private void registerWithSearcher(Object object) { - multiCaseSearcher.registerWithEventBus(object); - } - - /** - * Unregister an object with the MultiCaseSearcher so that the object's - * subscribe methods no longer receive results. - * - * @param object the object to unregister with the MultiCaseSearcher - */ - private void unregisterWithSearcher(Object object) { - multiCaseSearcher.unregisterWithEventBus(object); - } - - @Override - public void interrupt() { - super.interrupt(); - //in case it is running a method which causes InterruptedExceptions to be ignored - multiCaseSearcher.stopMultiCaseSearch(); - } - - @Override - public void run() { - multiCaseSearcher.performKeywordSearch(caseNodes, searchQuery, new MultiCaseKeywordSearchProgressIndicator(searchProgressBar)); - } - - } - -} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchProgressIndicator.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchProgressIndicator.java deleted file mode 100755 index d63b2a1f8e..0000000000 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchProgressIndicator.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * 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.keywordsearch.multicase; - -import javax.swing.JProgressBar; -import javax.swing.SwingUtilities; -import org.sleuthkit.autopsy.progress.ProgressIndicator; - -/** - * A progress indicator that updates a JProgressBar. - */ -final class MultiCaseKeywordSearchProgressIndicator implements ProgressIndicator { - - private final JProgressBar progress; - - /** - * Construct a new JProgressIndicator - * - * @param progressBar the JProgressBar you want this indicator to update - */ - MultiCaseKeywordSearchProgressIndicator(JProgressBar progressBar) { - progress = progressBar; - progress.setStringPainted(true); - } - - /** - * Start showing progress in the progress bar. - * - * @param message the message to be displayed on the progress bar, null to - * display percent complete - * @param max The total number of work units to be completed. - */ - @Override - public void start(String message, int max) { - SwingUtilities.invokeLater(() -> { - progress.setIndeterminate(false); - progress.setMinimum(0); - progress.setString(message); //the message - progress.setValue(0); - progress.setMaximum(max); - progress.setVisible(true); - }); - } - - /** - * Start showing progress in the progress bar. - * - * @param message the message to be displayed on the progress bar, null to - * display percent complete - */ - @Override - public void start(String message) { - SwingUtilities.invokeLater(() -> { - progress.setIndeterminate(true); - progress.setMinimum(0); - progress.setString(message); - progress.setValue(0); - progress.setVisible(true); - }); - } - - /** - * Switches the progress indicator to indeterminate mode (the total number - * of work units to be completed is unknown). - * - * @param message the message to be displayed on the progress bar, null to - * display percent complete - */ - @Override - public void switchToIndeterminate(String message) { - SwingUtilities.invokeLater(() -> { - progress.setIndeterminate(true); - progress.setString(message); - }); - } - - /** - * Switches the progress indicator to determinate mode (the total number of - * work units to be completed is known). - * - * @param message the message to be displayed on the progress bar, null to - * display percent complete - * @param current The number of work units completed so far. - * @param max The total number of work units to be completed. - */ - @Override - public void switchToDeterminate(String message, int current, int max) { - SwingUtilities.invokeLater(() -> { - progress.setIndeterminate(false); - progress.setMinimum(0); - progress.setString(message); - progress.setValue(current); - progress.setMaximum(max); - }); - } - - /** - * Updates the progress indicator with a progress message. - * - * @param message the message to be displayed on the progress bar, null to - * display percent complete - */ - @Override - public void progress(String message) { - SwingUtilities.invokeLater(() -> { - progress.setString(message); - }); - } - - /** - * Updates the progress indicator with the number of work units completed so - * far when in determinate mode (the total number of work units to be - * completed is known). - * - * @param current Number of work units completed so far. - */ - @Override - public void progress(int current) { - SwingUtilities.invokeLater(() -> { - progress.setValue(current); - }); - } - - /** - * Updates the progress indicator with a progress message and the number of - * work units completed so far when in determinate mode (the total number of - * work units to be completed is known). - * - * @param message the message to be displayed on the progress bar, null to - * display percent complete - * @param current Number of work units completed so far. - */ - @Override - public void progress(String message, int current) { - SwingUtilities.invokeLater(() -> { - progress.setString(message); - progress.setValue(current); - }); - } - - /** - * Finishes the progress indicator when the task is completed. - */ - @Override - public void finish() { - SwingUtilities.invokeLater(() -> { - progress.setVisible(false); - }); - } - -} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchTopComponent.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchTopComponent.java deleted file mode 100755 index 3ce6b28505..0000000000 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchTopComponent.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * 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.keywordsearch.multicase; - -import java.awt.BorderLayout; -import java.awt.Component; -import org.openide.util.NbBundle.Messages; -import org.openide.windows.TopComponent; -import org.openide.windows.WindowManager; -import org.openide.windows.Mode; - -@TopComponent.Description( - preferredID = "MultiCaseKeywordSearchTopComponent", - persistenceType = TopComponent.PERSISTENCE_NEVER -) -@TopComponent.Registration(mode = "multiCaseKeywordSearch", openAtStartup = false) -@Messages({ - "CTL_MultiCaseKeywordSearchTopComponentAction=Multi-case Keyword Search", - "CTL_MultiCaseKeywordSearchTopComponent=Multi-case Keyword Search"}) -/** - * A top level component for the multi case keyword search feature. - */ -final class MultiCaseKeywordSearchTopComponent extends TopComponent { - - public final static String PREFERRED_ID = "MultiCaseKeywordSearchTopComponent"; // NON-NLS - private static final long serialVersionUID = 1L; - private static boolean topComponentInitialized = false; - - @Messages({ - "MultiCaseKeywordSearchTopComponent.exceptionMessage.failedToCreatePanel=Failed to create Multi-case Keyword Search panel.",}) - /** - * Open the top level component if it is not already open, if it is open - * bring it to the front and select it. - */ - static void openTopComponent() { - final MultiCaseKeywordSearchTopComponent tc = (MultiCaseKeywordSearchTopComponent) WindowManager.getDefault().findTopComponent(PREFERRED_ID); - if (tc != null) { - if (tc.isOpened() == false) { - topComponentInitialized = true; - Mode mode = WindowManager.getDefault().findMode("multiCaseKeywordSearch"); // NON-NLS - if (mode != null) { - mode.dockInto(tc); - } - tc.open(); - } - tc.toFront(); - tc.requestActive(); - } - } - - /** - * Close the top level componet. - */ - static void closeTopComponent() { - if (topComponentInitialized) { - final TopComponent tc = WindowManager.getDefault().findTopComponent(PREFERRED_ID); - if (tc != null) { - try { - tc.close(); - } catch (Exception e) { - - } - } - } - } - - @Messages({"MultiCaseKeywordSearchTopComponent.name.text=Multi-case Keyword Search"}) - /** - * Construct a new "MultiCaseKeywordSearchTopComponent. - */ - MultiCaseKeywordSearchTopComponent() { - initComponents(); - setName(Bundle.MultiCaseKeywordSearchTopComponent_name_text()); - setDisplayName(Bundle.MultiCaseKeywordSearchTopComponent_name_text()); - setToolTipText(Bundle.MultiCaseKeywordSearchTopComponent_name_text()); - setSize(this.getPreferredSize()); - setLayout(new BorderLayout()); - MultiCaseKeywordSearchPanel searchPanel = new MultiCaseKeywordSearchPanel(); - searchPanel.setSize(searchPanel.getPreferredSize()); - searchPanel.setVisible(true); - add(searchPanel); - } - - @Override - public void componentOpened() { - super.componentOpened(); - WindowManager.getDefault().setTopComponentFloating(this, true); - } - - @Override - public boolean canClose() { - for (Component component : getComponents()) { - if (component instanceof MultiCaseKeywordSearchPanel) { - ((MultiCaseKeywordSearchPanel) component).closeSearchPanel(); - } - } - return super.canClose(); - } - - /** - * 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() { - - setPreferredSize(new java.awt.Dimension(1002, 444)); - - javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); - this.setLayout(layout); - layout.setHorizontalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGap(0, 902, Short.MAX_VALUE) - ); - layout.setVerticalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGap(0, 444, Short.MAX_VALUE) - ); - }// //GEN-END:initComponents - - // Variables declaration - do not modify//GEN-BEGIN:variables - // End of variables declaration//GEN-END:variables -} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseSearcher.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseSearcher.java deleted file mode 100755 index b26001a28e..0000000000 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseSearcher.java +++ /dev/null @@ -1,775 +0,0 @@ -/* - * 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.keywordsearch.multicase; - -import com.google.common.eventbus.EventBus; -import java.io.File; -import java.io.IOException; -import java.nio.file.LinkOption; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.stream.Collectors; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.xpath.XPath; -import javax.xml.xpath.XPathConstants; -import javax.xml.xpath.XPathExpression; -import javax.xml.xpath.XPathExpressionException; -import javax.xml.xpath.XPathFactory; -import org.apache.commons.lang.StringUtils; -import org.apache.solr.client.solrj.SolrQuery; -import org.apache.solr.client.solrj.SolrRequest; -import org.apache.solr.client.solrj.SolrServerException; -import org.apache.solr.client.solrj.impl.HttpSolrServer; -import org.apache.solr.client.solrj.request.CoreAdminRequest; -import org.apache.solr.client.solrj.response.CoreAdminResponse; -import org.apache.solr.client.solrj.response.QueryResponse; -import org.apache.solr.common.SolrDocument; -import org.apache.solr.common.SolrDocumentList; -import org.apache.solr.common.params.CoreAdminParams; -import org.apache.solr.common.params.CursorMarkParams; -import org.openide.util.Exceptions; -import org.openide.util.NbBundle; -import org.sleuthkit.autopsy.casemodule.CaseMetadata; -import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; -import org.sleuthkit.autopsy.coordinationservice.CoordinationService; -import org.sleuthkit.autopsy.core.UserPreferences; -import org.sleuthkit.autopsy.core.UserPreferencesException; -import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.coreutils.UNCPathUtilities; -import org.sleuthkit.autopsy.keywordsearch.Server; -import org.sleuthkit.autopsy.progress.ProgressIndicator; -import org.sleuthkit.datamodel.AbstractFile; -import org.sleuthkit.datamodel.BlackboardArtifact; -import org.sleuthkit.datamodel.CaseDbConnectionInfo; -import org.sleuthkit.datamodel.Content; -import org.sleuthkit.datamodel.Report; -import org.sleuthkit.datamodel.SleuthkitCase; -import org.sleuthkit.datamodel.TskCoreException; -import org.w3c.dom.Document; -import org.xml.sax.SAXException; - -/** - * Performs keyword searches across multiple cases - */ -final class MultiCaseSearcher { - - private static final String CASE_AUTO_INGEST_LOG_NAME = "AUTO_INGEST_LOG.TXT"; //NON-NLS - private static final String SEARCH_COMPLETE_MESSAGE = "SEARCH_COMPLETE"; - private static final String RESOURCES_LOCK_SUFFIX = "_RESOURCES"; //NON-NLS - private static final int CASE_DIR_READ_LOCK_TIMEOUT_HOURS = 12; //NON-NLS - private static final String SOLR_SERVER_URL_FORMAT_STRING = "http://%s:%s/solr"; //NON-NLS - private static final String SOLR_CORE_URL_FORMAT_STRING = "http://%s:%s/solr/%s"; //NON-NLS - private final static String SOLR_METADATA_FILE_NAME = "SolrCore.properties"; //NON-NLS - private static final String SOLR_CORE_NAME_XPATH = "/SolrCores/Core/CoreName/text()"; //NON-NLS - private static final String TEXT_INDEX_NAME_XPATH = "/SolrCores/Core/TextIndexPath/text()"; //NON-NLS - private static final String SOLR_CORE_INSTANCE_PATH_PROPERTY = "instanceDir"; //NON-NLS - private static final String SOLR_CONFIG_SET_NAME = "AutopsyConfig"; //NON-NLS - private static final int MAX_RESULTS_PER_CURSOR_MARK = 512; - private static final String SOLR_DOC_ID_FIELD = Server.Schema.ID.toString(); //NON-NLS - private static final String SOLR_DOC_CONTENT_STR_FIELD = Server.Schema.CONTENT_STR.toString(); //NON-NLS - private static final String SOLR_DOC_CHUNK_SIZE_FIELD = Server.Schema.CHUNK_SIZE.toString(); //NON-NLS - private static final String SOLR_DOC_ID_PARTS_SEPARATOR = "_"; - private static final Logger logger = Logger.getLogger(MultiCaseSearcher.class.getName()); - private final EventBus eventBus = new EventBus("MultiCaseSearcherEventBus"); - private static final UNCPathUtilities pathUtils = new UNCPathUtilities(); - private volatile boolean searchStopped = true; - - MultiCaseSearcher() { - - } - - static String getSearchCompleteMessage() { - return SEARCH_COMPLETE_MESSAGE; - } - - /** - * - * Performs keyword searches across multiple cases - * - * @param caseNames The names of the cases to search. - * @param query The keyword search query to perform. - * @param progressIndicator A progrss indicator for the search. - * - * @return The search results. - * - * @throws MultiCaseSearcherException - * @throws InterruptedException - */ - @NbBundle.Messages({ - "MultiCaseSearcher.progressMessage.findingCases=Finding selected cases", - "MultiCaseSearcher.progressMessage.creatingSolrQuery=Creating search query for Solr server", - "# {0} - total cases", - "MultiCaseSearcher.progressMessage.startingCaseSearches=Searching {0} case(s)", - "# {0} - case name", - "# {1} - case counter", - "# {2} - total cases", - "MultiCaseSearcher.progressMessage.acquiringSharedLockForCase=Acquiring shared lock for \"{0}\" ({1} of {2} case(s))", - "# {0} - case name", - "# {1} - case counter", - "# {2} - total cases", - "MultiCaseSearcher.progressMessage.loadingSolrCoreForCase=Loading Solr core for \"{0}\" ({1} of {2} case(s))", - "# {0} - case name", - "# {1} - case counter", - "# {2} - total cases", - "MultiCaseSearcher.progressMessage.openingCaseDbForCase=Opening case database for \"{0}\" ({1} of {2} case(s))", - "# {0} - case name", - "# {1} - case counter", - "# {2} - total cases", - "MultiCaseSearcher.progressMessage.executingSolrQueryForCase=Getting keyword hits for \"{0}\" ({1} of {2} case(s))", - "# {0} - case directory path", - "MultiCaseSearcher.exceptionMessage.failedToGetCaseDirReadlock=Failed to obtain read lock for case directory at {0}", - "MultiCaseSearcher.exceptionMessage.cancelledMessage=Search cancelled" - }) - void performKeywordSearch(final Collection caseNodes, final SearchQuery query, final ProgressIndicator progressIndicator) { - progressIndicator.start(Bundle.MultiCaseSearcher_progressMessage_findingCases()); - try { - searchStopped = false; //mark the search as started - final List caseMetadata = getMultiCaseMetadata(caseNodes); - checkForCancellation(); - //eventBus.post("number of cases to search determined"); - progressIndicator.progress(Bundle.MultiCaseSearcher_progressMessage_creatingSolrQuery()); - final SolrQuery solrQuery = createSolrQuery(query); - checkForCancellation(); - final int totalCases = caseMetadata.size(); - int caseCounter = 1; - progressIndicator.progress(Bundle.MultiCaseSearcher_progressMessage_startingCaseSearches(totalCases)); - int totalSteps = 5; - progressIndicator.switchToDeterminate(Bundle.MultiCaseSearcher_progressMessage_startingCaseSearches(totalCases), 0, totalCases * totalSteps); - int caseNumber = 0; - for (MultiCaseMetadata aCase : caseMetadata) { - CaseMetadata metadata = aCase.getCaseMetadata(); - String caseName = metadata.getCaseDisplayName(); - SleuthkitCase caseDatabase = null; - - int stepsCompleted = 0; - progressIndicator.progress(Bundle.MultiCaseSearcher_progressMessage_acquiringSharedLockForCase(caseName, caseCounter, totalCases), stepsCompleted + caseNumber * totalSteps); - try (CoordinationService.Lock caseDirReadLock = CoordinationService.getInstance().tryGetSharedLock(CoordinationService.CategoryNode.CASES, aCase.getCaseMetadata().getCaseDirectory(), CASE_DIR_READ_LOCK_TIMEOUT_HOURS, TimeUnit.HOURS)) { - if (null == caseDirReadLock) { - throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_failedToGetCaseDirReadlock(aCase.getCaseMetadata().getCaseDirectory())); - } - checkForCancellation(); - ++stepsCompleted; - progressIndicator.progress(Bundle.MultiCaseSearcher_progressMessage_loadingSolrCoreForCase(caseName, caseCounter, totalCases), stepsCompleted + caseNumber * totalSteps); - final HttpSolrServer solrServer = loadSolrCoreForCase(aCase); - checkForCancellation(); - ++stepsCompleted; - progressIndicator.progress(Bundle.MultiCaseSearcher_progressMessage_openingCaseDbForCase(caseName, caseCounter, totalCases), stepsCompleted + caseNumber * totalSteps); - caseDatabase = openCase(aCase); - checkForCancellation(); - ++stepsCompleted; - progressIndicator.progress(Bundle.MultiCaseSearcher_progressMessage_executingSolrQueryForCase(caseName, caseCounter, totalCases), stepsCompleted + caseNumber * totalSteps); - eventBus.post(executeQuery(solrServer, solrQuery, caseDatabase, aCase)); - ++stepsCompleted; - - progressIndicator.progress(stepsCompleted + caseNumber * totalSteps); - ++caseCounter; - } catch (CoordinationService.CoordinationServiceException ex) { - throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_failedToGetCaseDirReadlock(aCase.getCaseMetadata().getCaseDirectory()), ex); - } catch (MultiCaseSearcherException exception) { - logger.log(Level.INFO, "Exception encountered while performing multi-case keyword search", exception); - eventBus.post(exception); - } finally { - if (null != caseDatabase) { - closeCase(caseDatabase); - } - } - caseNumber++; - } - } catch (InterruptedException exception) { - logger.log(Level.INFO, Bundle.MultiCaseSearcher_exceptionMessage_cancelledMessage(), exception); - eventBus.post(exception); - } catch (MultiCaseSearcherException exception) { - logger.log(Level.WARNING, "Exception encountered while performing multi-case keyword search", exception); - eventBus.post(new InterruptedException("Exception encountered while performing multi-case keyword search")); - eventBus.post(exception); - } finally { - progressIndicator.finish(); - eventBus.post(SEARCH_COMPLETE_MESSAGE); - } - } - - /** - * Gets metadata for the cases associated with one or more with the search - * - * @param caseNames The names of the cases to search. - * - * @return The metadata for the cases. - * - * @throws MultiCaseSearcherException - * @throws InterruptedException - */ - private List getMultiCaseMetadata(final Collection caseNodes) throws MultiCaseSearcherException, InterruptedException { - final Map casesToCasePaths = caseNodes.stream() - .collect(Collectors.toMap(CaseNodeData::getDirectory, CaseNodeData::getName)); - checkForCancellation(); - final List cases = new ArrayList<>(); - for (Map.Entry entry : casesToCasePaths.entrySet()) { - final Path caseDirectoryPath = entry.getKey(); - final CaseMetadata caseMetadata = getCaseMetadata(caseDirectoryPath); - checkForCancellation(); - final TextIndexMetadata textIndexMetadata = getTextIndexMetadata(caseDirectoryPath); - checkForCancellation(); - cases.add(new MultiCaseMetadata(caseMetadata, textIndexMetadata)); - } - return cases; - } - - /** - * Gets the metadata for a case from the case metadata file in a given case - * directory. - * - * @param caseDirectoryPath A case directory path. - * - * @return The case metadata. - * - * @throws MultiCaseSearcherException - */ - @NbBundle.Messages({ - "# {0} - case directory", "MultiCaseSearcher.exceptionMessage.failedToFindCaseMetadata=Failed to find case metadata file in {0}", - "# {0} - case directory", "MultiCaseSearcher.exceptionMessage.failedToParseCaseMetadata=Failed to parse case file metadata in {0}" - }) - - private static CaseMetadata getCaseMetadata(Path caseDirectoryPath) throws MultiCaseSearcherException { - Path metadataPath = CaseMetadata.getCaseMetadataFile(caseDirectoryPath); - if (metadataPath != null) { - try { - return new CaseMetadata(metadataPath); - } catch (CaseMetadata.CaseMetadataException ex) { - throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_failedToParseCaseMetadata(caseDirectoryPath), ex); - } - } - throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_failedToFindCaseMetadata(caseDirectoryPath)); - } - - /** - * Gets the text index metadata from the Solr.properties file in a given - * case directory. - * - * @param caseDirectoryPath A case directory path. - * - * @return The text index metadata. - * - * @throws MultiCaseSearcherException - */ - @NbBundle.Messages({ - "# {0} - file name", "# {1} - case directory", "MultiCaseSearcher.exceptionMessage.missingSolrPropertiesFile=Missing {0} file in {1}", - "# {0} - file name", "# {1} - case directory", "MultiCaseSearcher.exceptionMessage.solrPropertiesFileParseError=Error parsing {0} file in {1}",}) - private static TextIndexMetadata getTextIndexMetadata(Path caseDirectoryPath) throws MultiCaseSearcherException { - final Path solrMetaDataFilePath = Paths.get(caseDirectoryPath.toString(), SOLR_METADATA_FILE_NAME); - final File solrMetaDataFile = solrMetaDataFilePath.toFile(); - if (!solrMetaDataFile.exists() || !solrMetaDataFile.canRead()) { - throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_missingSolrPropertiesFile(SOLR_METADATA_FILE_NAME, caseDirectoryPath)); - } - try { - final DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); - final Document doc = docBuilder.parse(solrMetaDataFile); - final XPath xPath = XPathFactory.newInstance().newXPath(); - XPathExpression xPathExpr = xPath.compile(SOLR_CORE_NAME_XPATH); - final String solrCoreName = (String) xPathExpr.evaluate(doc, XPathConstants.STRING); - xPathExpr = xPath.compile(TEXT_INDEX_NAME_XPATH); - final String relativeTextIndexPath = (String) xPathExpr.evaluate(doc, XPathConstants.STRING); - Path textIndexPath = caseDirectoryPath.resolve(relativeTextIndexPath); - textIndexPath = textIndexPath.getParent(); // Remove "index" path component - final String textIndexUNCPath = pathUtils.convertPathToUNC(textIndexPath.toString()); - return new TextIndexMetadata(caseDirectoryPath, solrCoreName, textIndexUNCPath); - } catch (ParserConfigurationException | SAXException | XPathExpressionException | IOException ex) { - throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_solrPropertiesFileParseError(SOLR_METADATA_FILE_NAME, caseDirectoryPath), ex); - } - } - - /** - * Converts a keyword search query into a Solr query. - * - * @param searchQuery A keyword search query. - * - * @return A Solr query. - */ - private static SolrQuery createSolrQuery(SearchQuery searchQuery) { - final SolrQuery solrQuery = new SolrQuery(); - solrQuery.setQuery(searchQuery.getSearchTerm()); - solrQuery.setRows(MAX_RESULTS_PER_CURSOR_MARK); - /* - * Note that setting the sort order is necessary for cursor based paging - * to work. - */ - solrQuery.setSort(SolrQuery.SortClause.asc(SOLR_DOC_ID_FIELD)); - solrQuery.setFields(SOLR_DOC_ID_FIELD, SOLR_DOC_CHUNK_SIZE_FIELD, SOLR_DOC_CONTENT_STR_FIELD); - return solrQuery; - } - - /** - * Connects to the Solr server and loads the Solr core for a given case. - * - * @param aCase - * - * @return A Solr server client object that can be used for executing - * queries of the specified text index. - * - * MultiCaseSearcherException - * - * @throws InterruptedException - */ - @NbBundle.Messages({ - "# {0} - connection info", - "# {1} - case name", - "# {2} - case directory", - "MultiCaseSearcher.exceptionMessage.errorLoadingCore=Error connecting to Solr server and loading core (URL: {0}) for case {1} in {2}" - }) - private HttpSolrServer loadSolrCoreForCase(MultiCaseMetadata aCase) throws MultiCaseSearcherException, InterruptedException { - TextIndexMetadata textIndexMetadata = aCase.getTextIndexMetadata(); - Server.IndexingServerProperties indexServer = Server.getMultiUserServerProperties(aCase.getCaseMetadata().getCaseDirectory()); - final String serverURL = String.format(SOLR_SERVER_URL_FORMAT_STRING, indexServer.getHost(), indexServer.getPort()); - try { - /* - * Connect to the Solr server. - */ - final HttpSolrServer solrServer = new HttpSolrServer(serverURL); - CoreAdminRequest statusRequest = new CoreAdminRequest(); - statusRequest.setCoreName(null); - statusRequest.setAction(CoreAdminParams.CoreAdminAction.STATUS); - statusRequest.setIndexInfoNeeded(false); - checkForCancellation(); - statusRequest.process(solrServer); - checkForCancellation(); - - /* - * Load the core for the text index if it is not already loaded. - */ - CoreAdminResponse response = CoreAdminRequest.getStatus(textIndexMetadata.getSolrCoreName(), solrServer); - if (null == response.getCoreStatus(textIndexMetadata.getSolrCoreName()).get(SOLR_CORE_INSTANCE_PATH_PROPERTY)) { - CoreAdminRequest.Create loadCoreRequest = new CoreAdminRequest.Create(); - loadCoreRequest.setDataDir(textIndexMetadata.getTextIndexPath()); - loadCoreRequest.setCoreName(textIndexMetadata.getSolrCoreName()); - loadCoreRequest.setConfigSet(SOLR_CONFIG_SET_NAME); - loadCoreRequest.setIsLoadOnStartup(false); - loadCoreRequest.setIsTransient(true); - solrServer.request(loadCoreRequest); - } - - /* - * Create a server client object that can be used for executing - * queries of the specified text index. - */ - final String coreURL = String.format(SOLR_CORE_URL_FORMAT_STRING, indexServer.getHost(), indexServer.getPort(), textIndexMetadata.getSolrCoreName()); - final HttpSolrServer coreServer = new HttpSolrServer(coreURL); - return coreServer; - - } catch (SolrServerException | IOException ex) { - throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_errorLoadingCore(serverURL, aCase.getCaseMetadata().getCaseName(), textIndexMetadata.getCaseDirectoryPath()), ex); - } - } - - /** - * Opens a case database. - * - * @param caseMetadata - * - * @return A case database. - * - * @throws MultiCaseSearcherException - * @throws InterruptedException - */ - @NbBundle.Messages({ - "# {0} - case_name", - "MultiCaseSearcher.exceptionMessage.failedToGetCaseDatabaseConnectionInfo=Failed to get case database connection info for case {0}", - "# {0} - PostgreSQL server host", - "# {1} - PostgreSQL server port", - "# {2} - case database name", - "# {3} - case directory", - "MultiCaseSearcher.exceptionMessage.errorOpeningCaseDatabase=Error connecting to PostgreSQL server (Host/Port: [{0}:{1}] and opening case database {2} for case at {3}" - }) - private SleuthkitCase openCase(MultiCaseMetadata aCase) throws MultiCaseSearcherException, InterruptedException { - CaseDbConnectionInfo dbConnectionInfo; - try { - dbConnectionInfo = UserPreferences.getDatabaseConnectionInfo(); - } catch (UserPreferencesException ex) { - throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_failedToGetCaseDatabaseConnectionInfo(aCase.getCaseMetadata().getCaseName()), ex); - } - checkForCancellation(); - final CaseMetadata caseMetadata = aCase.getCaseMetadata(); - try { - return SleuthkitCase.openCase(caseMetadata.getCaseDatabaseName(), UserPreferences.getDatabaseConnectionInfo(), caseMetadata.getCaseDirectory()); - } catch (UserPreferencesException | TskCoreException ex) { - throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_errorOpeningCaseDatabase(dbConnectionInfo.getHost(), dbConnectionInfo.getPort(), caseMetadata.getCaseDatabaseName(), caseMetadata.getCaseDirectory()), ex); - } - } - - /** - * Closes a case database. - * - * @param aCase a case database. - */ - private static void closeCase(SleuthkitCase aCase) { - aCase.close(); - } - - /** - * Executes a keyword search searchTerm in the text index of a case. - * - * @param solrServer The Solr server. - * @param solrQuery The Solr searchTerm. - * @param caseDatabase The case database. - * @param aCase The case metadata. - * - * @return A list of search results, possibly empty. - * - * @throws MultiCaseSearcherException - * @throws InterruptedException - */ - @NbBundle.Messages({ - "# {0} - query", - "# {1} - case_name", - "MultiCaseSearcher.exceptionMessage.solrQueryError=Failed to execute query \"{0}\" on case {1}" - }) - private Collection executeQuery(HttpSolrServer solrServer, SolrQuery solrQuery, SleuthkitCase caseDatabase, MultiCaseMetadata aCase) throws MultiCaseSearcherException, InterruptedException { - final List hits = new ArrayList<>(); - final Set uniqueObjectIds = new HashSet<>(); - String cursorMark = CursorMarkParams.CURSOR_MARK_START; - boolean allResultsProcessed = false; - while (!allResultsProcessed) { - checkForCancellation(); - solrQuery.set(CursorMarkParams.CURSOR_MARK_PARAM, cursorMark); - QueryResponse response; - try { - checkForCancellation(); - response = solrServer.query(solrQuery, SolrRequest.METHOD.POST); - } catch (SolrServerException ex) { - throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_solrQueryError(solrQuery.getQuery(), aCase.getCaseMetadata().getCaseName()), ex); - } - SolrDocumentList resultDocuments = response.getResults(); - for (SolrDocument resultDoc : resultDocuments) { - checkForCancellation(); - String solrDocumentId = resultDoc.getFieldValue(SOLR_DOC_ID_FIELD).toString(); - Long solrObjectId = parseSolrObjectId(solrDocumentId); - if (!uniqueObjectIds.contains(solrObjectId)) { - uniqueObjectIds.add(solrObjectId); - checkForCancellation(); - hits.add(processHit(solrObjectId, caseDatabase, aCase)); - } - } - checkForCancellation(); - String nextCursorMark = response.getNextCursorMark(); - if (cursorMark.equals(nextCursorMark)) { - allResultsProcessed = true; - } - cursorMark = nextCursorMark; - } - return hits; - } - - /** - * Parses a Solr document id to get the Solr object id. - * - * @param solrDocumentId A Solr document id. - * - * @return A Solr object id. - */ - private static Long parseSolrObjectId(String solrDocumentId) { - /** - * A Solr document id is of the form [solr_object_id] for Content object - * metadata documents and - * [solr_object_id][SOLR_DOC_ID_PARTS_SEPARATOR][chunk_id] for Content - * object text chunk documents. - */ - final String[] solrDocumentIdParts = solrDocumentId.split(SOLR_DOC_ID_PARTS_SEPARATOR); - if (1 == solrDocumentIdParts.length) { - return Long.parseLong(solrDocumentId); - } else { - return Long.parseLong(solrDocumentIdParts[0]); - } - } - - /** - * Creates a keyword search hit object for a Content object identified by - * its Solr object id. - * - * @param solrObjectId The Solr object id of a Content object. - * @param caseDatabase The case database of the case that has the Content. - * @param caseInfo Metadata about the case that has the content. - * - * @return - * - * @throws MultiCaseSearcherException - */ - @NbBundle.Messages({ - "# {0} - Solr document id", - "# {1} - case database name", - "# {2} - case directory", - "MultiCaseSearcher.exceptionMessage.hitProcessingError=Failed to query case database for processing of Solr object id {0} of case {1} in {2}" - }) - - private static SearchHit processHit(Long solrObjectId, SleuthkitCase caseDatabase, MultiCaseMetadata caseInfo) throws MultiCaseSearcherException { - try { - final long objectId = getObjectIdForSolrObjectId(solrObjectId, caseDatabase); - final CaseMetadata caseMetadata = caseInfo.getCaseMetadata(); - final String caseDisplayName = caseMetadata.getCaseDisplayName(); - final String caseDirectoryPath = caseMetadata.getCaseDirectory(); - final Content content = caseDatabase.getContentById(objectId); - final Content dataSource = content.getDataSource(); - final String dataSourceName = (dataSource == null) ? "" : dataSource.getName(); - SearchHit.SourceType sourceType = SearchHit.SourceType.FILE; - String sourceName = ""; - String sourcePath = ""; - if (content instanceof AbstractFile) { - AbstractFile sourceFile = (AbstractFile) content; - sourceName = sourceFile.getName(); - sourcePath = sourceFile.getLocalAbsPath(); - if (null == sourcePath) { - sourceType = SearchHit.SourceType.FILE; - sourcePath = sourceFile.getUniquePath(); - } else { - sourceType = SearchHit.SourceType.LOCAL_FILE; - sourceName = sourceFile.getName(); - } - } else if (content instanceof BlackboardArtifact) { - BlackboardArtifact sourceArtifact = (BlackboardArtifact) content; - sourceType = SearchHit.SourceType.ARTIFACT; - BlackboardArtifact.Type artifactType = caseDatabase.getArtifactType(sourceArtifact.getArtifactTypeName()); - sourceName = artifactType.getDisplayName(); - Content source = sourceArtifact.getParent(); - if (source instanceof AbstractFile) { - AbstractFile sourceFile = (AbstractFile) source; - sourcePath = sourceFile.getLocalAbsPath(); - if (null == sourcePath) { - sourcePath = sourceFile.getUniquePath(); - } - } else { - sourcePath = source.getUniquePath(); - } - } else if (content instanceof Report) { - Report report = (Report) content; - sourceType = SearchHit.SourceType.REPORT; - sourceName = report.getReportName(); - sourcePath = report.getUniquePath(); - } - - return new SearchHit(caseDisplayName, caseDirectoryPath, dataSourceName, sourceType, sourceName, sourcePath); - } catch (SQLException | TskCoreException ex) { - throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_hitProcessingError(solrObjectId, caseInfo.getCaseMetadata().getCaseName(), caseInfo.getCaseMetadata().getCaseDirectory()), ex); - } - } - - /** - * Gets the Sleuthkit object id that corresponds to the Solr object id of - * some content. - * - * @param solrObjectId A solr object id for some content. - * @param caseDatabase The case database for the case that includes the - * content. - * - * @return The Sleuthkit object id of the content. - * - * @throws MultiCaseSearcherException - * @throws TskCoreException - * @throws SQLException - */ - private static long getObjectIdForSolrObjectId(long solrObjectId, SleuthkitCase caseDatabase) throws MultiCaseSearcherException, TskCoreException, SQLException { - if (0 < solrObjectId) { - return solrObjectId; - } else { - try (SleuthkitCase.CaseDbQuery databaseQuery = caseDatabase.executeQuery("SELECT artifact_obj_id FROM blackboard_artifacts WHERE artifact_id = " + solrObjectId)) { - final ResultSet resultSet = databaseQuery.getResultSet(); - if (resultSet.next()) { - return resultSet.getLong("artifact_obj_id"); - } else { - throw new TskCoreException("Empty result set getting obj_id for artifact with artifact_id =" + solrObjectId); - } - } - } - } - - /** - * Checks to see if the current thread has been interrupted (i.e, the search - * has been cancelled) and throws an InterruptedException if it has been. - * - * @throws InterruptedException - */ - private void checkForCancellation() throws InterruptedException { - if (Thread.currentThread().isInterrupted() || searchStopped) { - throw new InterruptedException("Search Cancelled"); - } - } - - /** - * A bundle of metadata for a case. - */ - private final static class MultiCaseMetadata { - - private final CaseMetadata caseMetadata; - private final TextIndexMetadata textIndexMetadata; - - /** - * Contructs a bundle of metadata for a case - * - * @param caseMetadata The case metadata. - * @param textIndexMetaData The text index metadata for the case. - */ - private MultiCaseMetadata(CaseMetadata caseMetadata, TextIndexMetadata textIndexMetaData) { - this.caseMetadata = caseMetadata; - this.textIndexMetadata = textIndexMetaData; - } - - /** - * Gets the case metadata. - * - * @return The case metadata. - */ - private CaseMetadata getCaseMetadata() { - return this.caseMetadata; - } - - /** - * Gets the text index metadata for the case. - * - * @return The text index metadata. - */ - private TextIndexMetadata getTextIndexMetadata() { - return this.textIndexMetadata; - } - - } - - /** - * Bundles a case directory path, a Solr core fileName, and a text index UNC - * path. - */ - private final static class TextIndexMetadata { - - private final Path caseDirectoryPath; - private final String solrCoreName; - private final String textIndexUNCPath; - - /** - * Constructs an object that bundles a Solr core fileName and a text - * index UNC path. - * - * @param caseDirectoryPath The case directory path. - * @param solrCoreName The core fileName. - * @param textIndexUNCPath The text index path. - */ - private TextIndexMetadata(Path caseDirectoryPath, String solrCoreName, String textIndexUNCPath) { - this.caseDirectoryPath = caseDirectoryPath; - this.solrCoreName = solrCoreName; - this.textIndexUNCPath = textIndexUNCPath; - } - - /** - * Gets the case directory path. - * - * @return The path. - */ - private Path getCaseDirectoryPath() { - return this.caseDirectoryPath; - } - - /** - * Gets the Solr core fileName. - * - * @return The Solr core fileName. - */ - private String getSolrCoreName() { - return this.solrCoreName; - } - - /** - * - * Gets the UNC path of the text index. - * - * @return The path. - */ - private String getTextIndexPath() { - return this.textIndexUNCPath; - } - - } - - /** - * Exception thrown if there is an error executing a search. - */ - static final class MultiCaseSearcherException extends Exception { - - private static final long serialVersionUID = 1L; - - /** - * Constructs an instance of the exception thrown if there is an error - * executing a search. - * - * @param message The exception message. - */ - private MultiCaseSearcherException(String message) { - super(message); - } - - /** - * Constructs an instance of the exception thrown if there is an error - * executing a search. - * - * @param message The exception message. - * @param cause The Throwable that caused the error. - */ - private MultiCaseSearcherException(String message, Throwable cause) { - super(message, cause); - } - - } - - /** - * Tell the MultiCaseSearcher that it's current search can be stopped the - * next time it checks for cancellation. - */ - void stopMultiCaseSearch() { - //This is necessary because if the interrupt occurs during CoreAdminRequest.process, - //CoreAdminRequest.getStatus, or HttpSolrServer.query the interrupt gets ignored - searchStopped = true; - } - - /** - * Register an object with the MultiCaseSearcher eventBus so that it's - * subscribe methods can receive results. - * - * @param object the object to register with the eventBus - */ - void registerWithEventBus(Object object) { - eventBus.register(object); - } - - /** - * Unregister an object with the MultiCaseSearcher eventBus so that it's - * subscribe methods no longer receive results. - * - * @param object the object to unregister with the eventBus - */ - void unregisterWithEventBus(Object object) { - eventBus.unregister(object); - } - -} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SearchHit.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SearchHit.java deleted file mode 100755 index 9ed845f43c..0000000000 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SearchHit.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * 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.keywordsearch.multicase; - -import javax.annotation.concurrent.Immutable; - -/** - * A keyword search hit from a multi-case keyword search. - */ -@Immutable -final class SearchHit { - - private final String caseDisplayName; - private final String caseDirectoryPath; - private final String dataSourceName; - private final SourceType sourceType; - private final String sourceName; - private final String sourcePath; - - /** - * Constructs a keyword search hit from a multi-case search. - * - * @param caseDisplayName The display name of the case where the hit - * occurred. - * @param caseDirectoryPath The path of the directory of the case where the - * hit occurred. - * @param dataSourceName The name of the data source within the case - * where the hit occurred. - * @param sourceType The type of the source content object. - * @param sourceName The name of the source, e.g., a file name, an - * artifact type name, or a report module name. - * @param sourcePath The path of the source content, or the path of - * the parent source content object for an artifact - * source. - */ - SearchHit(String caseDisplayName, String caseDirectoryPath, String dataSourceName, SourceType sourceType, String sourceName, String sourcePath) { - this.caseDisplayName = caseDisplayName; - this.caseDirectoryPath = caseDirectoryPath; - this.dataSourceName = dataSourceName; - this.sourceType = sourceType; - this.sourceName = sourceName; - this.sourcePath = sourcePath; - } - - /** - * Gets the display name of the case where the hit occurred. - * - * @return The case display name. - */ - String getCaseDisplayName() { - return this.caseDisplayName; - } - - /** - * Gets the path of the directory of the case where the hit occurred. - * - * @return The case directory path. - */ - String getCaseDirectoryPath() { - return this.caseDirectoryPath; - } - - /** - * Gets the name of the data source within the case where the hit occurred. - * - * @return - */ - String getDataSourceName() { - return this.dataSourceName; - } - - /** - * Gets the type of the source content object. - * - * @return The source type. - */ - SourceType getSourceType() { - return this.sourceType; - } - - /** - * Gets the name of the source, e.g., a file name, an artifact type name, or - * a report module name. - * - * @return The source name. - */ - String getSourceName() { - return this.sourceName; - } - - /** - * Gets the path of the source content, or the path of the parent source - * content object for an artifact source. - * - * @return The source object path. - */ - String getSourcePath() { - return this.sourcePath; - } - - /** - * An enumeration of the source types for keyword search hits. - */ - enum SourceType { - FILE("File"), - LOCAL_FILE("Local File"), - ARTIFACT("Artifact"), - REPORT("Report"); - - private final String displayName; - - private SourceType(String displayName) { - this.displayName = displayName; - } - - String getDisplayName() { - return this.displayName; - } - } - -} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SearchQuery.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SearchQuery.java deleted file mode 100755 index 89b224e06c..0000000000 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SearchQuery.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * 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.keywordsearch.multicase; - -import javax.annotation.concurrent.Immutable; -import org.sleuthkit.autopsy.keywordsearch.Server; - -/** - * A keyword search query. - */ -@Immutable -final class SearchQuery { - - private static final String SEARCH_TERM_CHARS_TO_ESCAPE = "/+-&|!(){}[]^\"~*?:\\"; - private static final String SOLR_DOC_CONTENT_STR_FIELD = Server.Schema.CONTENT_STR.toString(); //NON-NLS - private final String searchTerm; - - /** - * Constructs a multicase keyword search query. - * - * @param queryType The query type. - * @param searchTerm The search term for the query. - */ - SearchQuery(QueryType queryType, String searchTerm) { - switch (queryType) { - case EXACT_MATCH: - this.searchTerm = prepareExactMatchSearchTerm(searchTerm); - break; - case SUBSTRING: - this.searchTerm = prepareSubstringSearchTerm(searchTerm); - break; - case REGEX: - this.searchTerm = prepareRegexSearchTerm(searchTerm); - break; - default: - this.searchTerm = searchTerm; - break; - } - } - - /** - * Gets the search term. - * - * @return The query. - */ - String getSearchTerm() { - return searchTerm; - } - - /** - * Escapes and quotes a given search term as required for an exact match - * search query. - * - * @param searchTerm A "raw" input search term. - * - * @return A search term suitable for an exact match query. - */ - private static String prepareExactMatchSearchTerm(String searchTerm) { - String escapedSearchTerm = escapeSearchTerm(searchTerm); - if (!searchTerm.startsWith("\"")) { - escapedSearchTerm = "\"" + escapedSearchTerm; - } - if (!searchTerm.endsWith("\"")) { - escapedSearchTerm += "\""; - } - return escapedSearchTerm; - } - - /** - * Adds delimiters and possibly wildcards to a given search terms as - * required for a regular expression search query. - * - * @param searchTerm A "raw" input search term. - * - * @return A search term suitable for a regex query. - */ - private static String prepareRegexSearchTerm(String searchTerm) { - /* - * Add slash delimiters and, if necessary, wildcards (.*) at the - * beginning and end of the search term. The wildcards are added because - * Lucerne automatically adds a '^' prefix and '$' suffix to the search - * terms for regex searches. Without the '.*' wildcards, the search term - * will have to match the entire content_str field, which is not - * generally the intent of the user. - */ - String regexSearchTerm = SOLR_DOC_CONTENT_STR_FIELD - + ":/" - + (searchTerm.startsWith(".*") ? "" : ".*") - + searchTerm.toLowerCase() - + (searchTerm.endsWith(".*") ? "" : ".*") - + "/"; - return regexSearchTerm; - } - - /** - * Escapes and adds delimiters and wpossibly wildcards to a given search - * term as required for a substring search. - * - * @param searchTerm A "raw" input search term. - * - * @return A search term suitable for a substring query. - */ - private static String prepareSubstringSearchTerm(String searchTerm) { - String escapedSearchTerm = escapeSearchTerm(searchTerm); - return prepareRegexSearchTerm(escapedSearchTerm); - } - - /** - * Escapes a search term as required for a Lucene query. - * - * @param searchTerm A "raw" input search term. - * - * @return An escaped version of the "raw" input search term. - */ - public static String escapeSearchTerm(String searchTerm) { - String rawSearchTerm = searchTerm.trim(); - if (0 == rawSearchTerm.length()) { - return rawSearchTerm; - } - StringBuilder escapedSearchTerm = new StringBuilder(rawSearchTerm.length()); - for (int i = 0; i < rawSearchTerm.length(); ++i) { - final char nextChar = rawSearchTerm.charAt(i); - if (SEARCH_TERM_CHARS_TO_ESCAPE.contains(Character.toString(nextChar))) { - escapedSearchTerm.append("\\"); - } - escapedSearchTerm.append(nextChar); - } - return escapedSearchTerm.toString(); - } - - /** - * An enumeration of the supported query types for keywod searches. - */ - enum QueryType { - EXACT_MATCH, - SUBSTRING, - REGEX; - } - -} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCaseDialogCustomizer.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCaseDialogCustomizer.java deleted file mode 100755 index be5fee183f..0000000000 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCaseDialogCustomizer.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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.keywordsearch.multicase; - -import java.util.ArrayList; -import java.util.List; -import javax.swing.Action; -import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; -import org.sleuthkit.autopsy.casemodule.multiusercasesbrowser.MultiUserCaseBrowserCustomizer; - -/** - * Customizer for SelectMultiUserCasesPanel. Displays the 'Create date' and - * 'Directory' columns - */ -public class SelectMultiUserCaseDialogCustomizer implements MultiUserCaseBrowserCustomizer { - - @Override - public List getColumns() { - List properties = new ArrayList<>(); - properties.add(Column.CREATE_DATE); - properties.add(Column.DIRECTORY); - return properties; - } - - @Override - public List getSortColumns() { - List sortColumns = new ArrayList<>(); - sortColumns.add(new SortColumn(Column.CREATE_DATE, false, 1)); - return sortColumns; - } - - @Override - public boolean allowMultiSelect() { - return true; - } - - @Override - public List getActions(CaseNodeData nodeData) { - return new ArrayList<>(); - } - - @Override - public Action getPreferredAction(CaseNodeData nodeData) { - return null; - } - -} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCasesDialog.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCasesDialog.java deleted file mode 100755 index 75d449c932..0000000000 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCasesDialog.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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.keywordsearch.multicase; - -import java.awt.Dialog; -import java.beans.PropertyVetoException; -import org.openide.nodes.Node; -import org.openide.windows.WindowManager; -import org.sleuthkit.autopsy.keywordsearch.multicase.MultiCaseKeywordSearchPanel.ChangeListener; - -/** - * Dialog that will display the SelectMultiUserCasesPanel - */ -class SelectMultiUserCasesDialog extends javax.swing.JDialog { - - private static final long serialVersionUID = 1L; - private static SelectMultiUserCasesDialog instance; - private static SelectMultiUserCasesPanel multiUserCasesPanel; - - /** - * Gets the singleton JDialog that allows a user to open a multi-user case. - * - * @return The singleton JDialog instance. - */ - public synchronized static SelectMultiUserCasesDialog getInstance() { - if (instance == null) { - instance = new SelectMultiUserCasesDialog(); - instance.init(); - } - return instance; - } - - /** - * Listen for new case selections from the user. - * - * @param l Listener on new case selection events - */ - void subscribeToNewCaseSelections(ChangeListener l) { - multiUserCasesPanel.subscribeToNewCaseSelections(l); - } - - /** - * Set the node selections for the window - * - * @param selections Nodes to be automatically selected in the explorer view - */ - void setNodeSelections(Node[] selections) { - try { - multiUserCasesPanel.setSelections(selections); - } catch (PropertyVetoException ex) { - //Do-nothing - } - } - - /** - * Constructs a singleton JDialog that allows a user to open a multi-user - * case. - */ - private SelectMultiUserCasesDialog() { - super(WindowManager.getDefault().getMainWindow(), "Select Multi-User Cases", Dialog.ModalityType.APPLICATION_MODAL); - } - - /** - * Registers a keyboard action to hide the dialog when the escape key is - * pressed and adds a OpenMultiUserCasePanel child component. - */ - private void init() { - multiUserCasesPanel = new SelectMultiUserCasesPanel(this); - add(multiUserCasesPanel); - pack(); - setResizable(false); - multiUserCasesPanel.refreshDisplay(); - } -} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCasesPanel.form b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCasesPanel.form deleted file mode 100755 index 2751a26eae..0000000000 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCasesPanel.form +++ /dev/null @@ -1,103 +0,0 @@ - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCasesPanel.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCasesPanel.java deleted file mode 100755 index decae45b49..0000000000 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCasesPanel.java +++ /dev/null @@ -1,228 +0,0 @@ -/* - * 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.keywordsearch.multicase; - -import java.beans.PropertyVetoException; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import javax.swing.JDialog; -import org.openide.explorer.ExplorerManager; -import org.openide.nodes.Node; -import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; -import org.sleuthkit.autopsy.casemodule.multiusercasesbrowser.MultiUserCasesBrowserPanel; -import org.sleuthkit.autopsy.keywordsearch.multicase.MultiCaseKeywordSearchPanel.ChangeListener; - -/** - * Panel for multi-user case selection - */ -class SelectMultiUserCasesPanel extends javax.swing.JPanel { - - private static final long serialVersionUID = 1L; - private final JDialog parentDialog; - private final MultiUserCasesBrowserPanel caseBrowserPanel; - private final List listeners; - - /** - * Constructs a JPanel that allows a user to open a multi-user case. - * - * @param parentDialog The parent dialog of the panel, may be null. If - * provided, the dialog is hidden when this poanel's - * cancel button is pressed. - */ - SelectMultiUserCasesPanel(JDialog parentDialog) { - initComponents(); - this.parentDialog = parentDialog; - initComponents(); // Machine generated code - caseBrowserPanel = new MultiUserCasesBrowserPanel(new ExplorerManager(), new SelectMultiUserCaseDialogCustomizer()); - multiUserCaseScrollPane.add(caseBrowserPanel); - multiUserCaseScrollPane.setViewportView(caseBrowserPanel); - listeners = new ArrayList<>(); - } - - /** - * Refreshes the child component that displays the multi-user cases known to - * the coordination service.. - */ - void refreshDisplay() { - caseBrowserPanel.displayCases(); - } - - /** - * Subscribes to the selections when the user presses the OK button. - * - * @param listener - */ - void subscribeToNewCaseSelections(ChangeListener listener) { - listeners.add(listener); - } - - /** - * Sets the selections in the panel - * - * @param selections - * - * @throws PropertyVetoException - */ - void setSelections(Node[] selections) throws PropertyVetoException { - caseBrowserPanel.getExplorerManager().setSelectedNodes(selections); - caseBrowserPanel.requestFocus(); - } - - /** - * 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() { - - multiUserCaseScrollPane = new javax.swing.JScrollPane(); - selectAllButton = new javax.swing.JButton(); - deselectAllButton = new javax.swing.JButton(); - jLabel1 = new javax.swing.JLabel(); - confirmSelections = new javax.swing.JButton(); - cancelButton = new javax.swing.JButton(); - refreshButton = new javax.swing.JButton(); - - org.openide.awt.Mnemonics.setLocalizedText(selectAllButton, org.openide.util.NbBundle.getMessage(SelectMultiUserCasesPanel.class, "SelectMultiUserCasesPanel.selectAllButton.text")); // NOI18N - selectAllButton.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - selectAllButtonActionPerformed(evt); - } - }); - - org.openide.awt.Mnemonics.setLocalizedText(deselectAllButton, org.openide.util.NbBundle.getMessage(SelectMultiUserCasesPanel.class, "SelectMultiUserCasesPanel.deselectAllButton.text")); // NOI18N - deselectAllButton.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - deselectAllButtonActionPerformed(evt); - } - }); - - org.openide.awt.Mnemonics.setLocalizedText(jLabel1, org.openide.util.NbBundle.getMessage(SelectMultiUserCasesPanel.class, "SelectMultiUserCasesPanel.jLabel1.text")); // NOI18N - - org.openide.awt.Mnemonics.setLocalizedText(confirmSelections, org.openide.util.NbBundle.getMessage(SelectMultiUserCasesPanel.class, "SelectMultiUserCasesPanel.confirmSelections.text")); // NOI18N - confirmSelections.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - confirmSelectionsActionPerformed(evt); - } - }); - - org.openide.awt.Mnemonics.setLocalizedText(cancelButton, org.openide.util.NbBundle.getMessage(SelectMultiUserCasesPanel.class, "SelectMultiUserCasesPanel.cancelButton.text")); // NOI18N - cancelButton.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - cancelButtonActionPerformed(evt); - } - }); - - org.openide.awt.Mnemonics.setLocalizedText(refreshButton, org.openide.util.NbBundle.getMessage(SelectMultiUserCasesPanel.class, "SelectMultiUserCasesPanel.refreshButton.text")); // NOI18N - refreshButton.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - refreshButtonActionPerformed(evt); - } - }); - - 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(multiUserCaseScrollPane) - .addGroup(layout.createSequentialGroup() - .addComponent(selectAllButton) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(deselectAllButton) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addComponent(jLabel1, javax.swing.GroupLayout.PREFERRED_SIZE, 365, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 286, Short.MAX_VALUE) - .addComponent(refreshButton) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addComponent(confirmSelections, javax.swing.GroupLayout.PREFERRED_SIZE, 63, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(cancelButton))) - .addContainerGap()) - ); - layout.setVerticalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addContainerGap() - .addComponent(multiUserCaseScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 486, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(jLabel1) - .addComponent(selectAllButton) - .addComponent(deselectAllButton) - .addComponent(confirmSelections) - .addComponent(cancelButton) - .addComponent(refreshButton)) - .addContainerGap(15, Short.MAX_VALUE)) - ); - }// //GEN-END:initComponents - - private void selectAllButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_selectAllButtonActionPerformed - try { - caseBrowserPanel.getExplorerManager().setSelectedNodes(caseBrowserPanel.getExplorerManager().getRootContext().getChildren().getNodes()); - } catch (PropertyVetoException ex) { - //Ignore - } - }//GEN-LAST:event_selectAllButtonActionPerformed - - private void deselectAllButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_deselectAllButtonActionPerformed - try { - caseBrowserPanel.getExplorerManager().setSelectedNodes(new Node[0]); - } catch (PropertyVetoException ex) { - //Ignore - } - }//GEN-LAST:event_deselectAllButtonActionPerformed - - private void confirmSelectionsActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_confirmSelectionsActionPerformed - //Pull out the CaseNodeData objects from the selections - Node[] selections = caseBrowserPanel.getExplorerManager().getSelectedNodes(); - List caseNodeData = Stream.of(selections) - .map(n -> n.getLookup().lookup(CaseNodeData.class)) - .collect(Collectors.toList()); - listeners.forEach((l) -> { - l.nodeSelectionChanged(selections, caseNodeData); - }); - parentDialog.setVisible(false); - }//GEN-LAST:event_confirmSelectionsActionPerformed - - private void cancelButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cancelButtonActionPerformed - parentDialog.setVisible(false); - }//GEN-LAST:event_cancelButtonActionPerformed - - private void refreshButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_refreshButtonActionPerformed - caseBrowserPanel.displayCases(); - }//GEN-LAST:event_refreshButtonActionPerformed - - - // Variables declaration - do not modify//GEN-BEGIN:variables - private javax.swing.JButton cancelButton; - private javax.swing.JButton confirmSelections; - private javax.swing.JButton deselectAllButton; - private javax.swing.JLabel jLabel1; - private javax.swing.JScrollPane multiUserCaseScrollPane; - private javax.swing.JButton refreshButton; - private javax.swing.JButton selectAllButton; - // End of variables declaration//GEN-END:variables -} diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/BinaryCookieReader.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/BinaryCookieReader.java index 3e24a1fe5d..05ec607834 100755 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/BinaryCookieReader.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/BinaryCookieReader.java @@ -70,7 +70,7 @@ public final class BinaryCookieReader implements Iterable { * the file is a binarycookie file. This function does not keep the file * open. * - * @param file binarycookie file + * @param cookieFile binarycookie file * @return An instance of the reader * @throws FileNotFoundException * @throws IOException diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Bundle.properties b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Bundle.properties index 7c75aeb9d0..6cd8dede00 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Bundle.properties +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Bundle.properties @@ -1,5 +1,5 @@ OpenIDE-Module-Display-Category=Ingest Module -OpenIDE-Module-Long-Description=Recent Activity ingest module.\n\n\The module extracts useful information about the recent user activity on the disk image being ingested, such as:\n\n- Recently open documents,\n- Web acitivity (sites visited, stored cookies, bookmarked sites, search engine queries, file downloads),\n- Recently attached devices,\n- Installed programs.\n\n\The module currently supports Windows only disk images.\n\The plugin is also fully functional when deployed on Windows version of Autopsy. +OpenIDE-Module-Long-Description=Recent Activity ingest module.\n\n\The module extracts useful information about the recent user activity on the disk image being ingested, such as:\n\n- Recently open documents,\n- Web activity (sites visited, stored cookies, book marked sites, search engine queries, file downloads),\n- Recently attached devices,\n- Installed programs.\n\nThe module currently supports Windows only disk images.\nThe plugin is also fully functional when deployed on Windows version of Autopsy. OpenIDE-Module-Name=RecentActivity OpenIDE-Module-Short-Description=Recent Activity finder ingest module Chrome.moduleName=Chrome @@ -93,7 +93,7 @@ RecentDocumentsByLnk.parentModuleName=Recent Activity SearchEngineURLQueryAnalyzer.moduleName.text=Search Engine SearchEngineURLQueryAnalyzer.engineName.none=NONE SearchEngineURLQueryAnalyzer.domainSubStr.none=NONE -SearchEngineURLQueryAnalyzer.toString=Name: {0}\nDomain Substring: {1}\n\count: {2}\nSplit Tokens: \n{3} +SearchEngineURLQueryAnalyzer.toString=Name: {0}\nDomain Substring: {1}\nCount: {2}\nSplit Tokens: \n{3} SearchEngineURLQueryAnalyzer.parentModuleName.noSpace=RecentActivity SearchEngineURLQueryAnalyzer.parentModuleName=Recent Activity UsbDeviceIdMapper.parseAndLookup.text=Product: {0} diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Bundle.properties-MERGED b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Bundle.properties-MERGED index 66df1c457f..17d744eee1 100755 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Bundle.properties-MERGED +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Bundle.properties-MERGED @@ -64,7 +64,7 @@ ExtractZone_progress_Msg=Extracting :Zone.Identifer files ExtractZone_Restricted=Restricted Sites Zone ExtractZone_Trusted=Trusted Sites Zone OpenIDE-Module-Display-Category=Ingest Module -OpenIDE-Module-Long-Description=Recent Activity ingest module.\n\n\The module extracts useful information about the recent user activity on the disk image being ingested, such as:\n\n- Recently open documents,\n- Web acitivity (sites visited, stored cookies, bookmarked sites, search engine queries, file downloads),\n- Recently attached devices,\n- Installed programs.\n\n\The module currently supports Windows only disk images.\n\The plugin is also fully functional when deployed on Windows version of Autopsy. +OpenIDE-Module-Long-Description=Recent Activity ingest module.\n\nThe module extracts useful information about the recent user activity on the disk image being ingested, such as:\n\n- Recently open documents,\n- Web activity (sites visited, stored cookies, book marked sites, search engine queries, file downloads),\n- Recently attached devices,\n- Installed programs.\n\nThe module currently supports Windows only disk images.\nThe plugin is also fully functional when deployed on Windows version of Autopsy. OpenIDE-Module-Name=RecentActivity OpenIDE-Module-Short-Description=Recent Activity finder ingest module Chrome.moduleName=Chrome @@ -194,7 +194,7 @@ SearchEngineURLQueryAnalyzer.init.exception.msg=Unable to find {0}. SearchEngineURLQueryAnalyzer.moduleName.text=Search Engine SearchEngineURLQueryAnalyzer.engineName.none=NONE SearchEngineURLQueryAnalyzer.domainSubStr.none=NONE -SearchEngineURLQueryAnalyzer.toString=Name: {0}\nDomain Substring: {1}\n\count: {2}\nSplit Tokens: \n{3} +SearchEngineURLQueryAnalyzer.toString=Name: {0}\nDomain Substring: {1}\nCount: {2}\nSplit Tokens: \n{3} SearchEngineURLQueryAnalyzer.parentModuleName.noSpace=RecentActivity SearchEngineURLQueryAnalyzer.parentModuleName=Recent Activity UsbDeviceIdMapper.parseAndLookup.text=Product: {0} diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Chrome.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Chrome.java index 5006ee27b5..79fab2cc0e 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Chrome.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Chrome.java @@ -108,21 +108,39 @@ class Chrome extends Extract { progressBar.progress(Bundle.Progress_Message_Chrome_History()); this.getHistory(); + if (context.dataSourceIngestIsCancelled()) { + return; + } progressBar.progress(Bundle.Progress_Message_Chrome_Bookmarks()); this.getBookmark(); + if (context.dataSourceIngestIsCancelled()) { + return; + } progressBar.progress(Bundle.Progress_Message_Chrome_Cookies()); this.getCookie(); + if (context.dataSourceIngestIsCancelled()) { + return; + } progressBar.progress(Bundle.Progress_Message_Chrome_Logins()); this.getLogins(); + if (context.dataSourceIngestIsCancelled()) { + return; + } progressBar.progress(Bundle.Progress_Message_Chrome_AutoFill()); this.getAutofill(); + if (context.dataSourceIngestIsCancelled()) { + return; + } progressBar.progress(Bundle.Progress_Message_Chrome_Downloads()); this.getDownload(); + if (context.dataSourceIngestIsCancelled()) { + return; + } progressBar.progress(Bundle.Progress_Message_Chrome_Cache()); ChromeCacheExtractor chromeCacheExtractor = new ChromeCacheExtractor(dataSource, context, progressBar); diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ChromeCacheExtractor.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ChromeCacheExtractor.java index ce7c1c44ee..dbb6366889 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ChromeCacheExtractor.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ChromeCacheExtractor.java @@ -261,7 +261,12 @@ final class ChromeCacheExtractor { indexFiles = findCacheIndexFiles(); // Process each of the caches - for (AbstractFile indexFile: indexFiles) { + for (AbstractFile indexFile: indexFiles) { + + if (context.dataSourceIngestIsCancelled()) { + return; + } + processCacheIndexFile(indexFile); } @@ -325,6 +330,12 @@ final class ChromeCacheExtractor { // Process each address in the table for (int i = 0; i < indexHdr.getTableLen(); i++) { + + if (context.dataSourceIngestIsCancelled()) { + cleanup(); + return; + } + CacheAddress addr = new CacheAddress(indexFileROBuffer.getInt() & UINT32_MASK, cachePath); if (addr.isInitialized()) { progressBar.progress( NbBundle.getMessage(this.getClass(), @@ -339,6 +350,11 @@ final class ChromeCacheExtractor { } } } + + if (context.dataSourceIngestIsCancelled()) { + cleanup(); + return; + } derivedFiles.forEach((derived) -> { services.fireModuleContentEvent(new ModuleContentEvent(derived)); diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Extract.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Extract.java index 2938bb8dd5..19b76806c4 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Extract.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Extract.java @@ -170,16 +170,21 @@ abstract class Extract { ResultSet temprs; List> list; String connectionString = "jdbc:sqlite:" + path; //NON-NLS + SQLiteDBConnect tempdbconnect = null; try { - SQLiteDBConnect tempdbconnect = new SQLiteDBConnect("org.sqlite.JDBC", connectionString); //NON-NLS + tempdbconnect = new SQLiteDBConnect("org.sqlite.JDBC", connectionString); //NON-NLS temprs = tempdbconnect.executeQry(query); list = this.resultSetToArrayList(temprs); - tempdbconnect.closeConnection(); } catch (SQLException ex) { logger.log(Level.SEVERE, "Error while trying to read into a sqlite db." + connectionString, ex); //NON-NLS errorMessages.add(NbBundle.getMessage(this.getClass(), "Extract.dbConn.errMsg.failedToQueryDb", getName())); return Collections.>emptyList(); } + finally { + if (tempdbconnect != null) { + tempdbconnect.closeConnection(); + } + } return list; } diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractEdge.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractEdge.java index c3032e8cc2..8eebdedd75 100755 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractEdge.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractEdge.java @@ -311,6 +311,10 @@ final class ExtractEdge extends Extract { } for (File file : historyFiles) { + if (context.dataSourceIngestIsCancelled()) { + return; + } + Scanner fileScanner; try { fileScanner = new Scanner(new FileInputStream(file.toString())); @@ -324,6 +328,10 @@ final class ExtractEdge extends Extract { try { List headers = null; while (fileScanner.hasNext()) { + if (context.dataSourceIngestIsCancelled()) { + return; + } + String line = fileScanner.nextLine(); if (headers == null) { headers = Arrays.asList(line.toLowerCase().split(",")); @@ -413,6 +421,10 @@ final class ExtractEdge extends Extract { } for (File file : containerFiles) { + if (context.dataSourceIngestIsCancelled()) { + return; + } + Scanner fileScanner; try { fileScanner = new Scanner(new FileInputStream(file.toString())); @@ -426,6 +438,10 @@ final class ExtractEdge extends Extract { try { List headers = null; while (fileScanner.hasNext()) { + if (context.dataSourceIngestIsCancelled()) { + return; + } + String line = fileScanner.nextLine(); if (headers == null) { headers = Arrays.asList(line.toLowerCase().split(",")); @@ -468,6 +484,10 @@ final class ExtractEdge extends Extract { } for (File file : downloadFiles) { + if (context.dataSourceIngestIsCancelled()) { + return; + } + Scanner fileScanner; try { fileScanner = new Scanner(new FileInputStream(file.toString())); @@ -480,6 +500,10 @@ final class ExtractEdge extends Extract { try { List headers = null; while (fileScanner.hasNext()) { + if (context.dataSourceIngestIsCancelled()) { + return; + } + String line = fileScanner.nextLine(); if (headers == null) { headers = Arrays.asList(line.toLowerCase().split(",")); diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractIE.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractIE.java index 099579ecae..0df3aa6b12 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractIE.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractIE.java @@ -364,6 +364,9 @@ class ExtractIE extends Extract { bbartifacts.addAll(parsePascoOutput(indexFile, filename).stream() .filter(bbart -> bbart.getArtifactTypeID() == ARTIFACT_TYPE.TSK_WEB_HISTORY.getTypeID()) .collect(Collectors.toList())); + if (context.dataSourceIngestIsCancelled()) { + return; + } foundHistory = true; //Delete index.dat file since it was succcessfully by Pasco @@ -465,6 +468,11 @@ class ExtractIE extends Extract { return bbartifacts; } while (fileScanner.hasNext()) { + + if (context.dataSourceIngestIsCancelled()) { + return bbartifacts; + } + String line = fileScanner.nextLine(); if (!line.startsWith("URL")) { //NON-NLS continue; @@ -565,7 +573,7 @@ class ExtractIE extends Extract { this.indexArtifact(bbart); bbartifacts.add(bbart); } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error writing Internet Explorer web history artifact to the blackboard.", ex); //NON-NLS + logger.log(Level.SEVERE, "Error writing Internet Explorer web history artifact to the blackboard. Pasco results will be incomplete", ex); //NON-NLS } } fileScanner.close(); diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractRegistry.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractRegistry.java index 77338655a5..aa23e0d7bd 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractRegistry.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractRegistry.java @@ -253,6 +253,7 @@ class ExtractRegistry extends Extract { logger.log(Level.WARNING, "Keyword search service not found. Report will not be indexed"); } else { searchService.index(report); + report.close(); } } catch (TskCoreException e) { this.addErrorMessage("Error adding regripper output as Autopsy report: " + e.getLocalizedMessage()); //NON-NLS @@ -391,6 +392,11 @@ class ExtractRegistry extends Extract { // that we will submit in a ModuleDataEvent for additional processing. Collection wifiBBartifacts = new ArrayList<>(); for (int i = 0; i < len; i++) { + + if (context.dataSourceIngestIsCancelled()) { + return false; + } + Element tempnode = (Element) children.item(i); String dataType = tempnode.getNodeName(); @@ -809,11 +815,11 @@ class ExtractRegistry extends Extract { } catch (FileNotFoundException ex) { logger.log(Level.SEVERE, "Error finding the registry file.", ex); //NON-NLS } catch (SAXException ex) { - logger.log(Level.SEVERE, "Error parsing the registry XML: {0}", ex); //NON-NLS + logger.log(Level.SEVERE, "Error parsing the registry XML.", ex); //NON-NLS } catch (IOException ex) { - logger.log(Level.SEVERE, "Error building the document parser: {0}", ex); //NON-NLS + logger.log(Level.SEVERE, "Error building the document parser.", ex); //NON-NLS } catch (ParserConfigurationException ex) { - logger.log(Level.SEVERE, "Error configuring the registry parser: {0}", ex); //NON-NLS + logger.log(Level.SEVERE, "Error configuring the registry parser.", ex); //NON-NLS } finally { try { if (fstream != null) { diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractSafari.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractSafari.java index 41de0f59d8..164bbe1391 100755 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractSafari.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractSafari.java @@ -28,6 +28,7 @@ import com.dd.plist.PropertyListParser; import java.io.File; import java.io.IOException; import java.nio.file.Path; +import java.nio.file.Paths; import java.text.ParseException; import java.util.ArrayList; import java.util.Collection; @@ -49,7 +50,6 @@ import org.sleuthkit.autopsy.ingest.ModuleDataEvent; import org.sleuthkit.autopsy.recentactivity.BinaryCookieReader.Cookie; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; -import org.sleuthkit.datamodel.BlackboardAttribute; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.TskCoreException; import org.xml.sax.SAXException; @@ -121,7 +121,7 @@ final class ExtractSafari extends Extract { } catch (IOException | TskCoreException ex) { this.addErrorMessage(Bundle.ExtractSafari_Error_Getting_History()); - LOG.log(Level.SEVERE, "Exception thrown while processing history file: {0}", ex); //NON-NLS + LOG.log(Level.SEVERE, "Exception thrown while processing history file.", ex); //NON-NLS } progressBar.progress(Bundle.Progress_Message_Safari_Bookmarks()); @@ -129,7 +129,7 @@ final class ExtractSafari extends Extract { processBookmarkPList(dataSource, context); } catch (IOException | TskCoreException | SAXException | PropertyListFormatException | ParseException | ParserConfigurationException ex) { this.addErrorMessage(Bundle.ExtractSafari_Error_Parsing_Bookmark()); - LOG.log(Level.SEVERE, "Exception thrown while parsing Safari Bookmarks file: {0}", ex); //NON-NLS + LOG.log(Level.SEVERE, "Exception thrown while parsing Safari Bookmarks file.", ex); //NON-NLS } progressBar.progress(Bundle.Progress_Message_Safari_Downloads()); @@ -137,15 +137,15 @@ final class ExtractSafari extends Extract { processDownloadsPList(dataSource, context); } catch (IOException | TskCoreException | SAXException | PropertyListFormatException | ParseException | ParserConfigurationException ex) { this.addErrorMessage(Bundle.ExtractSafari_Error_Parsing_Bookmark()); - LOG.log(Level.SEVERE, "Exception thrown while parsing Safari Download.plist file: {0}", ex); //NON-NLS + LOG.log(Level.SEVERE, "Exception thrown while parsing Safari Download.plist file.", ex); //NON-NLS } progressBar.progress(Bundle.Progress_Message_Safari_Cookies()); try { processBinaryCookieFile(dataSource, context); - } catch (IOException | TskCoreException ex) { + } catch (TskCoreException ex) { this.addErrorMessage(Bundle.ExtractSafari_Error_Parsing_Cookies()); - LOG.log(Level.SEVERE, "Exception thrown while processing Safari cookies file: {0}", ex); //NON-NLS + LOG.log(Level.SEVERE, "Exception thrown while processing Safari cookies file.", ex); //NON-NLS } } @@ -246,7 +246,7 @@ final class ExtractSafari extends Extract { * @throws TskCoreException * @throws IOException */ - private void processBinaryCookieFile(Content dataSource, IngestJobContext context) throws TskCoreException, IOException { + private void processBinaryCookieFile(Content dataSource, IngestJobContext context) throws TskCoreException { FileManager fileManager = getCurrentCase().getServices().getFileManager(); List files = fileManager.findFiles(dataSource, COOKIE_FILE_NAME, COOKIE_FOLDER); @@ -261,7 +261,11 @@ final class ExtractSafari extends Extract { if (context.dataSourceIngestIsCancelled()) { break; } - getCookies(context, file); + try { + getCookies(context, file); + } catch (IOException ex) { + LOG.log(Level.WARNING, String.format("Failed to get cookies from file %s", Paths.get(file.getUniquePath(), file.getName()).toString()), ex); + } } } @@ -287,7 +291,7 @@ final class ExtractSafari extends Extract { } try { - Collection bbartifacts = getHistoryArtifacts(historyFile, tempHistoryFile.toPath()); + Collection bbartifacts = getHistoryArtifacts(historyFile, tempHistoryFile.toPath(), context); if (!bbartifacts.isEmpty()) { services.fireModuleDataEvent(new ModuleDataEvent( RecentActivityExtracterModuleFactory.getModuleName(), @@ -319,7 +323,7 @@ final class ExtractSafari extends Extract { File tempFile = createTemporaryFile(context, file); try { - Collection bbartifacts = getBookmarkArtifacts(file, tempFile); + Collection bbartifacts = getBookmarkArtifacts(file, tempFile, context); if (!bbartifacts.isEmpty()) { services.fireModuleDataEvent(new ModuleDataEvent( RecentActivityExtracterModuleFactory.getModuleName(), @@ -385,7 +389,7 @@ final class ExtractSafari extends Extract { try { tempFile = createTemporaryFile(context, file); - Collection bbartifacts = getCookieArtifacts(file, tempFile); + Collection bbartifacts = getCookieArtifacts(file, tempFile, context); if (!bbartifacts.isEmpty()) { services.fireModuleDataEvent(new ModuleDataEvent( @@ -409,7 +413,7 @@ final class ExtractSafari extends Extract { * history artifacts * @throws TskCoreException */ - private Collection getHistoryArtifacts(AbstractFile origFile, Path tempFilePath) throws TskCoreException { + private Collection getHistoryArtifacts(AbstractFile origFile, Path tempFilePath, IngestJobContext context) throws TskCoreException { List> historyList = this.dbConnect(tempFilePath.toString(), HISTORY_QUERY); if (historyList == null || historyList.isEmpty()) { @@ -418,6 +422,10 @@ final class ExtractSafari extends Extract { Collection bbartifacts = new ArrayList<>(); for (HashMap row : historyList) { + if (context.dataSourceIngestIsCancelled()) { + return bbartifacts; + } + String url = row.get(HEAD_URL).toString(); String title = row.get(HEAD_TITLE).toString(); Long time = (Double.valueOf(row.get(HEAD_TIME).toString())).longValue(); @@ -444,13 +452,13 @@ final class ExtractSafari extends Extract { * @throws SAXException * @throws TskCoreException */ - private Collection getBookmarkArtifacts(AbstractFile origFile, File tempFile) throws IOException, PropertyListFormatException, ParseException, ParserConfigurationException, SAXException, TskCoreException { + private Collection getBookmarkArtifacts(AbstractFile origFile, File tempFile, IngestJobContext context) throws IOException, PropertyListFormatException, ParseException, ParserConfigurationException, SAXException, TskCoreException { Collection bbartifacts = new ArrayList<>(); try { NSDictionary root = (NSDictionary) PropertyListParser.parse(tempFile); - parseBookmarkDictionary(bbartifacts, origFile, root); + parseBookmarkDictionary(bbartifacts, origFile, root, context); } catch (PropertyListFormatException ex) { PropertyListFormatException plfe = new PropertyListFormatException(origFile.getName() + ": " + ex.getMessage()); plfe.setStackTrace(ex.getStackTrace()); @@ -542,7 +550,7 @@ final class ExtractSafari extends Extract { * @throws TskCoreException * @throws IOException */ - private Collection getCookieArtifacts(AbstractFile origFile, File tempFile) throws TskCoreException, IOException { + private Collection getCookieArtifacts(AbstractFile origFile, File tempFile, IngestJobContext context) throws TskCoreException, IOException { Collection bbartifacts = null; BinaryCookieReader reader = BinaryCookieReader.initalizeReader(tempFile); @@ -551,6 +559,10 @@ final class ExtractSafari extends Extract { Iterator iter = reader.iterator(); while (iter.hasNext()) { + if (context.dataSourceIngestIsCancelled()) { + return bbartifacts; + } + Cookie cookie = iter.next(); BlackboardArtifact bbart = origFile.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_COOKIE); @@ -571,13 +583,18 @@ final class ExtractSafari extends Extract { * @param root NSDictionary object to parse * @throws TskCoreException */ - private void parseBookmarkDictionary(Collection bbartifacts, AbstractFile origFile, NSDictionary root) throws TskCoreException { + private void parseBookmarkDictionary(Collection bbartifacts, AbstractFile origFile, NSDictionary root, IngestJobContext context) throws TskCoreException { + + if (context.dataSourceIngestIsCancelled()) { + return; + } + if (root.containsKey(PLIST_KEY_CHILDREN)) { NSArray children = (NSArray) root.objectForKey(PLIST_KEY_CHILDREN); if (children != null) { for (NSObject obj : children.getArray()) { - parseBookmarkDictionary(bbartifacts, origFile, (NSDictionary) obj); + parseBookmarkDictionary(bbartifacts, origFile, (NSDictionary) obj, context); } } } else if (root.containsKey(PLIST_KEY_URL)) { diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractZoneIdentifier.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractZoneIdentifier.java index 56c6748712..96e889a093 100755 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractZoneIdentifier.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractZoneIdentifier.java @@ -100,6 +100,11 @@ final class ExtractZoneIdentifier extends Extract { Collection downloadArtifacts = new ArrayList<>(); for (AbstractFile zoneFile : zoneFiles) { + + if (context.dataSourceIngestIsCancelled()) { + return; + } + try { processZoneFile(context, dataSource, zoneFile, sourceArtifacts, downloadArtifacts, knownPathIDs); } catch (TskCoreException ex) { @@ -292,7 +297,7 @@ final class ExtractZoneIdentifier extends Extract { /** * Wrapper class for information in the :ZoneIdentifier file. The - * Zone.Identifier file has a simple format of key=value. There + * Zone.Identifier file has a simple format of \key\=\value\. There * are four known keys: ZoneId, ReferrerUrl, HostUrl, and * LastWriterPackageFamilyName. Not all browsers will put all values in the * file, in fact most will only supply the ZoneId. Only Edge supplies the diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Firefox.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Firefox.java index 8430beb9b1..7552c3632f 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Firefox.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Firefox.java @@ -153,6 +153,11 @@ class Firefox extends Extract { Collection bbartifacts = new ArrayList<>(); int j = 0; for (AbstractFile historyFile : historyFiles) { + + if (context.dataSourceIngestIsCancelled()) { + return; + } + if (historyFile.getSize() == 0) { continue; } @@ -184,6 +189,11 @@ class Firefox extends Extract { List> tempList = this.dbConnect(temps, HISTORY_QUERY); logger.log(Level.INFO, "{0} - Now getting history from {1} with {2} artifacts identified.", new Object[]{moduleName, temps, tempList.size()}); //NON-NLS for (HashMap result : tempList) { + + if (context.dataSourceIngestIsCancelled()) { + return; + } + String url = result.get("url").toString(); Collection bbattributes = new ArrayList<>(); @@ -277,6 +287,11 @@ class Firefox extends Extract { List> tempList = this.dbConnect(temps, BOOKMARK_QUERY); logger.log(Level.INFO, "{0} - Now getting bookmarks from {1} with {2} artifacts identified.", new Object[]{moduleName, temps, tempList.size()}); //NON-NLS for (HashMap result : tempList) { + + if (context.dataSourceIngestIsCancelled()) { + break; + } + String url = result.get("url").toString(); Collection bbattributes = new ArrayList<>(); @@ -376,6 +391,11 @@ class Firefox extends Extract { List> tempList = this.dbConnect(temps, query); logger.log(Level.INFO, "{0} - Now getting cookies from {1} with {2} artifacts identified.", new Object[]{moduleName, temps, tempList.size()}); //NON-NLS for (HashMap result : tempList) { + + if (context.dataSourceIngestIsCancelled()) { + break; + } + String host = result.get("host").toString(); Collection bbattributes = new ArrayList<>(); @@ -487,6 +507,11 @@ class Firefox extends Extract { List> tempList = this.dbConnect(temps, DOWNLOAD_QUERY); logger.log(Level.INFO, "{0}- Now getting downloads from {1} with {2} artifacts identified.", new Object[]{moduleName, temps, tempList.size()}); //NON-NLS for (HashMap result : tempList) { + + if (context.dataSourceIngestIsCancelled()) { + break; + } + String source = result.get("source").toString(); Collection bbattributes = new ArrayList<>(); @@ -621,6 +646,11 @@ class Firefox extends Extract { logger.log(Level.INFO, "{0} - Now getting downloads from {1} with {2} artifacts identified.", new Object[]{moduleName, temps, tempList.size()}); //NON-NLS for (HashMap result : tempList) { + + if (context.dataSourceIngestIsCancelled()) { + break; + } + String url = result.get("url").toString(); Collection bbattributes = new ArrayList<>(); @@ -764,6 +794,11 @@ class Firefox extends Extract { List> tempList = this.dbConnect(tempFilePath, formHistoryQuery); logger.log(Level.INFO, "{0} - Now getting history from {1} with {2} artifacts identified.", new Object[]{moduleName, tempFilePath, tempList.size()}); //NON-NLS for (HashMap result : tempList) { + + if (context.dataSourceIngestIsCancelled()) { + break; + } + Collection bbattributes = new ArrayList<>(); String fieldName = ((result.get("fieldname").toString() != null) ? result.get("fieldname").toString() : ""); diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Util.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Util.java index fd9630cebd..ae4e3d0c89 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Util.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Util.java @@ -148,8 +148,9 @@ class Util { String query = "PRAGMA table_info(" + tablename + ")"; //NON-NLS boolean found = false; ResultSet temprs; + SQLiteDBConnect tempdbconnect = null; try { - SQLiteDBConnect tempdbconnect = new SQLiteDBConnect("org.sqlite.JDBC", "jdbc:sqlite:" + connection); //NON-NLS + tempdbconnect = new SQLiteDBConnect("org.sqlite.JDBC", "jdbc:sqlite:" + connection); //NON-NLS temprs = tempdbconnect.executeQry(query); while (temprs.next()) { if (temprs.getString("name") == null ? column == null : temprs.getString("name").equals(column)) { //NON-NLS @@ -159,6 +160,11 @@ class Util { } catch (Exception ex) { logger.log(Level.WARNING, "Error while trying to get columns from sqlite db." + connection, ex); //NON-NLS } + finally{ + if (tempdbconnect != null) { + tempdbconnect.closeConnection(); + } + } return found; } diff --git a/build-windows-installer.xml b/build-windows-installer.xml index 61f006a5a3..3cb3876df9 100644 --- a/build-windows-installer.xml +++ b/build-windows-installer.xml @@ -109,6 +109,8 @@ + + @@ -117,16 +119,9 @@ - - - - + - - - - - + @@ -139,7 +134,9 @@ - + + + @@ -153,48 +150,13 @@ - - - - + - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -228,6 +190,11 @@ + + + + + diff --git a/docs/doxygen-user/auto_ingest.dox b/docs/doxygen-user/auto_ingest.dox new file mode 100644 index 0000000000..246d1efe22 --- /dev/null +++ b/docs/doxygen-user/auto_ingest.dox @@ -0,0 +1,92 @@ +/*! \page auto_ingest_page Automated Ingest + +\section auto_ingest_overview Overview + +Auto ingest allows one or many computers to process \ref ds_page "data sources" automatically with minimal support from a user. The resulting \ref multiuser_page "multi-user cases" can be opened and reviewed by analysts, using any of the normal functions in Autopsy. + +There are three types of computers in an Automated Processing Deployment: +
    +
  • Automated Ingest Node: +These computers are responsible for monitoring the Shared Images Folder and detecting when new images have been copied in. Each writes its results to the Shared Cases Folder. +
  • Examiner Node: These computers can open a case during processing or after it has been analyzed by the Automated Ingest Node. They allow the examiner to review the results, tag files, and perform additional analysis as needed. +
  • Services/Storage Node: These computers run the services needed for \ref multiuser_page "multi-user cases", hold the images to be processed and store the analyzed Autopsy cases. +
+ +The general workflow is as follows: +
    +
  1. Disk images or other types of data sources are added to the shared images folder. This folder will contain all the disk and phone images that are copied into the system. They must be copied into here before they can be analyzed. As more than one machine may need to access this folder across the network, use UNC paths (if possible) to refer to this folder. +
  2. A \ref auto_ingest_manifest_creation "manifest file" is added for each data source that is to be processed. +
  3. An auto ingest node finds that manifest file and begins processing the data source. It will make a case in the shared cases folder if there is not one there already. This folder will contain all of the analysis results after automated analysis has been performed on the images. This folder will not contain the images, those will stay in the Shared Images Folder. As more than one machine may need to access this folder across the network, use UNC paths (if possible) to refer to this folder. +
  4. An analyst on an examiner node opens the case and starts their analysis. This can happen while an auto ingest node is processing data or afterwards. +
+ +An Automated Processing Deployment could have an architecture, such as this: + +\image html AutoIngest\overview_pic1.png + +Another illustration, including the network infrastructure, is shown below: + +\image html AutoIngest\overview_pic2.png + +\section auto_ingest_setup_section Configuration + +Configuring a group of computers for auto ingest is described on the \ref auto_ingest_setup_page page. + +\section auto_ingest_ex_usage Examiner Node Usage + +An examiner node in an auto ingest environment is generally the same as any normal Autopsy client set up for \ref multiuser_page "multi-user cases." Any number of examiner nodes can open cases that have been created by the auto ingest nodes. The cases do not need to be complete. + +The examiner can open the auto ingest dashboard through the Tools menu. This allows the user to see what cases and data sources are scheduled, in progress, or done. + +\image html AutoIngest\examiner_dashboard.png + +\section auto_ingest_ain_usage Auto Ingest Node Usage + +\subsection auto_ingest_manifest_creation Preparing Data for Auto Ingest + +Users will manually copy images to the source images folder (using subfolders if desired) and schedule them to be ingested by creating one file in the folder alongside the image to be ingested. This file is a manifest file describing the image. This file's name must end in "_Manifest.xml." + +\image html AutoIngest\manifest_file_in_file_explorer.png + +The following is an example of an Autopsy manifest file. Line breaks/spaces are not required, but are shown here for better human readability. +\verbatim + + XperiaCase + 50549 + mtd3_userdata.bin +\endverbatim + +The following is a description of each required field: +
  • CaseName: Case name. Multiple data sources can belong to the same case. +
  • DeviceId: (Optional) A globally unique ID representing device this data source came from. This can be an integer or a UUID. +
  • DataSource: File name of the data source. Does not include the path. +
+Any amount of additional data may be included in the XML file as long as the fields above are present. + +Manifest files can be automatically generated by using the \ref manifest_tool_page. + +\subsection auto_ingest_running Running an Auto Ingest Node + +When auto ingest mode is enabled, Autopsy will open with a different UI than normal, allowing the user to see what cases are being processed, which are done, and which are next in the queue. You can also change the priority of cases and reprocess cases that may have had an error. + +\image html AutoIngest\auto_ingest_in_progress.png + +The user must press the "Start" button to being the auto ingest process. Note that if the computer running Autopsy in auto ingest mode is restarted, someone must log into it to restart Autopsy. It does not start by itself. When "Start" is pressed, the node will scan through the Shared Images folder looking for manifest files. This scan happens periodically when ingest is running. It can also be started manually using the "Refresh" button. + +The UI for the auto ingest node will display what images are scheduled for analysis, what is currently running, and what has been completed. If a newly added image should be the highest priority, then you can select it and choose "Prioritize Case". This will prioritize all images within the same case to be top priority. You may also prioritize only a single data source (job) using the "Prioritize Job" button in the same manner. If you have prioritized something by mistake, the "Deprioritize" buttons will undo it. + +In the middle area, you can see the currently running jobs. You have the option of cancelling an entire image that is being analyzed or to cancel only the current module that is running. The latter is used when one of the modules has been running for too long and you think that the module is having trouble with the image and will never complete. If the auto ingest node loses connection to either the database or Solr services it will automatically cancel the currently running job and will pause processing. Once the connectivity issue has been resolved you must manually resume processing. + +If an error occurs while processing a job, or if a job was set up incorrectly, the "Reprocess Job" button can be used to move a completed job back into the Pending Jobs table, where it can be prioritized if desired. No case data is deleted which may result in some duplication in the results. + +"Delete Case" will remove a case from the list and remove all of its data. This will not remove the original image, manifest file, or anything else from the input directory. A case can not be deleted if it is currently open in any Examiner Node or if an auto ingest node is currently working on a job related to the case. Care should be used with the delete case button. Note that once a case is deleted the path to its data sources must be changed before they can be reprocessed (i.e., rename the base folder). + +The "Auto Ingest Metrics" button displays processing data for all of the auto ingest nodes in the system from a user-entered starting date. + +\image html AutoIngest\metrics.png + +\section auto_ingest_administration_section Auto Ingest Node Administration + +See the \ref auto_ingest_admin_page for information on how to enable administrator features. + +*/ \ No newline at end of file diff --git a/docs/doxygen-user/auto_ingest_administration.dox b/docs/doxygen-user/auto_ingest_administration.dox new file mode 100644 index 0000000000..972ef510bd --- /dev/null +++ b/docs/doxygen-user/auto_ingest_administration.dox @@ -0,0 +1,91 @@ +/*! \page auto_ingest_admin_page Auto Ingest Administration + +\section auto_ingest_admin_overview Overview + +Examiner nodes in an \ref auto_ingest_page environment can be given a type of administrator access. This allows an admin to: + +
  • Access admin-only options on the Auto Ingest Jobs Panel, including: +
      +
    • Prioritizing jobs and cases +
    • Cancelling jobs +
    • Deleting and reprocessing jobs +
    +
  • Access the Auto Ingest Nodes Panel, which allows the user to: +
    • View the currently active auto ingest nodes +
    • Pause/resume/shutdown the active auto ingest nodes +
    • View/enabled the health monitor +
    • View auto ingest metrics +
+ +\section auto_ingest_admin_setup Setup + +The admin panel is enabled by creating the file "admin" in the user config directory. Note that the name must be exactly that with no extension. It also works to make a folder named "admin" instead of a file which can be easier on machines where the file extension is hidden. No restart is needed; simply reopen the Auto Ingest Dashboard after creating the file. + +For an installed copy of Autopsy, the file will go under \c "C:\Users\\AppData\Roaming\Autopsy\config". + +\image html AutoIngest\admin_file.png + +\section auto_ingest_admin_jobs_panel Auto Ingest Jobs Panel + +With the admin file in place, the user can right-click on jobs in each of the tables of the jobs panel to perform different actions. In the Pending Jobs table, the context menu allows cases and individual jobs to be prioritized. + +\image html AutoIngest\admin_jobs_panel.png + +In the Running Jobs tables, the ingest progress can be viewed and the current job can be cancelled. Note that cancellation can take some time. + +\image html AutoIngest\admin_jobs_cancel.png + +In the Completed Jobs table, the user can reprocess a job (generally useful when a job had errors), delete a case (if no other machines are using it) and view the case log. + +\image html AutoIngest\admin_jobs_completed.png + +\section auto_ingest_admin_nodes_panel Auto Ingest Nodes Panel + +The Nodes panel displays the status of every online auto ingest node. Additionally, an admin can pause or resume a node, or shut down a node entirely (i.e., exit the Autopsy app). + +\image html AutoIngest\admin_nodes_panel.png + +\section auto_ingest_admin_cases_panel Cases Panel + +The Cases panel shows information about each auto ingest case - the name, creation and last accessed times, the case directory, and flags for which parts of the case have been deleted. + +\image html AutoIngest\cases_panel.png + +If you right-click on a case, you can open it, see the log, delete the case, or view properties of the case. + +\image html AutoIngest\cases_context_menu.png + +Note that you can select multiple cases at once to delete. If you choose to delete a case (or cases), you'll see the following confirmation dialog: + +\image html case_delete_confirm.png + +\section auto_ingest_admin_health_monitor Health Monitor + +The health monitor shows timing stats and the general state of the system. The Health Monitor is accessed from the Auto Ingest Nodes panel. To enable health monitoring, click on the Health Monitor button to get the following screen and then press the "Enable monitor" button. + +\image html AutoIngest\health_monitor_disabled.png + +This will enable the health monitor metrics on every node (both auto ingest nodes and examiner nodes) that is using this PostgreSQL server. Once enabled, the monitor will display the collected metrics. + +\image html AutoIngest\health_monitor.png + +By default, the graphs will show all metrics collected in the last day. + +The Timing Metrics area shows how long various tasks took to perform. There are several options in the Timing Metrics section: +
  • Max days to display: Choose to show the last day, week, two week, or month +
  • Filter by host: Show only metrics that came from the selected host +
  • Show trend line: Show or hide the red trend line +
  • Do not plot outliers: Redraws the graph allowing very high metrics to go off the screen. Can be helpful with data where a couple of entries took an exceptionally long time. +
+ +The User Metrics section shows open cases and logged on nodes. For the open cases section, the count is the number of distinct cases open. If ten nodes have the same case open, the count will be one. The logged in users section shows the total number of active nodes, with auto ingest nodes on the bottom in green and examiner nodes on top in blue. The User Metrics section only has one option: +
  • Max days to display: Choose to show the last day, week, two week, or month +
+ +\section auto_ingest_admin_metrics Auto Ingest Metrics + +The Auto Ingest Metrics can be accessed the Auto Ingest Nodes panel and shows data about the jobs completed in a selected time frame. + +\image html AutoIngest\metrics.png + +*/ \ No newline at end of file diff --git a/docs/doxygen-user/auto_ingest_setup.dox b/docs/doxygen-user/auto_ingest_setup.dox new file mode 100644 index 0000000000..48d33d67fe --- /dev/null +++ b/docs/doxygen-user/auto_ingest_setup.dox @@ -0,0 +1,104 @@ +/*! \page auto_ingest_setup_page Auto Ingest Configuration + +\section auto_ingest_setup_overview Overview + +A multi-user installation requires several network-based services, such as a central database and a messaging system, and automated ingest requires one or more auto ingest nodes. While you may run all of the external services on a single node, this is not likely to be ideal - spreading the services out across several machines can improve throughput. Keeping in mind that all the following machines need to be able to communicate with each other with network visibility to the shared drive, here is a description of a possible configuration: + + + + + + + + +
Number of MachinesServices
One
  • Solr - Install Solr on the highest-powered machine; the more CPUs the better.
  • +
  • The case output folders can also be put on this machine.
One
  • ActiveMQ - This service has minimal memory and disk requirements.
  • +
  • PostgreSQL - This service has minimal memory and disk requirements.
One
  • Shared image folder - This machine needs a large amount of disk space but doesn't need the fastest hardware.
One or more
  • Automated Ingest Node(s) - These machines don't need much disk space but benefit from additional memory and processing power.
One or more
  • Examiner Node(s) - See \ref installation_page for recommended system requirements.
+ +Solr is going to be a sizeable resource hog. A big performance increase will be seen if you put solid state drives (SSD) in the machine running Solr, and have that machine also host the large network drive on the SSDs as a place to store case output. The source images to can be on SAS drives (slower than SSD) with very little impact on performance. This idea here is to have the most resource-intensive operations on the fastest hardware. Using this strategy, there are actually two large network stores, one for input images and one for output cases. + +\section auto_ingest_setup_services Installing Services and Configuring Autopsy +Follow the instructions on the \ref install_multiuser_page page to set up the necessary services and configure your Autopsy clients to use them. After this is complete, you should be able to \ref multiuser_page "create and use multi-user cases". + +\section auto_ingest_setup_ain_config Auto Ingest Node Configuration + +While Examiner nodes only require multi-user cases to be set up, the auto ingest nodes need additional configuration. To start, go to the "Auto Ingest" tab on the Options menu and select the "Auto Ingest mode" radio button. If you haven't saved your multi-user settings there will be a warning message displayed here - if you see it, go back to the "Multi-User" tab and make sure you've entered all the required fields and then hit the "Apply" button. + +\image html AutoIngest\auto_ingest_mode_setup.png + +\subsection auto_ingest_config_folders Folder Configuration + +The first thing to do is to set two folder locations. The shared images folder is the base folder for all data that will be ingested through the auto ingest node. The shared cases folder is the base folder for the cases that will be created by the auto ingest node. + +\subsection auto_ingest_config_ingest_settings Ingest Module Settings +The "Ingest Module Settings" button is used to configure the \ref ingest_page you want to run during auto-ingest. One note is that on auto-ingest nodes, we recommend that you configure the Keyword Search module to not perform periodic keyword searches. When a user is in front of the computer, this feature exists to provide frequent updates, but it is not needed on this node. To configure this, choose the Keyword Search item in the Options window. Select the "General" tab and choose the option for no periodic search. + +\image html AutoIngest\no_periodic_searches.png + +\subsection auto_ingest_advanced_settings Advanced Settings + +The "Advanced Settings" button will bring up the automated ingest job settings. As expressed in the warning statement, care must be used when making changes on this panel. + +\image html AutoIngest\advanced_settings.png + +The Automated Ingest Job Settings section contains the following options: +
+
System synchronization wait time
+
A wait time used by auto ingest nodes to ensure proper synchronization of node operations in circumstances where delays may occur, e.g., a wait to compensate for network file system latency effects on the visibility of newly created shared directories and files.
+
External processes time out
+
Autopsy components that spawn potentially long-running processes have the option to use this setting, if it is enabled, to terminate those processes if the specified time out period has elapsed. Each component that uses this feature is responsible for implementing its own policy for the handling of incomplete processing when an external process time out occurs. Core components that use external process time outs include the \ref recent_activity_page and \ref photorec_carver_page ingest modules.
+
Interval between input scans
+
The interval between scans of the auto ingest input directories for manifest files. Note that the actual timing of input scans by each node depends on both this setting and node startup time.
+
Maximum job retries allowed
+
The maximum number of times a crashed auto ingest job will be automatically retried. No distinction is made between jobs that crash due to system error conditions such as power outages and jobs that crash due to input data source corruption. In general, input data source corruption should be handled gracefully by Autopsy, but this setting provides insurance against unforeseen issues with input data viability.
+
Target concurrent jobs per case
+
A soft limit on the number of concurrent jobs per case when multiple cases are processed simultaneously by a group of auto ingest nodes. This setting specifies a target rather than a hard limit because nodes are never idled if there are ingest jobs to do and nodes work cooperatively rather than rely on a centralized, load-balancing job scheduling service.
+
Number of threads to use for file ingest
+
The number of threads an auto ingest node dedicates to analyzing files from input data sources in parallel. Note that analysis of input data source files themselves is always single-threaded.
+
+ +\subsection auto_ingest_file_export File Export + +The "File Export" button will bring up the \ref file_export_page settings. This allows certain types of files to be automatically exported during auto ingest. Setting up this feature requires knowledge of internal Autopsy data structures and can be ignored for users. + +\subsection auto_ingest_shared_config Shared Configuration + +When using multiple auto ingest nodes, configuration can be centralized and shared with any auto ingest node that desires to use it. This is called Shared Configuration. The general idea is that you will set up one node (the "master") and upload that configuration to a central location. Then the other auto ingest nodes (the "secondary" nodes) will download that configuration whenever they start a new job. This saves time because you only need to configure one node, and ensures consistency across the auto ingest nodes. + +\subsubsection auto_ingest_shared_config_master Master Node + +On the computer that is going to be the configuration master automated ingest node, follow the configuration steps described in above to configure the node. +If you would like every automated ingest node to share the configuration settings, check the first checkbox in the Shared Configuration section of the Auto Ingest settings panel. Next select a folder to store the shared configuration in. This folder must be a path to a network share that the other machines in the system will have access to. Use a UNC path if possible. Next, check the "Use this node as a master node that can upload settings" checkbox which should enable the "Save & Upload Config" button. If this does not happen, look for a red error message explaining what settings are missing. + +\image html AutoIngest\master_node.png + +After saving and uploading the configuration, hit the "Save" button to exit the Options panel. + +\subsubsection auto_ingest_shared_config_secondary Secondary Node + +Once one node has uploaded shared configuration data, the remaining nodes can be set up to download it, skipping over some of the configuration steps above. + +To set up a secondary node, start by going through the \ref install_multiuser_page "multi-user configuration." Apply those changes, then switch to the Auto Ingest tab on the Options panel. Check the box to enable auto ingest, and then the box to enable shared configuration and enter the same folder used on the master node. The "Download Config" button should now be enabled and can be used to get the rest of the configuration automatically. Afterwards a dialog will likely appear telling you to restart Autopsy. + +\subsubsection auto_ingest_shared_config_notes Notes + +Some notes on shared configuration: +
  • The \ref auto_ingest_error_suppression "error suppression registry edit" below will need to be done on each node +
  • After the initial setup, the current shared configuration data will be updated before each job (no need to manually download it again) +
  • A few options require a restart to take effect (for example, most of the multi-user settings). If these are downloaded automatically while automated ingest is running, they will not be used until the automated ingest node is restarted. +
  • There is currently a limitation on where hash databases can be saved. Each database will be downloaded to the same folder it was in on the master node, which will cause errors if that drive letter is not present or the folder is not writeable on every node. +
  • Shared copies of the hash databases are also not currently supported. Each node will download its own copy of each database. +
+ + +\subsection auto_ingest_error_suppression Error Suppression + +On an auto ingest node, we also strongly recommend that you configure the system to suppress error dialogs that Windows may display if an application crashes. Some of the modules that Autopsy runs have crashed on some test data in the past and if an error dialog is displayed all processing stops. + +Disabling the error messages is done by setting the following registry key to "1", as shown in the screenshot below. +\verbatim HKCU\Software\Microsoft\Windows\Windows Error Reporting\DontShowUI\endverbatim + +\image html AutoIngest\error_suppression.png + + +*/ \ No newline at end of file diff --git a/docs/doxygen-user/case_management.dox b/docs/doxygen-user/case_management.dox index b4d9d0daba..84ba460917 100644 --- a/docs/doxygen-user/case_management.dox +++ b/docs/doxygen-user/case_management.dox @@ -38,15 +38,24 @@ To open a case, either: \image html multi_user_case_select.png -\section case_properties Viewing Case Properties -You can view the case properties by going to the "Case" menu and clicking "Case Properties". +\section case_properties Viewing Case Details and the Data Source Summary +You can view the case properties by going to the "Case" menu and clicking "Case Details". \image html case_properties.png -You can use the "Ingest History" tab to view which data sources had which modules run upon them, and when, as shown in the screenshot below. +Most of the case properties can be edited through the "Edit Details" button. -\image html case-properties-history-tab.PNG +You can view the data source summary by going to the "Case" menu and clicking "Data Source Summary". The table at the top shows general information about each data source in the case. In the lower half, the first tab shows more detailed information about the selected data source. +\image html data_source_summary_details.png + +The second tab, "Counts", shows the number of files found of various types and number of extracted results. + +\image html data_source_summary_counts.png + +The third tab, "Ingest History", shows each ingest job, the time it was completed, and which modules were run as part of the job. + +\image html data_source_summary_ingest.png */ diff --git a/docs/doxygen-user/central_repo.dox b/docs/doxygen-user/central_repo.dox index e8cd01c1ba..b08ef7dcad 100644 --- a/docs/doxygen-user/central_repo.dox +++ b/docs/doxygen-user/central_repo.dox @@ -143,7 +143,7 @@ If you would like to prevent the Interesting Items from being created in a parti through the run time ingest properties. Note that this only disables the Interesting Item results - all properties are still added to the central repository. -\image html central_repo_disable_flagging.png +\image html central_repo_ingest_settings.png \section cr_viewing_results Viewing Results @@ -161,20 +161,19 @@ properties from the central repository. If the selected file or artifact is asso to one or more properties in the database, the associated properties will be displayed. Note: the Content Viewer will display ALL associated properties available in the database. It ignores the user's enabled/disabled Correlation Properties. -By default, the rows in the content viewer will have background colors to indicate if they are known to be of interest. Properties that are notable -will have a Red background, all others will have a White background. +The other occurrences are grouped by case and then data source. The rows in the content viewer have background colors to indicate if they are known to be of interest. Properties that are notable +will have a Red background, all others will have a White background. The notable status will also be displayed in the "Known" column. \image html central_repo_content_viewer.png The user can click on any column heading to sort by the values in that column. -If the user right-clicks on a row, a menu will be displayed. +If the user selects a row and then right-clicks, a menu will be displayed. This menu has several options. -# Select All -# Export Selected Rows to CSV -# Show Case Details -# Show Frequency --# Add/Edit Comment Select All @@ -206,9 +205,9 @@ the Case -> Case Properties menu. This shows how common the selected file is. The value is the percentage of case/data source tuples that have the selected property. -Add/Edit Comment +\subsection central_repo_comment Add/Edit Comment -This allows you to add a comment for this entry or edit an existing comment. If you want instead to edit the comment of the originally selected node, it can be done by right clicking on the original item in the result viewer and selecting "Add/Edit Central Repository Comment". +If you want instead to edit the comment of a node, it can be done by right clicking on the original item in the result viewer and selecting "Add/Edit Central Repository Comment". \image html central_repo_comment_menu.png diff --git a/docs/doxygen-user/command_line_ingest.dox b/docs/doxygen-user/command_line_ingest.dox new file mode 100644 index 0000000000..bb248220c9 --- /dev/null +++ b/docs/doxygen-user/command_line_ingest.dox @@ -0,0 +1,49 @@ +/*! \page command_line_ingest_page Command Line Ingest + +\section command_line_ingest_overview Overview + +The Command Line Ingest feature allows you to process a \ref ds_page "data source" with Autopsy from the command line. Autopsy will automatically create a case with the settings you specify and will generate a \ref report_case_uco report. + +\section command_line_ingest_config Configuration + +Go to Tools->Options and then select the "Command Line Ingest" tab. + +\image html command_line_ingest_options.png + +First, enter the output folder for the cases. Next, use the button to open the ingest module settings. Here you can configure the \ref ingest_page settings that will be used when running from the command line. + +\section command_line_ingest_run Running Autopsy + +In a command prompt, navigate to the Autopsy bin folder. This is normally located at "C:\Program Files\Autopsy-version\bin". + +\image html command_line_ingest_bin_dir.png + +Now run autopsy with the following parameters, substituting the path to your data source and your desired case name. Both \ref ds_img "disk images" and \ref ds_log "logical files" are supported. Note that the case name must be unique for each run. + +\verbatim +autopsy64.exe --inputPath=(data source path) --caseName=(case name) --runFromCommandLine=true +\endverbatim + +In the example below, we're going to process a disk image with path "R:\work\images\xp-sp3-v4.E01" and name the case "xpCase". + +\image html command_line_ingest_command_entry.png + +You'll start seeing output in the command prompt and the Autopsy UI will open. In the middle of the UI you'll see the following dialog: + +\image html command_line_ingest_dialog.png + +Once Autopsy finishes processing you'll be back at the command window. Press enter to return to the command prompt. + +\image html command_line_ingest_console_output.png + +\section command_line_ingest_results Viewing Results + +You can open the case created on the command line like any other Autopsy case. Simply go to "Open Case" and then browse to the output folder you set up in the \ref command_line_ingest_config section and look for the folder starting with your case name. It will have a timestamp appended to the name you specified. + +\image html command_line_ingest_open_case.png + +If you are only interested in the \ref report_case_uco report then you don't need to open Autopsy. The report can be found in the case folder under "Reports\CASE-UCO" and then an automatically generated data source name containing the ID and timestamp. + +\image html command_line_ingest_report.png + +*/ \ No newline at end of file diff --git a/docs/doxygen-user/common_files.dox b/docs/doxygen-user/common_files.dox index f1a09b7b47..1dde4f2a54 100644 --- a/docs/doxygen-user/common_files.dox +++ b/docs/doxygen-user/common_files.dox @@ -54,7 +54,7 @@ You can choose to hide matches that appear with a high frequency in the Central Each search displays its results in a new tab. The title of the tab will include the search parameters. -\subsection common_properties_sort_by_count Sort by number of occurrences +\subsection common_properties_sort_by_count Sort by number of data sources \image html common_properties_result.png diff --git a/docs/doxygen-user/content_viewer.dox b/docs/doxygen-user/content_viewer.dox index 3c35060700..3a06fa89fa 100644 --- a/docs/doxygen-user/content_viewer.dox +++ b/docs/doxygen-user/content_viewer.dox @@ -10,10 +10,18 @@ When a Result type is selected in the Result Viewer (as opposed to a file), most \section cv_hex Hex -The Hex tab is nearly always available and shows the contents of the file. +The Hex Content Viewer is nearly always available and shows you the raw and exact contents of a file. In this content viewer, the data of the file is represented as hexadecimal values grouped in 2 groups of 8 bytes, followed by one group of 16 ASCII characters which are derived from each pair of hex values (each byte). Non-printable ASCII characters and characters that would take more than one character space are typically represented by a dot (".") in the following ASCII field. \image html content_viewer_hex.png +If desired, you can open the file in an external hex editor. This is configured through the "External Viewer" tab on the options panel. HxD has been tested to work, but alternate hex editors may also be compatible. + +\image html content_viewer_hex_editor_setup.png + +Note that this process saves the file to disk before launching the hex editor. A progress indicator will be displayed in the lower right corner of the application. If you wish to cancel the file export, click the 'X' to the right of the progress bar. + +\image html content_viewer_hxd_progress.png + \section cv_strings Strings The Strings tab shows all text strings found in the file. Different scripts can be chosen from the drop-down menu to display results for non-Latin alphabets. @@ -24,7 +32,7 @@ The Strings tab shows all text strings found in the file. Different scripts can For certain file types, the Application tab can display the contents in a user friendly format. The following screenshots show some examples of what the Application tab will display. -It will display most image types: +It will display most image types, which can be scaled and rotated: \image html content_viewer_app_image.png @@ -36,6 +44,10 @@ And plist file data will be shown and can be exported: \image html content_viewer_app_plist.png +HTML files can be displayed closer to their original form: + +\image html content_viewer_html.png + \section cv_indexed_text Indexed Text The Indexed Text tab shows the text that has been indexed by the Keyword Search module. You can switch the "Text Source" Field to "Result Text" to see which text has been indexed for associated results. diff --git a/docs/doxygen-user/data_sources.dox b/docs/doxygen-user/data_sources.dox index d472118f04..d0b9f12753 100644 --- a/docs/doxygen-user/data_sources.dox +++ b/docs/doxygen-user/data_sources.dox @@ -54,11 +54,13 @@ Autopsy supports disk images in the following formats: To add a disk image: --# Choose "Disk Image or VM File" from the data source types. --# Browse to the first file in the disk image. You need to specify only the first file and Autopsy will find the rest. --# Choose the timezone that the disk image came from. This is most important for when adding FAT file systems because it does not store timezone information and Autopsy will not know how to normalize to UTC. --# Choose to perform orphan file finding on FAT file systems. This can be a time intensive process because it will require that Autopsy look at each sector in the device. --# Optionally choose the sector size. The Auto Detect mode will work correctly on the majority of images, but if adding the data source fails you may want to try the other sector sizes. +
    +
  1. Choose "Disk Image or VM File" from the data source types. +
  2. Browse to the first file in the disk image. You need to specify only the first file and Autopsy will find the rest.
  3. Choose to perform orphan file finding on FAT file systems. This can be a time intensive process because it will require that Autopsy look at each sector in the device. +
  4. Choose the timezone that the disk image came from. This is most important for when adding FAT file systems because it does not store timezone information and Autopsy will not know how to normalize to UTC. +
  5. Optionally choose the sector size. The Auto Detect mode will work correctly on the majority of images, but if adding the data source fails you may want to try the other sector sizes. +
  6. Optionally enter one or more hashes for the image. These will be saved under the image metadata and can be verified using the \ref data_source_integrity_page. +
\section ds_local Adding a Local Disk diff --git a/docs/doxygen-user/experimental.dox b/docs/doxygen-user/experimental.dox index 4593a16df8..c5fcc742a8 100644 --- a/docs/doxygen-user/experimental.dox +++ b/docs/doxygen-user/experimental.dox @@ -6,13 +6,13 @@ The Experimental module, as the name implies, contains code that is not yet part \section exp_setup Enabling the Experimental Module -To start, go to Tools->Plugins and select the "Installed" tab, then check the box next to "Experimental" and click "Activate" and go throught the next couple of screens. A reset should not be required. +To start, go to Tools->Plugins and select the "Installed" tab, then check the box next to "Experimental" and click "Activate" and go through the next couple of screens. A restart should not be required. \image html experimental_plugins_menu.png \section exp_features Current Experimental Features -- Auto Ingest +- \ref auto_ingest_page - \ref object_detection_page - \ref volatility_dsp_page diff --git a/docs/doxygen-user/file_export.dox b/docs/doxygen-user/file_export.dox new file mode 100644 index 0000000000..84502c00cc --- /dev/null +++ b/docs/doxygen-user/file_export.dox @@ -0,0 +1,69 @@ +/*! \page file_export_page File Export + +\section file_export_overview Overview + +If enabled, the File Exporter will run after each \ref auto_ingest_page job and export any files from that data source that match the supplied rules. Most users will not need to use this feature - analysts can open the auto ingest cases in an examiner node and look through the data there. + +\section file_export_setup Configuration + +After enabling the file exporter, the first thing to do is set two output folders. The "Files Folder" is the base directory for all exported files, and the "Reports Folder" is the base directory for reports (lists of every file exported for each data source). If possible, it is best to use UNC paths. + +\image html AutoIngest\file_exporter_main.png + +Next you'll make rules for the files you want to export. Each rule must have a name and at least one condition set. If more than one condition is set, then all conditions must be true to export the file. When you're done setting up your rule, press the "Save" button to save it. You'll see the new rule in the list on the left side. + +All of the saved rules will be run against each data source. There's no way to set a rule as inactive, so if you make a rule and don't want it to run you'll have to use the "Delete Rule" button to remove it. + +You'll need to run at the \ref hash_db_page and \ref file_type_identification_page to use the file exporter. You may need to run additional modules based on any attributes in your rules. + +\subsection file_exporter_mime MIME Type + +The first condition is based on MIME type. To enable it, check the box before "MIME Type", then select a MIME type from the list and choose whether you want to match it or not match it. Multiple MIME types can not be selected at this time. The following shows a rule that will match all PNG images. + +\image html AutoIngest\file_export_png.png + +\subsection file_exporter_size File Size + +The second condition is based on file size. You can choose a file size (using the list on the right to change the units) and then select whether files should be larger, smaller, equal to, or not equal to that size. The following shows a rule that will match plain text files that are over 1kB. + +\image html AutoIngest\file_export_size.png + +\subsection file_exporter_attributes Attributes + +The third condition is based on blackboard artifacts and attributes, which is how Autopsy stores most of its analysis results. A file will be exported if it is linked to a matching attribute. Using this type of condition will require some familiarity with exactly how these attributes are being created and what data we expect to see in them. There's some information to get started in the Sleuthkit documentation. You will most likely also have to open an Autopsy database file to verify the exact attribute types being used to hold the data you're interested in. + +To make an attribute condition, select the artifact type and then the attribute type that you are interested in. On the next line you can enter a value and set what relation you want the attribute to have to it (equals, not equals, greater/less than). Not all options will make sense with all data types. Then use the "Add Attribute" button to add it to the attribute list. If you make a mistake, use the "Delete Attribute" button to erase it. The following shows a rule that will export any files that had a keyword hit for the word "bomb" in them. + +\image html AutoIngest\file_export_keyword.png + +It's possible to do more general matching on the artifacts. Suppose you wanted to export all files that the \ref encryption_page flagged as "Encryption Suspected". These files will have a TSK_ENCRYPTION_SUSPECTED artifact with a single "TSK_COMMENT" attribute that contains the entropy calculated for the file. In this case we can use the "not equals" operator on a string that we wouldn't expect to see in the TSK_COMMENT field to effectively change the condition to "has an associated TSK_ENCRYPTION_SUSPECTED artifact." + +\image html AutoIngest\file_export_encrypton.png + +\section file_export_output Output + +The exported files are found under the files folder that was specified in the \ref file_export_setup step and then organized at the top layer by the device ID of the data source. + +\image html AutoIngest\file_export_dir_structure.png + +Exported files are named with their hash and stored in subfolders based on parts of that hash, to prevent any single folder from becoming very large. + +\image html AutoIngest\file_export_file_loc.png + +The report files are also found in subfolders under the device ID and then the rule name. + +\image html AutoIngest\file_export_json_loc.png + +This json file will contain information about the file, and any associated artifact that was part of the rule's conditions. +\verbatim +{"7C89F280C337AB3E997D20527B8EC6F8":{"Filename":"\\\\WIN-4913\\AutopsyData\\FileExportFiles\\37567\\text-plain\\7C\\89\\F2\\80\\7C89F280C337AB3E997D20527B8EC6F8", +"Type":"text/plain","MD5":"7C89F280C337AB3E997D20527B8EC6F8","File data":{"Modified":["0000-00-00 00:00:00"],"Changed":["0000-00-0000:00:00"], +"Accessed":["0000-00-00 00:00:00"],"Created":["0000-00-00 00:00:00"],"Extension":["txt"],"Filename":["File about explosions.txt"],"Size":["54"], +"Source Path":["/kwTest_2019_03_14_12_53_33//File about explosions.txt"],"Flags (Dir)":["Allocated"],"Flags (Meta)":["Allocated"], +"Mode":["r---------"],"User ID":["0"],"Group ID":["0"],"Meta Addr":["0"],"Attr Addr":["1-0"],"Dir Type":["r"],"MetaType":["r"], +"Known":["unknown"]},"TSK_KEYWORD_HIT":{"TSK_KEYWORD":["bomb"]}, +"TSK_KEYWORD_HIT":{"TSK_KEYWORD_PREVIEW":["keyword search for the word bomb in this file.\n\n\n------"]}, +"TSK_KEYWORD_HIT":{"TSK_SET_NAME":["bomb"]},"TSK_KEYWORD_HIT":{"TSK_KEYWORD_SEARCH_TYPE":["0"]}}} +\endverbatim + +*/ \ No newline at end of file diff --git a/docs/doxygen-user/images/AutoIngest/admin_file.png b/docs/doxygen-user/images/AutoIngest/admin_file.png new file mode 100644 index 0000000000..0112d84ef3 Binary files /dev/null and b/docs/doxygen-user/images/AutoIngest/admin_file.png differ diff --git a/docs/doxygen-user/images/AutoIngest/admin_jobs_cancel.png b/docs/doxygen-user/images/AutoIngest/admin_jobs_cancel.png new file mode 100644 index 0000000000..ba3fb81691 Binary files /dev/null and b/docs/doxygen-user/images/AutoIngest/admin_jobs_cancel.png differ diff --git a/docs/doxygen-user/images/AutoIngest/admin_jobs_completed.png b/docs/doxygen-user/images/AutoIngest/admin_jobs_completed.png new file mode 100644 index 0000000000..f1046371de Binary files /dev/null and b/docs/doxygen-user/images/AutoIngest/admin_jobs_completed.png differ diff --git a/docs/doxygen-user/images/AutoIngest/admin_jobs_panel.png b/docs/doxygen-user/images/AutoIngest/admin_jobs_panel.png new file mode 100644 index 0000000000..32dfa89cdd Binary files /dev/null and b/docs/doxygen-user/images/AutoIngest/admin_jobs_panel.png differ diff --git a/docs/doxygen-user/images/AutoIngest/admin_nodes_panel.png b/docs/doxygen-user/images/AutoIngest/admin_nodes_panel.png new file mode 100644 index 0000000000..08488323dd Binary files /dev/null and b/docs/doxygen-user/images/AutoIngest/admin_nodes_panel.png differ diff --git a/docs/doxygen-user/images/AutoIngest/advanced_settings.png b/docs/doxygen-user/images/AutoIngest/advanced_settings.png new file mode 100644 index 0000000000..8dd88696d3 Binary files /dev/null and b/docs/doxygen-user/images/AutoIngest/advanced_settings.png differ diff --git a/docs/doxygen-user/images/AutoIngest/auto_ingest_in_progress.png b/docs/doxygen-user/images/AutoIngest/auto_ingest_in_progress.png new file mode 100644 index 0000000000..cf894b13c3 Binary files /dev/null and b/docs/doxygen-user/images/AutoIngest/auto_ingest_in_progress.png differ diff --git a/docs/doxygen-user/images/AutoIngest/auto_ingest_mode_setup.png b/docs/doxygen-user/images/AutoIngest/auto_ingest_mode_setup.png new file mode 100644 index 0000000000..e11db06246 Binary files /dev/null and b/docs/doxygen-user/images/AutoIngest/auto_ingest_mode_setup.png differ diff --git a/docs/doxygen-user/images/AutoIngest/case_delete_confirm.png b/docs/doxygen-user/images/AutoIngest/case_delete_confirm.png new file mode 100644 index 0000000000..6d3acec6ae Binary files /dev/null and b/docs/doxygen-user/images/AutoIngest/case_delete_confirm.png differ diff --git a/docs/doxygen-user/images/AutoIngest/cases_context_menu.png b/docs/doxygen-user/images/AutoIngest/cases_context_menu.png new file mode 100644 index 0000000000..e83c083f49 Binary files /dev/null and b/docs/doxygen-user/images/AutoIngest/cases_context_menu.png differ diff --git a/docs/doxygen-user/images/AutoIngest/cases_panel.png b/docs/doxygen-user/images/AutoIngest/cases_panel.png new file mode 100644 index 0000000000..c932b57bf1 Binary files /dev/null and b/docs/doxygen-user/images/AutoIngest/cases_panel.png differ diff --git a/docs/doxygen-user/images/AutoIngest/error_suppression.png b/docs/doxygen-user/images/AutoIngest/error_suppression.png new file mode 100644 index 0000000000..91d2805029 Binary files /dev/null and b/docs/doxygen-user/images/AutoIngest/error_suppression.png differ diff --git a/docs/doxygen-user/images/AutoIngest/examiner_dashboard.png b/docs/doxygen-user/images/AutoIngest/examiner_dashboard.png new file mode 100644 index 0000000000..7e0da4d353 Binary files /dev/null and b/docs/doxygen-user/images/AutoIngest/examiner_dashboard.png differ diff --git a/docs/doxygen-user/images/AutoIngest/file_export_dir_structure.png b/docs/doxygen-user/images/AutoIngest/file_export_dir_structure.png new file mode 100644 index 0000000000..2e9003831c Binary files /dev/null and b/docs/doxygen-user/images/AutoIngest/file_export_dir_structure.png differ diff --git a/docs/doxygen-user/images/AutoIngest/file_export_encrypton.png b/docs/doxygen-user/images/AutoIngest/file_export_encrypton.png new file mode 100644 index 0000000000..829ee730f0 Binary files /dev/null and b/docs/doxygen-user/images/AutoIngest/file_export_encrypton.png differ diff --git a/docs/doxygen-user/images/AutoIngest/file_export_file_loc.png b/docs/doxygen-user/images/AutoIngest/file_export_file_loc.png new file mode 100644 index 0000000000..8311283585 Binary files /dev/null and b/docs/doxygen-user/images/AutoIngest/file_export_file_loc.png differ diff --git a/docs/doxygen-user/images/AutoIngest/file_export_json_loc.png b/docs/doxygen-user/images/AutoIngest/file_export_json_loc.png new file mode 100644 index 0000000000..2652c387cc Binary files /dev/null and b/docs/doxygen-user/images/AutoIngest/file_export_json_loc.png differ diff --git a/docs/doxygen-user/images/AutoIngest/file_export_keyword.png b/docs/doxygen-user/images/AutoIngest/file_export_keyword.png new file mode 100644 index 0000000000..c2b6a26b78 Binary files /dev/null and b/docs/doxygen-user/images/AutoIngest/file_export_keyword.png differ diff --git a/docs/doxygen-user/images/AutoIngest/file_export_png.png b/docs/doxygen-user/images/AutoIngest/file_export_png.png new file mode 100644 index 0000000000..2d872401d0 Binary files /dev/null and b/docs/doxygen-user/images/AutoIngest/file_export_png.png differ diff --git a/docs/doxygen-user/images/AutoIngest/file_export_size.png b/docs/doxygen-user/images/AutoIngest/file_export_size.png new file mode 100644 index 0000000000..c4e2eb7917 Binary files /dev/null and b/docs/doxygen-user/images/AutoIngest/file_export_size.png differ diff --git a/docs/doxygen-user/images/AutoIngest/file_exporter_main.png b/docs/doxygen-user/images/AutoIngest/file_exporter_main.png new file mode 100644 index 0000000000..2aa3e99778 Binary files /dev/null and b/docs/doxygen-user/images/AutoIngest/file_exporter_main.png differ diff --git a/docs/doxygen-user/images/AutoIngest/health_monitor.png b/docs/doxygen-user/images/AutoIngest/health_monitor.png new file mode 100644 index 0000000000..2550900ee8 Binary files /dev/null and b/docs/doxygen-user/images/AutoIngest/health_monitor.png differ diff --git a/docs/doxygen-user/images/AutoIngest/health_monitor_disabled.png b/docs/doxygen-user/images/AutoIngest/health_monitor_disabled.png new file mode 100644 index 0000000000..ae8bcf64e9 Binary files /dev/null and b/docs/doxygen-user/images/AutoIngest/health_monitor_disabled.png differ diff --git a/docs/doxygen-user/images/AutoIngest/manifest_file_in_file_explorer.png b/docs/doxygen-user/images/AutoIngest/manifest_file_in_file_explorer.png new file mode 100644 index 0000000000..2df4d7a777 Binary files /dev/null and b/docs/doxygen-user/images/AutoIngest/manifest_file_in_file_explorer.png differ diff --git a/docs/doxygen-user/images/AutoIngest/manifest_tool_ui.png b/docs/doxygen-user/images/AutoIngest/manifest_tool_ui.png index 5c5c9458d1..70766fc8a4 100644 Binary files a/docs/doxygen-user/images/AutoIngest/manifest_tool_ui.png and b/docs/doxygen-user/images/AutoIngest/manifest_tool_ui.png differ diff --git a/docs/doxygen-user/images/AutoIngest/master_node.png b/docs/doxygen-user/images/AutoIngest/master_node.png new file mode 100644 index 0000000000..3f0813047f Binary files /dev/null and b/docs/doxygen-user/images/AutoIngest/master_node.png differ diff --git a/docs/doxygen-user/images/AutoIngest/metrics.png b/docs/doxygen-user/images/AutoIngest/metrics.png new file mode 100644 index 0000000000..bff656a259 Binary files /dev/null and b/docs/doxygen-user/images/AutoIngest/metrics.png differ diff --git a/docs/doxygen-user/images/AutoIngest/no_periodic_searches.png b/docs/doxygen-user/images/AutoIngest/no_periodic_searches.png new file mode 100644 index 0000000000..40326527d2 Binary files /dev/null and b/docs/doxygen-user/images/AutoIngest/no_periodic_searches.png differ diff --git a/docs/doxygen-user/images/AutoIngest/overview_pic1.png b/docs/doxygen-user/images/AutoIngest/overview_pic1.png new file mode 100644 index 0000000000..29852cb6e3 Binary files /dev/null and b/docs/doxygen-user/images/AutoIngest/overview_pic1.png differ diff --git a/docs/doxygen-user/images/AutoIngest/overview_pic2.png b/docs/doxygen-user/images/AutoIngest/overview_pic2.png new file mode 100644 index 0000000000..99dcc07266 Binary files /dev/null and b/docs/doxygen-user/images/AutoIngest/overview_pic2.png differ diff --git a/docs/doxygen-user/images/case-properties-history-tab.PNG b/docs/doxygen-user/images/case-properties-history-tab.PNG deleted file mode 100644 index 0a5d8f25b7..0000000000 Binary files a/docs/doxygen-user/images/case-properties-history-tab.PNG and /dev/null differ diff --git a/docs/doxygen-user/images/case_properties.png b/docs/doxygen-user/images/case_properties.png index 75784e1cc6..9079484f6f 100644 Binary files a/docs/doxygen-user/images/case_properties.png and b/docs/doxygen-user/images/case_properties.png differ diff --git a/docs/doxygen-user/images/central_repo_content_viewer.png b/docs/doxygen-user/images/central_repo_content_viewer.png index f556d9580c..7c0a137a86 100644 Binary files a/docs/doxygen-user/images/central_repo_content_viewer.png and b/docs/doxygen-user/images/central_repo_content_viewer.png differ diff --git a/docs/doxygen-user/images/central_repo_disable_flagging.png b/docs/doxygen-user/images/central_repo_disable_flagging.png deleted file mode 100644 index d84442b32b..0000000000 Binary files a/docs/doxygen-user/images/central_repo_disable_flagging.png and /dev/null differ diff --git a/docs/doxygen-user/images/command_line_ingest_bin_dir.png b/docs/doxygen-user/images/command_line_ingest_bin_dir.png new file mode 100644 index 0000000000..6ca5d2fe87 Binary files /dev/null and b/docs/doxygen-user/images/command_line_ingest_bin_dir.png differ diff --git a/docs/doxygen-user/images/command_line_ingest_command_entry.png b/docs/doxygen-user/images/command_line_ingest_command_entry.png new file mode 100644 index 0000000000..c346aa937a Binary files /dev/null and b/docs/doxygen-user/images/command_line_ingest_command_entry.png differ diff --git a/docs/doxygen-user/images/command_line_ingest_console_output.png b/docs/doxygen-user/images/command_line_ingest_console_output.png new file mode 100644 index 0000000000..ad1756ceea Binary files /dev/null and b/docs/doxygen-user/images/command_line_ingest_console_output.png differ diff --git a/docs/doxygen-user/images/command_line_ingest_dialog.png b/docs/doxygen-user/images/command_line_ingest_dialog.png new file mode 100644 index 0000000000..51caccb308 Binary files /dev/null and b/docs/doxygen-user/images/command_line_ingest_dialog.png differ diff --git a/docs/doxygen-user/images/command_line_ingest_open_case.png b/docs/doxygen-user/images/command_line_ingest_open_case.png new file mode 100644 index 0000000000..f95b008ef9 Binary files /dev/null and b/docs/doxygen-user/images/command_line_ingest_open_case.png differ diff --git a/docs/doxygen-user/images/command_line_ingest_options.png b/docs/doxygen-user/images/command_line_ingest_options.png new file mode 100644 index 0000000000..f654290ab2 Binary files /dev/null and b/docs/doxygen-user/images/command_line_ingest_options.png differ diff --git a/docs/doxygen-user/images/command_line_ingest_report.png b/docs/doxygen-user/images/command_line_ingest_report.png new file mode 100644 index 0000000000..9b26ccbd45 Binary files /dev/null and b/docs/doxygen-user/images/command_line_ingest_report.png differ diff --git a/docs/doxygen-user/images/common_properties_cr.png b/docs/doxygen-user/images/common_properties_cr.png index e9b1eb3a1e..007570aa44 100644 Binary files a/docs/doxygen-user/images/common_properties_cr.png and b/docs/doxygen-user/images/common_properties_cr.png differ diff --git a/docs/doxygen-user/images/common_properties_intra_case.png b/docs/doxygen-user/images/common_properties_intra_case.png index f301123716..044eef3c8e 100644 Binary files a/docs/doxygen-user/images/common_properties_intra_case.png and b/docs/doxygen-user/images/common_properties_intra_case.png differ diff --git a/docs/doxygen-user/images/common_properties_result.png b/docs/doxygen-user/images/common_properties_result.png index a95b417ef4..575078a0ab 100644 Binary files a/docs/doxygen-user/images/common_properties_result.png and b/docs/doxygen-user/images/common_properties_result.png differ diff --git a/docs/doxygen-user/images/content_viewer_app_image.png b/docs/doxygen-user/images/content_viewer_app_image.png index e13531e81c..8ceff5cb0a 100644 Binary files a/docs/doxygen-user/images/content_viewer_app_image.png and b/docs/doxygen-user/images/content_viewer_app_image.png differ diff --git a/docs/doxygen-user/images/content_viewer_hex_editor_setup.png b/docs/doxygen-user/images/content_viewer_hex_editor_setup.png new file mode 100644 index 0000000000..4b9d7276e7 Binary files /dev/null and b/docs/doxygen-user/images/content_viewer_hex_editor_setup.png differ diff --git a/docs/doxygen-user/images/content_viewer_html.png b/docs/doxygen-user/images/content_viewer_html.png new file mode 100644 index 0000000000..7baae5b4fd Binary files /dev/null and b/docs/doxygen-user/images/content_viewer_html.png differ diff --git a/docs/doxygen-user/images/content_viewer_hxd_progress.png b/docs/doxygen-user/images/content_viewer_hxd_progress.png new file mode 100644 index 0000000000..33dca02584 Binary files /dev/null and b/docs/doxygen-user/images/content_viewer_hxd_progress.png differ diff --git a/docs/doxygen-user/images/content_viewer_other_occurrences.png b/docs/doxygen-user/images/content_viewer_other_occurrences.png index f6de4e0923..64010b2886 100644 Binary files a/docs/doxygen-user/images/content_viewer_other_occurrences.png and b/docs/doxygen-user/images/content_viewer_other_occurrences.png differ diff --git a/docs/doxygen-user/images/data_source_disk_image.png b/docs/doxygen-user/images/data_source_disk_image.png index 76049174c6..6a59e90bea 100644 Binary files a/docs/doxygen-user/images/data_source_disk_image.png and b/docs/doxygen-user/images/data_source_disk_image.png differ diff --git a/docs/doxygen-user/images/data_source_summary_counts.png b/docs/doxygen-user/images/data_source_summary_counts.png new file mode 100644 index 0000000000..03808ade20 Binary files /dev/null and b/docs/doxygen-user/images/data_source_summary_counts.png differ diff --git a/docs/doxygen-user/images/data_source_summary_details.png b/docs/doxygen-user/images/data_source_summary_details.png new file mode 100644 index 0000000000..960d2c30f2 Binary files /dev/null and b/docs/doxygen-user/images/data_source_summary_details.png differ diff --git a/docs/doxygen-user/images/data_source_summary_ingest.png b/docs/doxygen-user/images/data_source_summary_ingest.png new file mode 100644 index 0000000000..3247d0ee0d Binary files /dev/null and b/docs/doxygen-user/images/data_source_summary_ingest.png differ diff --git a/docs/doxygen-user/images/hex-content-viewer-tab.PNG b/docs/doxygen-user/images/hex-content-viewer-tab.PNG deleted file mode 100644 index 198d25e707..0000000000 Binary files a/docs/doxygen-user/images/hex-content-viewer-tab.PNG and /dev/null differ diff --git a/docs/doxygen-user/images/javaproperties.PNG b/docs/doxygen-user/images/javaproperties.PNG deleted file mode 100644 index 360440d5c9..0000000000 Binary files a/docs/doxygen-user/images/javaproperties.PNG and /dev/null differ diff --git a/docs/doxygen-user/images/picture-content-viewer-tab.PNG b/docs/doxygen-user/images/picture-content-viewer-tab.PNG deleted file mode 100644 index 169f8eadb1..0000000000 Binary files a/docs/doxygen-user/images/picture-content-viewer-tab.PNG and /dev/null differ diff --git a/docs/doxygen-user/images/string-content-viewer-tab.PNG b/docs/doxygen-user/images/string-content-viewer-tab.PNG deleted file mode 100644 index dcf3b82aba..0000000000 Binary files a/docs/doxygen-user/images/string-content-viewer-tab.PNG and /dev/null differ diff --git a/docs/doxygen-user/images/text-view.PNG b/docs/doxygen-user/images/text-view.PNG deleted file mode 100644 index ca0b8c3ed5..0000000000 Binary files a/docs/doxygen-user/images/text-view.PNG and /dev/null differ diff --git a/docs/doxygen-user/installSolr.dox b/docs/doxygen-user/installSolr.dox index 48c61d11ae..4ecdf1f3ee 100644 --- a/docs/doxygen-user/installSolr.dox +++ b/docs/doxygen-user/installSolr.dox @@ -12,26 +12,7 @@ You will need: \section install_solr_install Installation \subsection install_solr_install_java JRE Installation -1. Install the Java JRE if needed. You can test this by running _where java_ from the command line. If you see output like the yellow results below, you have a JRE. -

-\image html symlinkjava.PNG -

-If you need the JRE, install it with the default settings. - -2. Create a Windows environment variable for your JavaHome with the path to your 64-bit version of the JRE. If you do not know the path, the correct _JavaHome_ path can be obtained by running the command _where java_ from the Windows command line. An example is shown below. Do not include the "bin" folder in the path you place into the _JavaHome_ variable. A correct example of the final result will look something like this:    - JavaHome="C:\Program Files\Java\jre1.8.0_111" -

- \image html wherejava.PNG -

-

- Note that if you get something like the following when running the "where java" command, it is a symbolic link to the Java installation and you need to trace it to the proper folder as explained below. -

- \image html symlinkjava.PNG -

- To trace a symbolic link to the proper folder, use Windows Explorer to navigate to the path shown (C:\\ProgramData\\Oracle\\Java\\javapath for the example above), then right click on _java.exe_ and Click on _Properties_. You will see the path you should use in the _Location_ field, shown in the screenshot below. Do not include the "bin" folder in the path you place into the _JavaHome_ variable. -

- \image html javaproperties.PNG -

+1. JREs are normally installed under "C:\Program Files\Java\jre(version)", so check there to see if you have one installed already. If not, get the installer from the link in the \ref install_solr_prereq and install it with the default settings. \subsection install_solr_install_solr Solr Installation diff --git a/docs/doxygen-user/main.dox b/docs/doxygen-user/main.dox index 231b9dc95c..db40ccd5fd 100644 --- a/docs/doxygen-user/main.dox +++ b/docs/doxygen-user/main.dox @@ -65,6 +65,7 @@ The following topics are available here: - \subpage live_triage_page - \subpage advanced_page - \subpage experimental_page +- \subpage command_line_ingest_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/multiuser.dox b/docs/doxygen-user/multiuser.dox index f57527a507..99141d0a22 100644 --- a/docs/doxygen-user/multiuser.dox +++ b/docs/doxygen-user/multiuser.dox @@ -2,40 +2,28 @@ \section creating_multi_user_cases Creating Multi-user cases -Multi-user cases allow multiple instances of Autopsy to have the same case open at the same time. When creating a case, users are now presented with a choice of Single-user or Multi-user as shown in the screenshot below. +Multi-user cases allow multiple instances of Autopsy to have the same case open at the same time. When creating a case, users are presented with a choice of Single-user or Multi-user as shown in the screenshot below. -

\image html case-newcase.PNG -

- -Single-user functions the same as always, with a back end SQLite database and a machine-local version of Solr. To create a multi-user case, the following must occur: - The network services must be installed, configured, and running. See \ref multiuser_install_services. - The Case folder needs to be in a shared folder that all other clients can also access at the same path (UNC or drive letter). - The data sources that are added with the Add Data Source wizard must be in a shared folder that all clients can access at the same path. - - \section multi_user_other Other Multi-user Information - When using a multi-user case, other nodes could be running data ingest on the same case. While this is happening, you will see a progress bar labelled with the hostname of the machine performing the ingest on the bottom right of Autopsy. The progress bar will continue to move back and forth until ingest has been completed or cancelled. You can still run ingest on your local machine while this is ongoing. This is shown in the screenshot below. -

\image html othernodeingesting.PNG -

- When issues occur, there is an information "bubble" on the bottom right of the screen. It has an "i" inside a circle, with the color of the circle changed based upon the message. It uses red for bad and blue for good. See the screenshot below. -

\image html messagebubbles.PNG -

- Clicking on the information "bubble" brings up the list of prior notifications that have not been dismissed by clicking on the "x". As you can see in the screenshot below, the network cable was unplugged from the machine and it lost all connection to the three services. When the cable was reconnected, it found the services again. -

\image html messagebubblesbigger.PNG -

- When creating multi-user cases, we recommend using UNC paths to specify drive names. Drive mapping will work, but it is sometimes difficult to get all the machines participating in a case to map to the same drive letters for the same resources. It is much simpler to use fully-specified UNC paths in the form of \\\\hostname\\sharename\\folder. diff --git a/docs/doxygen-user/uilayout.dox b/docs/doxygen-user/uilayout.dox index 33de3a9024..c0906dd0cc 100644 --- a/docs/doxygen-user/uilayout.dox +++ b/docs/doxygen-user/uilayout.dox @@ -101,70 +101,17 @@ The Results Viewer can be also activated for saved results and it can show a hig Below is an example of a "Table Results Viewer" window: \image html table-result-viewer-tab.PNG -
-
-
+ \section ui_content Content Viewer -\subpage content_viewer_page "More..." -
-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 \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 part of a plug-in framework. You can install modules that will add more viewer types. This section describes the viewers that come by default with Autopsy. - -\subsection result_content_viewers Result Content Viewer -Content Viewer shows the artifacts (saved results) associated with the item selected in the Result Viewer. - -Example -Below is an example of "Result Content Viewer" window: -\image html result-viewer-example-1.PNG - -\subsection hex_content_viewer Hex Content Viewer -Hex Content Viewer shows you the raw and exact contents of a file. In this Hex Content Viewer, the data of the file is represented as hexadecimal values grouped in 2 groups of 8 bytes, followed by one group of 16 ASCII characters which are derived from each pair of hex values (each byte). Non-printable ASCII characters and characters that would take more than one character space are typically represented by a dot (".") in the following ASCII field. - -Example \n -Below is an example of "Hex Content Viewer" window: -\image html hex-content-viewer-tab.PNG - -\subsection media_content_viewer Media Content Viewer - -The Media Content Viewer will show a picture or video file. Video files can be played and paused. The size of the picture or video will be reduced to fit into the screen. If you want more complex analysis of the media, then you must export the file. - -If you select an non-picture file or an unsupported picture format on the "Result Viewers", this tab will be disabled. - -Example \n -Here's one of the example of the "Media Content Viewer": -\image html picture-content-viewer-tab.PNG - -\subsection string_content_viewer String Content Viewer - -The String Content Viewer scans (potentially binary) data of the file / folder and searches it for data that could be text. When appropriate data is found, the String Content Viewer shows data strings extracted from binary, decoded, and interpreted as UTF8/16 for the selected script/language. - -Note that this is different from the Text Content Viewer, which displays the text for a file that is stored in the keyword search index. The results may be the same or they could be different, depending how the data is interpreted by the indexer. - -Example \n -Below is an example of "String Content Viewer" window: -\image html string-content-viewer-tab.PNG - -\subsection text_content_viewer Text Content Viewer - -Text Content Viewer uses the keyword search index that may have been populated during Image Ingest. If a file has text stored in the index, then this tab will be enabled and it will be displayed to the user if a file or a result associated with a file is selected. - -This tab may have more text on it than the "String View", which relies on searching the file for text-looking data. Some files, like PDF, will not have text-looking data at the byte-level, but the keyword indexing process knows how to interpret a PDF file and produce text. For the files the indexer knows about, there may be the METADATA section at the end of the displayed extracted text. If an indexed document contains any metadata (such as creation date, author, etc), it will be displayed there. Note that, unlike the "String View", the Text View does not have its built-in settings for the script/language to use for extracted strings. This is because the script/language is used at indexing time, and that setting is associated with the Keyword Search indexer, not the viewer. - -If this tab is not enabled, then either the file has no text or you did not enable Keyword Search as an ingest module. Note that this viewer is also used to display highlighted keyword hits when operated in the "Search Matches" mode, selected on the right-hand side of the viewer's toolbar. - -\image html text-view.PNG +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. -
-
-
\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 -
-
-
+ \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.
diff --git a/setupSleuthkitBranch.py b/setupSleuthkitBranch.py index 0cdfab82f1..7faf94a723 100644 --- a/setupSleuthkitBranch.py +++ b/setupSleuthkitBranch.py @@ -44,7 +44,7 @@ def gitSleuthkitCheckout(branch, branchOwner): if (passed == 0): sys.exit() #exit if successful else: - print("Branch: " + branch + " does not exist for owner: " + branchOwner) + print("Branch: " + branch + " does not exist for github user: " + gitHubUser) def parseXML(xmlFile): ''' diff --git a/test/script/tskdbdiff.py b/test/script/tskdbdiff.py index d33b40686a..3c794ed5bf 100644 --- a/test/script/tskdbdiff.py +++ b/test/script/tskdbdiff.py @@ -74,16 +74,16 @@ class TskDbDiff(object): """ self._init_diff() - + id_obj_path_table = -1 # generate the gold database dumps if necessary if self._generate_gold_dump: - TskDbDiff._dump_output_db_nonbb(self.gold_db_file, self.gold_dump, self.isMultiUser, self.pgSettings) + id_obj_path_table = TskDbDiff._dump_output_db_nonbb(self.gold_db_file, self.gold_dump, self.isMultiUser, self.pgSettings) if self._generate_gold_bb_dump: - TskDbDiff._dump_output_db_bb(self.gold_db_file, self.gold_bb_dump, self.isMultiUser, self.pgSettings) + TskDbDiff._dump_output_db_bb(self.gold_db_file, self.gold_bb_dump, self.isMultiUser, self.pgSettings, id_obj_path_table) # generate the output database dumps (both DB and BB) - TskDbDiff._dump_output_db_nonbb(self.output_db_file, self._dump, self.isMultiUser, self.pgSettings) - TskDbDiff._dump_output_db_bb(self.output_db_file, self._bb_dump, self.isMultiUser, self.pgSettings) + id_obj_path_table = TskDbDiff._dump_output_db_nonbb(self.output_db_file, self._dump, self.isMultiUser, self.pgSettings) + TskDbDiff._dump_output_db_bb(self.output_db_file, self._bb_dump, self.isMultiUser, self.pgSettings, id_obj_path_table) # Compare non-BB dump_diff_pass = self._diff(self._dump, self.gold_dump, self._dump_diff) @@ -172,7 +172,7 @@ class TskDbDiff(object): return False - def _dump_output_db_bb(db_file, bb_dump_file, isMultiUser, pgSettings): + def _dump_output_db_bb(db_file, bb_dump_file, isMultiUser, pgSettings, id_obj_path_table): """Dumps sorted text results to the given output location. Smart method that deals with a blackboard comparison to avoid issues @@ -224,9 +224,9 @@ class TskDbDiff(object): # Get attributes for this artifact if isMultiUser: - attribute_cursor.execute("SELECT blackboard_attributes.source, blackboard_attribute_types.display_name, blackboard_attributes.value_type, blackboard_attributes.value_text, blackboard_attributes.value_int32, blackboard_attributes.value_int64, blackboard_attributes.value_double FROM blackboard_attributes INNER JOIN blackboard_attribute_types ON blackboard_attributes.attribute_type_id = blackboard_attribute_types.attribute_type_id WHERE artifact_id = %s ORDER BY blackboard_attributes.source, blackboard_attribute_types.display_name, blackboard_attributes.value_type, blackboard_attributes.value_text, blackboard_attributes.value_int32, blackboard_attributes.value_int64, blackboard_attributes.value_double", [art_id]) + attribute_cursor.execute("SELECT blackboard_attributes.source, blackboard_attributes.attribute_type_id, blackboard_attribute_types.display_name, blackboard_attributes.value_type, blackboard_attributes.value_text, blackboard_attributes.value_int32, blackboard_attributes.value_int64, blackboard_attributes.value_double FROM blackboard_attributes INNER JOIN blackboard_attribute_types ON blackboard_attributes.attribute_type_id = blackboard_attribute_types.attribute_type_id WHERE artifact_id = %s ORDER BY blackboard_attributes.source, blackboard_attribute_types.display_name, blackboard_attributes.value_type, blackboard_attributes.value_text, blackboard_attributes.value_int32, blackboard_attributes.value_int64, blackboard_attributes.value_double", [art_id]) else: - attribute_cursor.execute("SELECT blackboard_attributes.source, blackboard_attribute_types.display_name, blackboard_attributes.value_type, blackboard_attributes.value_text, blackboard_attributes.value_int32, blackboard_attributes.value_int64, blackboard_attributes.value_double FROM blackboard_attributes INNER JOIN blackboard_attribute_types ON blackboard_attributes.attribute_type_id = blackboard_attribute_types.attribute_type_id WHERE artifact_id =? ORDER BY blackboard_attributes.source, blackboard_attribute_types.display_name, blackboard_attributes.value_type, blackboard_attributes.value_text, blackboard_attributes.value_int32, blackboard_attributes.value_int64, blackboard_attributes.value_double", [art_id]) + attribute_cursor.execute("SELECT blackboard_attributes.source, blackboard_attributes.attribute_type_id, blackboard_attribute_types.display_name, blackboard_attributes.value_type, blackboard_attributes.value_text, blackboard_attributes.value_int32, blackboard_attributes.value_int64, blackboard_attributes.value_double FROM blackboard_attributes INNER JOIN blackboard_attribute_types ON blackboard_attributes.attribute_type_id = blackboard_attribute_types.attribute_type_id WHERE artifact_id =? ORDER BY blackboard_attributes.source, blackboard_attribute_types.display_name, blackboard_attributes.value_type, blackboard_attributes.value_text, blackboard_attributes.value_int32, blackboard_attributes.value_int64, blackboard_attributes.value_double", [art_id]) attributes = attribute_cursor.fetchall() @@ -255,7 +255,9 @@ class TskDbDiff(object): elif attr["value_type"] == 1: attr_value_as_string = str(attr["value_int32"]) elif attr["value_type"] == 2: - attr_value_as_string = str(attr["value_int64"]) + attr_value_as_string = str(attr["value_int64"]) + if attr["attribute_type_id"] == 36 and id_obj_path_table != -1 and int(attr_value_as_string) > 0: #normalize positive TSK_PATH_IDs from being object id to a path if the obj_id_path_table was generated + attr_value_as_string = id_obj_path_table[int(attr_value_as_string)] elif attr["value_type"] == 3: attr_value_as_string = "%20.10f" % float((attr["value_double"])) #use exact format from db schema to avoid python auto format double value to (0E-10) scientific style elif attr["value_type"] == 4: @@ -368,6 +370,7 @@ class TskDbDiff(object): # cleanup the backup if backup_db_file: os.remove(backup_db_file) + return id_obj_path_table def dump_output_db(db_file, dump_file, bb_dump_file, isMultiUser, pgSettings): @@ -378,8 +381,8 @@ class TskDbDiff(object): dump_file: a pathto_File, the location to dump the non-blackboard database items bb_dump_file: a pathto_File, the location to dump the blackboard database items """ - TskDbDiff._dump_output_db_nonbb(db_file, dump_file, isMultiUser, pgSettings) - TskDbDiff._dump_output_db_bb(db_file, bb_dump_file, isMultiUser, pgSettings) + id_obj_path_table = TskDbDiff._dump_output_db_nonbb(db_file, dump_file, isMultiUser, pgSettings) + TskDbDiff._dump_output_db_bb(db_file, bb_dump_file, isMultiUser, pgSettings, id_obj_path_table) def _get_tmp_file(base, ext): diff --git a/thirdparty/ewfexport_exec/32-bit/vcruntime140.dll b/thirdparty/ewfexport_exec/32-bit/vcruntime140.dll new file mode 100755 index 0000000000..d9fb793ac5 Binary files /dev/null and b/thirdparty/ewfexport_exec/32-bit/vcruntime140.dll differ diff --git a/thirdparty/ewfexport_exec/64-bit/vcruntime140.dll b/thirdparty/ewfexport_exec/64-bit/vcruntime140.dll new file mode 100755 index 0000000000..fa755e5cc8 Binary files /dev/null and b/thirdparty/ewfexport_exec/64-bit/vcruntime140.dll differ