diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java b/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java index 80f89c0fab..8a4f4b6c6f 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CaseOpenAction.java @@ -37,16 +37,19 @@ import org.openide.DialogDisplayer; import org.openide.NotifyDescriptor; import org.sleuthkit.autopsy.coreutils.Logger; import java.util.logging.Level; +import org.openide.util.HelpCtx; +import org.openide.util.actions.CallableSystemAction; import org.sleuthkit.autopsy.ingest.IngestManager; /** * An action that opens an existing case. */ @ServiceProvider(service = CaseOpenAction.class) -public final class CaseOpenAction implements ActionListener { +public final class CaseOpenAction extends CallableSystemAction implements ActionListener { private static final Logger logger = Logger.getLogger(CaseOpenAction.class.getName()); private static final String PROP_BASECASE = "LBL_BaseCase_PATH"; //NON-NLS + private static final long serialVersionUID = 1L; private final JFileChooser fileChooser = new JFileChooser(); private final FileFilter caseMetadataFileFilter; @@ -140,4 +143,18 @@ public final class CaseOpenAction implements ActionListener { }).start(); } } + + @Override + public void performAction() { + } + + @Override + public String getName() { + return NbBundle.getMessage(CaseOpenAction.class, "CTL_OpenAction"); + } + + @Override + public HelpCtx getHelpCtx() { + return HelpCtx.DEFAULT_HELP; + } } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/ImageDSProcessor.java b/Core/src/org/sleuthkit/autopsy/casemodule/ImageDSProcessor.java index 5f68f07c21..2e9486c20c 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/ImageDSProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/ImageDSProcessor.java @@ -18,6 +18,9 @@ */ package org.sleuthkit.autopsy.casemodule; +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; import javax.swing.JPanel; import java.util.ArrayList; import java.util.Calendar; @@ -26,9 +29,12 @@ import java.util.UUID; import javax.swing.filechooser.FileFilter; import org.openide.util.NbBundle; import org.openide.util.lookup.ServiceProvider; +import org.openide.util.lookup.ServiceProviders; +import org.sleuthkit.autopsy.corecomponentinterfaces.AutomatedIngestDataSourceProcessor; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgressMonitor; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; +import org.sleuthkit.autopsy.coreutils.DataSourceUtils; /** * A image file data source processor that implements the DataSourceProcessor @@ -36,8 +42,11 @@ import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; * wizard. It also provides a run method overload to allow it to be used * independently of the wizard. */ -@ServiceProvider(service = DataSourceProcessor.class) -public class ImageDSProcessor implements DataSourceProcessor { +@ServiceProviders(value={ + @ServiceProvider(service=DataSourceProcessor.class), + @ServiceProvider(service=AutomatedIngestDataSourceProcessor.class)} +) +public class ImageDSProcessor implements AutomatedIngestDataSourceProcessor { private final static String DATA_SOURCE_TYPE = NbBundle.getMessage(ImageDSProcessor.class, "ImageDSProcessor.dsType.text"); private static final List allExt = new ArrayList<>(); @@ -231,4 +240,45 @@ public class ImageDSProcessor implements DataSourceProcessor { setDataSourceOptionsCalled = true; } + private static boolean isAcceptedByFiler(File file, List filters) { + for (FileFilter filter : filters) { + if (filter.accept(file)) { + return true; + } + } + return false; + } + + @Override + public int canProcess(Path dataSourcePath) throws AutomatedIngestDataSourceProcessorException { + + // check file extension for supported types + if (!isAcceptedByFiler(dataSourcePath.toFile(), filtersList)) { + return 0; + } + + try { + // verify that the image has a file system that TSK can process + Case currentCase = Case.getCurrentCase(); + if (!DataSourceUtils.imageHasFileSystem(dataSourcePath)) { + // image does not have a file system that TSK can process + return 0; + } + } catch (Exception ex) { + throw new AutomatedIngestDataSourceProcessorException("Exception inside canProcess() method", ex); + } + + // able to process the data source + return 100; + } + + @Override + public void process(String deviceId, Path dataSourcePath, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callBack) throws AutomatedIngestDataSourceProcessorException { + this.deviceId = deviceId; + this.imagePath = dataSourcePath.toString(); + this.timeZone = Calendar.getInstance().getTimeZone().getID(); + this.ignoreFatOrphanFiles = false; + setDataSourceOptionsCalled = true; + run(deviceId, dataSourcePath.toString(), timeZone, ignoreFatOrphanFiles, progressMonitor, callBack); + } } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskDSProcessor.java b/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskDSProcessor.java index 02590894f2..fc64a244ea 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskDSProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskDSProcessor.java @@ -18,14 +18,19 @@ */ package org.sleuthkit.autopsy.casemodule; +import java.io.File; +import java.nio.file.Path; import java.util.Calendar; import java.util.UUID; import javax.swing.JPanel; import org.openide.util.NbBundle; import org.openide.util.lookup.ServiceProvider; +import org.openide.util.lookup.ServiceProviders; +import org.sleuthkit.autopsy.corecomponentinterfaces.AutomatedIngestDataSourceProcessor; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgressMonitor; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; +import org.sleuthkit.autopsy.coreutils.DriveUtils; /** * A local drive data source processor that implements the DataSourceProcessor @@ -33,8 +38,11 @@ import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; * wizard. It also provides a run method overload to allow it to be used * independently of the wizard. */ -@ServiceProvider(service = DataSourceProcessor.class) -public class LocalDiskDSProcessor implements DataSourceProcessor { +@ServiceProviders(value={ + @ServiceProvider(service=DataSourceProcessor.class), + @ServiceProvider(service=AutomatedIngestDataSourceProcessor.class)} +) +public class LocalDiskDSProcessor implements AutomatedIngestDataSourceProcessor { private static final String DATA_SOURCE_TYPE = NbBundle.getMessage(LocalDiskDSProcessor.class, "LocalDiskDSProcessor.dsType.text"); private final LocalDiskPanel configPanel; @@ -210,4 +218,35 @@ public class LocalDiskDSProcessor implements DataSourceProcessor { setDataSourceOptionsCalled = true; } + @Override + public int canProcess(Path dataSourcePath) throws AutomatedIngestDataSourceProcessorException { + + // verify that the data source is not a file or a directory + File file = dataSourcePath.toFile(); + // ELTODO this needs to be tested more. should I keep isDirectory or just test for isFile? + if (file.isFile() || file.isDirectory()) { + return 0; + } + + // check whether data source is an existing disk or partition + // ELTODO this needs to be tested more. do these methods actually work correctly? + // or should I use PlatformUtil.getPhysicalDrives() and PlatformUtil.getPartitions() instead? + String path = dataSourcePath.toString(); + if ( (DriveUtils.isPhysicalDrive(path) || DriveUtils.isPartition(path)) && DriveUtils.driveExists(path) ) { + return 90; + } + + return 0; + } + + @Override + public void process(String deviceId, Path dataSourcePath, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callBack) throws AutomatedIngestDataSourceProcessorException { + this.deviceId = deviceId; + this.drivePath = dataSourcePath.toString(); + this.timeZone = Calendar.getInstance().getTimeZone().getID(); + this.ignoreFatOrphanFiles = false; + setDataSourceOptionsCalled = true; + run(deviceId, drivePath, timeZone, ignoreFatOrphanFiles, progressMonitor, callBack); + } + } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskPanel.java index 56262394bf..0d485b3392 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskPanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskPanel.java @@ -53,6 +53,7 @@ final class LocalDiskPanel extends JPanel { private static final Logger logger = Logger.getLogger(LocalDiskPanel.class.getName()); private static LocalDiskPanel instance; + private static final long serialVersionUID = 1L; private List disks; private LocalDiskModel model; private boolean enableNext = false; diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesDSProcessor.java b/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesDSProcessor.java index 5fa4c3ec16..99b0ea67bd 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesDSProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesDSProcessor.java @@ -18,12 +18,15 @@ */ package org.sleuthkit.autopsy.casemodule; +import java.nio.file.Path; import java.util.Arrays; import java.util.List; import java.util.UUID; import javax.swing.JPanel; import org.openide.util.NbBundle; import org.openide.util.lookup.ServiceProvider; +import org.openide.util.lookup.ServiceProviders; +import org.sleuthkit.autopsy.corecomponentinterfaces.AutomatedIngestDataSourceProcessor; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgressMonitor; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; @@ -34,8 +37,11 @@ import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; * integration with the add data source wizard. It also provides a run method * overload to allow it to be used independently of the wizard. */ -@ServiceProvider(service = DataSourceProcessor.class) -public class LocalFilesDSProcessor implements DataSourceProcessor { +@ServiceProviders(value={ + @ServiceProvider(service=DataSourceProcessor.class), + @ServiceProvider(service=AutomatedIngestDataSourceProcessor.class)} +) +public class LocalFilesDSProcessor implements AutomatedIngestDataSourceProcessor { private static final String DATA_SOURCE_TYPE = NbBundle.getMessage(LocalFilesDSProcessor.class, "LocalFilesDSProcessor.dsType"); private final LocalFilesPanel configPanel; @@ -200,4 +206,18 @@ public class LocalFilesDSProcessor implements DataSourceProcessor { setDataSourceOptionsCalled = true; } + @Override + public int canProcess(Path dataSourcePath) throws AutomatedIngestDataSourceProcessorException { + // Local files DSP can process any file by simply adding it as a logical file. + // It should return lowest possible non-zero confidence level and be treated + // as the "option of last resort" for auto ingest purposes + return 1; + } + + @Override + public void process(String deviceId, Path dataSourcePath, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callBack) throws AutomatedIngestDataSourceProcessorException { + this.localFilePaths = Arrays.asList(new String[]{dataSourcePath.toString()}); + run(deviceId, deviceId, this.localFilePaths, progressMonitor, callBack); + } + } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesPanel.java index ba1b357440..39485da30f 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesPanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesPanel.java @@ -40,6 +40,7 @@ import org.sleuthkit.autopsy.coreutils.PathValidator; */ final class LocalFilesPanel extends JPanel { + private static final long serialVersionUID = 1L; private final Set currentFiles = new TreeSet<>(); //keep currents in a set to disallow duplicates per add private boolean enableNext = false; private static LocalFilesPanel instance; diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/StartupWindowProvider.java b/Core/src/org/sleuthkit/autopsy/casemodule/StartupWindowProvider.java index 195512b183..1b30f50e60 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/StartupWindowProvider.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/StartupWindowProvider.java @@ -59,12 +59,10 @@ public class StartupWindowProvider implements StartupWindowInterface { = Lookup.getDefault().lookupAll(StartupWindowInterface.class); int windowsCount = startupWindows.size(); - if (windowsCount > 2) { - logger.log(Level.WARNING, "More than 2 (" + windowsCount + ") start up windows discovered, will use the first custom one"); //NON-NLS - } else if (windowsCount == 1) { + if (windowsCount == 1) { startupWindowToUse = startupWindows.iterator().next(); logger.log(Level.INFO, "Will use the default startup window: " + startupWindowToUse.toString()); //NON-NLS - } else { + } else if (windowsCount == 2) { //pick the non default one Iterator it = startupWindows.iterator(); while (it.hasNext()) { @@ -73,17 +71,25 @@ public class StartupWindowProvider implements StartupWindowInterface { startupWindowToUse = window; logger.log(Level.INFO, "Will use the custom startup window: " + startupWindowToUse.toString()); //NON-NLS break; - } } - - if (startupWindowToUse == null) { - logger.log(Level.SEVERE, "Unexpected error, no custom startup window found, using the default"); //NON-NLS - startupWindowToUse = new org.sleuthkit.autopsy.casemodule.StartupWindow(); + } else { + // select first non-Autopsy start up window + Iterator it = startupWindows.iterator(); + while (it.hasNext()) { + StartupWindowInterface window = it.next(); + if (!window.getClass().getCanonicalName().startsWith("org.sleuthkit.autopsy")) { + startupWindowToUse = window; + logger.log(Level.INFO, "Will use the custom startup window: " + startupWindowToUse.toString()); //NON-NLS + break; + } } - } + if (startupWindowToUse == null) { + logger.log(Level.SEVERE, "Unexpected error, no startup window chosen, using the default"); //NON-NLS + startupWindowToUse = new org.sleuthkit.autopsy.casemodule.StartupWindow(); + } } } diff --git a/Core/src/org/sleuthkit/autopsy/core/RuntimeProperties.java b/Core/src/org/sleuthkit/autopsy/core/RuntimeProperties.java index 61e2c9112a..1ad74738dc 100644 --- a/Core/src/org/sleuthkit/autopsy/core/RuntimeProperties.java +++ b/Core/src/org/sleuthkit/autopsy/core/RuntimeProperties.java @@ -38,7 +38,7 @@ public class RuntimeProperties { * * @param coreComponentsActive True or false. */ - public static void setCoreComponentsActive(boolean coreComponentsActive) { + public synchronized static void setCoreComponentsActive(boolean coreComponentsActive) { if (!coreComponentsActiveSet) { RuntimeProperties.coreComponentsActive = coreComponentsActive; coreComponentsActiveSet = true; @@ -56,7 +56,7 @@ public class RuntimeProperties { * * @return True or false. */ - public static boolean coreComponentsAreActive() { + public synchronized static boolean coreComponentsAreActive() { return coreComponentsActive; } diff --git a/Core/src/org/sleuthkit/autopsy/corecomponentinterfaces/AutomatedIngestDataSourceProcessor.java b/Core/src/org/sleuthkit/autopsy/corecomponentinterfaces/AutomatedIngestDataSourceProcessor.java new file mode 100644 index 0000000000..9b7b0e4d6e --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/corecomponentinterfaces/AutomatedIngestDataSourceProcessor.java @@ -0,0 +1,86 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013-2016 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.corecomponentinterfaces; + +import java.nio.file.Path; + +/** + * Interface implemented by DataSourceProcessors in order to be supported by + * automated ingest capability. + * + * @author elivis + */ +public interface AutomatedIngestDataSourceProcessor extends DataSourceProcessor { + + /** + * Indicates whether the DataSourceProcessor is capable of processing the + * data source. Returns a confidence value. Method can throw an exception + * for a system level problem. The exception should not be thrown for an issue + * related to bad input data. + * + * @param dataSourcePath Path to the data source. + * + * @return Confidence value. Values between 0 and 100 are recommended. Zero + * or less means the data source is not supported by the + * DataSourceProcessor. Value of 100 indicates high certainty in + * being able to process the data source. + * + * @throws + * org.sleuthkit.autopsy.corecomponentinterfaces.AutomatedIngestDataSourceProcessor.AutomatedIngestDataSourceProcessorException + */ + int canProcess(Path dataSourcePath) throws AutomatedIngestDataSourceProcessorException; + + /** + * Adds a data source to the case database using a background task in a + * separate thread by calling DataSourceProcessor.run() method. Returns as + * soon as the background task is started. The background task uses a + * callback object to signal task completion and return results. Method can + * throw an exception for a system level problem. The exception should not + * be thrown for an issue related to bad input data. + * + * @param deviceId An ASCII-printable identifier for the device + * associated with the data source that is intended + * to be unique across multiple cases (e.g., a UUID). + * @param dataSourcePath Path to the data source. + * @param progressMonitor Progress monitor that will be used by the + * background task to report progress. + * @param callBack Callback that will be used by the background task + * to return results. + * + * @throws + * org.sleuthkit.autopsy.corecomponentinterfaces.AutomatedIngestDataSourceProcessor.AutomatedIngestDataSourceProcessorException + */ + void process(String deviceId, Path dataSourcePath, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callBack) throws AutomatedIngestDataSourceProcessorException; + + /** + * A custom exception for the use of AutomatedIngestDataSourceProcessor. + */ + public class AutomatedIngestDataSourceProcessorException extends Exception { + + private static final long serialVersionUID = 1L; + + public AutomatedIngestDataSourceProcessorException(String message) { + super(message); + } + + public AutomatedIngestDataSourceProcessorException(String message, Throwable cause) { + super(message, cause); + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/DataSourceUtils.java b/Core/src/org/sleuthkit/autopsy/coreutils/DataSourceUtils.java new file mode 100644 index 0000000000..61191125bd --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/coreutils/DataSourceUtils.java @@ -0,0 +1,44 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.coreutils; + +import org.sleuthkit.datamodel.SleuthkitJNI; +import java.io.IOException; +import java.nio.file.Path; + +/** + * Utility methods for working with data sources. + */ +public class DataSourceUtils { + + /** + * Calls TSK to determine whether a + * potential data source has a file system. + * + * @param dataSourcePath The path to the data source. + * + * @return True or false. + * + * @throws IOException if an error occurs while trying to determine if the + * data source has a file system. + */ + public static boolean imageHasFileSystem(Path dataSourcePath) throws IOException { + return SleuthkitJNI.isImageSupported(dataSourcePath.toString()); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/DriveUtils.java b/Core/src/org/sleuthkit/autopsy/coreutils/DriveUtils.java index 950ffb5bcd..59aa9f2d9b 100644 --- a/Core/src/org/sleuthkit/autopsy/coreutils/DriveUtils.java +++ b/Core/src/org/sleuthkit/autopsy/coreutils/DriveUtils.java @@ -56,7 +56,7 @@ public class DriveUtils { } /** - * Determines whether or not a drive exists by eading the first byte and + * Determines whether or not a drive exists by reading the first byte and * checking if it is a -1. * * @param path The path to test. diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByMimeType.java b/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByMimeType.java index 4165553e2a..bbd46ddd12 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByMimeType.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByMimeType.java @@ -68,6 +68,12 @@ public final class FileTypesByMimeType extends Observable implements AutopsyVisi private final HashMap> existingMimeTypes = new HashMap<>(); private static final Logger LOGGER = Logger.getLogger(FileTypesByMimeType.class.getName()); + private void removeListeners() { + deleteObservers(); + IngestManager.getInstance().removeIngestJobEventListener(pcl); + Case.removePropertyChangeListener(pcl); + } + /* * The pcl is in the class because it has the easiest mechanisms to add * and remove itself during its life cycles. @@ -75,9 +81,8 @@ public final class FileTypesByMimeType extends Observable implements AutopsyVisi private final PropertyChangeListener pcl = (PropertyChangeEvent evt) -> { String eventType = evt.getPropertyName(); if (eventType.equals(IngestManager.IngestJobEvent.COMPLETED.toString()) - || eventType.equals(IngestManager.IngestJobEvent.CANCELLED.toString()) - // || eventType.equals(Case.Events.DATA_SOURCE_ADDED.toString()) - || eventType.equals(IngestManager.IngestModuleEvent.CONTENT_CHANGED.toString())) { + || eventType.equals(IngestManager.IngestJobEvent.CANCELLED.toString())) { + /** * Checking for a current case is a stop gap measure until a * different way of handling the closing of cases is worked out. @@ -92,6 +97,10 @@ public final class FileTypesByMimeType extends Observable implements AutopsyVisi * Case is closed, do nothing. */ } + } else if (eventType.equals(Case.Events.CURRENT_CASE.toString())) { + if (evt.getNewValue() == null) { + removeListeners(); + } } }; @@ -123,15 +132,13 @@ public final class FileTypesByMimeType extends Observable implements AutopsyVisi allDistinctMimeTypesQuery.append(TskData.TSK_DB_FILES_TYPE_ENUM.LOCAL.ordinal()).append("))"); synchronized (existingMimeTypes) { existingMimeTypes.clear(); - } - if (skCase == null) { + if (skCase == null) { - return; - } - try (SleuthkitCase.CaseDbQuery dbQuery = skCase.executeQuery(allDistinctMimeTypesQuery.toString())) { - ResultSet resultSet = dbQuery.getResultSet(); - synchronized (existingMimeTypes) { + return; + } + try (SleuthkitCase.CaseDbQuery dbQuery = skCase.executeQuery(allDistinctMimeTypesQuery.toString())) { + ResultSet resultSet = dbQuery.getResultSet(); while (resultSet.next()) { final String mime_type = resultSet.getString("mime_type"); //NON-NLS if (!mime_type.isEmpty()) { @@ -144,17 +151,19 @@ public final class FileTypesByMimeType extends Observable implements AutopsyVisi } } } + } catch (TskCoreException | SQLException ex) { + LOGGER.log(Level.SEVERE, "Unable to populate File Types by MIME Type tree view from DB: ", ex); //NON-NLS } - } catch (TskCoreException | SQLException ex) { - LOGGER.log(Level.WARNING, "Unable to populate File Types by MIME Type tree view from DB: ", ex); //NON-NLS } + setChanged(); + notifyObservers(); } FileTypesByMimeType(SleuthkitCase skCase) { IngestManager.getInstance().addIngestJobEventListener(pcl); - IngestManager.getInstance().addIngestModuleEventListener(pcl); + Case.addPropertyChangeListener(pcl); this.skCase = skCase; populateHashMap(); } @@ -178,6 +187,7 @@ public final class FileTypesByMimeType extends Observable implements AutopsyVisi isEmptyMimeNode = true; } return isEmptyMimeNode; + } /** diff --git a/Core/src/org/sleuthkit/autopsy/ingest/IngestProgressSnapshotDialog.java b/Core/src/org/sleuthkit/autopsy/ingest/IngestProgressSnapshotDialog.java index 17c1b56e58..deb9d14ede 100755 --- a/Core/src/org/sleuthkit/autopsy/ingest/IngestProgressSnapshotDialog.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/IngestProgressSnapshotDialog.java @@ -60,11 +60,12 @@ public final class IngestProgressSnapshotDialog extends JDialog { super((Window) owner, TITLE, ModalityType.MODELESS); if (shouldBeModal && owner instanceof JDialog) { // if called from a modal dialog, manipulate the parent be just under this in z order, and not modal. final JDialog pseudoOwner = (JDialog) owner; + final ModalityType originalModality = pseudoOwner.getModalityType(); addWindowListener(new WindowAdapter() { @Override public void windowClosed(WindowEvent e) { // Put it back to how it was before we manipulated it. pseudoOwner.setVisible(false); - pseudoOwner.setModalityType(Dialog.ModalityType.APPLICATION_MODAL); + pseudoOwner.setModalityType(originalModality); pseudoOwner.toFront(); pseudoOwner.setVisible(true); } diff --git a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashLookupSettingsPanel.form b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashLookupSettingsPanel.form index c24125cec4..c089bea77b 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashLookupSettingsPanel.form +++ b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashLookupSettingsPanel.form @@ -66,7 +66,10 @@ - + + + + @@ -91,18 +94,20 @@ - + - + + + + - - - - - - + + + + + @@ -130,15 +135,19 @@ - - - + + + + + + + + + + - - - - + @@ -205,9 +214,9 @@ - + - + diff --git a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashLookupSettingsPanel.java b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashLookupSettingsPanel.java index 6954a9d77f..7d2b782ec1 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashLookupSettingsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashLookupSettingsPanel.java @@ -662,17 +662,18 @@ public final class HashLookupSettingsPanel extends IngestModuleGlobalSettingsPan .addGroup(jPanel1Layout.createSequentialGroup() .addGap(1, 1, 1) .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 395, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(0, 0, Short.MAX_VALUE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(jPanel1Layout.createSequentialGroup() - .addGap(10, 10, 10) + .addComponent(informationLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGap(309, 309, 309)) + .addGroup(jPanel1Layout.createSequentialGroup() .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(jPanel1Layout.createSequentialGroup() - .addComponent(informationLabel) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(informationSeparator, javax.swing.GroupLayout.PREFERRED_SIZE, 305, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addComponent(ingestWarningLabel) .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel1Layout.createSequentialGroup() + .addComponent(optionsLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(optionsSeparator, javax.swing.GroupLayout.PREFERRED_SIZE, 334, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(jPanel1Layout.createSequentialGroup() .addGap(10, 10, 10) .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(jPanel1Layout.createSequentialGroup() @@ -694,12 +695,15 @@ public final class HashLookupSettingsPanel extends IngestModuleGlobalSettingsPan .addGap(53, 53, 53) .addComponent(hashDbNameLabel)))) .addGroup(jPanel1Layout.createSequentialGroup() - .addComponent(optionsLabel) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(optionsSeparator, javax.swing.GroupLayout.PREFERRED_SIZE, 324, javax.swing.GroupLayout.PREFERRED_SIZE)))) - .addGroup(jPanel1Layout.createSequentialGroup() - .addGap(23, 23, 23) - .addComponent(sendIngestMessagesCheckBox)))) + .addGap(70, 70, 70) + .addComponent(informationSeparator, javax.swing.GroupLayout.PREFERRED_SIZE, 305, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(jPanel1Layout.createSequentialGroup() + .addGap(10, 10, 10) + .addComponent(ingestWarningLabel)) + .addGroup(jPanel1Layout.createSequentialGroup() + .addGap(25, 25, 25) + .addComponent(sendIngestMessagesCheckBox))) + .addGap(50, 50, 50)))) .addGroup(jPanel1Layout.createSequentialGroup() .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(hashDatabasesLabel) @@ -749,9 +753,9 @@ public final class HashLookupSettingsPanel extends IngestModuleGlobalSettingsPan .addComponent(indexButton) .addComponent(addHashesToDatabaseButton)) .addGap(18, 18, 18) - .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(optionsLabel) - .addComponent(optionsSeparator, javax.swing.GroupLayout.PREFERRED_SIZE, 6, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(optionsSeparator, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) .addGap(18, 18, 18) .addComponent(sendIngestMessagesCheckBox) .addGap(18, 18, 18) @@ -772,7 +776,9 @@ public final class HashLookupSettingsPanel extends IngestModuleGlobalSettingsPan this.setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(jScrollPane2, javax.swing.GroupLayout.DEFAULT_SIZE, 789, Short.MAX_VALUE) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addComponent(jScrollPane2) + .addContainerGap()) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) diff --git a/Experimental/build.xml b/Experimental/build.xml new file mode 100644 index 0000000000..b23ebb9ac3 --- /dev/null +++ b/Experimental/build.xml @@ -0,0 +1,8 @@ + + + + + + Builds, tests, and runs the project org.sleuthkit.autopsy.experimental. + + diff --git a/Experimental/manifest.mf b/Experimental/manifest.mf new file mode 100644 index 0000000000..80010b9686 --- /dev/null +++ b/Experimental/manifest.mf @@ -0,0 +1,7 @@ +Manifest-Version: 1.0 +AutoUpdate-Show-In-Client: true +OpenIDE-Module: org.sleuthkit.autopsy.experimental +OpenIDE-Module-Layer: org/sleuthkit/autopsy/experimental/autoingest/layer.xml +OpenIDE-Module-Localizing-Bundle: org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties +OpenIDE-Module-Specification-Version: 1.0 + diff --git a/Experimental/nbproject/build-impl.xml b/Experimental/nbproject/build-impl.xml new file mode 100644 index 0000000000..9b537ab812 --- /dev/null +++ b/Experimental/nbproject/build-impl.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + You must set 'suite.dir' to point to your containing module suite + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Experimental/nbproject/project.properties b/Experimental/nbproject/project.properties new file mode 100644 index 0000000000..140b9f1b5c --- /dev/null +++ b/Experimental/nbproject/project.properties @@ -0,0 +1,17 @@ +file.reference.c3p0-0.9.5.jar=release/modules/ext/c3p0-0.9.5.jar +file.reference.curator-client-2.8.0.jar=release/modules/ext/curator-client-2.8.0.jar +file.reference.curator-framework-2.8.0.jar=release/modules/ext/curator-framework-2.8.0.jar +file.reference.curator-recipes-2.8.0.jar=release/modules/ext/curator-recipes-2.8.0.jar +file.reference.jackson-core-2.7.0.jar=release/modules/ext/jackson-core-2.7.0.jar +file.reference.LGoodDatePicker-4.3.1.jar=release/modules/ext/LGoodDatePicker-4.3.1.jar +file.reference.mchange-commons-java-0.2.9.jar=release/modules/ext/mchange-commons-java-0.2.9.jar +file.reference.solr-solrj-4.9.1.jar=release/modules/ext/solr-solrj-4.9.1.jar +file.reference.tika-core-1.5.jar=release/modules/ext/tika-core-1.5.jar +file.reference.zookeeper-3.4.6.jar=release/modules/ext/zookeeper-3.4.6.jar +javac.source=1.8 +javac.compilerargs=-Xlint -Xlint:-serial +javadoc.reference.LGoodDatePicker-4.3.1.jar=release/modules/ext/LGoodDatePicker-4.3.1-javadoc.jar +javadoc.reference.solr-solrj-4.9.1.jar=release/modules/ext/solr-solrj-4.9.1-javadoc.jar +source.reference.curator-recipes-2.8.0.jar=release/modules/ext/curator-recipes-2.8.0-sources.jar +source.reference.LGoodDatePicker-4.3.1.jar=release/modules/ext/LGoodDatePicker-4.3.1-sources.jar +source.reference.solr-solrj-4.9.1.jar=release/modules/ext/solr-solrj-4.9.1-sources.jar diff --git a/Experimental/nbproject/project.xml b/Experimental/nbproject/project.xml new file mode 100644 index 0000000000..7709852e77 --- /dev/null +++ b/Experimental/nbproject/project.xml @@ -0,0 +1,184 @@ + + + org.netbeans.modules.apisupport.project + + + org.sleuthkit.autopsy.experimental + + + + org.netbeans.modules.options.api + + + + 1 + 1.44.1 + + + + org.openide.awt + + + + 7.65.1 + + + + org.openide.dialogs + + + + 7.41.1 + + + + org.openide.filesystems + + + + 9.8.1 + + + + org.openide.loaders + + + + 7.63.2 + + + + org.openide.modules + + + + 7.47.1 + + + + org.openide.util + + + + 9.5.1 + + + + org.openide.util.lookup + + + + 8.32.1 + + + + org.openide.util.ui + + + + 9.4.1 + + + + org.openide.windows + + + + 6.74.1 + + + + org.sleuthkit.autopsy.core + + + + 10 + 10.5 + + + + org.sleuthkit.autopsy.corelibs + + + + 3 + 1.1 + + + + org.sleuthkit.autopsy.keywordsearch + + + + 6 + 6.3 + + + + + org.sleuthkit.autopsy.experimental.autoingest + org.sleuthkit.autopsy.experimental.configuration + + + ext/zookeeper-3.4.6.jar + release/modules/ext/zookeeper-3.4.6.jar + + + ext/curator-client-2.8.0.jar + release/modules/ext/curator-client-2.8.0.jar + + + ext/mchange-commons-java-0.2.9.jar + release/modules/ext/mchange-commons-java-0.2.9.jar + + + ext/LGoodDatePicker-4.3.1.jar + release/modules/ext/LGoodDatePicker-4.3.1.jar + + + ext/tika-core-1.5.jar + release/modules/ext/tika-core-1.5.jar + + + ext/curator-recipes-2.8.0.jar + release/modules/ext/curator-recipes-2.8.0.jar + + + ext/jackson-core-2.7.0.jar + release/modules/ext/jackson-core-2.7.0.jar + + + ext/c3p0-0.9.5.jar + release/modules/ext/c3p0-0.9.5.jar + + + ext/curator-framework-2.8.0.jar + release/modules/ext/curator-framework-2.8.0.jar + + + ext/solr-solrj-4.9.1.jar + release/modules/ext/solr-solrj-4.9.1.jar + + + ext/httpclient-4.3.1.jar + release/modules/ext/httpclient-4.3.1.jar + + + ext/httpmime-4.3.1.jar + release/modules/ext/httpmime-4.3.1.jar + + + ext/httpcore-4.3.jar + release/modules/ext/httpcore-4.3.jar + + + ext/noggit-0.5.jar + release/modules/ext/noggit-0.5.jar + + + ext/postgresql-9.4-1201-jdbc41.jar + release/modules/ext/postgresql-9.4-1201-jdbc41.jar + + + + diff --git a/Experimental/nbproject/suite.properties b/Experimental/nbproject/suite.properties new file mode 100644 index 0000000000..29d7cc9bd6 --- /dev/null +++ b/Experimental/nbproject/suite.properties @@ -0,0 +1 @@ +suite.dir=${basedir}/.. diff --git a/Experimental/release/modules/ext/LGoodDatePicker-4.3.1-javadoc.jar b/Experimental/release/modules/ext/LGoodDatePicker-4.3.1-javadoc.jar new file mode 100644 index 0000000000..06ef34422b Binary files /dev/null and b/Experimental/release/modules/ext/LGoodDatePicker-4.3.1-javadoc.jar differ diff --git a/Experimental/release/modules/ext/LGoodDatePicker-4.3.1-sources.jar b/Experimental/release/modules/ext/LGoodDatePicker-4.3.1-sources.jar new file mode 100644 index 0000000000..b1bae3ec96 Binary files /dev/null and b/Experimental/release/modules/ext/LGoodDatePicker-4.3.1-sources.jar differ diff --git a/Experimental/release/modules/ext/LGoodDatePicker-4.3.1.jar b/Experimental/release/modules/ext/LGoodDatePicker-4.3.1.jar new file mode 100644 index 0000000000..4f730e4fe7 Binary files /dev/null and b/Experimental/release/modules/ext/LGoodDatePicker-4.3.1.jar differ diff --git a/Experimental/release/modules/ext/c3p0-0.9.5.jar b/Experimental/release/modules/ext/c3p0-0.9.5.jar new file mode 100644 index 0000000000..46fdddf7e7 Binary files /dev/null and b/Experimental/release/modules/ext/c3p0-0.9.5.jar differ diff --git a/Experimental/release/modules/ext/curator-client-2.8.0.jar b/Experimental/release/modules/ext/curator-client-2.8.0.jar new file mode 100644 index 0000000000..4ccc265cc4 Binary files /dev/null and b/Experimental/release/modules/ext/curator-client-2.8.0.jar differ diff --git a/Experimental/release/modules/ext/curator-framework-2.8.0.jar b/Experimental/release/modules/ext/curator-framework-2.8.0.jar new file mode 100644 index 0000000000..5e488892d1 Binary files /dev/null and b/Experimental/release/modules/ext/curator-framework-2.8.0.jar differ diff --git a/Experimental/release/modules/ext/curator-recipes-2.8.0-sources.jar b/Experimental/release/modules/ext/curator-recipes-2.8.0-sources.jar new file mode 100644 index 0000000000..10a9b76f58 Binary files /dev/null and b/Experimental/release/modules/ext/curator-recipes-2.8.0-sources.jar differ diff --git a/Experimental/release/modules/ext/curator-recipes-2.8.0.jar b/Experimental/release/modules/ext/curator-recipes-2.8.0.jar new file mode 100644 index 0000000000..34eb9c9677 Binary files /dev/null and b/Experimental/release/modules/ext/curator-recipes-2.8.0.jar differ diff --git a/Experimental/release/modules/ext/httpclient-4.3.1.jar b/Experimental/release/modules/ext/httpclient-4.3.1.jar new file mode 100644 index 0000000000..cdd1e3874c Binary files /dev/null and b/Experimental/release/modules/ext/httpclient-4.3.1.jar differ diff --git a/Experimental/release/modules/ext/httpcore-4.3.jar b/Experimental/release/modules/ext/httpcore-4.3.jar new file mode 100644 index 0000000000..ddfe6dacbc Binary files /dev/null and b/Experimental/release/modules/ext/httpcore-4.3.jar differ diff --git a/Experimental/release/modules/ext/httpmime-4.3.1.jar b/Experimental/release/modules/ext/httpmime-4.3.1.jar new file mode 100644 index 0000000000..2b26d7a79c Binary files /dev/null and b/Experimental/release/modules/ext/httpmime-4.3.1.jar differ diff --git a/Experimental/release/modules/ext/jackson-core-2.7.0.jar b/Experimental/release/modules/ext/jackson-core-2.7.0.jar new file mode 100644 index 0000000000..a37cc5e2a6 Binary files /dev/null and b/Experimental/release/modules/ext/jackson-core-2.7.0.jar differ diff --git a/Experimental/release/modules/ext/mchange-commons-java-0.2.9.jar b/Experimental/release/modules/ext/mchange-commons-java-0.2.9.jar new file mode 100644 index 0000000000..07e4b66eb3 Binary files /dev/null and b/Experimental/release/modules/ext/mchange-commons-java-0.2.9.jar differ diff --git a/Experimental/release/modules/ext/noggit-0.5.jar b/Experimental/release/modules/ext/noggit-0.5.jar new file mode 100644 index 0000000000..163ced7cc0 Binary files /dev/null and b/Experimental/release/modules/ext/noggit-0.5.jar differ diff --git a/Experimental/release/modules/ext/postgresql-9.4-1201-jdbc41.jar b/Experimental/release/modules/ext/postgresql-9.4-1201-jdbc41.jar new file mode 100644 index 0000000000..a3f19593fb Binary files /dev/null and b/Experimental/release/modules/ext/postgresql-9.4-1201-jdbc41.jar differ diff --git a/Experimental/release/modules/ext/solr-solrj-4.9.1-javadoc.jar b/Experimental/release/modules/ext/solr-solrj-4.9.1-javadoc.jar new file mode 100644 index 0000000000..c048a4629a Binary files /dev/null and b/Experimental/release/modules/ext/solr-solrj-4.9.1-javadoc.jar differ diff --git a/Experimental/release/modules/ext/solr-solrj-4.9.1-sources.jar b/Experimental/release/modules/ext/solr-solrj-4.9.1-sources.jar new file mode 100644 index 0000000000..c6b5602af2 Binary files /dev/null and b/Experimental/release/modules/ext/solr-solrj-4.9.1-sources.jar differ diff --git a/Experimental/release/modules/ext/solr-solrj-4.9.1.jar b/Experimental/release/modules/ext/solr-solrj-4.9.1.jar new file mode 100644 index 0000000000..069d3baa72 Binary files /dev/null and b/Experimental/release/modules/ext/solr-solrj-4.9.1.jar differ diff --git a/Experimental/release/modules/ext/swingx-all-1.6.4.jar b/Experimental/release/modules/ext/swingx-all-1.6.4.jar new file mode 100644 index 0000000000..3078bb6364 Binary files /dev/null and b/Experimental/release/modules/ext/swingx-all-1.6.4.jar differ diff --git a/Experimental/release/modules/ext/tika-core-1.5.jar b/Experimental/release/modules/ext/tika-core-1.5.jar new file mode 100644 index 0000000000..d8e45e3f28 Binary files /dev/null and b/Experimental/release/modules/ext/tika-core-1.5.jar differ diff --git a/Experimental/release/modules/ext/zookeeper-3.4.6.jar b/Experimental/release/modules/ext/zookeeper-3.4.6.jar new file mode 100644 index 0000000000..7c340be9f5 Binary files /dev/null and b/Experimental/release/modules/ext/zookeeper-3.4.6.jar differ diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ArchiveUtil.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ArchiveUtil.java new file mode 100644 index 0000000000..9683deb5c6 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ArchiveUtil.java @@ -0,0 +1,277 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.RandomAccessFile; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import net.sf.sevenzipjbinding.ISequentialOutStream; +import net.sf.sevenzipjbinding.ISevenZipInArchive; +import net.sf.sevenzipjbinding.SevenZip; +import net.sf.sevenzipjbinding.SevenZipException; +import net.sf.sevenzipjbinding.SevenZipNativeInitializationException; +import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream; +import net.sf.sevenzipjbinding.simple.ISimpleInArchive; +import net.sf.sevenzipjbinding.simple.ISimpleInArchiveItem; + +/** + * Set of utilities that handles archive file extraction. Uses 7zip library. + */ +final class ArchiveUtil { + + static final String[] SUPPORTED_EXTENSIONS = {"zip", "rar", "arj", "7z", "7zip", "gzip", "gz", "bzip2", "tar", "tgz",}; // NON-NLS + + + private ArchiveUtil() { + } + + /** + * Enum of mime types which support archive extraction + */ + private enum SupportedArchiveExtractionFormats { + + ZIP("application/zip"), //NON-NLS + SEVENZ("application/x-7z-compressed"), //NON-NLS + GZIP("application/gzip"), //NON-NLS + XGZIP("application/x-gzip"), //NON-NLS + XBZIP2("application/x-bzip2"), //NON-NLS + XTAR("application/x-tar"), //NON-NLS + XGTAR("application/x-gtar"), + XRAR("application/x-rar-compressed"); //NON-NLS + + private final String mimeType; + + SupportedArchiveExtractionFormats(final String mimeType) { + this.mimeType = mimeType; + } + + @Override + public String toString() { + return this.mimeType; + } + } + + /** + * Exception thrown when archive handling resulted in an error + */ + static class ArchiveExtractionException extends Exception { + + private static final long serialVersionUID = 1L; + + ArchiveExtractionException(String message) { + super(message); + } + + ArchiveExtractionException(String message, Throwable cause) { + super(message, cause); + } + } + + /** + * This method returns array of supported file extensions. + * + * @return String array of supported file extensions. + */ + static String[] getSupportedArchiveTypes(){ + return SUPPORTED_EXTENSIONS; + } + + /** + * This method returns true if the MIME type is currently supported. Else it + * returns false. + * + * @param mimeType File mime type + * + * @return This method returns true if the file format is currently + * supported. Else it returns false. + */ + static boolean isExtractionSupportedByMimeType(String mimeType) { + for (SupportedArchiveExtractionFormats s : SupportedArchiveExtractionFormats.values()) { + if (s.toString().equals(mimeType)) { + return true; + } + } + return false; + } + + /** + * This method returns true if the file extension is currently supported. + * Else it returns false. Attempt extension based detection in case Apache + * Tika based detection fails. + * + * @param extension File extension + * + * @return This method returns true if the file format is currently + * supported. Else it returns false. + */ + static boolean isExtractionSupportedByFileExtension(String extension) { + // attempt extension matching + for (String supportedExtension : SUPPORTED_EXTENSIONS) { + if (extension.equals(supportedExtension)) { + return true; + } + } + return false; + } + + /** + * Returns a list of file names contained within an archive. + * + * @param archiveFilePath Full path to the archive file + * + * @return List of file names contained within archive + * + * @throws + * ArchiveExtractionException + */ + static List getListOfFilesWithinArchive(String archiveFilePath) throws ArchiveExtractionException { + if (!SevenZip.isInitializedSuccessfully() && (SevenZip.getLastInitializationException() == null)) { + try { + SevenZip.initSevenZipFromPlatformJAR(); + } catch (SevenZipNativeInitializationException ex) { + throw new ArchiveExtractionException("AutoIngestDashboard_bnPause_paused", ex); + } + } + List files = new ArrayList<>(); + ISevenZipInArchive inArchive = null; + try { + RandomAccessFile randomAccessFile = new RandomAccessFile(new File(archiveFilePath), "r"); + inArchive = SevenZip.openInArchive(null, new RandomAccessFileInStream(randomAccessFile)); + final ISimpleInArchive simpleInArchive = inArchive.getSimpleInterface(); + for (ISimpleInArchiveItem item : simpleInArchive.getArchiveItems()) { + files.add(item.getPath()); + } + } catch (Exception ex) { + throw new ArchiveExtractionException("Exception while reading archive contents", ex); + } finally { + if (inArchive != null) { + try { + inArchive.close(); + } catch (SevenZipException ex) { + throw new ArchiveExtractionException("Exception while closing the archive", ex); + } + } + } + return files; + } + + /** + * Extracts contents of an archive file into a directory. + * + * @param archiveFilePath Full path to archive. + * @param destinationFolder Path to directory where results will be + * extracted to. + * + * @throws + * ArchiveExtractionException + */ + static void unpackArchiveFile(String archiveFilePath, String destinationFolder) throws ArchiveExtractionException { + if (!SevenZip.isInitializedSuccessfully() && (SevenZip.getLastInitializationException() == null)) { + try { + SevenZip.initSevenZipFromPlatformJAR(); + } catch (SevenZipNativeInitializationException ex) { + throw new ArchiveExtractionException("Unable to initialize 7Zip libraries", ex); + } + } + ISevenZipInArchive inArchive = null; + try { + RandomAccessFile randomAccessFile = new RandomAccessFile(new File(archiveFilePath), "r"); + inArchive = SevenZip.openInArchive(null, new RandomAccessFileInStream(randomAccessFile)); + final ISimpleInArchive simpleInArchive = inArchive.getSimpleInterface(); + + for (ISimpleInArchiveItem entry : simpleInArchive.getArchiveItems()) { + String entryPathInArchive = entry.getPath(); + Path fullPath = Paths.get(destinationFolder, entryPathInArchive); + File destFile = new File(fullPath.toString()); + File destinationParent = destFile.getParentFile(); + destinationParent.mkdirs(); + if (!entry.isFolder()) { + UnpackStream unpackStream = null; + try { + Long size = entry.getSize(); + unpackStream = new UnpackStream(destFile.toString(), size); + entry.extractSlow(unpackStream); + } catch (Exception ex) { + throw new ArchiveExtractionException("Exception while unpacking archive contents", ex); + } finally { + if (unpackStream != null) { + unpackStream.close(); + } + } + } + } + } catch (Exception ex) { + throw new ArchiveExtractionException("Exception while unpacking archive contents", ex); + } finally { + try { + if (inArchive != null) { + inArchive.close(); + } + } catch (SevenZipException ex) { + throw new ArchiveExtractionException("Exception while closing the archive", ex); + } + } + } + + /** + * Stream used to unpack an archive to local file + */ + private static class UnpackStream implements ISequentialOutStream { + + private OutputStream output; + private String destFilePath; + + UnpackStream(String destFilePath, long size) throws ArchiveExtractionException { + this.destFilePath = destFilePath; + try { + output = new FileOutputStream(destFilePath); + } catch (IOException ex) { + throw new ArchiveExtractionException("Exception while unpacking archive contents", ex); + } + + } + + @Override + public int write(byte[] bytes) throws SevenZipException { + try { + output.write(bytes); + } catch (IOException ex) { + throw new SevenZipException("Error writing unpacked file to " + destFilePath, ex); + } + return bytes.length; + } + + public void close() throws ArchiveExtractionException { + if (output != null) { + try { + output.flush(); + output.close(); + } catch (IOException ex) { + throw new ArchiveExtractionException("Exception while closing the archive", ex); + } + } + } + } +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestAlertFile.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestAlertFile.java new file mode 100644 index 0000000000..40fff351eb --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestAlertFile.java @@ -0,0 +1,108 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.logging.Level; + +/** + * Utility for creating and checking for the existence of an automated ingest + * alert file. The purpose of the file is to put a marker in the case directory + * when an error or warning occurs in connection with an automated ingest job. + */ +final class AutoIngestAlertFile { + + private static final String ERROR_FILE_NAME = "autoingest.alert"; + + /** + * Checks whether an automated ingest alert file exists in a case directory. + * + * @param caseDirectoryPath The case directory path. + * + * @return True or false. + */ + static boolean exists(Path caseDirectoryPath) { + return caseDirectoryPath.resolve(ERROR_FILE_NAME).toFile().exists(); + } + + /** + * Creates an automated ingest alert file in a case directory if such a file + * does not already exist. + * + * @param caseDirectoryPath The case directory path. + * + * @return True or false. + */ + static void create(Path caseDirectoryPath) throws AutoIngestAlertFileException { + try { + Files.createFile(caseDirectoryPath.resolve(ERROR_FILE_NAME)); + } catch (FileAlreadyExistsException ignored) { + /* + * The file already exists, the exception is not exceptional. + */ + } catch (IOException ex) { + /* + * FileAlreadyExistsException implementation is optional, so check + * for that case. + */ + if (!exists(caseDirectoryPath)) { + throw new AutoIngestAlertFileException(String.format("Error creating automated ingest alert file in %s", caseDirectoryPath), ex); + } + } + } + + /** + * Exception thrown when there is a problem creating an alert file. + */ + final static class AutoIngestAlertFileException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Constructs an exception to throw when there is a problem creating an + * alert file. + * + * @param message The exception message. + */ + private AutoIngestAlertFileException(String message) { + super(message); + } + + /** + * Constructs an exception to throw when there is a problem creating an + * alert file. + * + * @param message The exception message. + * @param cause The cause of the exception, if it was an exception. + */ + private AutoIngestAlertFileException(String message, Throwable cause) { + super(message, cause); + } + } + + /** + * Prevents instantiation of this utility class. + */ + private AutoIngestAlertFile() { + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestCase.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestCase.java new file mode 100644 index 0000000000..646191de39 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestCase.java @@ -0,0 +1,197 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Comparator; +import java.util.Date; +import java.util.Objects; +import java.util.logging.Level; +import org.sleuthkit.autopsy.casemodule.CaseMetadata; +import org.sleuthkit.autopsy.coreutils.Logger; + +/** + * A representation of a case created by automated ingest. + */ +class AutoIngestCase implements Comparable { + + private static final Logger logger = Logger.getLogger(AutoIngestCase.class.getName()); + private final Path caseDirectoryPath; + private final String caseName; + private final Path metadataFilePath; + private final Date createDate; + private Date lastModfiedDate; + + /** + * Constructs a representation of case created by automated ingest. + * + * @param caseDirectoryPath The case directory path. + */ + // RJCTODO: Throw instead of reporting error, let client decide what to do. + AutoIngestCase(Path caseDirectoryPath) { + this.caseDirectoryPath = caseDirectoryPath; + caseName = PathUtils.caseNameFromCaseDirectoryPath(caseDirectoryPath); + metadataFilePath = caseDirectoryPath.resolve(caseName + CaseMetadata.getFileExtension()); + BasicFileAttributes fileAttrs = null; + try { + fileAttrs = Files.readAttributes(metadataFilePath, BasicFileAttributes.class); + } catch (IOException ex) { + logger.log(Level.SEVERE, String.format("Error reading file attributes of case metadata file in %s, will use current time for case createDate/lastModfiedDate", caseDirectoryPath), ex); + } + if (null != fileAttrs) { + createDate = new Date(fileAttrs.creationTime().toMillis()); + lastModfiedDate = new Date(fileAttrs.lastModifiedTime().toMillis()); + } else { + createDate = new Date(); + lastModfiedDate = new Date(); + } + } + + /** + * Gets the case directory path. + * + * @return The case directory path. + */ + Path getCaseDirectoryPath() { + return this.caseDirectoryPath; + } + + /** + * Gets the case name. + * + * @return The case name. + */ + String getCaseName() { + return this.caseName; + } + + /** + * Gets the creation date for the case, defined as the create time of the + * case metadata file. + * + * @return The case creation date. + */ + Date getCreationDate() { + return this.createDate; + } + + /** + * Gets the last accessed date for the case, defined as the last modified + * time of the case metadata file. + * + * @return The last accessed date. + */ + // RJCTODO: Throw instead of reporting error, let client decide what to do. + Date getLastAccessedDate() { + try { + BasicFileAttributes fileAttrs = Files.readAttributes(metadataFilePath, BasicFileAttributes.class); + lastModfiedDate = new Date(fileAttrs.lastModifiedTime().toMillis()); + } catch (IOException ex) { + logger.log(Level.SEVERE, String.format("Error reading file attributes of case metadata file in %s, lastModfiedDate time not updated", caseDirectoryPath), ex); + } + return lastModfiedDate; + } + + /** + * Gets the status of this case based on the auto ingest result file in the + * case directory. + * + * @return See CaseStatus enum definition. + */ + CaseStatus getStatus() { + if (AutoIngestAlertFile.exists(caseDirectoryPath)) { + return CaseStatus.ALERT; + } else { + return CaseStatus.OK; + } + } + + /** + * Indicates whether or not some other object is "equal to" this + * AutoIngestCase object. + * + * @param other The other object. + * + * @return True or false. + */ + @Override + public boolean equals(Object other) { + if (!(other instanceof AutoIngestCase)) { + return false; + } + if (other == this) { + return true; + } + return this.caseDirectoryPath.toString().equals(((AutoIngestCase) other).caseDirectoryPath.toString()); + } + + /** + * Returns a hash code value for this AutoIngestCase object. + * + * @return The has code. + */ + @Override + public int hashCode() { + int hash = 7; + hash = 71 * hash + Objects.hashCode(this.caseDirectoryPath); + hash = 71 * hash + Objects.hashCode(this.createDate); + hash = 71 * hash + Objects.hashCode(this.caseName); + return hash; + } + + /** + * Compares this AutopIngestCase object with abnother AutoIngestCase object + * for order. + */ + @Override + public int compareTo(AutoIngestCase other) { + return -this.lastModfiedDate.compareTo(other.getLastAccessedDate()); + } + + /** + * Comparator for a descending order sort on date created. + */ + static class LastAccessedDateDescendingComparator implements Comparator { + + /** + * Compares two AutoIngestCase objects for order based on last accessed + * date (descending). + * + * @param object The first AutoIngestCase object + * @param otherObject The second AuotIngestCase object. + * + * @return A negative integer, zero, or a positive integer as the first + * argument is less than, equal to, or greater than the second. + */ + @Override + public int compare(AutoIngestCase object, AutoIngestCase otherObject) { + return -object.getLastAccessedDate().compareTo(otherObject.getLastAccessedDate()); + } + } + + enum CaseStatus { + + OK, + ALERT + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestCaseDeletedEvent.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestCaseDeletedEvent.java new file mode 100644 index 0000000000..ab559de58e --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestCaseDeletedEvent.java @@ -0,0 +1,66 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.Serializable; +import javax.annotation.concurrent.Immutable; +import org.sleuthkit.autopsy.events.AutopsyEvent; + +/** + * Event published when a case is deleted by the automated ingest manager. + */ +@Immutable +final class AutoIngestCaseDeletedEvent extends AutopsyEvent implements Serializable { + + private static final long serialVersionUID = 1L; + private final String caseName; + private final String nodeName; + + /** + * Constructs an event that is published when a case is deleted by the + * automated ingest manager. + * + * @param caseName The case name. + * @param nodeName The host name of the node that deleted the case. + */ + AutoIngestCaseDeletedEvent(String caseName, String nodeName) { + super(AutoIngestManager.Event.CASE_DELETED.toString(), null, null); + this.caseName = caseName; + this.nodeName = nodeName; + } + + /** + * RJCTODO + * + * @return + */ + String getCaseName() { + return caseName; + } + + /** + * RJCTODO + * + * @return + */ + String getNodeName() { + return nodeName; + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestCaseOpenAction.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestCaseOpenAction.java new file mode 100644 index 0000000000..aec567641d --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestCaseOpenAction.java @@ -0,0 +1,99 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.awt.event.ActionListener; +import java.util.logging.Level; +import org.openide.util.HelpCtx; +import org.openide.util.Lookup; +import org.openide.util.NbBundle; +import org.openide.util.actions.CallableSystemAction; +import org.openide.util.actions.SystemAction; +import org.sleuthkit.autopsy.casemodule.CaseCloseAction; +import org.sleuthkit.autopsy.casemodule.CaseOpenAction; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.StartupWindowProvider; +import org.sleuthkit.autopsy.experimental.configuration.AutoIngestUserPreferences; + +final class AutoIngestCaseOpenAction extends CallableSystemAction implements ActionListener { + + private static final Logger logger = Logger.getLogger(AutoIngestCaseOpenAction.class.getName()); + private static final long serialVersionUID = 1L; + + public AutoIngestCaseOpenAction() { + } + + @Override + public void actionPerformed(ActionEvent e) { + + AutoIngestUserPreferences.SelectedMode mode = AutoIngestUserPreferences.getMode(); + switch (mode) { + case REVIEW: + + if (Case.isCaseOpen()) { + /* + * In review mode, close the currently open case, if any, and + * then display the review mode cases panel. This can be + * accomplished by invoking CaseCloseAction because it calls + * StartupWindowProvider.getInstance().open() after it closes + * the current case. + */ + SystemAction.get(CaseCloseAction.class).actionPerformed(e); + } else { + // no case is open, so show the startup window + StartupWindowProvider.getInstance().open(); + } + break; + + case AUTOMATED: + /* + * New case action is disabled in auto ingest mode. + */ + break; + + case STANDALONE: + /** + * In standalone mode, invoke default Autopsy version of CaseOpenAction. + */ + Lookup.getDefault().lookup(CaseOpenAction.class).actionPerformed(e); + break; + + + default: + logger.log(Level.SEVERE, "Attempting to open case in unsupported mode {0}", mode.toString()); + } + } + + @Override + public void performAction() { + } + + @Override + public String getName() { + return NbBundle.getMessage(AutoIngestCaseOpenAction.class, "CTL_OpenAction"); + } + + @Override + public HelpCtx getHelpCtx() { + return HelpCtx.DEFAULT_HELP; + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestCasePrioritizedEvent.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestCasePrioritizedEvent.java new file mode 100644 index 0000000000..0687bc9c2e --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestCasePrioritizedEvent.java @@ -0,0 +1,65 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.Serializable; +import org.sleuthkit.autopsy.events.AutopsyEvent; + +/** + * Event published when an automated ingest manager prioritizes all or part of a + * case. + */ +public final class AutoIngestCasePrioritizedEvent extends AutopsyEvent implements Serializable { // RJCTODO: Rename to AutoIngestPrioritizationEvent + + private static final long serialVersionUID = 1L; + private final String caseName; + private final String nodeName; + + /** + * Constructs an event published when an automated ingest manager + * prioritizes all or part of a case. + * + * @param caseName The name of the case. + * @param nodeName The host name of the node that prioritized the case. + */ + public AutoIngestCasePrioritizedEvent(String nodeName, String caseName) { + super(AutoIngestManager.Event.CASE_PRIORITIZED.toString(), null, null); + this.caseName = caseName; + this.nodeName = nodeName; + } + + /** + * Gets the name of the prioritized case. + * + * @return The case name. + */ + public String getCaseName() { + return caseName; + } + + /** + * Gets the host name of the node that prioritized the case. + * + * @return The host name of the node. + */ + public String getNodeName() { + return nodeName; + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestDashboard.form b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestDashboard.form new file mode 100644 index 0000000000..b1d90f4479 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestDashboard.form @@ -0,0 +1,475 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestDashboard.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestDashboard.java new file mode 100644 index 0000000000..e71af46319 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestDashboard.java @@ -0,0 +1,1759 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.Cursor; +import java.awt.Desktop; +import java.awt.EventQueue; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.Observable; +import java.util.Observer; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Level; +import javax.swing.DefaultListSelectionModel; +import java.awt.Color; +import java.beans.PropertyChangeEvent; +import java.io.File; +import java.util.Collections; +import java.util.logging.Logger; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JTable; +import javax.swing.SwingWorker; +import javax.swing.UIManager; +import javax.swing.event.ListSelectionEvent; +import javax.swing.table.DefaultTableModel; +import javax.swing.table.TableColumn; +import org.netbeans.api.options.OptionsDisplayer; +import org.openide.DialogDisplayer; +import org.openide.LifecycleManager; +import org.openide.NotifyDescriptor; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; +import org.openide.util.NbBundle; +import org.openide.util.actions.CallableSystemAction; +import org.sleuthkit.autopsy.casemodule.CaseNewAction; +import org.sleuthkit.autopsy.casemodule.CaseOpenAction; +import org.sleuthkit.autopsy.core.ServicesMonitor; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import org.sleuthkit.autopsy.coreutils.NetworkUtils; +import org.sleuthkit.autopsy.coreutils.PlatformUtil; +import org.sleuthkit.autopsy.ingest.IngestManager; +import org.sleuthkit.autopsy.ingest.IngestProgressSnapshotDialog; +import org.sleuthkit.autopsy.experimental.autoingest.AutoIngestManager.CaseDeletionResult; +import org.sleuthkit.autopsy.experimental.autoingest.AutoIngestManager.JobsSnapshot; + +/** + * A panel for monitoring automated ingest by a cluster, and for controlling + * automated ingest for a single node within the cluster. There can be at most + * one such panel per node. + */ +public final class AutoIngestDashboard extends JPanel implements Observer { + + private static final long serialVersionUID = 1L; + private static final int GENERIC_COL_MIN_WIDTH = 30; + private static final int GENERIC_COL_MAX_WIDTH = 2000; + private static final int PENDING_TABLE_COL_PREFERRED_WIDTH = 280; + private static final int RUNNING_TABLE_COL_PREFERRED_WIDTH = 175; + private static final int ACTIVITY_TIME_COL_MIN_WIDTH = 250; + private static final int ACTIVITY_TIME_COL_MAX_WIDTH = 450; + private static final int TIME_COL_MIN_WIDTH = 30; + private static final int TIME_COL_MAX_WIDTH = 250; + private static final int TIME_COL_PREFERRED_WIDTH = 140; + private static final int NAME_COL_MIN_WIDTH = 100; + private static final int NAME_COL_MAX_WIDTH = 250; + private static final int NAME_COL_PREFERRED_WIDTH = 140; + private static final int ACTIVITY_COL_MIN_WIDTH = 70; + private static final int ACTIVITY_COL_MAX_WIDTH = 2000; + private static final int ACTIVITY_COL_PREFERRED_WIDTH = 300; + private static final int STATUS_COL_MIN_WIDTH = 55; + private static final int STATUS_COL_MAX_WIDTH = 250; + private static final int STATUS_COL_PREFERRED_WIDTH = 55; + private static final int COMPLETED_TIME_COL_MIN_WIDTH = 30; + private static final int COMPLETED_TIME_COL_MAX_WIDTH = 2000; + private static final int COMPLETED_TIME_COL_PREFERRED_WIDTH = 280; + private static final String UPDATE_TASKS_THREAD_NAME = "AID-update-tasks-%d"; + private static final String LOCAL_HOST_NAME = NetworkUtils.getLocalHostName(); + private static final Logger SYS_LOGGER = AutoIngestSystemLogger.getLogger(); + private static AutoIngestDashboard instance; + private final DefaultTableModel pendingTableModel; + private final DefaultTableModel runningTableModel; + private final DefaultTableModel completedTableModel; + private AutoIngestManager manager; + private ExecutorService updateExecutor; + private boolean isPaused; + private boolean autoIngestStarted; + private Color pendingTableBackground; + private Color pendingTablelForeground; + + /* + * The enum is used in conjunction with the DefaultTableModel class to + * provide table models for the JTables used to display a view of the + * pending jobs queue, running jobs list, and completed jobs list. The enum + * allows the columns of the table model to be described by either an enum + * ordinal or a column header string. + */ + private enum JobsTableModelColumns { + + CASE(NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.JobsTableModel.ColumnHeader.Case")), + DATA_SOURCE(NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.JobsTableModel.ColumnHeader.ImageFolder")), + HOST_NAME(NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.JobsTableModel.ColumnHeader.HostName")), + CREATED_TIME(NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.JobsTableModel.ColumnHeader.CreatedTime")), + STARTED_TIME(NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.JobsTableModel.ColumnHeader.StartedTime")), + COMPLETED_TIME(NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.JobsTableModel.ColumnHeader.CompletedTime")), + STAGE(NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.JobsTableModel.ColumnHeader.Stage")), + STAGE_TIME(NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.JobsTableModel.ColumnHeader.StageTime")), + STATUS(NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.JobsTableModel.ColumnHeader.Status")), + CASE_DIRECTORY_PATH(NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.JobsTableModel.ColumnHeader.CaseFolder")), + IS_LOCAL_JOB(NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.JobsTableModel.ColumnHeader.LocalJob")), + MANIFEST_FILE_PATH(NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.JobsTableModel.ColumnHeader.ManifestFilePath")); + + private final String header; + + private JobsTableModelColumns(String header) { + this.header = header; + } + + private String getColumnHeader() { + return header; + } + + private static final String[] headers = { + CASE.getColumnHeader(), + DATA_SOURCE.getColumnHeader(), + HOST_NAME.getColumnHeader(), + CREATED_TIME.getColumnHeader(), + STARTED_TIME.getColumnHeader(), + COMPLETED_TIME.getColumnHeader(), + STAGE.getColumnHeader(), + STATUS.getColumnHeader(), + STAGE_TIME.getColumnHeader(), + CASE_DIRECTORY_PATH.getColumnHeader(), + IS_LOCAL_JOB.getColumnHeader(), + MANIFEST_FILE_PATH.getColumnHeader()}; + } + + /** + * Gets the singleton automated ingest control and monitoring panel for this + * cluster node. + * + * @return The panel. + */ + public static AutoIngestDashboard getInstance() { + if (null == instance) { + /* + * Two stage construction is used here to avoid publishing a + * reference to the panel to the Observable auto ingest manager + * before object construction is complete. + */ + instance = new AutoIngestDashboard(); + } + return instance; + } + + /** + * Constructs a panel for monitoring automated ingest by a cluster, and for + * controlling automated ingest for a single node within the cluster. + */ + private AutoIngestDashboard() { + disableUiMenuActions(); + + manager = AutoIngestManager.getInstance(); + + pendingTableModel = new DefaultTableModel(JobsTableModelColumns.headers, 0) { + private static final long serialVersionUID = 1L; + + @Override + public boolean isCellEditable(int row, int column) { + return false; + } + }; + + runningTableModel = new DefaultTableModel(JobsTableModelColumns.headers, 0) { + private static final long serialVersionUID = 1L; + + @Override + public boolean isCellEditable(int row, int column) { + return false; + } + }; + + completedTableModel = new DefaultTableModel(JobsTableModelColumns.headers, 0) { + private static final long serialVersionUID = 1L; + + @Override + public boolean isCellEditable(int row, int column) { + return false; + } + }; + + initComponents(); // Generated code. + setServicesStatusMessage(); + initPendingJobsTable(); + initRunningJobsTable(); + initCompletedJobsTable(); + initButtons(); + + /* + * Must set this flag, otherwise pop up menus don't close properly. + */ + UIManager.put("PopupMenu.consumeEventOnClose", false); + } + + private void disableUiMenuActions() { + /* + * Disable the new case action in auto ingest mode. + */ + CallableSystemAction.get(CaseNewAction.class).setEnabled(false); + + /* + * Disable the new case action in auto ingest mode. + */ + CallableSystemAction.get(CaseOpenAction.class).setEnabled(false); + CallableSystemAction.get(AutoIngestCaseOpenAction.class).setEnabled(false); + + /* + * Permanently delete the "Open Recent Cases" item in the "Case" menu. + * This is quite drastic, as it also affects Autopsy standalone mode on + * this machine, but we need to make sure a user can't open case in + * automated ingest mode. "Open Recent Cases" item can't be disabled via + * CallableSystemAction because of how it is defined in layer.xml, i.e. + * it is defined as "folder", not "file". + */ + FileObject root = FileUtil.getConfigRoot(); + FileObject openRecentCasesMenu = root.getFileObject("Menu/Case/OpenRecentCase"); + if (openRecentCasesMenu != null) { + try { + openRecentCasesMenu.delete(); + } catch (IOException ignore) { + } + } + } + + /** + * Queries the services monitor and sets the text for the services status + * text box. + */ + private void setServicesStatusMessage() { + new SwingWorker() { + + String caseDatabaseServerStatus = getServiceStatus(ServicesMonitor.Service.REMOTE_CASE_DATABASE); + String keywordSearchServiceStatus = getServiceStatus(ServicesMonitor.Service.REMOTE_CASE_DATABASE); + String messagingStatus = getServiceStatus(ServicesMonitor.Service.REMOTE_CASE_DATABASE); + + @Override + protected Void doInBackground() throws Exception { + caseDatabaseServerStatus = getServiceStatus(ServicesMonitor.Service.REMOTE_CASE_DATABASE); + keywordSearchServiceStatus = getServiceStatus(ServicesMonitor.Service.REMOTE_CASE_DATABASE); + messagingStatus = getServiceStatus(ServicesMonitor.Service.REMOTE_CASE_DATABASE); + return null; + } + + /** + * Gets a status string for a given service. + * + * @param service The service to test. + * + * @return The status string. + */ + private String getServiceStatus(ServicesMonitor.Service service) { + String serviceStatus = NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.tbServicesStatusMessage.Message.Unknown"); + try { + ServicesMonitor servicesMonitor = ServicesMonitor.getInstance(); + serviceStatus = servicesMonitor.getServiceStatus(service.toString()); + if (serviceStatus.compareTo(ServicesMonitor.ServiceStatus.UP.toString()) == 0) { + serviceStatus = NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.tbServicesStatusMessage.Message.Up"); + } else { + serviceStatus = NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.tbServicesStatusMessage.Message.Down"); + } + } catch (ServicesMonitor.ServicesMonitorException ex) { + SYS_LOGGER.log(Level.SEVERE, String.format("Dashboard error getting service status for %s", service), ex); + } + return serviceStatus; + } + + @Override + protected void done() { + tbServicesStatusMessage.setText(NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.tbServicesStatusMessage.Message", caseDatabaseServerStatus, keywordSearchServiceStatus, keywordSearchServiceStatus, messagingStatus)); + String upStatus = NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.tbServicesStatusMessage.Message.Up"); + if (caseDatabaseServerStatus.compareTo(upStatus) != 0 + || keywordSearchServiceStatus.compareTo(upStatus) != 0 + || messagingStatus.compareTo(upStatus) != 0) { + tbServicesStatusMessage.setForeground(Color.RED); + } else { + tbServicesStatusMessage.setForeground(Color.BLACK); + } + } + + }.execute(); + } + + /** + * Sets up the JTable that presents a view of the system-wide pending jobs + * queue. + */ + private void initPendingJobsTable() { + /* + * Remove some of the jobs table model columns from the JTable. This + * does not remove the columns from the model, just from this table. + */ + pendingTable.removeColumn(pendingTable.getColumn(JobsTableModelColumns.HOST_NAME.getColumnHeader())); + pendingTable.removeColumn(pendingTable.getColumn(JobsTableModelColumns.STARTED_TIME.getColumnHeader())); + pendingTable.removeColumn(pendingTable.getColumn(JobsTableModelColumns.COMPLETED_TIME.getColumnHeader())); + pendingTable.removeColumn(pendingTable.getColumn(JobsTableModelColumns.STAGE.getColumnHeader())); + pendingTable.removeColumn(pendingTable.getColumn(JobsTableModelColumns.STAGE_TIME.getColumnHeader())); + pendingTable.removeColumn(pendingTable.getColumn(JobsTableModelColumns.CASE_DIRECTORY_PATH.getColumnHeader())); + pendingTable.removeColumn(pendingTable.getColumn(JobsTableModelColumns.IS_LOCAL_JOB.getColumnHeader())); + pendingTable.removeColumn(pendingTable.getColumn(JobsTableModelColumns.STATUS.getColumnHeader())); + pendingTable.removeColumn(pendingTable.getColumn(JobsTableModelColumns.MANIFEST_FILE_PATH.getColumnHeader())); + + /* + * Set up a column to display the cases associated with the jobs. + */ + TableColumn column; + column = pendingTable.getColumn(JobsTableModelColumns.CASE.getColumnHeader()); + column.setMinWidth(GENERIC_COL_MIN_WIDTH); + column.setMaxWidth(GENERIC_COL_MAX_WIDTH); + column.setPreferredWidth(PENDING_TABLE_COL_PREFERRED_WIDTH); + column.setWidth(PENDING_TABLE_COL_PREFERRED_WIDTH); + + /* + * Set up a column to display the image folders associated with the + * jobs. + */ + column = pendingTable.getColumn(JobsTableModelColumns.DATA_SOURCE.getColumnHeader()); + column.setMaxWidth(GENERIC_COL_MAX_WIDTH); + column.setPreferredWidth(PENDING_TABLE_COL_PREFERRED_WIDTH); + column.setWidth(PENDING_TABLE_COL_PREFERRED_WIDTH); + + /* + * Set up a column to display the create times of the jobs. + */ + column = pendingTable.getColumn(JobsTableModelColumns.CREATED_TIME.getColumnHeader()); + column.setCellRenderer(new LongDateCellRenderer()); + column.setMinWidth(TIME_COL_MIN_WIDTH); + column.setMaxWidth(TIME_COL_MAX_WIDTH); + column.setPreferredWidth(TIME_COL_PREFERRED_WIDTH); + column.setWidth(TIME_COL_PREFERRED_WIDTH); + + /** + * Prevent sorting when a column header is clicked. + */ + pendingTable.setAutoCreateRowSorter(false); + + /* + * Create a row selection listener to enable/disable the prioritize + * folder and prioritize case buttons. + */ + pendingTable.getSelectionModel().addListSelectionListener((ListSelectionEvent e) -> { + if (e.getValueIsAdjusting()) { + return; + } + int row = pendingTable.getSelectedRow(); + enablePendingTableButtons((row >= 0) && (row < pendingTable.getRowCount())); + }); + + /* + * Save the background color of the table so it can be restored on + * resume, after being grayed out on pause. Note the assumption that all + * of the tables use the same background color. + */ + pendingTableBackground = pendingTable.getBackground(); + pendingTablelForeground = pendingTable.getForeground(); + } + + /** + * Sets up the JTable that presents a view of the system-wide running jobs + * list. + */ + private void initRunningJobsTable() { + /* + * Remove some of the jobs table model columns from the JTable. This + * does not remove the columns from the model, just from this table. + */ + runningTable.removeColumn(runningTable.getColumn(JobsTableModelColumns.CREATED_TIME.getColumnHeader())); + runningTable.removeColumn(runningTable.getColumn(JobsTableModelColumns.STARTED_TIME.getColumnHeader())); + runningTable.removeColumn(runningTable.getColumn(JobsTableModelColumns.COMPLETED_TIME.getColumnHeader())); + runningTable.removeColumn(runningTable.getColumn(JobsTableModelColumns.STATUS.getColumnHeader())); + runningTable.removeColumn(runningTable.getColumn(JobsTableModelColumns.CASE_DIRECTORY_PATH.getColumnHeader())); + runningTable.removeColumn(runningTable.getColumn(JobsTableModelColumns.IS_LOCAL_JOB.getColumnHeader())); + runningTable.removeColumn(runningTable.getColumn(JobsTableModelColumns.MANIFEST_FILE_PATH.getColumnHeader())); + + /* + * Set up a column to display the cases associated with the jobs. + */ + TableColumn column; + column = runningTable.getColumn(JobsTableModelColumns.CASE.getColumnHeader()); + column.setMinWidth(GENERIC_COL_MIN_WIDTH); + column.setMaxWidth(GENERIC_COL_MAX_WIDTH); + column.setPreferredWidth(RUNNING_TABLE_COL_PREFERRED_WIDTH); + column.setWidth(RUNNING_TABLE_COL_PREFERRED_WIDTH); + + /* + * Set up a column to display the image folders associated with the + * jobs. + */ + column = runningTable.getColumn(JobsTableModelColumns.DATA_SOURCE.getColumnHeader()); + column.setMinWidth(GENERIC_COL_MIN_WIDTH); + column.setMaxWidth(GENERIC_COL_MAX_WIDTH); + column.setPreferredWidth(RUNNING_TABLE_COL_PREFERRED_WIDTH); + column.setWidth(RUNNING_TABLE_COL_PREFERRED_WIDTH); + + /* + * Set up a column to display the host names of the cluster nodes + * processing the jobs. + */ + column = runningTable.getColumn(JobsTableModelColumns.HOST_NAME.getColumnHeader()); + column.setMinWidth(NAME_COL_MIN_WIDTH); + column.setMaxWidth(NAME_COL_MAX_WIDTH); + column.setPreferredWidth(NAME_COL_PREFERRED_WIDTH); + column.setWidth(NAME_COL_PREFERRED_WIDTH); + + /* + * Set up a column to display the ingest activities associated with the + * jobs. + */ + column = runningTable.getColumn(JobsTableModelColumns.STAGE.getColumnHeader()); + column.setMinWidth(ACTIVITY_COL_MIN_WIDTH); + column.setMaxWidth(ACTIVITY_COL_MAX_WIDTH); + column.setPreferredWidth(ACTIVITY_COL_PREFERRED_WIDTH); + column.setWidth(ACTIVITY_COL_PREFERRED_WIDTH); + + /* + * Set up a column to display the ingest activity times associated with + * the jobs. + */ + column = runningTable.getColumn(JobsTableModelColumns.STAGE_TIME.getColumnHeader()); + column.setCellRenderer(new DurationCellRenderer()); + column.setMinWidth(GENERIC_COL_MIN_WIDTH); + column.setMaxWidth(ACTIVITY_TIME_COL_MAX_WIDTH); + column.setPreferredWidth(ACTIVITY_TIME_COL_MIN_WIDTH); + column.setWidth(ACTIVITY_TIME_COL_MIN_WIDTH); + + /* + * Prevent sorting when a column header is clicked. + */ + runningTable.setAutoCreateRowSorter(false); + + /* + * Create a row selection listener to enable/disable the cancel current + * job, cancel current module, and show progress buttons. + */ + runningTable.getSelectionModel().addListSelectionListener((ListSelectionEvent e) -> { + if (e.getValueIsAdjusting()) { + return; + } + updateRunningTableButtonsBasedOnSelectedRow(); + }); + } + + private void updateRunningTableButtonsBasedOnSelectedRow() { + int row = runningTable.getSelectedRow(); + if (row >= 0 && row < runningTable.getRowCount()) { + if ((boolean) runningTableModel.getValueAt(row, JobsTableModelColumns.IS_LOCAL_JOB.ordinal())) { + enableRunningTableButtons(true); + return; + } + } + enableRunningTableButtons(false); + } + + /** + * Sets up the JTable that presents a view of the system-wide competed jobs + * list. + */ + private void initCompletedJobsTable() { + /* + * Remove some of the jobs table model columns from the JTable. This + * does not remove the columns from the model, just from this table. + */ + completedTable.removeColumn(completedTable.getColumn(JobsTableModelColumns.STARTED_TIME.getColumnHeader())); + completedTable.removeColumn(completedTable.getColumn(JobsTableModelColumns.STAGE.getColumnHeader())); + completedTable.removeColumn(completedTable.getColumn(JobsTableModelColumns.STAGE_TIME.getColumnHeader())); + completedTable.removeColumn(completedTable.getColumn(JobsTableModelColumns.IS_LOCAL_JOB.getColumnHeader())); + completedTable.removeColumn(completedTable.getColumn(JobsTableModelColumns.HOST_NAME.getColumnHeader())); + completedTable.removeColumn(completedTable.getColumn(JobsTableModelColumns.CASE_DIRECTORY_PATH.getColumnHeader())); + completedTable.removeColumn(completedTable.getColumn(JobsTableModelColumns.MANIFEST_FILE_PATH.getColumnHeader())); + + /* + * Set up a column to display the cases associated with the jobs. + */ + TableColumn column; + column = completedTable.getColumn(JobsTableModelColumns.CASE.getColumnHeader()); + column.setMinWidth(COMPLETED_TIME_COL_MIN_WIDTH); + column.setMaxWidth(COMPLETED_TIME_COL_MAX_WIDTH); + column.setPreferredWidth(COMPLETED_TIME_COL_PREFERRED_WIDTH); + column.setWidth(COMPLETED_TIME_COL_PREFERRED_WIDTH); + + /* + * Set up a column to display the image folders associated with the + * jobs. + */ + column = completedTable.getColumn(JobsTableModelColumns.DATA_SOURCE.getColumnHeader()); + column.setMinWidth(COMPLETED_TIME_COL_MIN_WIDTH); + column.setMaxWidth(COMPLETED_TIME_COL_MAX_WIDTH); + column.setPreferredWidth(COMPLETED_TIME_COL_PREFERRED_WIDTH); + column.setWidth(COMPLETED_TIME_COL_PREFERRED_WIDTH); + + /* + * Set up a column to display the create times of the jobs. + */ + column = completedTable.getColumn(JobsTableModelColumns.CREATED_TIME.getColumnHeader()); + column.setCellRenderer(new LongDateCellRenderer()); + column.setMinWidth(TIME_COL_MIN_WIDTH); + column.setMaxWidth(TIME_COL_MAX_WIDTH); + column.setPreferredWidth(TIME_COL_PREFERRED_WIDTH); + column.setWidth(TIME_COL_PREFERRED_WIDTH); + + /* + * Set up a column to display the completed times of the jobs. + */ + column = completedTable.getColumn(JobsTableModelColumns.COMPLETED_TIME.getColumnHeader()); + column.setCellRenderer(new LongDateCellRenderer()); + column.setMinWidth(TIME_COL_MIN_WIDTH); + column.setMaxWidth(TIME_COL_MAX_WIDTH); + column.setPreferredWidth(TIME_COL_PREFERRED_WIDTH); + column.setWidth(TIME_COL_PREFERRED_WIDTH); + + /* + * Set up a column to display the statuses of the jobs, with a cell + * renderer that will choose an icon to represent the job status. + */ + column = completedTable.getColumn(JobsTableModelColumns.STATUS.getColumnHeader()); + column.setCellRenderer(new CaseStatusIconCellRenderer()); + column.setMinWidth(STATUS_COL_MIN_WIDTH); + column.setMaxWidth(STATUS_COL_MAX_WIDTH); + column.setPreferredWidth(STATUS_COL_PREFERRED_WIDTH); + column.setWidth(STATUS_COL_PREFERRED_WIDTH); + + /* + * Prevent sorting when a column header is clicked. + */ + completedTable.setAutoCreateRowSorter(false); + + /* + * Create a row selection listener to enable/disable the delete case and + * show log buttons. + */ + completedTable.getSelectionModel() + .addListSelectionListener((ListSelectionEvent e) -> { + if (e.getValueIsAdjusting()) { + return; + } + int row = completedTable.getSelectedRow(); + boolean enabled = row >= 0 && row < completedTable.getRowCount(); + bnDeleteCase.setEnabled(enabled); + bnShowCaseLog.setEnabled(enabled); + bnReprocessJob.setEnabled(enabled); + }); + } + + /** + * Sets the initial state of the buttons on the panel. + */ + private void initButtons() { + bnOptions.setEnabled(true); + bnDeleteCase.setEnabled(false); + enablePendingTableButtons(false); + bnShowCaseLog.setEnabled(false); + bnReprocessJob.setEnabled(false); + bnPause.setText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnStart.text")); + bnPause.setToolTipText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnStart.toolTipText")); + bnPause.setEnabled(true); //initial label for bnPause is 'Start' and it's enabled for user to start the process + bnRefresh.setEnabled(false); //at initial stage, nothing to refresh + enableRunningTableButtons(false); + tbStatusMessage.setText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnStart.startMessage")); + } + + /** + * Enables or disables buttons related to the running jobs table. + * + * @param enable Enable/disable the buttons. + */ + private void enableRunningTableButtons(Boolean enable) { + bnCancelJob.setEnabled(enable); + bnCancelModule.setEnabled(enable); + bnShowProgress.setEnabled(enable); + } + + /** + * Enables or disables buttons related to pending jobs table. + * + * @param enable Enable/disable the buttons. + */ + private void enablePendingTableButtons(Boolean enable) { + bnPrioritizeCase.setEnabled(enable); + bnPrioritizeJob.setEnabled(enable); + } + + /** + * Starts up the auto ingest manager and adds this panel as an observer, + * subscribes to services monitor events and starts a task to populate the + * auto ingest job tables. The Refresh and Pause buttons are enabled. + */ + private void startUp() { + + /* + * Starts up the auto ingest manager (AIM). + */ + try { + manager.startUp(); + autoIngestStarted = true; + } catch (AutoIngestManager.AutoIngestManagerStartupException ex) { + SYS_LOGGER.log(Level.SEVERE, "Dashboard error starting up auto ingest", ex); + tbStatusMessage.setText(NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.AutoIngestStartupError")); + manager = null; + + JOptionPane.showMessageDialog(this, + NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.AutoIngestStartupFailed.Message"), + NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.AutoIngestStartupFailed.Title"), + JOptionPane.ERROR_MESSAGE); + bnOptions.setEnabled(true); + + /* + * If the AIM cannot be started, there is nothing more to do. + */ + return; + } + + /* + * Subscribe to services monitor events. + */ + ServicesMonitor.getInstance().addSubscriber((PropertyChangeEvent evt) -> { + setServicesStatusMessage(); + }); + + /* + * Register with the AIM as an observer. + */ + manager.addObserver(this); + + /* + * Populate the pending, running, and completed auto ingest job tables. + */ + updateExecutor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat(UPDATE_TASKS_THREAD_NAME).build()); + updateExecutor.submit(new UpdateAllJobsTablesTask()); + manager.scanInputDirsNow(); + + //bnPause.setEnabled(true); + bnPause.setText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnPause.text")); + bnPause.setToolTipText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnPause.toolTipText")); + bnRefresh.setEnabled(true); + bnOptions.setEnabled(false); + + tbStatusMessage.setText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnPause.running")); + } + + /** + * Shuts down auto ingest by shutting down the auto ingest manager and doing + * an application exit. + */ + public void shutdown() { + /* + * Confirm that the user wants to proceed, letting him or her no that if + * there is a currently running job it will be cancelled. TODO (RC): If + * a wait cursor is provided, this could perhaps be made conditional on + * a running job check again. Or the simple check in isLocalJobRunning + * could be used. Was this previously used and I removed it thinking it + * was grabbing the monitor? + */ + Object[] options = { + NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.OK"), + NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.Cancel")}; + int reply = JOptionPane.OK_OPTION; + + if (null != manager && IngestManager.getInstance().isIngestRunning()) { + reply = JOptionPane.showOptionDialog(this, + NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.ExitConsequences"), + NbBundle.getMessage(AutoIngestDashboard.class, "ConfirmationDialog.ConfirmExitHeader"), + JOptionPane.DEFAULT_OPTION, + JOptionPane.WARNING_MESSAGE, + null, + options, + options[JOptionPane.NO_OPTION]); + } + if (reply == JOptionPane.OK_OPTION) { + /* + * Provide user feedback. Call setCursor on this to ensure it + * appears (if there is time to see it). + */ + this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + tbStatusMessage.setText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.ExitingStatus")); + + /* + * Shut down the table refresh task executor. + */ + if (null != updateExecutor) { + updateExecutor.shutdownNow(); + } + + /* + * Stop observing the auto ingest manager (AIM). + */ + if (null != manager) { + manager.deleteObserver(this); + } + + /* + * Shut down the AIM and close. + */ + new SwingWorker() { + + @Override + protected Void doInBackground() throws Exception { + if (null != manager) { + manager.shutDown(); + } + return null; + } + + @Override + protected void done() { + AutoIngestDashboard.this.setCursor(Cursor.getDefaultCursor()); + LifecycleManager.getDefault().exit(); + } + }.execute(); + } + } + + /** + * @inheritDoc + */ + @NbBundle.Messages({ + "AutoIngestDashboard.bnPause.paused=Paused", + "AutoIngestDashboard.PauseDueToDatabaseServiceDown=Paused, unable to communicate with case database service.", + "AutoIngestDashboard.PauseDueToKeywordSearchServiceDown=Paused, unable to communicate with keyword search service.", + "AutoIngestDashboard.PauseDueToCoordinationServiceDown=Paused, unable to communicate with coordination service.", + "AutoIngestDashboard.PauseDueToWriteStateFilesFailure=Paused, unable to write to shared images or cases location.", + "AutoIngestDashboard.PauseDueToSharedConfigError=Paused, unable to update shared configuration.", + "AutoIngestDashboard.PauseDueToIngestJobStartFailure=Paused, unable to start ingest job processing.", + "AutoIngestDashboard.PauseDueToFileExporterError=Paused, unable to load File Exporter settings.", + "AutoIngestDashboard.bnPause.running=Running", + "AutoIngestDashboard.bnStart.startMessage=Waiting to start", + "AutoIngestDashboard.bnStart.text=Start", + "AutoIngestDashboard.bnStart.toolTipText=Start processing auto ingest jobs" + }) + @Override + public void update(Observable o, Object arg) { + + if (arg instanceof AutoIngestManager.Event) { + switch ((AutoIngestManager.Event) arg) { + case INPUT_SCAN_COMPLETED: + case JOB_STARTED: + case JOB_COMPLETED: + case CASE_DELETED: + updateExecutor.submit(new UpdateAllJobsTablesTask()); + break; + case PAUSED_BY_REQUEST: + EventQueue.invokeLater(() -> { + tbStatusMessage.setText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnPause.paused")); + bnOptions.setEnabled(true); + bnRefresh.setEnabled(false); + isPaused = true; + }); + break; + case PAUSED_FOR_SYSTEM_ERROR: + EventQueue.invokeLater(() -> { + tbStatusMessage.setText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.PauseDueToSystemError")); + bnOptions.setEnabled(true); + bnRefresh.setEnabled(false); + pause(false); + isPaused = true; + setServicesStatusMessage(); + }); + break; + case RESUMED: + EventQueue.invokeLater(() -> { + tbStatusMessage.setText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnPause.running")); + }); + break; + case CASE_PRIORITIZED: + updateExecutor.submit(new UpdatePendingJobsTableTask()); + break; + case JOB_STATUS_UPDATED: + updateExecutor.submit(new UpdateRunningJobsTablesTask()); + break; + default: + break; + } + } + } + + /** + * Requests a pause of auto ingest processing by the auto ingest manager and + * handles updates to the components that implement the pause and resume + * feature. Note that this feature is needed to get around restrictions on + * changing ingest module selections and settings while an ingest job is + * running, and that the auto ingest manager will not actually pause until + * the current auto ingest job completes. + * + * @param buttonClicked Is this pause request in response to a user gesture + * or a nofification from the auto ingest manager + * (AIM)? + */ + private void pause(boolean buttonClicked) { + /** + * Gray out the cells in the pending table to give a visual indicator of + * the pausing/paused state. + */ + pendingTable.setBackground(Color.LIGHT_GRAY); + pendingTable.setForeground(Color.DARK_GRAY); + + /** + * Change the pause button text and tool tip to make it a resume button. + */ + bnPause.setText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnResume.text")); + bnPause.setToolTipText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnPause.toolTipTextResume")); + + if (buttonClicked) { + /** + * Ask the auto ingest manager to pause when it completes the + * currently running job, if any. + */ + manager.pause(); + bnRefresh.setEnabled(false); + } + } + + /** + * Requests a resume of auto ingest processing by the auto ingest manager + * and handles updates to the components that implement the pause and resume + * feature. Note that this feature is needed to get around restrictions on + * changing ingest module selections and settings while an ingest job is + * running, and that the auto ingest manager will not actually pause until + * the current auto ingest job completes. + */ + private void resume() { + /** + * Change the resume button text and tool tip to make it a pause button. + */ + bnOptions.setEnabled(false); + bnPause.setText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnPause.text")); + bnPause.setToolTipText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnPause.toolTipText")); + tbStatusMessage.setText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnPause.running")); + bnRefresh.setEnabled(true); + + /** + * Remove the graying out of the pending table. + */ + pendingTable.setBackground(pendingTableBackground); + pendingTable.setForeground(pendingTablelForeground); + + /** + * Ask the auto ingest manager to resume processing. + */ + manager.resume(); + } + + /** + * A runnable task that gets the pending auto ingest jobs list from the auto + * ingest manager and queues a components refresh task for execution in the + * EDT. + */ + private class UpdatePendingJobsTableTask implements Runnable { + + /** + * @inheritDoc + */ + @Override + public void run() { + List pendingJobs = new ArrayList<>(); + manager.getJobs(pendingJobs, null, null); + EventQueue.invokeLater(new RefreshComponentsTask(pendingJobs, null, null)); + } + } + + /** + * A runnable task that gets the running auto ingest jobs list from the auto + * ingest manager and queues a components refresh task for execution in the + * EDT. + */ + private class UpdateRunningJobsTablesTask implements Runnable { + + /** + * @inheritDoc + */ + @Override + public void run() { + List runningJobs = new ArrayList<>(); + manager.getJobs(null, runningJobs, null); + EventQueue.invokeLater(new RefreshComponentsTask(null, runningJobs, null)); + } + } + + /** + * A runnable task that gets the pending, running and completed auto ingest + * jobs lists from the auto ingest manager and queues a components refresh + * task for execution in the EDT. Note that this task is frequently used + * when only the pending and updated lists definitely need to be updated. + * This is because the cost of updating the running jobs list is both very + * small and it is beneficial to keep running job status up to date if there + * is a running job. + */ + private class UpdateAllJobsTablesTask implements Runnable { + + /** + * @inheritDoc + */ + @Override + public void run() { + List pendingJobs = new ArrayList<>(); + List runningJobs = new ArrayList<>(); + List completedJobs = new ArrayList<>(); + manager.getJobs(pendingJobs, runningJobs, completedJobs); + // Sort the completed jobs list by completed date + Collections.sort(completedJobs, new AutoIngestJob.ReverseDateCompletedComparator()); + EventQueue.invokeLater(new RefreshComponentsTask(pendingJobs, runningJobs, completedJobs)); + } + } + + /** + * A runnable task that refreshes the components on this panel to reflect + * the current state of one or more auto ingest job lists obtained from the + * auto ingest manager. + */ + private class RefreshComponentsTask implements Runnable { + + private final List pendingJobs; + private final List runningJobs; + private final List completedJobs; + + /** + * Constructs a runnable task that refreshes the components on this + * panel to reflect the current state of the auto ingest jobs. + * + * @param pendingJobs A list of pending jobs, may be null if the + * pending jobs are unchanged. + * @param runningJobs A list of running jobs, may be null if the + * running jobs are unchanged. + * @param completedJobs A list of completed jobs, may be null if the + * completed jobs are unchanged. + */ + RefreshComponentsTask(List pendingJobs, List runningJobs, List completedJobs) { + this.pendingJobs = pendingJobs; + this.runningJobs = runningJobs; + this.completedJobs = completedJobs; + } + + /** + * @inheritDoc + */ + @Override + public void run() { + /* + * NOTE: There is a problem with our approach of preserving table + * row selections - what if the number of rows has changed as result + * of calling refreshTable(). Then it is possible for what used to + * be (for example) row 1 to now be in some other row or be removed + * from the table. This code will re-set the selection back to what + * it used to be before calling refreshTable(), i.e. row 1 + */ + + if (null != pendingJobs) { + Path currentRow = getSelectedEntry(pendingTable, pendingTableModel); + refreshTable(pendingJobs, pendingTableModel, null); + setSelectedEntry(pendingTable, pendingTableModel, currentRow); + } + + if (null != runningJobs) { + if (!isLocalJobRunning()) { + enableRunningTableButtons(false); + } else { + updateRunningTableButtonsBasedOnSelectedRow(); + } + Path currentRow = getSelectedEntry(runningTable, runningTableModel); + refreshTable(runningJobs, runningTableModel, null); + setSelectedEntry(runningTable, runningTableModel, currentRow); + } + + if (null != completedJobs) { + Path currentRow = getSelectedEntry(completedTable, completedTableModel); + refreshTable(completedJobs, completedTableModel, null); + setSelectedEntry(completedTable, completedTableModel, currentRow); + } + } + + /** + * Checks whether there is a job that is running on local AIN. + * + * @return true is local job is found, false otherwise. + */ + private boolean isLocalJobRunning() { + for (AutoIngestJob job : runningJobs) { + if (isLocalJob(job)) { + return true; + } + } + return false; + } + + /** + * Checks whether or not an automated ingest job is local to this node. + * + * @param job The job. + * + * @return True or fale. + */ + private boolean isLocalJob(AutoIngestJob job) { + return job.getNodeName().equals(LOCAL_HOST_NAME); + } + + /** + * Get a path representing the current selection on the table passed in. + * If there is no selection, return null. + * + * @param table The table to get + * @param tableModel The tableModel of the table to get + * + * @return a path representing the current selection + */ + Path getSelectedEntry(JTable table, DefaultTableModel tableModel) { + try { + int currentlySelectedRow = table.getSelectedRow(); + if (currentlySelectedRow >= 0 && currentlySelectedRow < table.getRowCount()) { + return Paths.get(tableModel.getValueAt(currentlySelectedRow, JobsTableModelColumns.CASE.ordinal()).toString(), + tableModel.getValueAt(currentlySelectedRow, JobsTableModelColumns.DATA_SOURCE.ordinal()).toString()); + } + } catch (Exception ignored) { + return null; + } + return null; + } + + /** + * Set the selection on the table to the passed-in path's item, if that + * item exists in the table. If it does not, clears the table selection. + * + * @param table The table to set + * @param tableModel The tableModel of the table to set + * @param path The path of the item to set + */ + void setSelectedEntry(JTable table, DefaultTableModel tableModel, Path path) { + if (path != null) { + try { + for (int row = 0; row < table.getRowCount(); ++row) { + Path temp = Paths.get(tableModel.getValueAt(row, JobsTableModelColumns.CASE.ordinal()).toString(), + tableModel.getValueAt(row, JobsTableModelColumns.DATA_SOURCE.ordinal()).toString()); + if (temp.compareTo(path) == 0) { // found it + table.setRowSelectionInterval(row, row); + return; + } + } + } catch (Exception ignored) { + table.clearSelection(); + } + } + table.clearSelection(); + } + } + + /** + * Reloads the table model for an auto ingest jobs table, refreshing the + * JTable that uses the model. + * + * @param jobs The list of auto ingest jobs. + * @param tableModel The table model. + * @param comparator An optional comparator (may be null) for sorting the + * table model. + */ + private void refreshTable(List jobs, DefaultTableModel tableModel, Comparator comparator) { + try { + if (comparator != null) { + jobs.sort(comparator); + } + tableModel.setRowCount(0); + for (AutoIngestJob job : jobs) { + AutoIngestJob.StageDetails status = job.getStageDetails(); + tableModel.addRow(new Object[]{ + job.getManifest().getCaseName(), // CASE + job.getManifest().getDataSourcePath().getFileName(), // DATA_SOURCE + job.getNodeName(), // HOST_NAME + job.getManifest().getDateFileCreated(), // CREATED_TIME + job.getStageStartDate(), // STARTED_TIME + job.getCompletedDate(), // COMPLETED_TIME + status.getDescription(), // ACTIVITY + job.hasErrors(), // STATUS + ((Date.from(Instant.now()).getTime()) - (status.getStartDate().getTime())), // ACTIVITY_TIME + job.getCaseDirectoryPath(), // CASE_DIRECTORY_PATH + job.getNodeName().equals(LOCAL_HOST_NAME), // IS_LOCAL_JOB + job.getManifest().getFilePath()}); // MANIFEST_FILE_PATH + } + } catch (Exception ex) { + SYS_LOGGER.log(Level.SEVERE, "Dashboard error refreshing table", ex); + } + } + + /** + * Get the current lists of jobs and update the UI. + */ + private void refreshTables(){ + JobsSnapshot jobsSnapshot = manager.getCurrentJobsSnapshot(); + refreshTable(jobsSnapshot.getCompletedJobs(), completedTableModel, null); + refreshTable(jobsSnapshot.getPendingJobs(), pendingTableModel, null); + refreshTable(jobsSnapshot.getRunningJobs(), runningTableModel, 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. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + pendingScrollPane = new javax.swing.JScrollPane(); + pendingTable = new javax.swing.JTable(); + runningScrollPane = new javax.swing.JScrollPane(); + runningTable = new javax.swing.JTable(); + 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(); + bnRefresh = new javax.swing.JButton(); + bnCancelModule = new javax.swing.JButton(); + bnExit = new javax.swing.JButton(); + bnOptions = new javax.swing.JButton(); + bnShowProgress = new javax.swing.JButton(); + bnPause = new javax.swing.JButton(); + bnPrioritizeCase = new javax.swing.JButton(); + bnShowCaseLog = new javax.swing.JButton(); + tbStatusMessage = new javax.swing.JTextField(); + lbStatus = new javax.swing.JLabel(); + bnPrioritizeJob = new javax.swing.JButton(); + lbServicesStatus = new javax.swing.JLabel(); + tbServicesStatusMessage = new javax.swing.JTextField(); + bnOpenLogDir = new javax.swing.JButton(); + bnReprocessJob = new javax.swing.JButton(); + + pendingTable.setModel(pendingTableModel); + pendingTable.setToolTipText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.pendingTable.toolTipText")); // NOI18N + pendingTable.setAutoResizeMode(javax.swing.JTable.AUTO_RESIZE_ALL_COLUMNS); + pendingTable.setRowHeight(20); + pendingTable.setSelectionModel(new DefaultListSelectionModel() { + private static final long serialVersionUID = 1L; + @Override + public void setSelectionInterval(int index0, int index1) { + if (index0 == pendingTable.getSelectedRow()) { + pendingTable.clearSelection(); + } else { + super.setSelectionInterval(index0, index1); + } + } + }); + pendingTable.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); + pendingScrollPane.setViewportView(pendingTable); + + runningTable.setModel(runningTableModel); + runningTable.setToolTipText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.runningTable.toolTipText")); // NOI18N + runningTable.setAutoResizeMode(javax.swing.JTable.AUTO_RESIZE_ALL_COLUMNS); + runningTable.setRowHeight(20); + runningTable.setSelectionModel(new DefaultListSelectionModel() { + private static final long serialVersionUID = 1L; + @Override + public void setSelectionInterval(int index0, int index1) { + if (index0 == runningTable.getSelectedRow()) { + runningTable.clearSelection(); + } else { + super.setSelectionInterval(index0, index1); + } + } + }); + runningTable.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); + runningScrollPane.setViewportView(runningTable); + + completedTable.setModel(completedTableModel); + completedTable.setToolTipText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.completedTable.toolTipText")); // NOI18N + completedTable.setAutoResizeMode(javax.swing.JTable.AUTO_RESIZE_ALL_COLUMNS); + completedTable.setRowHeight(20); + completedTable.setSelectionModel(new DefaultListSelectionModel() { + private static final long serialVersionUID = 1L; + @Override + public void setSelectionInterval(int index0, int index1) { + if (index0 == completedTable.getSelectedRow()) { + completedTable.clearSelection(); + } else { + super.setSelectionInterval(index0, index1); + } + } + }); + completedTable.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); + completedScrollPane.setViewportView(completedTable); + + org.openide.awt.Mnemonics.setLocalizedText(bnCancelJob, org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnCancelJob.text")); // NOI18N + bnCancelJob.setToolTipText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnCancelJob.toolTipText")); // NOI18N + bnCancelJob.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnCancelJobActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(bnDeleteCase, org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnDeleteCase.text")); // NOI18N + bnDeleteCase.setToolTipText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnDeleteCase.toolTipText")); // NOI18N + 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(AutoIngestDashboard.class, "AutoIngestDashboard.lbPending.text")); // NOI18N + + lbRunning.setFont(new java.awt.Font("Tahoma", 0, 14)); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(lbRunning, org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.lbRunning.text")); // NOI18N + + lbCompleted.setFont(new java.awt.Font("Tahoma", 0, 14)); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(lbCompleted, org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.lbCompleted.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(bnRefresh, org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnRefresh.text")); // NOI18N + bnRefresh.setToolTipText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnRefresh.toolTipText")); // NOI18N + bnRefresh.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnRefreshActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(bnCancelModule, org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnCancelModule.text")); // NOI18N + bnCancelModule.setToolTipText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnCancelModule.toolTipText")); // NOI18N + bnCancelModule.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnCancelModuleActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(bnExit, org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnExit.text")); // NOI18N + bnExit.setToolTipText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnExit.toolTipText")); // NOI18N + bnExit.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnExitActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(bnOptions, org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnOptions.text")); // NOI18N + bnOptions.setToolTipText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnOptions.toolTipText")); // NOI18N + bnOptions.setEnabled(false); + bnOptions.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnOptionsActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(bnShowProgress, org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnShowProgress.text")); // NOI18N + bnShowProgress.setToolTipText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnShowProgress.toolTipText")); // NOI18N + bnShowProgress.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnShowProgressActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(bnPause, org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnPause.text")); // NOI18N + bnPause.setToolTipText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnPause.toolTipText")); // NOI18N + bnPause.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnPauseActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(bnPrioritizeCase, org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnPrioritizeCase.text")); // NOI18N + bnPrioritizeCase.setToolTipText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnPrioritizeCase.toolTipText")); // NOI18N + bnPrioritizeCase.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnPrioritizeCaseActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(bnShowCaseLog, org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnShowCaseLog.text")); // NOI18N + bnShowCaseLog.setToolTipText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnShowCaseLog.toolTipText")); // NOI18N + bnShowCaseLog.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnShowCaseLogActionPerformed(evt); + } + }); + + tbStatusMessage.setEditable(false); + tbStatusMessage.setFont(new java.awt.Font("Tahoma", 1, 12)); // NOI18N + tbStatusMessage.setText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.tbStatusMessage.text")); // NOI18N + tbStatusMessage.setBorder(null); + + lbStatus.setFont(new java.awt.Font("Tahoma", 0, 14)); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(lbStatus, org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.lbStatus.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(bnPrioritizeJob, org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnPrioritizeJob.text")); // NOI18N + bnPrioritizeJob.setToolTipText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnPrioritizeJob.toolTipText")); // NOI18N + bnPrioritizeJob.setActionCommand(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnPrioritizeJob.actionCommand")); // NOI18N + bnPrioritizeJob.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnPrioritizeJobActionPerformed(evt); + } + }); + + lbServicesStatus.setFont(new java.awt.Font("Tahoma", 0, 14)); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(lbServicesStatus, org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.lbServicesStatus.text")); // NOI18N + + tbServicesStatusMessage.setEditable(false); + tbServicesStatusMessage.setFont(new java.awt.Font("Tahoma", 1, 12)); // NOI18N + tbServicesStatusMessage.setText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.tbServicesStatusMessage.text")); // NOI18N + tbServicesStatusMessage.setBorder(null); + + org.openide.awt.Mnemonics.setLocalizedText(bnOpenLogDir, org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnOpenLogDir.text")); // NOI18N + bnOpenLogDir.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnOpenLogDirActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(bnReprocessJob, org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnReprocessJob.text")); // NOI18N + bnReprocessJob.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnReprocessJobActionPerformed(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) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(lbPending, javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(pendingScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 920, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(bnPrioritizeCase, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(bnPrioritizeJob, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(bnPause) + .addGap(18, 18, 18) + .addComponent(bnRefresh, javax.swing.GroupLayout.PREFERRED_SIZE, 100, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(18, 18, 18) + .addComponent(bnOptions) + .addGap(18, 18, 18) + .addComponent(bnOpenLogDir) + .addGap(18, 18, 18) + .addComponent(bnExit, javax.swing.GroupLayout.PREFERRED_SIZE, 94, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(runningScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 920, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(completedScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 920, 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, 117, Short.MAX_VALUE) + .addComponent(bnShowProgress, javax.swing.GroupLayout.DEFAULT_SIZE, 116, Short.MAX_VALUE) + .addComponent(bnCancelModule, javax.swing.GroupLayout.DEFAULT_SIZE, 117, Short.MAX_VALUE) + .addComponent(bnDeleteCase, javax.swing.GroupLayout.DEFAULT_SIZE, 117, 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.createSequentialGroup() + .addComponent(lbStatus) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(tbStatusMessage, javax.swing.GroupLayout.PREFERRED_SIZE, 861, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(lbCompleted) + .addComponent(lbRunning) + .addGroup(layout.createSequentialGroup() + .addComponent(lbServicesStatus) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(tbServicesStatusMessage, javax.swing.GroupLayout.PREFERRED_SIZE, 861, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addGap(0, 0, Short.MAX_VALUE))) + .addContainerGap()) + ); + + layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {bnCancelJob, bnCancelModule, bnDeleteCase, bnExit, bnOpenLogDir, bnOptions, bnPause, bnRefresh, bnShowProgress}); + + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(lbStatus, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(tbStatusMessage, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(lbServicesStatus, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(tbServicesStatusMessage, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(lbPending, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(pendingScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 215, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(layout.createSequentialGroup() + .addGap(82, 82, 82) + .addComponent(bnPrioritizeCase) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(bnPrioritizeJob))) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(lbRunning) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGap(34, 34, 34) + .addComponent(bnShowProgress) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(bnCancelJob) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(bnCancelModule)) + .addGroup(layout.createSequentialGroup() + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(runningScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 133, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGap(68, 68, 68) + .addComponent(bnReprocessJob) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(bnDeleteCase) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(bnShowCaseLog)) + .addGroup(layout.createSequentialGroup() + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(lbCompleted) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(completedScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 179, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(bnExit) + .addComponent(bnOpenLogDir)) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(bnPause) + .addComponent(bnRefresh) + .addComponent(bnOptions))))) + .addContainerGap()) + ); + + layout.linkSize(javax.swing.SwingConstants.VERTICAL, new java.awt.Component[] {bnCancelJob, bnCancelModule, bnDeleteCase, bnExit, bnOpenLogDir, bnOptions, bnRefresh, bnShowProgress}); + + }// //GEN-END:initComponents + + /** + * Handles a click on the refresh button. Requests an immediate scan of the + * input folders for new jobs and queues a refresh of all three of the jobs + * tables. + * + * @param evt - The button click event. + */ + private void bnRefreshActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnRefreshActionPerformed + this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + manager.scanInputDirsAndWait(); + refreshTables(); + 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. + */ + private void bnDeleteCaseActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnDeleteCaseActionPerformed + if (completedTableModel.getRowCount() < 0 || completedTable.getSelectedRow() < 0) { + return; + } + + String caseName = (String) completedTable.getValueAt(completedTable.getSelectedRow(), JobsTableModelColumns.CASE.ordinal()); + Object[] options = { + org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "ConfirmationDialog.Delete"), + org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "ConfirmationDialog.DoNotDelete") + }; + Object[] msgContent = {org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "ConfirmationDialog.DeleteAreYouSure") + "\"" + caseName + "\"?"}; + int reply = JOptionPane.showOptionDialog(this, + msgContent, + org.openide.util.NbBundle.getMessage(AutoIngestDashboard.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 (completedTableModel.getRowCount() > 0 && completedTable.getSelectedRow() >= 0) { + Path caseDirectoryPath = (Path) completedTableModel.getValueAt(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 in use.", caseName), + org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.DeletionFailed"), + JOptionPane.INFORMATION_MESSAGE); + } else if (CaseDeletionResult.PARTIALLY_DELETED == result) { + JOptionPane.showMessageDialog(this, + String.format("Could not delete case %s. See system log for details.", caseName), + org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.DeletionFailed"), + JOptionPane.INFORMATION_MESSAGE); + } + } + } + }//GEN-LAST:event_bnDeleteCaseActionPerformed + + /** + * Handles a click on the cancel auto ingest job button. Cancels the + * selected job. + * + * @param evt The button click event. + */ + private void bnCancelJobActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnCancelJobActionPerformed + Object[] options = { + org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "ConfirmationDialog.CancelJob"), + org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "ConfirmationDialog.DoNotCancelJob")}; + int reply = JOptionPane.showOptionDialog(this, + NbBundle.getMessage(AutoIngestDashboard.class, "ConfirmationDialog.CancelJobAreYouSure"), + NbBundle.getMessage(AutoIngestDashboard.class, "ConfirmationDialog.ConfirmCancellationHeader"), + JOptionPane.DEFAULT_OPTION, + JOptionPane.WARNING_MESSAGE, + null, + options, + options[1]); + if (reply == 0) { + /* + * Call setCursor on this to ensure it appears (if there is time to + * see it). + */ + this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + manager.cancelCurrentJob(); + refreshTables(); + this.setCursor(Cursor.getDefaultCursor()); + } + }//GEN-LAST:event_bnCancelJobActionPerformed + + /** + * Handles a click on the show auto ingest job progress button. Displays an + * ingest job progress panel. + * + * @param evt The button click event. + */ + private void bnShowProgressActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnShowProgressActionPerformed + IngestProgressSnapshotDialog dialog = new IngestProgressSnapshotDialog(this.getTopLevelAncestor(), true); + }//GEN-LAST:event_bnShowProgressActionPerformed + + /** + * Handles a click on the pause/resume auto ingest job button. Sends a + * pause/resume request to the auto ingest manager. + * + * @param evt The button click event. + */ + private void bnPauseActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnPauseActionPerformed + + if (!autoIngestStarted) { + //put up a wait cursor during the start up operation + this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + + startUp(); + + this.setCursor(null); + //done for startup + return; + } + if (!isPaused) { + tbStatusMessage.setText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.bnPause.pausing")); + pause(true); + } else { + resume(); + } + isPaused = !isPaused; + }//GEN-LAST:event_bnPauseActionPerformed + + /** + * Handles a click on the options button. Displays the options window. + * + * @param evt The button click event. + */ + private void bnOptionsActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnOptionsActionPerformed + setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + OptionsDisplayer.getDefault().open(); + setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + }//GEN-LAST:event_bnOptionsActionPerformed + + /** + * Handles a click on the cancel ingest module button. Cancels the currently + * running data source level ingest module for the selected job. + * + * @param evt The button click event. + */ + private void bnCancelModuleActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnCancelModuleActionPerformed + Object[] options = { + org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "ConfirmationDialog.CancelModule"), + org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "ConfirmationDialog.DoNotCancelModule")}; + int reply = JOptionPane.showOptionDialog(this, + NbBundle.getMessage(AutoIngestDashboard.class, "ConfirmationDialog.CancelModuleAreYouSure"), + NbBundle.getMessage(AutoIngestDashboard.class, "ConfirmationDialog.ConfirmCancellationHeader"), + JOptionPane.DEFAULT_OPTION, + JOptionPane.WARNING_MESSAGE, + null, + options, + options[1]); + if (reply == 0) { + /* + * Call setCursor on this to ensure it appears (if there is time to + * see it). + */ + this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + manager.cancelCurrentDataSourceLevelIngestModule(); + refreshTables(); + this.setCursor(Cursor.getDefaultCursor()); + } + }//GEN-LAST:event_bnCancelModuleActionPerformed + + /** + * Handles a click on the exit button. Shuts down auto ingest. + * + * @param evt The button click event. + */ + private void bnExitActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnExitActionPerformed + shutdown(); + }//GEN-LAST:event_bnExitActionPerformed + + /** + * Handle a click on the prioritize case button. Requests prioritization of + * all of the auto ingest jobs for a case. + * + * @param evt The button click event. + */ + private void bnPrioritizeCaseActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnPrioritizeCaseActionPerformed + if (pendingTableModel.getRowCount() > 0 && pendingTable.getSelectedRow() >= 0) { + this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + String caseName = (pendingTableModel.getValueAt(pendingTable.getSelectedRow(), JobsTableModelColumns.CASE.ordinal())).toString(); + manager.prioritizeCase(caseName); + refreshTables(); + pendingTable.clearSelection(); + enablePendingTableButtons(false); + AutoIngestDashboard.this.setCursor(Cursor.getDefaultCursor()); + } + }//GEN-LAST:event_bnPrioritizeCaseActionPerformed + + /** + * Handles a click on the show log button. Displays the auto ingest job log + * for a case in NotePad. + * + * @param evt The button click event. + */ + private void bnShowCaseLogActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnShowCaseLogActionPerformed + try { + int selectedRow = completedTable.getSelectedRow(); + if (selectedRow != -1) { + Path caseDirectoryPath = (Path) completedTableModel.getValueAt(selectedRow, JobsTableModelColumns.CASE_DIRECTORY_PATH.ordinal()); + if (null != caseDirectoryPath) { + Path pathToLog = AutoIngestJobLogger.getLogPath(caseDirectoryPath); + if (pathToLog.toFile().exists()) { + Desktop.getDesktop().edit(pathToLog.toFile()); + } else { + JOptionPane.showMessageDialog(this, org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.ShowLogFailed.Message"), + org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.ShowLogFailed.Title"), JOptionPane.ERROR_MESSAGE); + } + } else { + MessageNotifyUtil.Message.warn("The case directory for this job has been deleted."); + } + } + } catch (IOException ex) { + SYS_LOGGER.log(Level.SEVERE, "Dashboard error attempting to display case auto ingest log", ex); + Object[] options = {org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "DisplayLogDialog.okay")}; + JOptionPane.showOptionDialog(this, + org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "DisplayLogDialog.cannotFindLog"), + org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "DisplayLogDialog.unableToShowLogFile"), + JOptionPane.DEFAULT_OPTION, + JOptionPane.PLAIN_MESSAGE, + null, + options, + options[0]); + } + }//GEN-LAST:event_bnShowCaseLogActionPerformed + + private void bnPrioritizeJobActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnPrioritizeJobActionPerformed + if (pendingTableModel.getRowCount() > 0 && pendingTable.getSelectedRow() >= 0) { + this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + Path manifestFilePath = (Path) (pendingTableModel.getValueAt(pendingTable.getSelectedRow(), JobsTableModelColumns.MANIFEST_FILE_PATH.ordinal())); + manager.prioritizeJob(manifestFilePath); + refreshTables(); + pendingTable.clearSelection(); + enablePendingTableButtons(false); + AutoIngestDashboard.this.setCursor(Cursor.getDefaultCursor()); + } + }//GEN-LAST:event_bnPrioritizeJobActionPerformed + + private void bnOpenLogDirActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnOpenLogDirActionPerformed + Path logDirPath = Paths.get(PlatformUtil.getUserDirectory().getAbsolutePath(), "var", "log"); + File logDir = logDirPath.toFile(); + try { + Desktop.getDesktop().open(logDir); + } catch (IOException ex) { + DialogDisplayer.getDefault().notify(new NotifyDescriptor.Message( + String.format("Unable to open log directory %s:\n%s", logDirPath, ex.getLocalizedMessage()), + NotifyDescriptor.ERROR_MESSAGE)); + } + }//GEN-LAST:event_bnOpenLogDirActionPerformed + + private void bnReprocessJobActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnReprocessJobActionPerformed + if (completedTableModel.getRowCount() < 0 || completedTable.getSelectedRow() < 0) { + return; + } + this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + Path manifestPath = (Path) completedTableModel.getValueAt(completedTable.getSelectedRow(), JobsTableModelColumns.MANIFEST_FILE_PATH.ordinal()); + manager.reprocessJob(manifestPath); + refreshTables(); + AutoIngestDashboard.this.setCursor(Cursor.getDefaultCursor()); + }//GEN-LAST:event_bnReprocessJobActionPerformed + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton bnCancelJob; + private javax.swing.JButton bnCancelModule; + private javax.swing.JButton bnDeleteCase; + private javax.swing.JButton bnExit; + private javax.swing.JButton bnOpenLogDir; + private javax.swing.JButton bnOptions; + private javax.swing.JButton bnPause; + private javax.swing.JButton bnPrioritizeCase; + private javax.swing.JButton bnPrioritizeJob; + private javax.swing.JButton bnRefresh; + private javax.swing.JButton bnReprocessJob; + private javax.swing.JButton bnShowCaseLog; + private javax.swing.JButton bnShowProgress; + private javax.swing.JScrollPane completedScrollPane; + private javax.swing.JTable completedTable; + private javax.swing.JLabel lbCompleted; + private javax.swing.JLabel lbPending; + private javax.swing.JLabel lbRunning; + private javax.swing.JLabel lbServicesStatus; + private javax.swing.JLabel lbStatus; + private javax.swing.JScrollPane pendingScrollPane; + private javax.swing.JTable pendingTable; + private javax.swing.JScrollPane runningScrollPane; + private javax.swing.JTable runningTable; + private javax.swing.JTextField tbServicesStatusMessage; + private javax.swing.JTextField tbStatusMessage; + // End of variables declaration//GEN-END:variables + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJob.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJob.java new file mode 100644 index 0000000000..ada7f61206 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJob.java @@ -0,0 +1,537 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.Serializable; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Instant; +import java.util.Comparator; +import java.util.Date; +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.Immutable; +import javax.annotation.concurrent.ThreadSafe; +import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; +import org.sleuthkit.autopsy.coreutils.NetworkUtils; +import org.sleuthkit.autopsy.ingest.IngestJob; + +/** + * An automated ingest job for a manifest. The manifest specifies a co-located + * data source and a case to which the data source is to be added. + */ +@ThreadSafe +public final class AutoIngestJob implements Comparable, Serializable { + + private static final long serialVersionUID = 1L; + private static final String LOCAL_HOST_NAME = NetworkUtils.getLocalHostName(); + private final Manifest manifest; + private final String nodeName; + @GuardedBy("this") + private String caseDirectoryPath; + @GuardedBy("this") + private Integer priority; + @GuardedBy("this") + private Stage stage; + @GuardedBy("this") + private Date stageStartDate; + @GuardedBy("this") + transient private DataSourceProcessor dataSourceProcessor; + @GuardedBy("this") + transient private IngestJob ingestJob; + @GuardedBy("this") + transient private boolean cancelled; // RJCTODO: Document + @GuardedBy("this") + transient private boolean completed; // RJCTODO: Document + @GuardedBy("this") + private Date completedDate; + @GuardedBy("this") + private boolean errorsOccurred; + + /** + * Constructs an automated ingest job for a manifest. The manifest specifies + * a co-located data source and a case to which the data source is to be + * added. + * + * @param manifest The manifest + * @param caseDirectoryPath The path to the case directory for the job, may + * be null. + * @param priority The priority of the job. The higher the number, + * the higher the priority. + * @param nodeName If the job is in progress, the node doing the + * processing, otherwise the locla host. + * @param stage The processing stage for display purposes. + * @param completedDate The date when the job was completed. Use the + * epoch (January 1, 1970, 00:00:00 GMT) to + * indicate the the job is not completed, i.e., new + * Date(0L). + */ + // RJCTODO: The null case directory is error-prone and the nodeName is confusing. + AutoIngestJob(Manifest manifest, Path caseDirectoryPath, int priority, String nodeName, Stage stage, Date completedDate, boolean errorsOccurred) { + this.manifest = manifest; + if (null != caseDirectoryPath) { + this.caseDirectoryPath = caseDirectoryPath.toString(); + } else { + this.caseDirectoryPath = ""; + } + this.priority = priority; + this.nodeName = nodeName; + this.stage = stage; + this.stageStartDate = manifest.getDateFileCreated(); + this.completedDate = completedDate; + this.errorsOccurred = errorsOccurred; + } + + /** + * Gets the auto ingest jobmanifest. + * + * @return The manifest. + */ + Manifest getManifest() { + return this.manifest; + } + + /** + * Queries whether or not a case directory path has been set for this auto + * ingest job. + * + * @return True or false + */ + // RJCTODO: Use this or lose this + synchronized boolean hasCaseDirectoryPath() { + return (false == this.caseDirectoryPath.isEmpty()); + } + + /** + * Sets the path to the case directory of the case associated with this job. + * + * @param caseDirectoryPath The path to the case directory. + */ + synchronized void setCaseDirectoryPath(Path caseDirectoryPath) { + this.caseDirectoryPath = caseDirectoryPath.toString(); + } + + /** + * Gets the path to the case directory of the case associated with this job, + * may be null. + * + * @return The case directory path or null if the case directory has not + * been created yet. + */ + synchronized Path getCaseDirectoryPath() { + if (!caseDirectoryPath.isEmpty()) { + return Paths.get(caseDirectoryPath); + } else { + return null; + } + } + + /** + * Sets the priority of the job. A higher number indicates a higher + * priority. + * + * @param priority The priority. + */ + synchronized void setPriority(Integer priority) { + this.priority = priority; + } + + /** + * Gets the priority of the job. A higher number indicates a higher + * priority. + * + * @return The priority. + */ + synchronized Integer getPriority() { + return this.priority; + } + + /** + * RJCTODO + * + * @param newStage + */ + synchronized void setStage(Stage newStage) { + setStage(newStage, Date.from(Instant.now())); + } + + /** + * RJCTODO + * + * @param state + * @param stateStartedDate + */ + synchronized void setStage(Stage newState, Date stateStartedDate) { + if (Stage.CANCELLING == this.stage && Stage.COMPLETED != newState) { + return; + } + this.stage = newState; + this.stageStartDate = stateStartedDate; + } + + /** + * RJCTODO: + * + * @return + */ + synchronized Stage getStage() { + return this.stage; + } + + /** + * RJCTODO + * + * @return + */ + synchronized Date getStageStartDate() { + return this.stageStartDate; + } + + /** + * RJCTODO + * + * @return + */ + synchronized StageDetails getStageDetails() { + String description; + Date startDate; + if (Stage.CANCELLING != this.stage && null != this.ingestJob) { + IngestJob.ProgressSnapshot progress = this.ingestJob.getSnapshot(); + IngestJob.DataSourceIngestModuleHandle ingestModuleHandle = progress.runningDataSourceIngestModule(); + if (null != ingestModuleHandle) { + /** + * A first or second stage data source level ingest module is + * running. Reporting this takes precedence over reporting + * generic file analysis. + */ + startDate = ingestModuleHandle.startTime(); + if (!ingestModuleHandle.isCancelled()) { + description = ingestModuleHandle.displayName(); + } else { + description = String.format(Stage.CANCELLING_MODULE.getDisplayText(), ingestModuleHandle.displayName()); // RJCTODO: FIx this + } + } else { + /** + * If no data source level ingest module is running, then either + * it is still the first stage of analysis and file level ingest + * modules are running or another ingest job is still running. + * Note that there can be multiple ingest jobs running in + * parallel. For example, there is an ingest job created to + * ingest each extracted virtual machine. + */ + description = Stage.ANALYZING_FILES.getDisplayText(); + startDate = progress.fileIngestStartTime(); + } + } else { + description = this.stage.getDisplayText(); + startDate = this.stageStartDate; + } + return new StageDetails(description, startDate); + } + + synchronized void setDataSourceProcessor(DataSourceProcessor dataSourceProcessor) { + this.dataSourceProcessor = dataSourceProcessor; + } + + /** + * RJCTODO + */ + // RJCTODO: Consider moving this class into AIM and making this private + synchronized void setIngestJob(IngestJob ingestJob) { + this.ingestJob = ingestJob; + } + + /** + * RJCTODO + */ + // RJCTODO: Consider moving this class into AIM and making this private. + // Or move the AID into a separate package. Or do not worry about it. + synchronized IngestJob getIngestJob() { + return this.ingestJob; + } + + /** + * RJCTODO + */ + synchronized void cancel() { + setStage(Stage.CANCELLING); + cancelled = true; + errorsOccurred = true; + if (null != dataSourceProcessor) { + dataSourceProcessor.cancel(); + } + if (null != ingestJob) { + ingestJob.cancel(IngestJob.CancellationReason.USER_CANCELLED); + } + } + + /** + * RJCTODO + */ + synchronized boolean isCancelled() { + return cancelled; + } + + /** + * RJCTODO + */ + synchronized void setCompleted() { + setStage(Stage.COMPLETED); + completed = true; + } + + /** + * RJCTODO + * + * @return + */ + synchronized boolean isCompleted() { + return completed; + } + + /** + * Sets the date the job was completed, with or without cancellation or + * errors. + * + * @param completedDate The completion date. + */ + synchronized void setCompletedDate(Date completedDate) { + this.completedDate = completedDate; + } + + /** + * Gets the date the job was completed, with or without cancellation or + * errors. + * + * @return True or false. + */ + synchronized Date getCompletedDate() { + return completedDate; // RJCTODO: Consider returning null if == 0 (epoch) + } + + /** + * Sets whether or not erros occurred during the processing of the job. + * + * @param errorsOccurred True or false; + */ + synchronized void setErrorsOccurred(boolean errorsOccurred) { + this.errorsOccurred = errorsOccurred; + } + + /** + * Queries whether or not erros occurred during the processing of the job. + * + * @return True or false. + */ + synchronized boolean hasErrors() { + return this.errorsOccurred; + } + + /** + * RJCTODO Gets name of the node associated with the job, possibly a remote + * hose if the job is in progress. + * + * @return The node name. + */ + String getNodeName() { + return nodeName; + } + + /** + * RJCTODO + * + * @param obj + * + * @return + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof AutoIngestJob)) { + return false; + } + if (obj == this) { + return true; + } + return this.getManifest().getFilePath().equals(((AutoIngestJob) obj).getManifest().getFilePath()); + } + + /** + * RJCTODO + * + * @return + */ + @Override + public int hashCode() { + // RJCTODO: Update this + int hash = 7; +// hash = 71 * hash + Objects.hashCode(this.dateCreated); + return hash; + } + + /** + * RJCTODO Default sorting is by ready file creation date, descending + * + * @param o + * + * @return + */ + @Override + public int compareTo(AutoIngestJob o) { + return -this.getManifest().getDateFileCreated().compareTo(o.getManifest().getDateFileCreated()); + } + + /** + * Custom comparator that allows us to sort List on reverse + * chronological date modified (descending) + */ + static class ReverseDateCompletedComparator implements Comparator { + + /** + * RJCTODO + * + * @param o1 + * @param o2 + * + * @return + */ + @Override + public int compare(AutoIngestJob o1, AutoIngestJob o2) { + return -o1.getStageStartDate().compareTo(o2.getStageStartDate()); + } + } + + /** + * Comparator that sorts auto ingest jobs by priority in descending order. + */ + public static class PriorityComparator implements Comparator { + + /** + * RJCTODO + * + * @param job + * @param anotherJob + * + * @return + */ + @Override + public int compare(AutoIngestJob job, AutoIngestJob anotherJob) { + return -(job.getPriority().compareTo(anotherJob.getPriority())); + } + + } + + /** + * Custom comparator that allows us to sort List on case name + * alphabetically except for jobs for the current host, which are placed at + * the top of the list. + */ + static class AlphabeticalComparator implements Comparator { + + /** + * RJCTODO + * + * @param o1 + * @param o2 + * + * @return + */ + @Override + public int compare(AutoIngestJob o1, AutoIngestJob o2) { + if (o1.getNodeName().equalsIgnoreCase(LOCAL_HOST_NAME)) { + return -1; // o1 is for current case, float to top + } else if (o2.getNodeName().equalsIgnoreCase(LOCAL_HOST_NAME)) { + return 1; // o2 is for current case, float to top + } else { + return o1.getManifest().getCaseName().compareToIgnoreCase(o2.getManifest().getCaseName()); + } + } + } + + /** + * RJCTODO + */ + // RJCTODO: Combine this enum with StageDetails to make a single class. + enum Stage { + + PENDING("Pending"), + STARTING("Starting"), + UPDATING_SHARED_CONFIG("Updating shared configuration"), + CHECKING_SERVICES("Checking services"), + OPENING_CASE("Opening case"), + IDENTIFYING_DATA_SOURCE("Identifying data source type"), + ADDING_DATA_SOURCE("Adding data source"), + ANALYZING_DATA_SOURCE("Analyzing data source"), + ANALYZING_FILES("Analyzing files"), + EXPORTING_FILES("Exporting files"), + CANCELLING_MODULE("Cancelling module"), + CANCELLING("Cancelling"), + COMPLETED("Completed"); + + private final String displayText; + + private Stage(String displayText) { + this.displayText = displayText; + } + + String getDisplayText() { + return displayText; + } + + } + + /** + * RJCTODO + */ + @Immutable + static final class StageDetails { + + private final String description; + private final Date startDate; + + /** + * RJCTODO + * + * @param description + * @param startDate + */ + private StageDetails(String description, Date startDate) { + this.description = description; + this.startDate = startDate; + } + + /** + * RJCTODO + * + * @return + */ + String getDescription() { + return this.description; + } + + /** + * RJCTODO + * + * @return + */ + Date getStartDate() { + return this.startDate; + } + + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJobCompletedEvent.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJobCompletedEvent.java new file mode 100644 index 0000000000..ee5d06287d --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJobCompletedEvent.java @@ -0,0 +1,55 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.Serializable; + +/** + * Event published when an automated ingest manager completes processing an + * automated ingest job. + */ +public final class AutoIngestJobCompletedEvent extends AutoIngestJobEvent implements Serializable { + + private static final long serialVersionUID = 1L; + private final boolean retry; + + /** + * Constructs an event published when an automated ingest manager completes + * processing an automated ingest job. + * + * @param job The completed job. + * @param shouldRetry Whether or not the job actually completed or needs to + * be attempted again. + */ + public AutoIngestJobCompletedEvent(AutoIngestJob job, boolean shouldRetry) { + super(AutoIngestManager.Event.JOB_COMPLETED, job); + this.retry = shouldRetry; + } + + /** + * Queries whether or not the job actually completed or needs to be + * attempted again. + * + * @return True or false. + */ + public boolean shouldRetry() { + return this.retry; + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJobEvent.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJobEvent.java new file mode 100644 index 0000000000..e1c9464d00 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJobEvent.java @@ -0,0 +1,51 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.Serializable; +import javax.annotation.concurrent.Immutable; +import org.sleuthkit.autopsy.events.AutopsyEvent; + +/** + * RJCTODO + */ +@Immutable +abstract class AutoIngestJobEvent extends AutopsyEvent implements Serializable { + + private static final long serialVersionUID = 1L; + private final AutoIngestJob job; + + /** + * RJCTODO + * + */ + AutoIngestJobEvent(AutoIngestManager.Event eventSubType, AutoIngestJob job) { + super(eventSubType.toString(), null, null); + this.job = job; + } + + /** + * RJCTODO + * @return + */ + AutoIngestJob getJob() { + return this.job; + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJobLogger.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJobLogger.java new file mode 100644 index 0000000000..f3828d5594 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJobLogger.java @@ -0,0 +1,478 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.util.Date; +import org.sleuthkit.autopsy.coreutils.NetworkUtils; +import org.sleuthkit.autopsy.experimental.coordinationservice.CoordinationService; +import org.sleuthkit.autopsy.experimental.coordinationservice.CoordinationService.Lock; +import org.sleuthkit.autopsy.experimental.coordinationservice.CoordinationService.CoordinationServiceException; +import java.util.concurrent.TimeUnit; +import java.util.List; +import javax.annotation.concurrent.Immutable; +import org.sleuthkit.autopsy.ingest.IngestModuleError; +import org.sleuthkit.autopsy.ingest.IngestManager.IngestManagerException; + +/** + * A logger for the processing of an auto ingest job by an auto ingest node. An + * exclusive coordination service lock on the log file is used to serialize + * access to it by each auto ingest node so that log entries do not become + * garbled. + *

+ * Normally, the log messages are written to the case auto ingest log in the + * case directory. If there is an error writing to the log, the message is + * preserved by writing it to the auto ingest system log, along with the cause + * of the error. + */ +@Immutable +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; + private static final String DATE_FORMAT_STRING = "yyyy/MM/dd HH:mm:ss"; + private static final SimpleDateFormat logDateFormat = new SimpleDateFormat(DATE_FORMAT_STRING); + private final Path manifestPath; + private final String manifestFileName; + private final String dataSourceFileName; + private final Path caseDirectoryPath; + private final String hostName; + + /** + * Message category added to log messages to make searching for various + * classes of messages easier, e.g., to make error messages stand out. + */ + private enum MessageCategory { + /** + * Qualifies a log message about normal automated ingest processing. + */ + INFO, + /** + * Qualifies a log message about an unexpected event or condtion during + * automated ingest processing. + */ + WARNING, + /** + * Qualifies a log message about an error event or condition during + * automated ingest processing. + */ + ERROR + } + + /** + * Gets the path to the automated ingest log for a case. + * + * @param caseDirectoryPath The path to the case directory where the log + * resides. + * + * @return The path to the automated ingest case log for the case. + */ + static Path getLogPath(Path caseDirectoryPath) { + return Paths.get(caseDirectoryPath.toString(), LOG_FILE_NAME); + } + + /** + * Constructs a logger for the processing of an auto ingest job by an auto + * ingest node. The log messages are written to the case auto ingest log, a + * user-friendly log of of the automated processing for a case that resides + * in the case directory. + * + * The auto iongest log for a case is not intended to be a comprehensive. + * Advanced users doing troubleshooting of an automated ingest cluster + * should also consult the Autopsy and system logs as needed. + * + * @param manifestPath The manifest for the auto ingest job. + * @param caseDirectoryPath The case directory. + */ + AutoIngestJobLogger(Path manifestPath, String dataSourceFileName, Path caseDirectoryPath) { + this.manifestPath = manifestPath; + manifestFileName = manifestPath.getFileName().toString(); + this.dataSourceFileName = dataSourceFileName; + this.caseDirectoryPath = caseDirectoryPath; + hostName = NetworkUtils.getLocalHostName(); + } + + /** + * Logs the cancellation of an auto ingest job during processing. + * + * @throws AutoIngestJobLoggerException if there is an error writing the log + * message. + * @throws InterruptedException if interrupted while blocked waiting + * to acquire an exclusive lock on the + * log file. + */ + void logJobCancelled() throws AutoIngestJobLoggerException, InterruptedException { + log(MessageCategory.WARNING, "Auto ingest job cancelled during processing"); + } + + /** + * Logs the presence of a manifest file without a matching data source. + * + * @throws AutoIngestJobLoggerException if there is an error writing the log + * message. + * @throws InterruptedException if interrupted while blocked waiting + * to acquire an exclusive lock on the + * log file. + */ + void logMissingDataSource() throws AutoIngestJobLoggerException, InterruptedException { + log(MessageCategory.ERROR, "Data source file not found"); + } + + /** + * Logs a failure to extract an archived data source. + * + * @throws AutoIngestJobLoggerException if there is an error writing the log + * message. + * @throws InterruptedException if interrupted while blocked waiting + * to acquire an exclusive lock on the + * log file. + */ + void logFailedToExtractDataSource() throws AutoIngestJobLoggerException, InterruptedException { + log(MessageCategory.ERROR, "Failed to extract data source from archive"); + } + + /** + * Logs a failure to parse a Cellebrite logical report data source. + * + * @throws AutoIngestJobLoggerException if there is an error writing the log + * message. + * @throws InterruptedException if interrupted while blocked waiting + * to acquire an exclusive lock on the + * log file. + */ + void logFailedToParseLogicalReportDataSource() throws AutoIngestJobLoggerException, InterruptedException { + log(MessageCategory.ERROR, "Failed to parse Cellebrite logical report data source"); + } + + /** + * Logs a failure to identify data source processor for the data source. + * + * @throws AutoIngestJobLoggerException if there is an error writing the log + * message. + * @throws InterruptedException if interrupted while blocked waiting + * to acquire an exclusive lock on the + * log file. + */ + void logFailedToIdentifyDataSource() throws AutoIngestJobLoggerException, InterruptedException { + log(MessageCategory.ERROR, String.format("Failed to identify data source")); + } + + /** + * Logs cancellation of the addition of a data source to the case database. + * + * @throws AutoIngestJobLoggerException if there is an error writing the log + * message. + * @throws InterruptedException if interrupted while blocked waiting + * to acquire an exclusive lock on the + * log file. + */ + void logDataSourceProcessorCancelled() throws AutoIngestJobLoggerException, InterruptedException { // RJCTODO: Is this used now? + log(MessageCategory.WARNING, "Cancelled adding data source to case"); + } + + /** + * Logs selection of a data source processor + * @param dsp Name of the data source processor + * @throws AutoIngestJobLoggerException if there is an error writing the log + * message. + * @throws InterruptedException if interrupted while blocked waiting + * to acquire an exclusive lock on the + * log file. + */ + void logDataSourceProcessorSelected(String dsp) throws AutoIngestJobLoggerException, InterruptedException{ + log(MessageCategory.INFO, "Using data source processor: " + dsp); + } + + /** + * Logs the failure of the selected data source processor. + * @param dsp Name of the data source processor + * @throws AutoIngestJobLoggerException if there is an error writing the log + * message. + * @throws InterruptedException if interrupted while blocked waiting + * to acquire an exclusive lock on the + * log file. + */ + void logDataSourceProcessorError(String dsp) throws AutoIngestJobLoggerException, InterruptedException{ + log(MessageCategory.ERROR, "Error processing with data source processor: " + dsp); + } + + /** + * Logs the addition of a data source to the case database. + * + * @throws AutoIngestJobLoggerException if there is an error writing the log + * message. + * @throws InterruptedException if interrupted while blocked waiting + * to acquire an exclusive lock on the + * log file. + */ + void logDataSourceAdded() throws AutoIngestJobLoggerException, InterruptedException { + log(MessageCategory.INFO, "Added data source to case"); + } + + /** + * Logs an failure adding a data source to the case database. + * + * @throws AutoIngestJobLoggerException if there is an error writing the log + * message. + * @throws InterruptedException if interrupted while blocked waiting + * to acquire an exclusive lock on the + * log file. + */ + void logFailedToAddDataSource() throws AutoIngestJobLoggerException, InterruptedException { + log(MessageCategory.ERROR, "Failed to add data source to case"); + } + + /** + * Logs failure of a data source to produce content. + * + * @throws AutoIngestJobLoggerException if there is an error writing the log + * message. + * @throws InterruptedException if interrupted while blocked waiting + * to acquire an exclusive lock on the + * log file. + */ + void logNoDataSourceContent() throws AutoIngestJobLoggerException, InterruptedException { + log(MessageCategory.ERROR, "Data source failed to produce content"); + } + + /** + * Logs failure to analyze a data source due to ingest job settings errors. + * + * @throws AutoIngestJobLoggerException if there is an error writing the log + * message. + * @throws InterruptedException if interrupted while blocked waiting + * to acquire an exclusive lock on the + * log file. + */ + void logIngestJobSettingsErrors() throws AutoIngestJobLoggerException, InterruptedException { + log(MessageCategory.ERROR, "Failed to analyze data source due to settings errors"); + } + + /** + * Logs failure to analyze a data source due to ingest module startup + * errors. + * + * @throws AutoIngestJobLoggerException if there is an error writing the log + * message. + * @throws InterruptedException if interrupted while blocked waiting + * to acquire an exclusive lock on the + * log file. + */ + void logIngestModuleStartupErrors() throws AutoIngestJobLoggerException, InterruptedException { + log(MessageCategory.ERROR, "Failed to analyze data source due to ingest module startup errors"); + } + + /** + * Logs failure to analyze a data source because the analysis could not be + * started due to an ingest manager exception. + * + * @param ex The ingest manager exception. + * + * @throws AutoIngestJobLoggerException if there is an error writing the log + * message. + * @throws InterruptedException if interrupted while blocked waiting + * to acquire an exclusive lock on the + * log file. + */ + void logAnalysisStartupError() throws AutoIngestJobLoggerException, InterruptedException { + log(MessageCategory.ERROR, "Failed to analyze data source due to ingest job startup error"); + } + + /** + * Logs the completion of analysis of a data source by the ingest modules. + * + * @throws AutoIngestJobLoggerException if there is an error writing the log + * message. + * @throws InterruptedException if interrupted while blocked waiting + * to acquire an exclusive lock on the + * log file. + */ + void logAnalysisCompleted() throws AutoIngestJobLoggerException, InterruptedException { + log(MessageCategory.INFO, "Analysis of data source completed"); + } + + /** + * Logs the cancellation of analysis of a data source by an individual + * ingest module. + * + * @param cancelledModuleName The display name of the cancelled ingest + * module. + * + * @throws AutoIngestJobLoggerException if there is an error writing the log + * message. + * @throws InterruptedException if interrupted while blocked waiting + * to acquire an exclusive lock on the + * log file. + */ + void logIngestModuleCancelled(String cancelledModuleName) throws AutoIngestJobLoggerException, InterruptedException { + log(MessageCategory.WARNING, String.format("%s analysis of data source cancelled", cancelledModuleName)); + } + + /** + * Logs the cancellation of analysis of a data source by the ingest modules. + * + * @throws AutoIngestJobLoggerException if there is an error writing the log + * message. + * @throws InterruptedException if interrupted while blocked waiting + * to acquire an exclusive lock on the + * log file. + */ + void logAnalysisCancelled() throws AutoIngestJobLoggerException, InterruptedException { + log(MessageCategory.WARNING, "Analysis of data source cancelled"); + } + + /** + * Logs that automated file export is not enabled. + * + * @throws AutoIngestJobLoggerException if there is an error writing the log + * message. + * @throws InterruptedException if interrupted while blocked waiting + * to acquire an exclusive lock on the + * log file. + */ + void logFileExportDisabled() throws AutoIngestJobLoggerException, InterruptedException { + log(MessageCategory.WARNING, "Automated file export is not enabled"); + } + + /** + * Logs completion of file export. + * + * @throws AutoIngestJobLoggerException if there is an error writing the log + * message. + * @throws InterruptedException if interrupted while blocked waiting + * to acquire an exclusive lock on the + * log file. + */ + void logFileExportCompleted() throws AutoIngestJobLoggerException, InterruptedException { + log(MessageCategory.INFO, "Automated file export completed"); + } + + /** + * Logs failure to complete file export. + * + * @throws AutoIngestJobLoggerException if there is an error writing the log + * message. + * @throws InterruptedException if interrupted while blocked waiting + * to acquire an exclusive lock on the + * log file. + */ + void logFileExportError() throws AutoIngestJobLoggerException, InterruptedException { + log(MessageCategory.ERROR, "Error exporting files"); + } + + /** + * Logs discovery of a crashed auto ingest job for which recovery will be + * attempted. + * + * @throws AutoIngestJobLoggerException if there is an error writing the log + * message. + * @throws InterruptedException if interrupted while blocked waiting + * to acquire an exclusive lock on the + * log file. + */ + void logCrashRecoveryWithRetry() throws AutoIngestJobLoggerException, InterruptedException { + log(MessageCategory.ERROR, "Detected crash while processing, reprocessing"); + } + + /** + * Logs discovery of a crashed auto ingest job for which recovery will not + * be attempted because the retry limit for the job has been reached. + * + * @throws AutoIngestJobLoggerException if there is an error writing the log + * message. + * @throws InterruptedException if interrupted while blocked waiting + * to acquire an exclusive lock on the + * log file. + */ + void logCrashRecoveryNoRetry() throws AutoIngestJobLoggerException, InterruptedException { + log(MessageCategory.ERROR, "Detected crash while processing, reached retry limit for processing"); + } + + /** + * Writes a message to the case auto ingest log. + *

+ * An exclusive coordination service lock on the log file is used to + * serialize access to the log file by each auto ingest node so that log + * entries do not become garbled. + * + * @param category The message category. + * @param message The message. + * + * @throws AutoIngestJobLoggerException if there is an error writing the log + * message. + * @throws InterruptedException if interrupted while blocked waiting + * to acquire an exclusive lock on the + * log file. + */ + private void log(MessageCategory category, String message) throws AutoIngestJobLoggerException, InterruptedException { + try (Lock lock = CoordinationService.getInstance(CoordinationServiceNamespace.getRoot()).tryGetExclusiveLock(CoordinationService.CategoryNode.CASES, getLogPath(caseDirectoryPath).toString(), LOCK_TIME_OUT, LOCK_TIME_OUT_UNIT)) { + if (null != lock) { + File logFile = getLogPath(caseDirectoryPath).toFile(); + try (PrintWriter writer = new PrintWriter(new BufferedWriter(new FileWriter(logFile, logFile.exists())), true)) { + writer.println(String.format("%s %s: %s: %s: %-8s: %s", logDateFormat.format((Date.from(Instant.now()).getTime())), hostName, manifestFileName, dataSourceFileName, category.toString(), message)); + } catch (IOException ex) { + throw new AutoIngestJobLoggerException(String.format("Failed to write case auto ingest log message (\"%s\") for %s", message, manifestPath), ex); + } + } else { + throw new AutoIngestJobLoggerException(String.format("Failed to write case auto ingest log message (\"%s\") for %s due to time out acquiring log lock", message, manifestPath)); + } + } catch (CoordinationServiceException ex) { + throw new AutoIngestJobLoggerException(String.format("Failed to write case auto ingest log message (\"%s\") for %s", message, manifestPath), ex); + } + } + + /** + * Exception thrown when there is a problem writing a log message. + */ + final static class AutoIngestJobLoggerException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Constructs an exception to throw when there is a problem writing a + * log message. + * + * @param message The exception message. + */ + private AutoIngestJobLoggerException(String message) { + super(message); + } + + /** + * Constructs an exception to throw when there is a problem writing a + * log message. + * + * @param message The exception message. + * @param cause The cause of the exception, if it was an exception. + */ + private AutoIngestJobLoggerException(String message, Throwable cause) { + super(message, cause); + } + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJobStartedEvent.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJobStartedEvent.java new file mode 100644 index 0000000000..de2ef46ffd --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJobStartedEvent.java @@ -0,0 +1,38 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.Serializable; + +/** + * Event published when auto ingest manager (AIM) starts processing an auto + * ingest job. + */ +public final class AutoIngestJobStartedEvent extends AutoIngestJobEvent implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * RJCTODO + */ + public AutoIngestJobStartedEvent(AutoIngestJob job) { + super(AutoIngestManager.Event.JOB_STARTED, job); + } + +} \ No newline at end of file diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJobStatusEvent.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJobStatusEvent.java new file mode 100644 index 0000000000..23a444f7d7 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJobStatusEvent.java @@ -0,0 +1,38 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.Serializable; + +/** + * Event published periodically when an automated ingest manager (AIM) is processing + * an automated ingest job. + */ +public final class AutoIngestJobStatusEvent extends AutoIngestJobEvent implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * RJCTODO + */ + public AutoIngestJobStatusEvent(AutoIngestJob job) { + super(AutoIngestManager.Event.JOB_STATUS_UPDATED, job); + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java new file mode 100644 index 0000000000..eec64f2b84 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java @@ -0,0 +1,2943 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import org.sleuthkit.autopsy.experimental.configuration.AutoIngestUserPreferences; +import java.io.File; +import java.io.IOException; +import static java.nio.file.FileVisitOption.FOLLOW_LINKS; +import java.nio.file.FileVisitResult; +import static java.nio.file.FileVisitResult.CONTINUE; +import static java.nio.file.FileVisitResult.TERMINATE; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributes; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import org.sleuthkit.autopsy.modules.vmextractor.VirtualMachineFinder; +import org.sleuthkit.autopsy.core.UserPreferences; +import org.sleuthkit.datamodel.CaseDbConnectionInfo; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +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.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.Observable; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.stream.Collectors; +import javax.annotation.concurrent.GuardedBy; +import javax.annotation.concurrent.Immutable; +import javax.annotation.concurrent.ThreadSafe; +import javax.swing.filechooser.FileFilter; +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.io.FilenameUtils; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.CaseActionException; +import org.sleuthkit.autopsy.ingest.IngestManager; +import org.openide.modules.InstalledFileLocator; +import org.sleuthkit.autopsy.casemodule.Case.CaseType; +import org.sleuthkit.autopsy.casemodule.GeneralFilter; +import org.sleuthkit.autopsy.casemodule.ImageDSProcessor; +import org.sleuthkit.autopsy.core.RuntimeProperties; +import org.sleuthkit.autopsy.core.ServicesMonitor; +import org.sleuthkit.autopsy.core.UserPreferencesException; +import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback; +import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgressMonitor; +import org.sleuthkit.autopsy.coreutils.ExecUtil; +import org.sleuthkit.autopsy.coreutils.NetworkUtils; +import org.sleuthkit.autopsy.coreutils.PlatformUtil; +import org.sleuthkit.autopsy.events.AutopsyEvent; +import org.sleuthkit.autopsy.events.AutopsyEventPublisher; +import org.sleuthkit.autopsy.ingest.IngestJob; +import org.sleuthkit.autopsy.ingest.IngestJobSettings; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.autopsy.experimental.coordinationservice.CoordinationService; +import org.sleuthkit.autopsy.experimental.coordinationservice.CoordinationService.CoordinationServiceException; +import org.sleuthkit.autopsy.experimental.coordinationservice.CoordinationService.Lock; +import org.sleuthkit.autopsy.experimental.configuration.SharedConfiguration; +import org.apache.solr.client.solrj.impl.HttpSolrServer; +import org.openide.util.Lookup; +import org.sleuthkit.autopsy.casemodule.CaseMetadata; +import org.sleuthkit.autopsy.casemodule.LocalFilesDSProcessor; +import org.sleuthkit.autopsy.core.ServicesMonitor.ServicesMonitorException; +import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback.DataSourceProcessorResult; +import org.sleuthkit.autopsy.coreutils.FileUtil; +import org.sleuthkit.autopsy.events.AutopsyEventException; +import org.sleuthkit.autopsy.ingest.IngestJob.CancellationReason; +import org.sleuthkit.autopsy.ingest.IngestJobStartResult; +import org.sleuthkit.autopsy.ingest.IngestModuleError; +import org.sleuthkit.autopsy.experimental.autoingest.FileExporter.FileExportException; +import org.sleuthkit.autopsy.experimental.autoingest.ManifestFileParser.ManifestFileParserException; +import org.sleuthkit.autopsy.experimental.autoingest.ManifestNodeData.ProcessingStatus; +import static org.sleuthkit.autopsy.experimental.autoingest.ManifestNodeData.ProcessingStatus.PENDING; +import static org.sleuthkit.autopsy.experimental.autoingest.ManifestNodeData.ProcessingStatus.PROCESSING; +import static org.sleuthkit.autopsy.experimental.autoingest.ManifestNodeData.ProcessingStatus.COMPLETED; +import static org.sleuthkit.autopsy.experimental.autoingest.ManifestNodeData.ProcessingStatus.DELETED; +import org.sleuthkit.autopsy.corecomponentinterfaces.AutomatedIngestDataSourceProcessor; +import org.sleuthkit.autopsy.corecomponentinterfaces.AutomatedIngestDataSourceProcessor.AutomatedIngestDataSourceProcessorException; +import org.sleuthkit.autopsy.coreutils.FileUtil; +import org.sleuthkit.autopsy.experimental.autoingest.AutoIngestAlertFile.AutoIngestAlertFileException; +import org.sleuthkit.autopsy.experimental.autoingest.AutoIngestJobLogger.AutoIngestJobLoggerException; +import org.sleuthkit.autopsy.experimental.configuration.SharedConfiguration.SharedConfigurationException; +import org.sleuthkit.autopsy.ingest.IngestJob.CancellationReason; + +/** + * An auto ingest manager is responsible for processing auto ingest jobs defined + * by manifest files that can be added to any level of a designated input + * directory tree. + *

+ * Each manifest file specifies a co-located data source and a case to which the + * data source is to be added. The case directories for the cases reside in a + * designated output directory tree. + *

+ * There should be at most one auto ingest manager per host (auto ingest node). + * Multiple auto ingest nodes may be combined to form an auto ingest cluster. + * The activities of the auto ingest nodes in a cluster are coordinated by way + * of a coordination service and the nodes communicate via event messages. + */ +public final class AutoIngestManager extends Observable implements PropertyChangeListener { + + private static final int NUM_INPUT_SCAN_SCHEDULING_THREADS = 1; + private static final String INPUT_SCAN_SCHEDULER_THREAD_NAME = "AIM-input-scan-scheduler-%d"; + private static final String INPUT_SCAN_THREAD_NAME = "AIM-input-scan-%d"; + private static int DEFAULT_JOB_PRIORITY = 0; + private static final String AUTO_INGEST_THREAD_NAME = "AIM-job-processing-%d"; + private static final String LOCAL_HOST_NAME = NetworkUtils.getLocalHostName(); + private static final String EVENT_CHANNEL_NAME = "Auto-Ingest-Manager-Events"; + private static final Set EVENT_LIST = new HashSet<>(Arrays.asList(new String[]{ + Event.JOB_STATUS_UPDATED.toString(), + Event.JOB_COMPLETED.toString(), + Event.CASE_PRIORITIZED.toString(), + Event.JOB_STARTED.toString()})); + private static final long JOB_STATUS_EVENT_INTERVAL_SECONDS = 10; + 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 java.util.logging.Logger SYS_LOGGER = AutoIngestSystemLogger.getLogger(); + private static AutoIngestManager instance; + private final AutopsyEventPublisher eventPublisher; + private final Object scanMonitor; + private final ScheduledThreadPoolExecutor inputScanSchedulingExecutor; + private final ExecutorService inputScanExecutor; + private final ExecutorService jobProcessingExecutor; + private final ScheduledThreadPoolExecutor jobStatusPublishingExecutor; + private final ConcurrentHashMap hostNamesToLastMsgTime; + private final ConcurrentHashMap hostNamesToRunningJobs; + private final Object jobsLock; + @GuardedBy("jobsLock") + private final Map> casesToManifests; + @GuardedBy("jobsLock") + private List pendingJobs; + @GuardedBy("jobsLock") + private AutoIngestJob currentJob; + @GuardedBy("jobsLock") + private List completedJobs; + private CoordinationService coordinationService; + private JobProcessingTask jobProcessingTask; + private Future jobProcessingTaskFuture; + private Path rootInputDirectory; + private Path rootOutputDirectory; + private volatile State state; + private volatile ErrorState errorState; + + /** + * 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 + * designated input directory tree. + * + * @return A singleton AutoIngestManager instance. + */ + synchronized static AutoIngestManager getInstance() { + if (instance == null) { + instance = new AutoIngestManager(); + } + return instance; + } + + /** + * Constructs an auto ingest manager responsible for processing auto ingest + * jobs defined by manifest files that can be added to any level of a + * designated input directory tree. + */ + private AutoIngestManager() { + SYS_LOGGER.log(Level.INFO, "Initializing auto ingest"); + state = State.IDLE; + eventPublisher = new AutopsyEventPublisher(); + scanMonitor = new Object(); + inputScanSchedulingExecutor = new ScheduledThreadPoolExecutor(NUM_INPUT_SCAN_SCHEDULING_THREADS, new ThreadFactoryBuilder().setNameFormat(INPUT_SCAN_SCHEDULER_THREAD_NAME).build()); + inputScanExecutor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat(INPUT_SCAN_THREAD_NAME).build()); + jobProcessingExecutor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat(AUTO_INGEST_THREAD_NAME).build()); + jobStatusPublishingExecutor = new ScheduledThreadPoolExecutor(1, new ThreadFactoryBuilder().setNameFormat(JOB_STATUS_PUBLISHING_THREAD_NAME).build()); + hostNamesToRunningJobs = new ConcurrentHashMap<>(); + hostNamesToLastMsgTime = new ConcurrentHashMap<>(); + jobsLock = new Object(); + casesToManifests = new HashMap<>(); + pendingJobs = new ArrayList<>(); + completedJobs = new ArrayList<>(); + } + + /** + * Starts up auto ingest. + * + * @throws AutoIngestManagerStartupException if there is a problem starting + * auto ingest. + */ + void startUp() throws AutoIngestManagerStartupException { + SYS_LOGGER.log(Level.INFO, "Auto ingest starting"); + try { + coordinationService = CoordinationService.getInstance(CoordinationServiceNamespace.getRoot()); + } catch (CoordinationServiceException ex) { + throw new AutoIngestManagerStartupException("Failed to get coordination service", ex); + } + try { + eventPublisher.openRemoteEventChannel(EVENT_CHANNEL_NAME); + SYS_LOGGER.log(Level.INFO, "Opened auto ingest event channel"); + } catch (AutopsyEventException ex) { + throw new AutoIngestManagerStartupException("Failed to open aut ingest event channel", ex); + } + rootInputDirectory = Paths.get(AutoIngestUserPreferences.getAutoModeImageFolder()); + rootOutputDirectory = Paths.get(AutoIngestUserPreferences.getAutoModeResultsFolder()); + inputScanSchedulingExecutor.scheduleAtFixedRate(new InputDirScanSchedulingTask(), 0, AutoIngestUserPreferences.getMinutesOfInputScanInterval(), TimeUnit.MINUTES); + jobProcessingTask = new JobProcessingTask(); + jobProcessingTaskFuture = jobProcessingExecutor.submit(jobProcessingTask); + jobStatusPublishingExecutor.scheduleAtFixedRate(new PeriodicJobStatusEventTask(), JOB_STATUS_EVENT_INTERVAL_SECONDS, JOB_STATUS_EVENT_INTERVAL_SECONDS, TimeUnit.SECONDS); + eventPublisher.addSubscriber(EVENT_LIST, instance); + RuntimeProperties.setCoreComponentsActive(false); + state = State.RUNNING; + errorState = ErrorState.NONE; + } + + /** + * Gets the state of the auto ingest manager: idle, running, shutting dowm. + * + * @return The state. + */ + State getState() { + return state; + } + + /** + * Gets the error state of the autop ingest manager. + * + * @return The error state, may be NONE. + */ + ErrorState getErrorState() { + return errorState; + } + + /** + * Handles auto ingest events published by other auto ingest nodes. + * + * @param event An auto ingest event from another node. + */ + @Override + public void propertyChange(PropertyChangeEvent event) { + if (event instanceof AutopsyEvent) { + if (((AutopsyEvent) event).getSourceType() == AutopsyEvent.SourceType.REMOTE) { + if (event instanceof AutoIngestJobStartedEvent) { + handleRemoteJobStartedEvent((AutoIngestJobStartedEvent) event); + } else if (event instanceof AutoIngestJobStatusEvent) { + handleRemoteJobStatusEvent((AutoIngestJobStatusEvent) event); + } else if (event instanceof AutoIngestJobCompletedEvent) { + handleRemoteJobCompletedEvent((AutoIngestJobCompletedEvent) event); + } else if (event instanceof AutoIngestCasePrioritizedEvent) { + handleRemoteCasePrioritizationEvent((AutoIngestCasePrioritizedEvent) event); + } else if (event instanceof AutoIngestCaseDeletedEvent) { + handleRemoteCaseDeletedEvent((AutoIngestCaseDeletedEvent) event); + } + } + } + } + + /** + * Processes a job started event from another node by removing the job from + * the pending queue, if it is present, and adding the job in the event to + * the collection of jobs running on other hosts. + *

+ * Note that the processing stage of the job will be whatever it was when + * the job was serialized for inclusion in the event message. + * + * @param event A job started from another auto ingest node. + */ + private void handleRemoteJobStartedEvent(AutoIngestJobStartedEvent event) { + String hostName = event.getJob().getNodeName(); + hostNamesToLastMsgTime.put(hostName, Instant.now()); + synchronized (jobsLock) { + Path manifestFilePath = event.getJob().getManifest().getFilePath(); + for (Iterator iterator = pendingJobs.iterator(); iterator.hasNext();) { + AutoIngestJob pendingJob = iterator.next(); + if (pendingJob.getManifest().getFilePath().equals(manifestFilePath)) { + iterator.remove(); + break; + } + } + } + hostNamesToRunningJobs.put(event.getJob().getNodeName(), event.getJob()); + setChanged(); + notifyObservers(Event.JOB_STARTED); + } + + /** + * Processes a job status event from another node by adding the job in the + * event to the collection of jobs running on other hosts. + *

+ * Note that the processing stage of the job will be whatever it was when + * the job was serialized for inclusion in the event message. + * + * @param event An job status event from another auto ingest node. + */ + private void handleRemoteJobStatusEvent(AutoIngestJobStatusEvent event) { + String hostName = event.getJob().getNodeName(); + hostNamesToLastMsgTime.put(hostName, Instant.now()); + hostNamesToRunningJobs.put(hostName, event.getJob()); + setChanged(); + notifyObservers(Event.JOB_STATUS_UPDATED); + } + + /** + * Processes a job completed event from another node by removing the job in + * the event from the collection of jobs running on other hosts and adding + * it to the list of completed jobs. + *

+ * Note that the processing stage of the job will be whatever it was when + * the job was serialized for inclusion in the event message. + * + * @param event An job completed event from another auto ingest node. + */ + private void handleRemoteJobCompletedEvent(AutoIngestJobCompletedEvent event) { + String hostName = event.getJob().getNodeName(); + hostNamesToLastMsgTime.put(hostName, Instant.now()); + hostNamesToRunningJobs.remove(hostName); + if (event.shouldRetry() == false) { + synchronized (jobsLock) { + completedJobs.add(event.getJob()); + } + } + //scanInputDirsNow(); + setChanged(); + notifyObservers(Event.JOB_COMPLETED); + } + + /** + * Processes a job/case prioritization event from another node by triggering + * an immediate input directory scan. + * + * @param event A prioritization event from another auto ingest node. + */ + private void handleRemoteCasePrioritizationEvent(AutoIngestCasePrioritizedEvent event) { + String hostName = event.getNodeName(); + hostNamesToLastMsgTime.put(hostName, Instant.now()); + scanInputDirsNow(); + setChanged(); + notifyObservers(Event.CASE_PRIORITIZED); + } + + /** + * Processes a case deletin event from another node by triggering an + * immediate input directory scan. + * + * @param event A case deleted event from another auto ingest node. + */ + private void handleRemoteCaseDeletedEvent(AutoIngestCaseDeletedEvent event) { + String hostName = event.getNodeName(); + hostNamesToLastMsgTime.put(hostName, Instant.now()); + scanInputDirsNow(); + setChanged(); + notifyObservers(Event.CASE_DELETED); + } + + /** + * Shuts down auto ingest. + */ + void shutDown() { + if (State.RUNNING != state) { + return; + } + SYS_LOGGER.log(Level.INFO, "Auto ingest shutting down"); + state = State.SHUTTING_DOWN; + try { + eventPublisher.removeSubscriber(EVENT_LIST, instance); + stopInputFolderScans(); + stopJobProcessing(); + eventPublisher.closeRemoteEventChannel(); + cleanupJobs(); + + } catch (InterruptedException ex) { + SYS_LOGGER.log(Level.SEVERE, "Auto ingest interrupted during shut down", ex); + } + SYS_LOGGER.log(Level.INFO, "Auto ingest shut down"); + state = State.IDLE; + } + + /** + * Cancels any input scan scheduling tasks and input scan tasks and shuts + * down their executors. + */ + private void stopInputFolderScans() throws InterruptedException { + inputScanSchedulingExecutor.shutdownNow(); + inputScanExecutor.shutdownNow(); + while (!inputScanSchedulingExecutor.awaitTermination(30, TimeUnit.SECONDS)) { + SYS_LOGGER.log(Level.WARNING, "Auto ingest waited at least thirty seconds for input scan scheduling executor to shut down, continuing to wait"); //NON-NLS + } + while (!inputScanExecutor.awaitTermination(30, TimeUnit.SECONDS)) { + SYS_LOGGER.log(Level.WARNING, "Auto ingest waited at least thirty seconds for input scan executor to shut down, continuing to wait"); //NON-NLS + } + } + + /** + * Cancels the job processing task and shuts down its executor. + */ + private void stopJobProcessing() throws InterruptedException { + synchronized (jobsLock) { + if (null != currentJob) { + cancelCurrentJob(); + } + jobProcessingTaskFuture.cancel(true); + jobProcessingExecutor.shutdown(); + } + while (!jobProcessingExecutor.awaitTermination(30, TimeUnit.SECONDS)) { + SYS_LOGGER.log(Level.WARNING, "Auto ingest waited at least thirty seconds for job processing executor to shut down, continuing to wait"); //NON-NLS + } + } + + /** + * Clears the job lists and resets the current job to null. + */ + private void cleanupJobs() { + synchronized (jobsLock) { + pendingJobs.clear(); + currentJob = null; + completedJobs.clear(); + } + } + + /** + * Gets snapshots of the pending jobs queue, running jobs list, and + * completed jobs list. Any of these collection can be excluded by passing a + * null for the correspioding in/out list parameter. + * + * @param pendingJobs A list to be populated with pending jobs, can be + * null. + * @param runningJobs A list to be populated with running jobs, can be + * null. + * @param completedJobs A list to be populated with competed jobs, can be + * null. + */ + void getJobs(List pendingJobs, List runningJobs, List completedJobs) { + synchronized (jobsLock) { + if (null != pendingJobs) { + pendingJobs.clear(); + pendingJobs.addAll(this.pendingJobs); + } + if (null != runningJobs) { + runningJobs.clear(); + if (null != currentJob) { + runningJobs.add(currentJob); + } + for (AutoIngestJob job : hostNamesToRunningJobs.values()) { + runningJobs.add(job); + runningJobs.sort(new AutoIngestJob.AlphabeticalComparator()); // RJCTODO: This sort should be done in the AID + } + } + if (null != completedJobs) { + completedJobs.clear(); + completedJobs.addAll(this.completedJobs); + } + } + } + + /** + * Triggers an immediate scan of the input directories. + */ + void scanInputDirsNow() { + if (State.RUNNING != state) { + return; + } + inputScanExecutor.submit(new InputDirScanTask()); + } + + /** + * Start a scan of the input directories and wait for scan to complete. + */ + void scanInputDirsAndWait(){ + if (State.RUNNING != state) { + return; + } + SYS_LOGGER.log(Level.INFO, "Starting input scan of {0}", rootInputDirectory); + InputDirScanner scanner = new InputDirScanner(); + scanner.scan(); + SYS_LOGGER.log(Level.INFO, "Completed input scan of {0}", rootInputDirectory); + } + + /** + * Pauses processing of the pending jobs queue. The currently running job + * will continue to run to completion. + */ + void pause() { + if (State.RUNNING != state) { + return; + } + jobProcessingTask.requestPause(); + } + + /** + * Resumes processing of the pending jobs queue. + */ + void resume() { + if (State.RUNNING != state) { + return; + } + jobProcessingTask.requestResume(); + } + + /** + * Bumps the priority of all pending ingest jobs for a specified case. + * + * @param caseName The name of the case to be prioritized. + */ + void prioritizeCase(final String caseName) { + + if (state != State.RUNNING) { + return; + } + + List prioritizedJobs = new ArrayList<>(); + int maxPriority = 0; + synchronized (jobsLock) { + for (AutoIngestJob job : pendingJobs) { + if (job.getPriority() > maxPriority) { + maxPriority = job.getPriority(); + } + if (job.getManifest().getCaseName().equals(caseName)) { + prioritizedJobs.add(job); + } + } + if (!prioritizedJobs.isEmpty()) { + ++maxPriority; + for (AutoIngestJob job : prioritizedJobs) { + String manifestNodePath = job.getManifest().getFilePath().toString(); + try { + ManifestNodeData nodeData = new ManifestNodeData(coordinationService.getNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestNodePath)); + nodeData.setPriority(maxPriority); + coordinationService.setNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestNodePath, nodeData.toArray()); + } catch (CoordinationServiceException ex) { + SYS_LOGGER.log(Level.SEVERE, String.format("Coordination service error while prioritizing %s", manifestNodePath), ex); + } catch (InterruptedException ex) { + SYS_LOGGER.log(Level.SEVERE, "Unexpected interrupt while updating coordination service node data for {0}", manifestNodePath); + } + job.setPriority(maxPriority); + } + } + + Collections.sort(pendingJobs, new AutoIngestJob.PriorityComparator()); + } + + if (!prioritizedJobs.isEmpty()) { + new Thread(() -> { + eventPublisher.publishRemotely(new AutoIngestCasePrioritizedEvent(LOCAL_HOST_NAME, caseName)); + }).start(); + } + } + + /** + * Bumps the priority of an auto ingest job. + * + * @param manifestPath The manifest file path for the job to be prioritized. + */ + void prioritizeJob(Path manifestPath) { + if (state != State.RUNNING) { + return; + } + + int maxPriority = 0; + AutoIngestJob prioritizedJob = null; + synchronized (jobsLock) { + for (AutoIngestJob job : pendingJobs) { + if (job.getPriority() > maxPriority) { + maxPriority = job.getPriority(); + } + if (job.getManifest().getFilePath().equals(manifestPath)) { + prioritizedJob = job; + } + } + if (null != prioritizedJob) { + ++maxPriority; + String manifestNodePath = prioritizedJob.getManifest().getFilePath().toString(); + try { + ManifestNodeData nodeData = new ManifestNodeData(coordinationService.getNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestNodePath)); + nodeData.setPriority(maxPriority); + coordinationService.setNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestNodePath, nodeData.toArray()); + } catch (CoordinationServiceException ex) { + SYS_LOGGER.log(Level.SEVERE, String.format("Coordination service error while prioritizing %s", manifestNodePath), ex); + } catch (InterruptedException ex) { + SYS_LOGGER.log(Level.SEVERE, "Unexpected interrupt while updating coordination service node data for {0}", manifestNodePath); + } + prioritizedJob.setPriority(maxPriority); + } + + Collections.sort(pendingJobs, new AutoIngestJob.PriorityComparator()); + } + + if (null != prioritizedJob) { + final String caseName = prioritizedJob.getManifest().getCaseName(); + new Thread(() -> { + eventPublisher.publishRemotely(new AutoIngestCasePrioritizedEvent(LOCAL_HOST_NAME, caseName)); + }).start(); + } + } + + /** + * Reprocesses a completed auto ingest job. + * + * @param manifestPath The manifiest file path for the completed job. + * + */ + void reprocessJob(Path manifestPath) { + AutoIngestJob completedJob = null; + synchronized (jobsLock) { + for (Iterator iterator = completedJobs.iterator(); iterator.hasNext();) { + AutoIngestJob job = iterator.next(); + if (job.getManifest().getFilePath().equals(manifestPath)) { + completedJob = job; + iterator.remove(); + break; + } + } + + if (null != completedJob && null != completedJob.getCaseDirectoryPath()) { + try { + ManifestNodeData nodeData = new ManifestNodeData(PENDING, DEFAULT_JOB_PRIORITY, 0, new Date(0), true); + coordinationService.setNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestPath.toString(), nodeData.toArray()); + pendingJobs.add(new AutoIngestJob(completedJob.getManifest(), completedJob.getCaseDirectoryPath(), DEFAULT_JOB_PRIORITY, LOCAL_HOST_NAME, AutoIngestJob.Stage.PENDING, new Date(0), true)); + } catch (CoordinationServiceException ex) { + SYS_LOGGER.log(Level.SEVERE, String.format("Coordination service error while reprocessing %s", manifestPath), ex); + completedJobs.add(completedJob); + } catch (InterruptedException ex) { + SYS_LOGGER.log(Level.SEVERE, "Unexpected interrupt while updating coordination service node data for {0}", manifestPath); + completedJobs.add(completedJob); + } + } + + Collections.sort(pendingJobs, new AutoIngestJob.PriorityComparator()); + } + } + + /** + * 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; + } + + /* + * Acquire an exclusive lock on the case so it can be safely deleted. + * This will fail if the case is open for review or a deletion operation + * on this case is already in progress on another node. + */ + CaseDeletionResult result = CaseDeletionResult.FULLY_DELETED; + List manifestFileLocks = new ArrayList<>(); + try (Lock caseLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.CASES, caseDirectoryPath.toString())) { + if (null == caseLock) { + return CaseDeletionResult.FAILED; + } + synchronized (jobsLock) { + /* + * Do a fresh input directory scan. + */ + InputDirScanner scanner = new InputDirScanner(); + scanner.scan(); + Set manifestPaths = casesToManifests.get(caseName); + if (null == manifestPaths) { + SYS_LOGGER.log(Level.SEVERE, "No manifest paths found for case {0}", caseName); + return CaseDeletionResult.FAILED; + } + + /* + * Get all of the required manifest locks. + */ + 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) { + SYS_LOGGER.log(Level.SEVERE, String.format("Error attempting to acquire manifest lock for %s for case %s", manifestPath, caseName), ex); + return CaseDeletionResult.FAILED; + } + } + + /* + * Get the case metadata. + */ + CaseMetadata metaData; + Path caseMetaDataFilePath = Paths.get(caseDirectoryPath.toString(), caseName + CaseMetadata.getFileExtension()); + try { + metaData = new CaseMetadata(caseMetaDataFilePath); + } catch (CaseMetadata.CaseMetadataException ex) { + SYS_LOGGER.log(Level.SEVERE, String.format("Failed to delete case metadata file %s for case %s", caseMetaDataFilePath, caseName)); + return CaseDeletionResult.FAILED; + } + + /* + * Mark each job (manifest file) as deleted + */ + for (Path manifestPath : manifestPaths) { + try { + ManifestNodeData nodeData = new ManifestNodeData(coordinationService.getNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestPath.toString())); + nodeData.setStatus(ManifestNodeData.ProcessingStatus.DELETED); + coordinationService.setNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestPath.toString(), nodeData.toArray()); + } catch (InterruptedException | CoordinationServiceException ex) { + SYS_LOGGER.log(Level.SEVERE, String.format("Error attempting to set delete flag on manifest data for %s for case %s", manifestPath, caseName), ex); + return CaseDeletionResult.PARTIALLY_DELETED; + } + } + + /* + * Try to unload/delete the Solr core from the Solr server. Do + * this before deleting the case directory because the index + * files are in the case directory and the deletion will fail if + * the core is not unloaded first. + */ + String textIndexName = metaData.getTextIndexName(); + try { + unloadSolrCore(metaData.getTextIndexName()); + } catch (Exception ex) { + /* + * Could be a problem, or it could be that the core was + * already unloaded (e.g., by the server due to resource + * constraints). + */ + SYS_LOGGER.log(Level.WARNING, String.format("Error deleting text index %s for %s", textIndexName, caseName), ex); //NON-NLS + } + + /* + * Delete the case database from the database server. + */ + String caseDatabaseName = metaData.getCaseDatabaseName(); + try { + deleteCaseDatabase(caseDatabaseName); + } catch (SQLException ex) { + SYS_LOGGER.log(Level.SEVERE, String.format("Unable to delete case database %s for %s", caseDatabaseName, caseName), ex); //NON-NLS + result = CaseDeletionResult.PARTIALLY_DELETED; + } catch (UserPreferencesException ex) { + SYS_LOGGER.log(Level.SEVERE, String.format("Error accessing case database connection info, unable to delete case database %s for %s", caseDatabaseName, caseName), ex); //NON-NLS + result = CaseDeletionResult.PARTIALLY_DELETED; + } catch (ClassNotFoundException ex) { + SYS_LOGGER.log(Level.SEVERE, String.format("Cannot load database driver, unable to delete case database %s for %s", caseDatabaseName, caseName), ex); //NON-NLS + result = CaseDeletionResult.PARTIALLY_DELETED; + } + + /* + * Delete the case directory. + */ + File caseDirectory = caseDirectoryPath.toFile(); + FileUtil.deleteDir(caseDirectory); + if (caseDirectory.exists()) { + SYS_LOGGER.log(Level.SEVERE, String.format("Failed to delete case directory %s for case %s", caseDirectoryPath, caseName)); + 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)); + setChanged(); + notifyObservers(Event.CASE_DELETED); + return result; + + } catch (CoordinationServiceException ex) { + SYS_LOGGER.log(Level.SEVERE, String.format("Error acquiring coordination service lock on case %s", caseName), ex); + return CaseDeletionResult.FAILED; + + } finally { + for (Lock lock : manifestFileLocks) { + try { + lock.release(); + } catch (CoordinationServiceException ex) { + SYS_LOGGER.log(Level.SEVERE, String.format("Failed to release manifest file lock when deleting case %s", caseName), ex); + } + } + } + } + + /** + * Get the current snapshot of the job lists. + * @return Snapshot of jobs lists + */ + JobsSnapshot getCurrentJobsSnapshot(){ + synchronized(jobsLock){ + List runningJobs = new ArrayList<>(); + getJobs(null, runningJobs, null); + return new JobsSnapshot(pendingJobs, runningJobs, completedJobs); + } + } + + /** + * Tries to unload the Solr core for a case. + * + * @param caseName The case name. + * @param coreName The name of the core to unload. + * + * @throws Exception if there is a problem unloading the core or it has + * already been unloaded (e.g., by the server due to + * resource constraints), or there is a problem deleting + * files associated with the core + */ + private void unloadSolrCore(String coreName) throws Exception { + /* + * Send a core unload request to the Solr server, with the parameters + * that request deleting the index and the instance directory + * (deleteInstanceDir removes everything related to the core, the index + * directory, the configuration files, etc.) set to true. + */ + String url = "http://" + UserPreferences.getIndexingServerHost() + ":" + UserPreferences.getIndexingServerPort() + "/solr"; + HttpSolrServer solrServer = new HttpSolrServer(url); + org.apache.solr.client.solrj.request.CoreAdminRequest.unloadCore(coreName, true, true, solrServer); + } + + /** + * Tries to delete the case database for a case. + * + * @param caseFolderPath The case name. + * @param caseDatbaseName The case database name. + */ + private void deleteCaseDatabase(String caseDatbaseName) throws UserPreferencesException, ClassNotFoundException, SQLException { + CaseDbConnectionInfo 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 \"" + caseDatbaseName + "\""; //NON-NLS + statement.execute(deleteCommand); + } + } + + /** + * Removes a set of auto ingest jobs from a collection of jobs. + * + * @param manifestPaths The manifest file paths for the jobs. + * @param jobs The collection of jobs. + */ + private void removeJobs(Set manifestPaths, List jobs) { + for (Iterator iterator = jobs.iterator(); iterator.hasNext();) { + AutoIngestJob job = iterator.next(); + Path manifestPath = job.getManifest().getFilePath(); + if (manifestPaths.contains(manifestPath)) { + iterator.remove(); + } + } + } + + /** + * Starts the process of cancelling the current job. + * + * Note that the current job is included in the running list for a while + * because it can take some time + * for the automated ingest process for the job to be shut down in + * an orderly fashion. + */ + void cancelCurrentJob() { + if (State.RUNNING != state) { + return; + } + synchronized (jobsLock) { + if (null != currentJob) { + currentJob.cancel(); + SYS_LOGGER.log(Level.INFO, "Cancelling automated ingest for manifest {0}", currentJob.getManifest().getFilePath()); + } + } + } + + /** + * Cancels the currently running data-source-level ingest module for the + * current job. + */ + void cancelCurrentDataSourceLevelIngestModule() { + if (State.RUNNING != state) { + return; + } + synchronized (jobsLock) { + if (null != currentJob) { + IngestJob ingestJob = currentJob.getIngestJob(); + if (null != ingestJob) { + IngestJob.DataSourceIngestModuleHandle moduleHandle = ingestJob.getSnapshot().runningDataSourceIngestModule(); + if (null != moduleHandle) { + currentJob.setStage(AutoIngestJob.Stage.CANCELLING_MODULE); + moduleHandle.cancel(); + SYS_LOGGER.log(Level.INFO, "Cancelling {0} module for manifest {1}", new Object[]{moduleHandle.displayName(), currentJob.getManifest().getFilePath()}); + } + } + } + } + } + + /** + * A task that submits an input directory scan task to the input directory + * scan task executor. + */ + private final class InputDirScanSchedulingTask implements Runnable { + + /** + * Constructs a task that submits an input directory scan task to the + * input directory scan task executor. + */ + private InputDirScanSchedulingTask() { + SYS_LOGGER.log(Level.INFO, "Periodic input scan scheduling task started"); + } + + /** + * Submits an input directory scan task to the input directory scan task + * executor. + */ + @Override + public void run() { + scanInputDirsNow(); + } + } + + /** + * A task that scans the input directory tree and refreshes the pending jobs + * queue and the completed jobs list. Crashed job recovery is perfomed as + * needed. + */ + private final class InputDirScanTask implements Callable { + + /** + * Scans the input directory tree and refreshes the pending jobs queue + * and the completed jobs list. Crashed job recovery is performed as + * needed. + */ + @Override + public Void call() throws Exception { + if (Thread.currentThread().isInterrupted()) { + return null; + } + SYS_LOGGER.log(Level.INFO, "Starting input scan of {0}", rootInputDirectory); + InputDirScanner scanner = new InputDirScanner(); + scanner.scan(); + SYS_LOGGER.log(Level.INFO, "Completed input scan of {0}", rootInputDirectory); + setChanged(); + notifyObservers(Event.INPUT_SCAN_COMPLETED); + return null; + } + + } + + /** + * A FileVisitor that searches the input directories for manifest files. The + * search results are used to refresh the pending jobs queue and the + * completed jobs list. Crashed job recovery is perfomed as needed. + */ + private final class InputDirScanner implements FileVisitor { + + private final List newPendingJobsList = new ArrayList<>(); + private final List newCompletedJobsList = new ArrayList<>(); + + /** + * Searches the input directories for manifest files. The search results + * are used to refresh the pending jobs queue and the completed jobs + * list. + */ + private void scan() { + synchronized (jobsLock) { + if (Thread.currentThread().isInterrupted()) { + return; + } + try { + newPendingJobsList.clear(); + newCompletedJobsList.clear(); + Files.walkFileTree(rootInputDirectory, EnumSet.of(FOLLOW_LINKS), Integer.MAX_VALUE, this); + Collections.sort(newPendingJobsList, new AutoIngestJob.PriorityComparator()); + AutoIngestManager.this.pendingJobs = newPendingJobsList; + AutoIngestManager.this.completedJobs = newCompletedJobsList; + + } catch (IOException ex) { + SYS_LOGGER.log(Level.SEVERE, String.format("Error scanning the input directory %s", rootInputDirectory), ex); + } + } + synchronized (scanMonitor) { + scanMonitor.notify(); + } + } + + /** + * Invoked for an input directory before entries 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 dirAttrs The basic file attributes of the directory about to + * be visited. + * + * @return TERMINATE if the task thread has been interrupted, CONTINUE + * if it has not. + * + * @throws IOException if an I/O error occurs, but this implementation + * does not throw. + */ + @Override + public FileVisitResult preVisitDirectory(Path dirPath, BasicFileAttributes dirAttrs) throws IOException { + if (Thread.currentThread().isInterrupted()) { + return TERMINATE; + } + return CONTINUE; + } + + /** + * 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. + * + * @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. + * + * @throws IOException if an I/O error occurs, but this implementation + * does not throw. + */ + @Override + public FileVisitResult visitFile(Path filePath, BasicFileAttributes attrs) throws IOException { + if (Thread.currentThread().isInterrupted()) { + return TERMINATE; + } + + Manifest manifest = null; + for (ManifestFileParser parser : Lookup.getDefault().lookupAll(ManifestFileParser.class)) { + if (parser.fileIsManifest(filePath)) { + try { + manifest = parser.parse(filePath); + break; + } catch (ManifestFileParserException ex) { + SYS_LOGGER.log(Level.SEVERE, String.format("Error attempting to parse %s with parser %s", filePath, parser.getClass().getCanonicalName()), ex); + } + } + if (Thread.currentThread().isInterrupted()) { + return TERMINATE; + } + } + + if (Thread.currentThread().isInterrupted()) { + 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) { + ManifestNodeData nodeData = new ManifestNodeData(rawData); + if (nodeData.coordSvcNodeDataWasSet()) { + ProcessingStatus processingStatus = nodeData.getStatus(); + switch (processingStatus) { + case PENDING: + addPendingJob(manifest, nodeData); + break; + case PROCESSING: + doRecoveryIfCrashed(manifest); + break; + case COMPLETED: + addCompletedJob(manifest, nodeData); + break; + case DELETED: + // Do nothing - we dont'want to add it to any job list or do recovery + break; + default: + SYS_LOGGER.log(Level.SEVERE, "Unknown ManifestNodeData.ProcessingStatus"); + break; + } + } else { + addNewPendingJob(manifest); + } + } else { + addNewPendingJob(manifest); + } + } catch (CoordinationServiceException ex) { + SYS_LOGGER.log(Level.SEVERE, String.format("Error getting node data for %s", manifestPath), ex); + return CONTINUE; + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + return TERMINATE; + } + } + + if (!Thread.currentThread().isInterrupted()) { + return CONTINUE; + } else { + return TERMINATE; + } + } + + /** + * Adds a job to process a manifest to the pending jobs queue. + * + * @param manifest The manifest. + * @param nodeData The data stored in the coordination service node for + * the manifest. + */ + private void addPendingJob(Manifest manifest, ManifestNodeData nodeData) { + Path caseDirectory = PathUtils.findCaseDirectory(rootOutputDirectory, manifest.getCaseName()); + newPendingJobsList.add(new AutoIngestJob(manifest, caseDirectory, nodeData.getPriority(), LOCAL_HOST_NAME, AutoIngestJob.Stage.PENDING, new Date(0), false)); + } + + /** + * Adds a job to process a manifest to the pending jobs queue. + * + * @param manifest The manifest. + * + * @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 { + // TODO (JIRA-1960): This is something of a hack, grabbing the lock to create the node. + // Is use of Curator.create().forPath() possible instead? + try (Lock manifestLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.MANIFESTS, manifest.getFilePath().toString())) { + if (null != manifestLock) { + ManifestNodeData newNodeData = new ManifestNodeData(PENDING, DEFAULT_JOB_PRIORITY, 0, new Date(0), false); + coordinationService.setNodeData(CoordinationService.CategoryNode.MANIFESTS, manifest.getFilePath().toString(), newNodeData.toArray()); + newPendingJobsList.add(new AutoIngestJob(manifest, null, DEFAULT_JOB_PRIORITY, LOCAL_HOST_NAME, AutoIngestJob.Stage.PENDING, new Date(0), false)); + } + } catch (CoordinationServiceException ex) { + SYS_LOGGER.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. + * + * @param manifest + * + * @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) throws InterruptedException { + String manifestPath = manifest.getFilePath().toString(); + try { + Lock manifestLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.MANIFESTS, manifestPath); + if (null != manifestLock) { + try { + ManifestNodeData nodeData = new ManifestNodeData(coordinationService.getNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestPath)); + if (nodeData.coordSvcNodeDataWasSet() && ProcessingStatus.PROCESSING == nodeData.getStatus()) { + SYS_LOGGER.log(Level.SEVERE, "Attempting crash recovery for {0}", manifestPath); + int numberOfCrashes = nodeData.getNumberOfCrashes(); + ++numberOfCrashes; + nodeData.setNumberOfCrashes(numberOfCrashes); + if (numberOfCrashes <= AutoIngestUserPreferences.getMaxNumTimesToProcessImage()) { + nodeData.setStatus(PENDING); + Path caseDirectoryPath = PathUtils.findCaseDirectory(rootOutputDirectory, manifest.getCaseName()); + newPendingJobsList.add(new AutoIngestJob(manifest, caseDirectoryPath, nodeData.getPriority(), LOCAL_HOST_NAME, AutoIngestJob.Stage.PENDING, new Date(0), true)); + if (null != caseDirectoryPath) { + try { + AutoIngestAlertFile.create(caseDirectoryPath); + } catch (AutoIngestAlertFileException ex) { + SYS_LOGGER.log(Level.SEVERE, String.format("Error creating alert file for crashed job for %s", manifestPath), ex); + } + try { + new AutoIngestJobLogger(manifest.getFilePath(), manifest.getDataSourceFileName(), caseDirectoryPath).logCrashRecoveryWithRetry(); + } catch (AutoIngestJobLoggerException ex) { + SYS_LOGGER.log(Level.SEVERE, String.format("Error creating case auto ingest log entry for crashed job for %s", manifestPath), ex); + } + } + } else { + nodeData.setStatus(COMPLETED); + Path caseDirectoryPath = PathUtils.findCaseDirectory(rootOutputDirectory, manifest.getCaseName()); + newCompletedJobsList.add(new AutoIngestJob(manifest, caseDirectoryPath, nodeData.getPriority(), LOCAL_HOST_NAME, AutoIngestJob.Stage.COMPLETED, new Date(), true)); + if (null != caseDirectoryPath) { + try { + AutoIngestAlertFile.create(caseDirectoryPath); + } catch (AutoIngestAlertFileException ex) { + SYS_LOGGER.log(Level.SEVERE, String.format("Error creating alert file for crashed job for %s", manifestPath), ex); + } + try { + new AutoIngestJobLogger(manifest.getFilePath(), manifest.getDataSourceFileName(), caseDirectoryPath).logCrashRecoveryNoRetry(); + } catch (AutoIngestJobLoggerException ex) { + SYS_LOGGER.log(Level.SEVERE, String.format("Error creating case auto ingest log entry for crashed job for %s", manifestPath), ex); + } + } + } + try { + coordinationService.setNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestPath, nodeData.toArray()); + } catch (CoordinationServiceException ex) { + SYS_LOGGER.log(Level.SEVERE, String.format("Error attempting to set node data for %s", manifestPath), ex); + } + } + } catch (CoordinationServiceException ex) { + SYS_LOGGER.log(Level.SEVERE, String.format("Error attempting to get node data for %s", manifestPath), ex); + } finally { + try { + manifestLock.release(); + } catch (CoordinationServiceException ex) { + SYS_LOGGER.log(Level.SEVERE, String.format("Error attempting to release exclusive lock for %s", manifestPath), ex); + } + } + } + } catch (CoordinationServiceException ex) { + SYS_LOGGER.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 manifest The manifest. + * @param nodeData The data stored in the coordination service node for + * the manifest. + */ + private void addCompletedJob(Manifest manifest, ManifestNodeData nodeData) { + Path caseDirectoryPath = PathUtils.findCaseDirectory(rootOutputDirectory, manifest.getCaseName()); + if (null != caseDirectoryPath) { + newCompletedJobsList.add(new AutoIngestJob(manifest, caseDirectoryPath, nodeData.getPriority(), LOCAL_HOST_NAME, AutoIngestJob.Stage.COMPLETED, nodeData.getCompletedDate(), nodeData.getErrorsOccurred())); + } else { + SYS_LOGGER.log(Level.WARNING, String.format("Job completed for %s, but cannot find case directory, ignoring job", manifest.getFilePath())); + } + } + + /** + * Invoked for a file that could not be visited because an I/O exception + * was thrown when visiting a file. Logs the exceptino and checks if the + * task thread has been interrupted because auto ingest is shutting down + * and terminates the scan if that is the case. + * + * @param file The file. + * @param ex The exception. + * + * @return TERMINATE if auto ingest is shutting down, CONTINUE if it has + * not. + * + * @throws IOException if an I/O error occurs, but this implementation + * does not throw. + */ + @Override + public FileVisitResult visitFileFailed(Path file, IOException ex) throws IOException { + SYS_LOGGER.log(Level.SEVERE, String.format("Error while visiting %s during input directories scan", file.toString()), ex); + if (Thread.currentThread().isInterrupted()) { + return TERMINATE; + } + return CONTINUE; + } + + /** + * Invoked for an input directory after entries 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 dirAttrs The basic file attributes of the directory about to + * be visited. + * + * @return TERMINATE if the task thread has been interrupted, CONTINUE + * if it has not. + * + * @throws IOException if an I/O error occurs, but this implementation + * does not throw. + */ + @Override + public FileVisitResult postVisitDirectory(Path dirPath, IOException unused) throws IOException { + if (Thread.currentThread().isInterrupted()) { + return TERMINATE; + } + return CONTINUE; + } + + } + + /** + * A single instance of this job processing task is used by the auto ingest + * manager to process auto ingest jobs. The task does a blocking take from a + * completion service for the input directory scan tasks that refresh the + * pending jobs queue. + *

+ * The job processing task can be paused between jobs (it waits on the + * monitor of its pause lock object) and resumed (by notifying the monitor + * of its pause lock object). This supports doing things that should be done + * between jobs: orderly shutdown of auto ingest and changes to the ingest + * configuration (settings). Note that the ingest configuration may be + * specific to the host machine or shared between multiple nodes, in which + * case it is downloaded from a specified location before each job. + *

+ * The task pauses itself if system errors occur, e.g., problems with the + * coordination service, database server, Solr server, etc. The idea behind + * this is to avoid attempts to process jobs when the auto ingest system is + * not in a state to produce consistent and correct results. It is up to a + * system administrator to examine the auto ingest system logs, etc., to + * find a remedy for the problem and then resume the task. + *

+ * Note that the task also waits on the monitor of its ingest lock object + * both when the data source processor and the analysis modules are running + * in other threads. Notifies are done via a data source processor callback + * and an ingest job event handler, respectively. + */ + private final class JobProcessingTask implements Runnable { + + private static final String AUTO_INGEST_MODULE_OUTPUT_DIR = "AutoIngest"; + private final Object ingestLock; + private final Object pauseLock; + @GuardedBy("pauseLock") + private boolean pauseRequested; + @GuardedBy("pauseLock") + private boolean waitingForInputScan; + + /** + * Constructs a job processing task used by the auto ingest manager to + * process auto ingest jobs. + */ + private JobProcessingTask() { + ingestLock = new Object(); + pauseLock = new Object(); + errorState = ErrorState.NONE; + } + + /** + * Processes auto ingest jobs, blocking on a completion service for + * input directory scan tasks and waiting on a pause lock object when + * paused by a client or because of a system error. The task is also in + * a wait state when the data source processor or the analysis modules + * for a job are running. + */ + @Override + public void run() { + SYS_LOGGER.log(Level.INFO, "Job processing task started"); + while (true) { + try { + if (jobProcessingTaskFuture.isCancelled()) { + break; + } + waitForInputDirScan(); + if (jobProcessingTaskFuture.isCancelled()) { + break; + } + try { + processJobs(); + } catch (Exception ex) { // Exception firewall + if (jobProcessingTaskFuture.isCancelled()) { + break; + } + if (ex instanceof CoordinationServiceException) { + errorState = ErrorState.COORDINATION_SERVICE_ERROR; + } else if (ex instanceof SharedConfigurationException) { + errorState = ErrorState.SHARED_CONFIGURATION_DOWNLOAD_ERROR; + } else if (ex instanceof ServicesMonitorException) { + errorState = ErrorState.SERVICES_MONITOR_COMMUNICATION_ERROR; + } else if (ex instanceof DatabaseServerDownException) { + errorState = ErrorState.DATABASE_SERVER_ERROR; + } else if (ex instanceof KeywordSearchServerDownException) { + errorState = ErrorState.KEYWORD_SEARCH_SERVER_ERROR; + } else if (ex instanceof CaseManagementException) { + errorState = ErrorState.CASE_MANAGEMENT_ERROR; + } else if (ex instanceof AnalysisStartupException) { + errorState = ErrorState.ANALYSIS_STARTUP_ERROR; + } else if (ex instanceof FileExportException) { + errorState = ErrorState.FILE_EXPORT_ERROR; + } else if (ex instanceof AutoIngestAlertFileException) { + errorState = ErrorState.ALERT_FILE_ERROR; + } else if (ex instanceof AutoIngestJobLoggerException) { + errorState = ErrorState.JOB_LOGGER_ERROR; + } else if (ex instanceof AutomatedIngestDataSourceProcessorException) { + errorState = ErrorState.DATA_SOURCE_PROCESSOR_ERROR; + } else if (ex instanceof InterruptedException) { + throw (InterruptedException) ex; + } else { + errorState = ErrorState.UNEXPECTED_EXCEPTION; + } + SYS_LOGGER.log(Level.SEVERE, "Auto ingest system error", ex); + pauseForSystemError(); + } + } catch (InterruptedException ex) { + break; + } + } + SYS_LOGGER.log(Level.INFO, "Job processing task stopped"); + } + + /** + * Makes a request to suspend job processing. The request will not be + * serviced immediately if the task is doing a job. + */ + private void requestPause() { + synchronized (pauseLock) { + SYS_LOGGER.log(Level.INFO, "Job processing pause requested"); + pauseRequested = true; + if (waitingForInputScan) { + /* + * If the flag is set, the job processing task is blocked + * waiting for an input directory scan to complete, so + * notify any observers that the task is paused. This works + * because as soon as the task stops waiting for a scan to + * complete, it checks the pause requested flag. If the flag + * is set, the task immediately waits on the pause lock + * object. + */ + setChanged(); + notifyObservers(Event.PAUSED_BY_REQUEST); + } + } + } + + /** + * Makes a request to resume job processing. + */ + private void requestResume() { + synchronized (pauseLock) { + SYS_LOGGER.log(Level.INFO, "Job processing resume requested"); + pauseRequested = false; + if (waitingForInputScan) { + /* + * If the flag is set, the job processing task is blocked + * waiting for an input directory scan to complete, but + * notify any observers that the task is resumed anyway. + * This works because as soon as the task stops waiting for + * a scan to complete, it checks the pause requested flag. + * If the flag is not set, the task immediately begins + * processing the pending jobs queue. + */ + setChanged(); + notifyObservers(Event.RESUMED); + } + pauseLock.notifyAll(); + } + } + + /** + * Checks for a request to suspend jobs processing. If there is one, + * blocks until resumed or interrupted. + * + * @throws InterruptedException if the thread running the job processing + * task is interrupted while blocked, i.e., + * if auto ingest is shutting down. + */ + private void pauseIfRequested() throws InterruptedException { + synchronized (pauseLock) { + if (pauseRequested) { + SYS_LOGGER.log(Level.INFO, "Job processing paused by request"); + pauseRequested = false; + setChanged(); + notifyObservers(Event.PAUSED_BY_REQUEST); + pauseLock.wait(); + SYS_LOGGER.log(Level.INFO, "Job processing resumed after pause request"); + setChanged(); + notifyObservers(Event.RESUMED); + } + } + } + + /** + * Pauses auto ingest to allow a sys admin to address a system error. + * + * @throws InterruptedException if the thread running the job processing + * task is interrupted while blocked, i.e., + * if auto ingest is shutting down. + */ + private void pauseForSystemError() throws InterruptedException { + synchronized (pauseLock) { + SYS_LOGGER.log(Level.SEVERE, "Job processing paused for system error"); + setChanged(); + notifyObservers(Event.PAUSED_FOR_SYSTEM_ERROR); + pauseLock.wait(); + errorState = ErrorState.NONE; + SYS_LOGGER.log(Level.INFO, "Job processing resumed after system error"); + setChanged(); + notifyObservers(Event.RESUMED); + } + } + + /** + * Waits until an input directory scan has completed, with pause request + * checks before and after the wait. + * + * @throws InterruptedException if the thread running the job processing + * task is interrupted while blocked, i.e., + * if auto ingest is shutting down. + */ + private void waitForInputDirScan() throws InterruptedException { + synchronized (pauseLock) { + pauseIfRequested(); + /* + * The waiting for scan flag is needed for the special case of a + * client making a pause request while this task is blocked on + * the input directory scan task completion service. Although, + * the task is unable to act on the request until the next scan + * completes, when it unblocks it will check the pause requested + * flag and promptly pause if the flag is set. Thus, setting the + * waiting for scan flag allows a pause request in a client + * thread to responsively notify any observers that processing + * is already effectively paused. + */ + waitingForInputScan = true; + } + SYS_LOGGER.log(Level.INFO, "Job processing waiting for input scan completion"); + synchronized (scanMonitor) { + scanMonitor.wait(); + } + SYS_LOGGER.log(Level.INFO, "Job processing finished wait for input scan completion"); + synchronized (pauseLock) { + waitingForInputScan = false; + pauseIfRequested(); + } + } + + /** + * Processes jobs until the pending jobs queue is empty. + * + * @throws CoordinationServiceException if there is an error + * acquiring or releasing + * coordination service locks + * or setting coordination + * service node data. + * @throws SharedConfigurationException if there is an error while + * downloading shared + * configuration. + * @throws ServicesMonitorException if there is an error + * querying the services + * monitor. + * @throws DatabaseServerDownException if the database server is + * down. + * @throws KeywordSearchServerDownException if the Solr server is down. + * @throws CaseManagementException if there is an error + * creating, opening or closing + * the case for the job. + * @throws AnalysisStartupException if there is an error + * starting analysis of the + * data source by the data + * source level and file level + * ingest modules. + * @throws FileExportException if there is an error + * exporting files. + * @throws AutoIngestAlertFileException if there is an error + * creating an alert file. + * @throws AutoIngestJobLoggerException if there is an error writing + * to the auto ingest log for + * the case. + * @throws InterruptedException if the thread running the + * job processing task is + * interrupted while blocked, + * i.e., if auto ingest is + * shutting down. + */ + private void processJobs() throws CoordinationServiceException, SharedConfigurationException, ServicesMonitorException, DatabaseServerDownException, KeywordSearchServerDownException, CaseManagementException, AnalysisStartupException, FileExportException, AutoIngestAlertFileException, AutoIngestJobLoggerException, InterruptedException, AutomatedIngestDataSourceProcessor.AutomatedIngestDataSourceProcessorException { + SYS_LOGGER.log(Level.INFO, "Started processing pending jobs queue"); + Lock manifestLock = JobProcessingTask.this.dequeueAndLockNextJob(); + while (null != manifestLock) { + try { + if (currentJob.isCancelled() || jobProcessingTaskFuture.isCancelled()) { + return; + } + processJob(); + } finally { + manifestLock.release(); + } + if (jobProcessingTaskFuture.isCancelled()) { + return; + } + pauseIfRequested(); + if (jobProcessingTaskFuture.isCancelled()) { + return; + } + manifestLock = JobProcessingTask.this.dequeueAndLockNextJob(); + } + } + + /** + * Inspects the pending jobs queue, looking for the next job that is + * ready for processing. If such a job is found, it is removed from the + * queue, made the current job, and a coordination service lock on the + * manifest for the job is returned. + *

+ * Note that two passes through the queue may be made, the first + * enforcing the maximum concurrent jobs per case setting, the second + * ignoring this constraint. This policy override prevents idling nodes + * when jobs are queued. + *

+ * Holding the manifest lock does the following: a) signals to all auto + * ingest nodes, including this one, that the job is in progress so it + * does not get put in pending jobs queues or completed jobs lists by + * input directory scans and b) prevents deletion of the input directory + * and the case directory because exclusive manifest locks for all of + * the manifests for a case must be acquired for delete operations. + * + * @return A manifest file lock if a ready job was found, null + * otherwise. + * + * @throws CoordinationServiceException if there is an error while + * acquiring or releasing a + * manifest file lock. + * @throws InterruptedException if the thread is interrupted while + * reading the lock data + */ + private Lock dequeueAndLockNextJob() throws CoordinationServiceException, InterruptedException { + SYS_LOGGER.log(Level.INFO, "Checking pending jobs queue for ready job, enforcing max jobs per case"); + Lock manifestLock; + synchronized (jobsLock) { + manifestLock = dequeueAndLockNextJob(true); + if (null != manifestLock) { + SYS_LOGGER.log(Level.INFO, "Dequeued job for {0}", currentJob.getManifest().getFilePath()); + } else { + SYS_LOGGER.log(Level.INFO, "No ready job"); + SYS_LOGGER.log(Level.INFO, "Checking pending jobs queue for ready job, not enforcing max jobs per case"); + manifestLock = dequeueAndLockNextJob(false); + if (null != manifestLock) { + SYS_LOGGER.log(Level.INFO, "Dequeued job for {0}", currentJob.getManifest().getFilePath()); + } else { + SYS_LOGGER.log(Level.INFO, "No ready job"); + } + } + } + return manifestLock; + } + + /** + * Inspects the pending jobs queue, looking for the next job that is + * ready for processing. If such a job is found, it is removed from the + * queue, made the current job, and a coordination service lock on the + * manifest for the job is returned. + * + * @param enforceMaxJobsPerCase Whether or not to enforce the maximum + * concurrent jobs per case setting. + * + * @return A manifest file lock if a ready job was found, null + * otherwise. + * + * @throws CoordinationServiceException if there is an error while + * acquiring or releasing a + * manifest file lock. + * @throws InterruptedException if the thread is interrupted while + * reading the lock data + */ + private Lock dequeueAndLockNextJob(boolean enforceMaxJobsPerCase) throws CoordinationServiceException, InterruptedException { + Lock manifestLock = null; + synchronized (jobsLock) { + Iterator iterator = pendingJobs.iterator(); + while (iterator.hasNext()) { + AutoIngestJob job = iterator.next(); + Path manifestPath = job.getManifest().getFilePath(); + manifestLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.MANIFESTS, manifestPath.toString()); + if (null == manifestLock) { + /* + * Skip the job. If it is exclusively locked for + * processing or deletion by another node, the remote + * job event handlers or the next input scan will flush + * it out of the pending queue. + */ + continue; + } + + ManifestNodeData nodeData = new ManifestNodeData(coordinationService.getNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestPath.toString())); + if(! nodeData.getStatus().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(); + continue; + } + + if (enforceMaxJobsPerCase) { + int currentJobsForCase = 0; + for (AutoIngestJob runningJob : hostNamesToRunningJobs.values()) { + if (0 == job.getManifest().getCaseName().compareTo(runningJob.getManifest().getCaseName())) { + ++currentJobsForCase; + } + } + if (currentJobsForCase >= AutoIngestUserPreferences.getMaxConcurrentJobsForOneCase()) { + manifestLock.release(); + manifestLock = null; + continue; + } + } + iterator.remove(); + currentJob = job; + break; + } + } + return manifestLock; + } + + /** + * Processes and auto ingest job. + * + * @throws CoordinationServiceException if there is an error + * acquiring or releasing + * coordination service locks + * or setting coordination + * service node data. + * @throws SharedConfigurationException if there is an error while + * downloading shared + * configuration. + * @throws ServicesMonitorException if there is an error + * querying the services + * monitor. + * @throws DatabaseServerDownException if the database server is + * down. + * @throws KeywordSearchServerDownException if the Solr server is down. + * @throws CaseManagementException if there is an error + * creating, opening or closing + * the case for the job. + * @throws AnalysisStartupException if there is an error + * starting analysis of the + * data source by the data + * source level and file level + * ingest modules. + * @throws FileExportException if there is an error + * exporting files. + * @throws AutoIngestAlertFileException if there is an error + * creating an alert file. + * @throws AutoIngestJobLoggerException if there is an error writing + * to the auto ingest log for + * the case. + * @throws InterruptedException if the thread running the + * job processing task is + * interrupted while blocked, + * i.e., if auto ingest is + * shutting down. + */ + private void processJob() throws CoordinationServiceException, SharedConfigurationException, ServicesMonitorException, DatabaseServerDownException, KeywordSearchServerDownException, CaseManagementException, AnalysisStartupException, FileExportException, AutoIngestAlertFileException, AutoIngestJobLoggerException, InterruptedException, AutomatedIngestDataSourceProcessor.AutomatedIngestDataSourceProcessorException { + Manifest manifest = currentJob.getManifest(); + String manifestPath = manifest.getFilePath().toString(); + ManifestNodeData nodeData = new ManifestNodeData(coordinationService.getNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestPath)); + nodeData.setStatus(PROCESSING); + coordinationService.setNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestPath, nodeData.toArray()); + SYS_LOGGER.log(Level.INFO, "Started processing of {0}", manifestPath); + currentJob.setStage(AutoIngestJob.Stage.STARTING); + setChanged(); + notifyObservers(Event.JOB_STARTED); + eventPublisher.publishRemotely(new AutoIngestJobStartedEvent(currentJob)); + try { + if (currentJob.isCancelled() || jobProcessingTaskFuture.isCancelled()) { + return; + } + attemptJob(); + + } finally { + if (jobProcessingTaskFuture.isCancelled()) { + currentJob.cancel(); + } + + nodeData = new ManifestNodeData(coordinationService.getNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestPath)); + if(currentJob.isCompleted() || currentJob.isCancelled()){ + nodeData.setStatus(COMPLETED); + Date completedDate = new Date(); + currentJob.setCompletedDate(completedDate); + nodeData.setCompletedDate(currentJob.getCompletedDate()); + nodeData.setErrorsOccurred(currentJob.hasErrors()); + } else { + // The job may get retried + nodeData.setStatus(PENDING); + } + coordinationService.setNodeData(CoordinationService.CategoryNode.MANIFESTS, manifestPath, nodeData.toArray()); + + + boolean retry = (!currentJob.isCancelled() && !currentJob.isCompleted()); + SYS_LOGGER.log(Level.INFO, "Completed processing of {0}, retry = {1}", new Object[]{manifestPath, retry}); + if (currentJob.isCancelled()) { + Path caseDirectoryPath = currentJob.getCaseDirectoryPath(); + if (null != caseDirectoryPath) { + AutoIngestAlertFile.create(caseDirectoryPath); // Do this first, it is more important than the case log + AutoIngestJobLogger jobLogger = new AutoIngestJobLogger(manifest.getFilePath(), manifest.getDataSourceFileName(), caseDirectoryPath); + jobLogger.logJobCancelled(); + } + } + synchronized (jobsLock) { + if (!retry) { + completedJobs.add(currentJob); + } + eventPublisher.publishRemotely(new AutoIngestJobCompletedEvent(currentJob, retry)); + currentJob = null; + setChanged(); + notifyObservers(Event.JOB_COMPLETED); + } + } + } + + /** + * Attempts processing of an auto ingest job. + * + * @throws CoordinationServiceException if there is an error + * acquiring or releasing + * coordination service locks + * or setting coordination + * service node data. + * @throws SharedConfigurationException if there is an error while + * downloading shared + * configuration. + * @throws ServicesMonitorException if there is an error + * querying the services + * monitor. + * @throws DatabaseServerDownException if the database server is + * down. + * @throws KeywordSearchServerDownException if the Solr server is down. + * @throws CaseManagementException if there is an error + * creating, opening or closing + * the case for the job. + * @throws AnalysisStartupException if there is an error + * starting analysis of the + * data source by the data + * source level and file level + * ingest modules. + * @throws InterruptedException if the thread running the + * job processing task is + * interrupted while blocked, + * i.e., if auto ingest is + * shutting down. + */ + private void attemptJob() throws CoordinationServiceException, SharedConfigurationException, ServicesMonitorException, DatabaseServerDownException, KeywordSearchServerDownException, CaseManagementException, AnalysisStartupException, FileExportException, AutoIngestAlertFileException, AutoIngestJobLoggerException, InterruptedException, AutomatedIngestDataSourceProcessor.AutomatedIngestDataSourceProcessorException { + updateConfiguration(); + if (currentJob.isCancelled() || jobProcessingTaskFuture.isCancelled()) { + return; + } + verifyRequiredSevicesAreRunning(); + if (currentJob.isCancelled() || jobProcessingTaskFuture.isCancelled()) { + return; + } + Case caseForJob = openCase(); + try { + if (currentJob.isCancelled() || jobProcessingTaskFuture.isCancelled()) { + return; + } + runIngestForJob(caseForJob); + + } finally { + try { + caseForJob.closeCase(); + } catch (CaseActionException ex) { + Manifest manifest = currentJob.getManifest(); + throw new CaseManagementException(String.format("Error closing case %s for %s", manifest.getCaseName(), manifest.getFilePath()), ex); + } + } + } + + /** + * Updates the ingest system settings by downloading the latest version + * of the settings if using shared configuration. + * + * @throws SharedConfigurationException if there is an error downloading + * shared configuration. + * @throws InterruptedException if the thread running the job + * processing task is interrupted + * while blocked, i.e., if auto + * ingest is shutting down. + */ + private void updateConfiguration() throws SharedConfigurationException, InterruptedException { + if (AutoIngestUserPreferences.getSharedConfigEnabled()) { + Manifest manifest = currentJob.getManifest(); + Path manifestPath = manifest.getFilePath(); + SYS_LOGGER.log(Level.INFO, "Downloading shared configuration for {0}", manifestPath); + currentJob.setStage(AutoIngestJob.Stage.UPDATING_SHARED_CONFIG); + new SharedConfiguration().downloadConfiguration(); + } + } + + /** + * Checks the availability of the services required to process an + * automated ingest job. + * + * @throws ServicesMonitorException if there is an error querying the + * services monitor. + * @throws DatabaseServerDownException if the database server is down. + * @throws SolrServerDownException if the keyword search server is + * down. + */ + private void verifyRequiredSevicesAreRunning() throws ServicesMonitorException, DatabaseServerDownException, KeywordSearchServerDownException { + Manifest manifest = currentJob.getManifest(); + Path manifestPath = manifest.getFilePath(); + SYS_LOGGER.log(Level.INFO, "Checking services availability for {0}", manifestPath); + currentJob.setStage(AutoIngestJob.Stage.CHECKING_SERVICES); + if (!isServiceUp(ServicesMonitor.Service.REMOTE_CASE_DATABASE.toString())) { + throw new DatabaseServerDownException("Case database server is down"); + } + if (!isServiceUp(ServicesMonitor.Service.REMOTE_KEYWORD_SEARCH.toString())) { + throw new KeywordSearchServerDownException("Keyword search server is down"); + } + } + + /** + * Tests service of interest to verify that it is running. + * + * @param serviceName Name of the service. + * + * @return True if the service is running, false otherwise. + * + * @throws ServicesMonitorException if there is an error querying the + * services monitor. + */ + private boolean isServiceUp(String serviceName) throws ServicesMonitorException { + return (ServicesMonitor.getInstance().getServiceStatus(serviceName).equals(ServicesMonitor.ServiceStatus.UP.toString())); + } + + /** + * Creates or opens the case for the current auto ingest job, acquiring + * an exclusive lock on the case name during the operation. + *

+ * IMPORTANT: The case name lock is used to ensure that only one auto + * ingest node at a time can attempt to create/open/delete a given case. + * The case name lock must be acquired both here and during case + * deletion. + * + * @return The case on success, null otherwise. + * + * @throws CoordinationServiceException if there is an error acquiring + * or releasing the case name lock. + * @throws CaseManagementException if there is an error opening the + * case. + * @throws InterruptedException if the thread running the auto + * ingest job processing task is + * interrupted while blocked, i.e., + * if auto ingest is shutting down. + */ + private Case openCase() throws CoordinationServiceException, CaseManagementException, InterruptedException { + Manifest manifest = currentJob.getManifest(); + String caseName = manifest.getCaseName(); + SYS_LOGGER.log(Level.INFO, "Opening case {0} for {1}", new Object[]{caseName, manifest.getFilePath()}); + currentJob.setStage(AutoIngestJob.Stage.OPENING_CASE); + try (Lock caseLock = coordinationService.tryGetExclusiveLock(CoordinationService.CategoryNode.CASES, caseName, 30, TimeUnit.MINUTES)) { + if (null != caseLock) { + try { + Path caseDirectoryPath = PathUtils.findCaseDirectory(rootOutputDirectory, caseName); + if (null != caseDirectoryPath) { + Path metadataFilePath = caseDirectoryPath.resolve(manifest.getCaseName() + CaseMetadata.getFileExtension()); + Case.open(metadataFilePath.toString()); + } else { + caseDirectoryPath = PathUtils.createCaseFolderPath(rootOutputDirectory, caseName); + Case.create(caseDirectoryPath.toString(), currentJob.getManifest().getCaseName(), "", "", CaseType.MULTI_USER_CASE); + /* + * Sleep a bit before releasing the lock to ensure + * that the new case folder is visible on the + * network. + */ + Thread.sleep(AutoIngestUserPreferences.getSecondsToSleepBetweenCases() * 1000); + } + currentJob.setCaseDirectoryPath(caseDirectoryPath); + Case caseForJob = Case.getCurrentCase(); + SYS_LOGGER.log(Level.INFO, "Opened case {0} for {1}", new Object[]{caseForJob.getName(), manifest.getFilePath()}); + return caseForJob; + + } catch (CaseActionException ex) { + throw new CaseManagementException(String.format("Error creating or opening case %s for %s", manifest.getCaseName(), manifest.getFilePath()), ex); + } catch (IllegalStateException ex) { + /* + * Deal with the unfortunate fact that + * Case.getCurrentCase throws IllegalStateException. + */ + throw new CaseManagementException(String.format("Error getting current case %s for %s", manifest.getCaseName(), manifest.getFilePath()), ex); + } + + } else { + throw new CaseManagementException(String.format("Timed out acquiring case name lock for %s for %s", manifest.getCaseName(), manifest.getFilePath())); + } + } + } + + /** + * Runs the ingest porocess for the current job. + * + * @param caseForJob The case for the job. + * + * @throws CoordinationServiceException if there is an error acquiring + * or releasing coordination + * service locks or setting + * coordination service node data. + * @throws AnalysisStartupException if there is an error starting + * analysis of the data source by + * the data source level and file + * level ingest modules. + * @throws FileExportException if there is an error exporting + * files. + * @throws AutoIngestAlertFileException if there is an error creating an + * alert file. + * @throws AutoIngestJobLoggerException if there is an error writing to + * the auto ingest log for the + * case. + * @throws InterruptedException if the thread running the job + * processing task is interrupted + * while blocked, i.e., if auto + * ingest is shutting down. + */ + private void runIngestForJob(Case caseForJob) throws CoordinationServiceException, AnalysisStartupException, FileExportException, AutoIngestAlertFileException, AutoIngestJobLoggerException, InterruptedException, AutomatedIngestDataSourceProcessor.AutomatedIngestDataSourceProcessorException { + Manifest manifest = currentJob.getManifest(); + String manifestPath = manifest.getFilePath().toString(); + try { + if (currentJob.isCancelled() || jobProcessingTaskFuture.isCancelled()) { + return; + } + ingestDataSource(caseForJob); + + } finally { + currentJob.setCompleted(); + } + } + + /** + * Ingests the data source specified in the manifest of the current job + * by using an appropriate data source processor to add the data source + * to the case database, passing the data source to the underlying + * ingest manager for analysis by data source and file level analysis + * modules, and exporting any files from the data source that satisfy + * the user-defined file export rules. + * + * @param caseForJob The case for the job. + * + * @throws AnalysisStartupException if there is an error starting + * analysis of the data source by + * the data source level and file + * level ingest modules. + * @throws FileExportException if there is an error exporting + * files. + * @throws AutoIngestAlertFileException if there is an error creating an + * alert file. + * @throws AutoIngestJobLoggerException if there is an error writing to + * the auto ingest log for the + * case. + * @throws InterruptedException if the thread running the job + * processing task is interrupted + * while blocked, i.e., if auto + * ingest is shutting down. + */ + private void ingestDataSource(Case caseForJob) throws AnalysisStartupException, FileExportException, AutoIngestAlertFileException, AutoIngestJobLoggerException, InterruptedException, AutomatedIngestDataSourceProcessor.AutomatedIngestDataSourceProcessorException { + if (currentJob.isCancelled() || jobProcessingTaskFuture.isCancelled()) { + return; + } + + DataSource dataSource = identifyDataSource(caseForJob); + if (null == dataSource) { + currentJob.setStage(AutoIngestJob.Stage.COMPLETED); + return; + } + + if (currentJob.isCancelled() || jobProcessingTaskFuture.isCancelled()) { + return; + } + + runDataSourceProcessor(caseForJob, dataSource); + if (dataSource.getContent().isEmpty()) { + currentJob.setStage(AutoIngestJob.Stage.COMPLETED); + return; + } + + if (currentJob.isCancelled() || jobProcessingTaskFuture.isCancelled()) { + return; + } + + try { + analyze(dataSource); + } finally { + /* + * Sleep to allow ingest event subscribers to do their event + * handling. + */ + Thread.sleep(AutoIngestUserPreferences.getSecondsToSleepBetweenCases() * 1000); // RJCTODO: Change the setting description to be more generic + } + + if (currentJob.isCancelled() || jobProcessingTaskFuture.isCancelled()) { + return; + } + + exportFiles(dataSource); + } + + /** + * Identifies the type of the data source specified in the manifest for + * the current job and extracts it if required. + * + * @return A data source object. + * + * @throws AutoIngestAlertFileException if there is an error creating an + * alert file. + * @throws AutoIngestJobLoggerException if there is an error writing to + * the auto ingest log for the + * case. + * @throws InterruptedException if the thread running the auto + * ingest job processing task is + * interrupted while blocked, i.e., + * if auto ingest is shutting down. + */ + private DataSource identifyDataSource(Case caseForJob) throws AutoIngestAlertFileException, AutoIngestJobLoggerException, InterruptedException { + Manifest manifest = currentJob.getManifest(); + Path manifestPath = manifest.getFilePath(); + SYS_LOGGER.log(Level.INFO, "Identifying data source for {0} ", manifestPath); + currentJob.setStage(AutoIngestJob.Stage.IDENTIFYING_DATA_SOURCE); + Path caseDirectoryPath = currentJob.getCaseDirectoryPath(); + AutoIngestJobLogger jobLogger = new AutoIngestJobLogger(manifestPath, manifest.getDataSourceFileName(), caseDirectoryPath); + Path dataSourcePath = manifest.getDataSourcePath(); + File dataSource = dataSourcePath.toFile(); + if (!dataSource.exists()) { + SYS_LOGGER.log(Level.SEVERE, "Missing data source for {0}", manifestPath); + currentJob.setErrorsOccurred(true); + AutoIngestAlertFile.create(caseDirectoryPath); // Do this first, it is more important than the case log + jobLogger.logMissingDataSource(); + return null; + } + String deviceId = manifest.getDeviceId(); + return new DataSource(deviceId, dataSourcePath); + } + + /** + * Passes the data source for the current job through a data source + * processor that adds it to the case database. + * + * @param dataSource The data source. + * + * @throws AutoIngestAlertFileException if there is an error creating an + * alert file. + * @throws AutoIngestJobLoggerException if there is an error writing to + * the auto ingest log for the + * case. + * @throws InterruptedException if the thread running the job + * processing task is interrupted + * while blocked, i.e., if auto + * ingest is shutting down. + */ + private void runDataSourceProcessor(Case caseForJob, DataSource dataSource) throws InterruptedException, AutoIngestAlertFileException, AutoIngestJobLoggerException, AutomatedIngestDataSourceProcessor.AutomatedIngestDataSourceProcessorException { + Manifest manifest = currentJob.getManifest(); + Path manifestPath = manifest.getFilePath(); + SYS_LOGGER.log(Level.INFO, "Adding data source for {0} ", manifestPath); + currentJob.setStage(AutoIngestJob.Stage.ADDING_DATA_SOURCE); + UUID taskId = UUID.randomUUID(); + DataSourceProcessorCallback callBack = new AddDataSourceCallback(caseForJob, dataSource, taskId); + DataSourceProcessorProgressMonitor progressMonitor = new DoNothingDSPProgressMonitor(); + Path caseDirectoryPath = currentJob.getCaseDirectoryPath(); + AutoIngestJobLogger jobLogger = new AutoIngestJobLogger(manifestPath, manifest.getDataSourceFileName(), caseDirectoryPath); + try { + caseForJob.notifyAddingDataSource(taskId); + + // lookup all AutomatedIngestDataSourceProcessors + Collection processorCandidates = Lookup.getDefault().lookupAll(AutomatedIngestDataSourceProcessor.class); + + Map validDataSourceProcessorsMap = new HashMap<>(); + for (AutomatedIngestDataSourceProcessor processor : processorCandidates) { + try { + int confidence = processor.canProcess(dataSource.getPath()); + if(confidence > 0){ + validDataSourceProcessorsMap.put(processor, confidence); + } + } catch (AutomatedIngestDataSourceProcessor.AutomatedIngestDataSourceProcessorException ex) { + SYS_LOGGER.log(Level.SEVERE, "Exception while determining whether data source processor {0} can process {1}", new Object[]{processor.getDataSourceType(), dataSource.getPath()}); + // rethrow the exception. It will get caught & handled upstream and will result in AIM auto-pause. + throw ex; + } + } + + // did we find a data source processor that can process the data source + if (validDataSourceProcessorsMap.isEmpty()) { + // This should never happen. We should add all unsupported data sources as logical files. + AutoIngestAlertFile.create(caseDirectoryPath); + currentJob.setErrorsOccurred(true); + jobLogger.logFailedToIdentifyDataSource(); + SYS_LOGGER.log(Level.WARNING, "Unsupported data source {0} for {1}", new Object[]{dataSource.getPath(), manifestPath}); // NON-NLS + return; + } + + // Get an ordered list of data source processors to try + List validDataSourceProcessors = validDataSourceProcessorsMap.entrySet().stream() + .sorted(Map.Entry.comparingByValue().reversed()) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + + synchronized (ingestLock) { + // Try each DSP in decreasing order of confidence + for(AutomatedIngestDataSourceProcessor selectedProcessor:validDataSourceProcessors){ + jobLogger.logDataSourceProcessorSelected(selectedProcessor.getDataSourceType()); + SYS_LOGGER.log(Level.INFO, "Identified data source type for {0} as {1}", new Object[]{manifestPath, selectedProcessor.getDataSourceType()}); + try { + selectedProcessor.process(dataSource.getDeviceId(), dataSource.getPath(), progressMonitor, callBack); + ingestLock.wait(); + return; + } catch (AutomatedIngestDataSourceProcessor.AutomatedIngestDataSourceProcessorException ex) { + // Log that the current DSP failed and set the error flag. We consider it an error + // if a DSP fails even if a later one succeeds since we expected to be able to process + // the data source which each DSP on the list. + AutoIngestAlertFile.create(caseDirectoryPath); + currentJob.setErrorsOccurred(true); + jobLogger.logDataSourceProcessorError(selectedProcessor.getDataSourceType()); + SYS_LOGGER.log(Level.SEVERE, "Exception while processing {0} with data source processor {1}", new Object[]{dataSource.getPath(), selectedProcessor.getDataSourceType()}); + } + } + // If we get to this point, none of the processors were successful + SYS_LOGGER.log(Level.SEVERE, "All data source processors failed to process {0}", dataSource.getPath()); + jobLogger.logFailedToAddDataSource(); + // Throw an exception. It will get caught & handled upstream and will result in AIM auto-pause. + throw new AutomatedIngestDataSourceProcessor.AutomatedIngestDataSourceProcessorException("Failed to process " + dataSource.getPath() + " with all data source processors"); + } + } finally { + currentJob.setDataSourceProcessor(null); + logDataSourceProcessorResult(dataSource); + } + } + + /** + * Logs the results of running a data source processor on the data + * source for the current job. + * + * @param dataSource The data source. + * + * @throws AutoIngestAlertFileException if there is an error creating an + * alert file. + * @throws AutoIngestJobLoggerException if there is an error writing to + * the auto ingest log for the + * case. + * @throws InterruptedException if the thread running the job + * processing task is interrupted + * while blocked, i.e., if auto + * ingest is shutting down. + */ + private void logDataSourceProcessorResult(DataSource dataSource) throws AutoIngestAlertFileException, AutoIngestJobLoggerException, InterruptedException { + Manifest manifest = currentJob.getManifest(); + Path manifestPath = manifest.getFilePath(); + Path caseDirectoryPath = currentJob.getCaseDirectoryPath(); + AutoIngestJobLogger jobLogger = new AutoIngestJobLogger(manifestPath, manifest.getDataSourceFileName(), caseDirectoryPath); + DataSourceProcessorResult resultCode = dataSource.getResultDataSourceProcessorResultCode(); + if (null != resultCode) { + switch (resultCode) { + case NO_ERRORS: + jobLogger.logDataSourceAdded(); + if (dataSource.getContent().isEmpty()) { + currentJob.setErrorsOccurred(true); + AutoIngestAlertFile.create(caseDirectoryPath); // Do this first, it is more important than the case log + jobLogger.logNoDataSourceContent(); + } + break; + + case NONCRITICAL_ERRORS: + for (String errorMessage : dataSource.getDataSourceProcessorErrorMessages()) { + SYS_LOGGER.log(Level.WARNING, "Non-critical error running data source processor for {0}: {1}", new Object[]{manifestPath, errorMessage}); + } + jobLogger.logDataSourceAdded(); + if (dataSource.getContent().isEmpty()) { + currentJob.setErrorsOccurred(true); + AutoIngestAlertFile.create(caseDirectoryPath); // Do this first, it is more important than the case log + jobLogger.logNoDataSourceContent(); + } + break; + + case CRITICAL_ERRORS: + for (String errorMessage : dataSource.getDataSourceProcessorErrorMessages()) { + SYS_LOGGER.log(Level.SEVERE, "Critical error running data source processor for {0}: {1}", new Object[]{manifestPath, errorMessage}); + } + currentJob.setErrorsOccurred(true); + AutoIngestAlertFile.create(caseDirectoryPath); // Do this first, it is more important than the case log + jobLogger.logFailedToAddDataSource(); + break; + } + } else { + /* + * TODO (JIRA-1711): Use cancellation feature of data source + * processors that support cancellation. This should be able to + * be done by adding a transient reference to the DSP to + * AutoIngestJob and calling cancel on the DSP, if not null, in + * cancelCurrentJob. + */ + SYS_LOGGER.log(Level.WARNING, "Cancellation while waiting for data source processor for {0}", manifestPath); + currentJob.setErrorsOccurred(true); + AutoIngestAlertFile.create(caseDirectoryPath); // Do this first, it is more important than the case log + jobLogger.logDataSourceProcessorCancelled(); + } + } + + /** + * Analyzes the data source content returned by the data source + * processor using the configured set of data source level and file + * level analysis modules. + * + * @param dataSource The data source to analyze. + * + * @throws AnalysisStartupException if there is an error analyzing + * the data source. + * @throws AutoIngestAlertFileException if there is an error creating an + * alert file. + * @throws AutoIngestJobLoggerException if there is an error writing to + * the auto ingest log for the + * case. + * @throws InterruptedException if the thread running the job + * processing task is interrupted + * while blocked, i.e., if auto + * ingest is shutting down. + */ + private void analyze(DataSource dataSource) throws AnalysisStartupException, AutoIngestAlertFileException, AutoIngestJobLoggerException, InterruptedException { + Manifest manifest = currentJob.getManifest(); + Path manifestPath = manifest.getFilePath(); + SYS_LOGGER.log(Level.INFO, "Starting ingest modules analysis for {0} ", manifestPath); + currentJob.setStage(AutoIngestJob.Stage.ANALYZING_DATA_SOURCE); + Path caseDirectoryPath = currentJob.getCaseDirectoryPath(); + AutoIngestJobLogger jobLogger = new AutoIngestJobLogger(manifestPath, manifest.getDataSourceFileName(), caseDirectoryPath); + IngestJobEventListener ingestJobEventListener = new IngestJobEventListener(); + IngestManager.getInstance().addIngestJobEventListener(ingestJobEventListener); + try { + synchronized (ingestLock) { + IngestJobSettings ingestJobSettings = new IngestJobSettings(AutoIngestUserPreferences.getAutoModeIngestModuleContextString()); + List settingsWarnings = ingestJobSettings.getWarnings(); + if (settingsWarnings.isEmpty()) { + IngestJobStartResult ingestJobStartResult = IngestManager.getInstance().beginIngestJob(dataSource.getContent(), ingestJobSettings); + IngestJob ingestJob = ingestJobStartResult.getJob(); + if (null != ingestJob) { + currentJob.setIngestJob(ingestJob); + /* + * Block until notified by the ingest job event + * listener or until interrupted because auto ingest + * is shutting down. + */ + ingestLock.wait(); + IngestJob.ProgressSnapshot jobSnapshot = ingestJob.getSnapshot(); + for (IngestJob.ProgressSnapshot.DataSourceProcessingSnapshot snapshot : jobSnapshot.getDataSourceSnapshots()) { // RJCTODO: Are "child" jobs IngestJobs or DataSourceIngestJobs? + if (!snapshot.isCancelled()) { + List cancelledModules = snapshot.getCancelledDataSourceIngestModules(); + if (!cancelledModules.isEmpty()) { + SYS_LOGGER.log(Level.WARNING, String.format("Ingest module(s) cancelled for %s", manifestPath)); + currentJob.setErrorsOccurred(true); + AutoIngestAlertFile.create(caseDirectoryPath); // Do this first, it is more important than the case log + for (String module : snapshot.getCancelledDataSourceIngestModules()) { + SYS_LOGGER.log(Level.WARNING, String.format("%s ingest module cancelled for %s", module, manifestPath)); + jobLogger.logIngestModuleCancelled(module); + } + } + jobLogger.logAnalysisCompleted(); + } else { + currentJob.setStage(AutoIngestJob.Stage.CANCELLING); + currentJob.setErrorsOccurred(true); + AutoIngestAlertFile.create(caseDirectoryPath); // Do this first, it is more important than the case log + jobLogger.logAnalysisCancelled(); + CancellationReason cancellationReason = snapshot.getCancellationReason(); + if (CancellationReason.NOT_CANCELLED != cancellationReason && CancellationReason.USER_CANCELLED != cancellationReason) { + throw new AnalysisStartupException(String.format("Analysis cacelled due to %s for %s", cancellationReason.getDisplayName(), manifestPath)); + } + } + } + } else if (!ingestJobStartResult.getModuleErrors().isEmpty()) { + for (IngestModuleError error : ingestJobStartResult.getModuleErrors()) { + SYS_LOGGER.log(Level.SEVERE, String.format("%s ingest module startup error for %s", error.getModuleDisplayName(), manifestPath), error.getThrowable()); + } + currentJob.setErrorsOccurred(true); + AutoIngestAlertFile.create(caseDirectoryPath); // Do this first, it is more important than the case log + jobLogger.logIngestModuleStartupErrors(); + throw new AnalysisStartupException(String.format("Error(s) during ingest module startup for %s", manifestPath)); + } else { + SYS_LOGGER.log(Level.SEVERE, String.format("Ingest manager ingest job start error for %s", manifestPath), ingestJobStartResult.getStartupException()); + currentJob.setErrorsOccurred(true); + AutoIngestAlertFile.create(caseDirectoryPath); // Do this first, it is more important than the case log + jobLogger.logAnalysisStartupError(); + throw new AnalysisStartupException("Ingest manager error starting job", ingestJobStartResult.getStartupException()); + } + } else { + for (String warning : ingestJobSettings.getWarnings()) { + SYS_LOGGER.log(Level.SEVERE, "Ingest job settings error for {0}: {1}", new Object[]{manifestPath, warning}); + } + currentJob.setErrorsOccurred(true); + AutoIngestAlertFile.create(caseDirectoryPath); // Do this first, it is more important than the case log + jobLogger.logIngestJobSettingsErrors(); + throw new AnalysisStartupException("Error(s) in ingest job settings"); + } + } + } finally { + IngestManager.getInstance().removeIngestJobEventListener(ingestJobEventListener); + currentJob.setIngestJob(null); // RJCTODO: Consider moving AutoIngestJob into AutoIngestManager so that this method can be made private + } + } + + /** + * Exports any files from the data source for the current job that + * satisfy any user-defined file export rules. + * + * @param dataSource The data source. + * + * @throws FileExportException if there is an error exporting + * the files. + * @throws AutoIngestAlertFileException if there is an error creating an + * alert file. + * @throws AutoIngestJobLoggerException if there is an error writing to + * the auto ingest log for the + * case. + * @throws InterruptedException if the thread running the job + * processing task is interrupted + * while blocked, i.e., if auto + * ingest is shutting down. + */ + private void exportFiles(DataSource dataSource) throws FileExportException, AutoIngestAlertFileException, AutoIngestJobLoggerException, InterruptedException { + Manifest manifest = currentJob.getManifest(); + Path manifestPath = manifest.getFilePath(); + SYS_LOGGER.log(Level.INFO, "Exporting files for {0}", manifestPath); + currentJob.setStage(AutoIngestJob.Stage.EXPORTING_FILES); + Path caseDirectoryPath = currentJob.getCaseDirectoryPath(); + AutoIngestJobLogger jobLogger = new AutoIngestJobLogger(manifestPath, manifest.getDataSourceFileName(), caseDirectoryPath); + try { + FileExporter fileExporter = new FileExporter(); + if (fileExporter.isEnabled()) { + fileExporter.process(manifest.getDeviceId(), dataSource.getContent(), currentJob::isCancelled); + jobLogger.logFileExportCompleted(); + } else { + SYS_LOGGER.log(Level.WARNING, "Exporting files not enabled for {0}", manifestPath); + jobLogger.logFileExportDisabled(); + } + } catch (FileExportException ex) { + SYS_LOGGER.log(Level.SEVERE, String.format("Error doing file export for %s", manifestPath), ex); + currentJob.setErrorsOccurred(true); + AutoIngestAlertFile.create(caseDirectoryPath); // Do this first, it is more important than the case log + jobLogger.logFileExportError(); + } + } + + /** + * A "callback" that collects the results of running a data source + * processor on a data source and unblocks the job processing thread + * when the data source processor finishes running in its own thread. + */ + @Immutable + class AddDataSourceCallback extends DataSourceProcessorCallback { + + private final Case caseForJob; + private final DataSource dataSourceInfo; + private final UUID taskId; + + /** + * Constructs a "callback" that collects the results of running a + * data source processor on a data source and unblocks the job + * processing thread when the data source processor finishes running + * in its own thread. + * + * @param caseForJob The case for the current job. + * @param dataSourceInfo The data source + * @param taskId The task id to associate with ingest job + * events. + */ + AddDataSourceCallback(Case caseForJob, DataSource dataSourceInfo, UUID taskId) { + this.caseForJob = caseForJob; + this.dataSourceInfo = dataSourceInfo; + this.taskId = taskId; + } + + /** + * Called by the data source processor when it finishes running in + * its own thread. + * + * @param result The result code for the processing of + * the data source. + * @param errorMessages Any error messages generated during the + * processing of the data source. + * @param dataSourceContent The content produced by processing the + * data source. + */ + @Override + public void done(DataSourceProcessorCallback.DataSourceProcessorResult result, List errorMessages, List dataSourceContent) { + if (!dataSourceContent.isEmpty()) { + caseForJob.notifyDataSourceAdded(dataSourceContent.get(0), taskId); + } else { + caseForJob.notifyFailedAddingDataSource(taskId); + } + dataSourceInfo.setDataSourceProcessorOutput(result, errorMessages, dataSourceContent); + dataSourceContent.addAll(dataSourceContent); + synchronized (ingestLock) { + ingestLock.notify(); + } + } + + /** + * Called by the data source processor when it finishes running in + * its own thread, if that thread is the AWT (Abstract Window + * Toolkit) event dispatch thread (EDT). + * + * @param result The result code for the processing of + * the data source. + * @param errorMessages Any error messages generated during the + * processing of the data source. + * @param dataSourceContent The content produced by processing the + * data source. + */ + @Override + public void doneEDT(DataSourceProcessorCallback.DataSourceProcessorResult result, List errorMessages, List dataSources) { + done(result, errorMessages, dataSources); + } + + } + + /** + * A data source processor progress monitor does nothing. There is + * currently no mechanism for showing or recording data source processor + * progress during an auto ingest job. + */ + private class DoNothingDSPProgressMonitor implements DataSourceProcessorProgressMonitor { + + /** + * Does nothing. + * + * @param indeterminate + */ + @Override + public void setIndeterminate(final boolean indeterminate) { + } + + /** + * Does nothing. + * + * @param progress + */ + @Override + public void setProgress(final int progress) { + } + + /** + * Does nothing. + * + * @param text + */ + @Override + public void setProgressText(final String text) { + } + + } + + /** + * An ingest job event listener that allows the job processing task to + * block until the analysis of a data source by the data source level + * and file level ingest modules is completed. + *

+ * Note that the ingest job can spawn "child" ingest jobs (e.g., if an + * embedded virtual machine is found), so the job processing task must + * remain blocked until ingest is no longer running. + */ + private class IngestJobEventListener implements PropertyChangeListener { + + /** + * Listens for local ingest job completed or cancelled events and + * notifies the job processing thread when such an event occurs and + * there are no "child" ingest jobs running. + * + * @param event + */ + @Override + public void propertyChange(PropertyChangeEvent event) { + if (AutopsyEvent.SourceType.LOCAL == ((AutopsyEvent) event).getSourceType()) { + String eventType = event.getPropertyName(); + if (eventType.equals(IngestManager.IngestJobEvent.COMPLETED.toString()) || eventType.equals(IngestManager.IngestJobEvent.CANCELLED.toString())) { + synchronized (ingestLock) { + if (!IngestManager.getInstance().isIngestRunning()) { + ingestLock.notify(); + } + } + } + } + } + }; + + /** + * Exception thrown when the services monitor reports that the database + * server is down. + */ + private final class DatabaseServerDownException extends Exception { + + private static final long serialVersionUID = 1L; + + private DatabaseServerDownException(String message) { + super(message); + } + + private DatabaseServerDownException(String message, Throwable cause) { + super(message, cause); + } + } + + /** + * Exception type thrown when the services monitor reports that the + * keyword search server is down. + */ + private final class KeywordSearchServerDownException extends Exception { + + private static final long serialVersionUID = 1L; + + private KeywordSearchServerDownException(String message) { + super(message); + } + + private KeywordSearchServerDownException(String message, Throwable cause) { + super(message, cause); + } + } + + /** + * Exception type thrown when there is a problem creating/opening the + * case for an auto ingest job. + */ + private final class CaseManagementException extends Exception { + + private static final long serialVersionUID = 1L; + + private CaseManagementException(String message) { + super(message); + } + + private CaseManagementException(String message, Throwable cause) { + super(message, cause); + } + } + + /** + * Exception type thrown when there is a problem analyzing a data source + * with data source level and file level ingest modules for an auto + * ingest job. + */ + private final class AnalysisStartupException extends Exception { + + private static final long serialVersionUID = 1L; + + private AnalysisStartupException(String message) { + super(message); + } + + private AnalysisStartupException(String message, Throwable cause) { + super(message, cause); + } + } + + } + + /** + * An instance of this runnable is responsible for periodically sending auto + * ingest job status event to remote auto ingest nodes and timing out stale + * remote jobs. The auto ingest job status event is sent only if auto ingest + * manager has a currently running auto ingest job. + */ + private final class PeriodicJobStatusEventTask implements Runnable { // RJCTODO: Rename to StatusPublishingTask, especially when publishing to the system dashboard + + private final long MAX_SECONDS_WITHOUT_UPDATE = JOB_STATUS_EVENT_INTERVAL_SECONDS * MAX_MISSED_JOB_STATUS_UPDATES; + + private PeriodicJobStatusEventTask() { + SYS_LOGGER.log(Level.INFO, "Periodic status publishing task started"); + } + + @Override + public void run() { + + try { + synchronized (jobsLock) { + if (currentJob != null) { + setChanged(); + notifyObservers(Event.JOB_STATUS_UPDATED); + eventPublisher.publishRemotely(new AutoIngestJobStatusEvent(currentJob)); + } + + if(AutoIngestUserPreferences.getStatusDatabaseLoggingEnabled()){ + String message; + boolean isError = false; + if(getErrorState().equals(ErrorState.NONE)){ + if(currentJob != null){ + message = "Processing " + currentJob.getManifest().getDataSourceFileName() + + " for case " + currentJob.getManifest().getCaseName(); + } else { + message = "Paused or waiting for next case"; + } + } else { + message = getErrorState().toString(); + isError = true; + } + try{ + StatusDatabaseLogger.logToStatusDatabase(message, isError); + } catch (SQLException | UserPreferencesException ex){ + SYS_LOGGER.log(Level.WARNING, "Failed to update status database", ex); + } + } + } + + // check whether any remote nodes have timed out + for (AutoIngestJob job : hostNamesToRunningJobs.values()) { + if (isStale(hostNamesToLastMsgTime.get(job.getNodeName()))) { + // remove the job from remote job running map. + /* + * NOTE: there is theoretically a check-then-act race + * condition but I don't it's worth introducing another + * lock because of it. If a job status update is + * received after we check the last message fileTime + * stamp (i.e. "check") but before we remove the remote + * job (i.e. "act") then the remote job will get added + * back into hostNamesToRunningJobs as a result of + * processing the job status update. + */ + SYS_LOGGER.log(Level.WARNING, "Auto ingest node {0} timed out while processing folder {1}", + new Object[]{job.getNodeName(), job.getManifest().getFilePath().toString()}); + hostNamesToRunningJobs.remove(job.getNodeName()); + setChanged(); + notifyObservers(Event.JOB_COMPLETED); + } + } + + } catch (Exception ex) { + SYS_LOGGER.log(Level.SEVERE, "Unexpected exception in PeriodicJobStatusEventTask", ex); //NON-NLS + } + } + + /** + * Determines whether or not the fileTime since the last message from + * node is greater than the maximum acceptable interval between + * messages. + * + * @return True or false. + */ + boolean isStale(Instant lastUpdateTime) { + return (Duration.between(lastUpdateTime, Instant.now()).toMillis() / 1000 > MAX_SECONDS_WITHOUT_UPDATE); + } + } + + /* + * The possible states of an auto ingest manager. + */ + private enum State { + IDLE, + RUNNING, + SHUTTING_DOWN; + } + + /* + * Events published by an auto ingest manager. The events are published + * locally to auto ingest manager clients that register as observers and are + * broadcast to other auto ingest nodes. // RJCTODO: Is this true? + */ + enum Event { + + INPUT_SCAN_COMPLETED, + JOB_STARTED, + JOB_STATUS_UPDATED, + JOB_COMPLETED, + CASE_PRIORITIZED, + CASE_DELETED, + PAUSED_BY_REQUEST, + PAUSED_FOR_SYSTEM_ERROR, + RESUMED + } + + /** + * The current auto ingest error state. + */ + private enum ErrorState { + NONE ("None"), + COORDINATION_SERVICE_ERROR ("Coordination service error"), + SHARED_CONFIGURATION_DOWNLOAD_ERROR("Shared configuration download error"), + SERVICES_MONITOR_COMMUNICATION_ERROR ("Services monitor communication error"), + DATABASE_SERVER_ERROR ("Database server error"), + KEYWORD_SEARCH_SERVER_ERROR ("Keyword search server error"), + CASE_MANAGEMENT_ERROR ("Case management error"), + ANALYSIS_STARTUP_ERROR ("Analysis startup error"), + FILE_EXPORT_ERROR ("File export error"), + ALERT_FILE_ERROR ("Alert file error"), + JOB_LOGGER_ERROR ("Job logger error"), + DATA_SOURCE_PROCESSOR_ERROR ("Data source processor error"), + UNEXPECTED_EXCEPTION ("Unknown error"); + + private final String desc; + + private ErrorState(String desc){ + this.desc = desc; + } + + @Override + public String toString(){ + return desc; + } + } + + /** + * A snapshot of the pending jobs queue, running jobs list, and completed + * jobs list. + */ + static final class JobsSnapshot { + + private final List pendingJobs; + private final List runningJobs; + private final List completedJobs; + + /** + * Constructs a snapshot of the pending jobs queue, running jobs list, + * and completed jobs list. + * + * @param pendingJobs The pending jobs queue. + * @param runningJobs The running jobs list. + * @param completedJobs The cmopleted jobs list. + */ + private JobsSnapshot(List pendingJobs, List runningJobs, List completedJobs) { + this.pendingJobs = new ArrayList<>(pendingJobs); + this.runningJobs = new ArrayList<>(runningJobs); + this.completedJobs = new ArrayList<>(completedJobs); + } + + /** + * Gets the snapshot of the pending jobs queue. + * + * @return The jobs collection. + */ + List getPendingJobs() { + return this.pendingJobs; + } + + /** + * Gets the snapshot of the running jobs list. + * + * @return The jobs collection. + */ + List getRunningJobs() { + return this.runningJobs; + } + + /** + * Gets the snapshot of the completed jobs list. + * + * @return The jobs collection. + */ + List getCompletedJobs() { + return this.completedJobs; + } + + } + + /** + * RJCTODO + */ + enum CaseDeletionResult { + FAILED, + PARTIALLY_DELETED, + FULLY_DELETED + } + + @ThreadSafe + private static final class DataSource { + + private final String deviceId; + private final Path path; + private DataSourceProcessorResult resultCode; + private List errorMessages; + private List content; + + DataSource(String deviceId, Path path) { + this.deviceId = deviceId; + this.path = path; + } + + String getDeviceId() { + return deviceId; + } + + Path getPath() { + return this.path; + } + + synchronized void setDataSourceProcessorOutput(DataSourceProcessorResult result, List errorMessages, List content) { + this.resultCode = result; + this.errorMessages = new ArrayList<>(errorMessages); + this.content = new ArrayList<>(content); + } + + synchronized DataSourceProcessorResult getResultDataSourceProcessorResultCode() { + return resultCode; + } + + synchronized List getDataSourceProcessorErrorMessages() { + return new ArrayList<>(errorMessages); + } + + synchronized List getContent() { + return new ArrayList<>(content); + } + + } + + static final class AutoIngestManagerStartupException extends Exception { + + private static final long serialVersionUID = 1L; + + private AutoIngestManagerStartupException(String message) { + super(message); + } + + private AutoIngestManagerStartupException(String message, Throwable cause) { + super(message, cause); + } + + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestSystemLogger.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestSystemLogger.java new file mode 100644 index 0000000000..068efcfce7 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestSystemLogger.java @@ -0,0 +1,93 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.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 java.util.logging.Logger; +import javax.annotation.concurrent.GuardedBy; +import org.sleuthkit.autopsy.coreutils.PlatformUtil; + +/** + * A logger for the auto ingest system log, a separate log from both the case + * auto ingest log and the application log. + */ +final class AutoIngestSystemLogger { + + private static final int LOG_SIZE = 0; // In bytes, zero is unlimited + private static final int LOG_FILE_COUNT = 10; + private static final Logger LOGGER = Logger.getLogger("AutoIngest"); //NON-NLS + private static final String NEWLINE = System.lineSeparator(); + @GuardedBy("AutoIngestSystemLogger") + private static boolean configured; + + /** + * Gets a logger for the auto ingest system log, separate from both the case + * auto ingest log and the application log. + * + * @return The logger. + */ + synchronized final static Logger getLogger() { + if (!configured) { + Path logFilePath = Paths.get(PlatformUtil.getUserDirectory().getAbsolutePath(), "var", "log", "auto_ingest.log"); + 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 (SecurityException | IOException ex) { + throw new RuntimeException(String.format("Error initializing file handler for %s", logFilePath), ex); //NON-NLS + } + configured = true; + } + return LOGGER; + } + + /** + * Prevents instantiation of this utility class. + */ + private AutoIngestSystemLogger() { + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutopsyManifestFileParser.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutopsyManifestFileParser.java new file mode 100644 index 0000000000..380bbb15a9 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutopsyManifestFileParser.java @@ -0,0 +1,122 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.nio.file.Path; +import java.util.HashMap; +import javax.annotation.concurrent.Immutable; +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.XPathFactory; +import org.openide.util.lookup.ServiceProvider; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +/** + * RJCTODO + */ +@Immutable +@ServiceProvider(service = ManifestFileParser.class) +public final class AutopsyManifestFileParser implements ManifestFileParser { + + private static final String MANIFEST_FILE_NAME_SIGNATURE = "_Manifest.xml"; + private static final String ROOT_ELEM_TAG_NAME = "Manifest"; + private static final String CASE_NAME_XPATH = "/Manifest/Collection/Name/text()"; + private static final String DEVICE_ID_XPATH = "/Manifest/Collection/Image/ID/text()"; + private static final String DATA_SOURCE_NAME_XPATH = "/Manifest/Collection/Image/Name/text()"; + + + /** + * RJCTODO + * + * @param filePath + * + * @return + */ + @Override + public boolean fileIsManifest(Path filePath) { + boolean fileIsManifest = false; + try { + Path fileName = filePath.getFileName(); + if (fileName.toString().endsWith(MANIFEST_FILE_NAME_SIGNATURE)) { + Document doc = this.createManifestDOM(filePath); + Element docElement = doc.getDocumentElement(); + fileIsManifest = docElement.getTagName().equals(ROOT_ELEM_TAG_NAME); + } + } catch (Exception unused) { + fileIsManifest = false; + } + return fileIsManifest; + } + + /** + * RJCTODO + * + * @param filePath + * + * @return + * + * @throws org.sleuthkit.autopsy.experimental.autoingest.ManifestFileParser.ManifestFileParserException + */ + @Override + public Manifest parse(Path filePath) throws ManifestFileParserException { + if (!fileIsManifest(filePath)) { + throw new ManifestFileParserException(String.format("%s not recognized as a manifest", filePath)); + } + + try { + Document doc = this.createManifestDOM(filePath); + XPath xpath = XPathFactory.newInstance().newXPath(); + XPathExpression expr = xpath.compile(CASE_NAME_XPATH); + String caseName = (String) expr.evaluate(doc, XPathConstants.STRING); + expr = xpath.compile(DEVICE_ID_XPATH); + String deviceId = (String) expr.evaluate(doc, XPathConstants.STRING); + expr = xpath.compile(DATA_SOURCE_NAME_XPATH); + String dataSourceName = (String) expr.evaluate(doc, XPathConstants.STRING); + Path dataSourcePath = filePath.getParent().resolve(dataSourceName); + return new Manifest(filePath, caseName, deviceId, dataSourcePath, new HashMap<>()); + } catch (Exception ex) { + throw new ManifestFileParserException(String.format("Error parsing manifest %s", filePath), ex); + } + } + + /** + * RJCTODO + * + * @param manifestFilePath + * + * @return + * + * @throws ParserConfigurationException + * @throws SAXException + * @throws IOException + */ + private Document createManifestDOM(Path manifestFilePath) throws ParserConfigurationException, SAXException, IOException { + DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder(); + return docBuilder.parse(manifestFilePath.toFile()); + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties new file mode 100644 index 0000000000..c119e136dd --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties @@ -0,0 +1,294 @@ +CTL_OpenAction=Open Case... +AutoIngestDashboard.bnRefresh.text=&Refresh +AutoIngestDashboard.lbCompleted.text=Completed Jobs +AutoIngestDashboard.lbRunning.text=Running Jobs +AutoIngestDashboard.lbPending.text=Pending Jobs +AutoIngestDashboard.bnCancelModule.text=Cancel &Module +AutoIngestDashboard.bnExit.text=&Exit +AutoIngestDashboard.bnOptions.text=&Options +AutoIngestDashboard.JobsTableModel.ColumnHeader.Case=Case +AutoIngestDashboard.JobsTableModel.ColumnHeader.ImageFolder=Data Source +AutoIngestDashboard.JobsTableModel.ColumnHeader.HostName=Host Name +AutoIngestDashboard.JobsTableModel.ColumnHeader.CreatedTime=Job Created +AutoIngestDashboard.JobsTableModel.ColumnHeader.StartedTime=Stage Started +AutoIngestDashboard.JobsTableModel.ColumnHeader.CompletedTime=Job Completed +AutoIngestDashboard.JobsTableModel.ColumnHeader.Stage=Stage +AutoIngestDashboard.JobsTableModel.ColumnHeader.Status=Status +AutoIngestDashboard.JobsTableModel.ColumnHeader.ManifestFilePath= Manifest File Path +AutoIngestDashboard.bnShowProgress.text=Ingest Progress +AutoIngestDashboard.bnResume.text=Resume +AutoIngestDashboard.bnPause.text=Pause +AutoIngestDashboard.bnPause.confirmHeader=Are you sure you want to pause? +AutoIngestDashboard.bnPause.warningText=Pause will occur after the current job completes processing. This could take a long time. Continue? +AutoIngestDashboard.bnPause.toolTipText=Suspend processing of Pending Jobs +AutoIngestDashboard.bnPause.toolTipTextResume=Resume processing of Pending Jobs +AutoIngestDashboard.bnPause.pausing=Pausing after current job completes... +AutoIngestDashboard.bnRefresh.toolTipText=Refresh displayed tables +AutoIngestDashboard.bnShowProgress.toolTipText=Show the progress of the currently running Job. This functionality is only available for jobs running on current AIM node. +AutoIngestDashboard.bnCancelModule.toolTipText=Cancel processing of the current module within the Job and move on to the next module within the Job. This functionality is only available for jobs running on current AIM node. +AutoIngestDashboard.bnExit.toolTipText=Exit Application +AutoIngestDashboard.Cancelling=Cancelling... +AutoIngestDashboard.bnOptions.toolTipText=Display options panel. All processing must be paused to open the options panel. +AutoIngestDashboard.pendingTable.toolTipText=The Pending table displays the order upcoming Jobs will be processed with the top of the list first +AutoIngestDashboard.runningTable.toolTipText=The Running table displays the currently running Job and information about it +AutoIngestDashboard.completedTable.toolTipText=The Completed table shows all Jobs that have been processed already +AutoIngestDashboard.JobsTableModel.ColumnHeader.StageTime=Time in Stage +AutoIngestDashboard.JobsTableModel.ColumnHeader.CaseFolder=Case Folder +AutoIngestDashboard.JobsTableModel.ColumnHeader.LocalJob= Local Job? +AutoIngestDashboard.DeletionFailed=Deletion failed for job +AutoIngestDashboard.ShowLogFailed.Title=Unable to display case log +AutoIngestDashboard.ShowLogFailed.Message=Case log file does not exist +AutoIngestDashboard.bnPrioritizeCase.toolTipText=Move all images associated with a case to top of Pending queue. +AutoIngestDashboard.bnPrioritizeCase.text=Prioritize Case +AutoIngestDashboard.ExitConsequences=This will cancel any currently running job on this host. Exiting while a job is running potentially leaves the case in an inconsistent or corrupted state. +AutoIngestDashboard.ExitingStatus=Exiting... +AutoIngestDashboard.OK=OK +AutoIngestDashboard.Cancel=Cancel +AutoIngestDashboard.AutoIngestStartupFailed.Message=Failed to start automated ingest.\nPlease see auto ingest system log for details. +AutoIngestDashboard.AutoIngestStartupFailed.Title=Automated Ingest Error +AutoIngestDashboard.AutoIngestStartupError=Failed to start automated ingest. Verify Multi-user Settings. +AutoIngestDashboard.AutoIngestStartupWarning.Title=Automated Ingest Warning +AutoIngestDashboard.AutoIngestStartupWarning.Message=Failed to establish remote communications with other automated ingest nodes.\nAuto ingest dashboard will only be able to display local ingest job events.\nPlease verify Multi-User settings (Options->Multi-User). See application log for details. +AutoIngestDashboard.UpdatingSharedConfig=Updating shared configuration +AutoIngestDashboard.SharedConfigurationDisabled=Shared configuration disabled +AutoIngestDashboard.EnableConfigurationSettings=Enable shared configuration from the options panel before uploading +AutoIngestDashboard.ErrorUploadingConfiguration=Error uploading configuration +AutoIngestDashboard.UploadSuccessTitle=Success +AutoIngestDashboard.UploadSuccess=Shared configuration successfully uploaded +AutoIngestDashboard.UploadFailedTitle=Failed +AutoIngestDashboard.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. +AutoIngestDashboard.ConfigLockedTitle=Configuration directory locked +AutoIngestDashboard.tbServicesStatusMessage.Message=Case databases {0}, keyword search {1}, coordination {2}, messaging {3} +AutoIngestDashboard.tbServicesStatusMessage.Message.Up=up +AutoIngestDashboard.tbServicesStatusMessage.Message.Down=down +AutoIngestDashboard.tbServicesStatusMessage.Message.Unknown=unknown +AutoIngestDashboard.PauseDueToSystemError=Paused due to system error, please consult the auto ingest system log +ConfirmationDialog.DoNotDelete=Do not delete +ConfirmationDialog.Delete=Permanently delete +ConfirmationDialog.DeleteAreYouSure=The entire case will be removed. Are you sure you want to delete case +ConfirmationDialog.ConfirmDeletion=Do you really want to cancel copy job +ConfirmationDialog.ConfirmDeletionHeader=Confirm Deletion +ConfirmationDialog.QuestionMark=? +ConfirmationDialog.DoNotCancelModule=Do Not Cancel Module +ConfirmationDialog.DoNotCancelJob=Do Not Cancel Job +ConfirmationDialog.DoNotCancel=Do not cancel +ConfirmationDialog.Cancel=Proceed with cancellation +ConfirmationDialog.CancelJob=Cancel Job +ConfirmationDialog.CancelModule=Cancel Module +ConfirmationDialog.CancelModuleAreYouSure=The analysis of the ingest module processing the current job will be canceled. Are you sure? +ConfirmationDialog.CancelJobAreYouSure=The currently running job will be canceled. Are you sure? +ConfirmationDialog.ConfirmCancellationHeader=Confirm Cancellation +ConfirmationDialog.Exit=Exit +ConfirmationDialog.DoNotExit=Do Not Exit +ConfirmationDialog.ConfirmExit=All incomplete copy jobs will be cancelled. Are you sure? +ConfirmationDialog.ConfirmExitHeader=Confirm Exit +OpenIDE-Module-Name=Experimental +ReviewModeCasePanel.bnRefresh.text=&Refresh +ReviewModeCasePanel.bnOpen.text=&Open +ReviewModeCasePanel.rbGroupLabel.text=Show Last 10: +ReviewModeCasePanel.rbDays.text=Days +ReviewModeCasePanel.rbWeeks.text=Weeks +ReviewModeCasePanel.rbMonths.text=Months +ReviewModeCasePanel.rbAllCases.text=Everything +ReviewModeCasePanel.cannotOpenCase=Cannot Open Case +ReviewModeCasePanel.casePathNotFound=Case path not found +ReviewModeCasePanel.caseIsLocked=Single-user case is locked. +DisplayLogDialog.cannotOpenLog=Unable to open the selected case log file +DisplayLogDialog.cannotFindLog=Unable to find the selected case log file +DisplayLogDialog.unableToShowLogFile=Unable to show log file +DisplayLogDialog.okay=Okay +ReviewModeCasePanel.bnShowLog.text=&Show Log +CopyFilesPanel.lbFrom.text=From Source +CopyFilesPanel.lbTo.text=Destination Case +CopyFilesPanel.bnCopy.text=&Copy +CopyFilesPanel.bnCancel.text=C&ancel +CopyFilesPanel.lbStatus.text=Status +CopyFilesPanel.tbCaseName.text= +CopyFilesPanel.tbSourceName.text= +CopyFilesPanel.bnCaseName.text=B&rowse +CopyFilesPanel.bnSourceName.text=&Browse +CopyFilesPanel.spRetryCount.toolTipText=Skip this copy job and move on to the next after failing this number of times. +CopyFilesPanel.rbExistingCase.text= +CopyFilesPanel.rbNewCase.text= +CopyFilesPanel.lbFailureText.text=failures +CopyFilesPanel.lpSkipAfterText.text=Skip after +CopyFilesPanel.lbExistingCase.text=Use Existing Case +CopyFilesPanel.lbNewCase.text=Create New Case +CopyFilesPanel.lbTitle.text=Schedule New Copy Job +CopyFilesPanel.confirmCancellation=Do you really want to cancel the copy operation? +CopyFilesPanel.confirmCopyHeader=Confirm Copy to Existing Case +CopyFilesPanel.confirmCopyAdd=exists. Do you really want to copy more files to this Case? +CopyFilesPanel.confirmCopyYes=Copy +CopyFilesPanel.confirmCopyNo=Do not copy +CopyFilesPanel.lbScheduledJobs.text=Scheduled Copy Jobs +CopyFilesPanel.lbRunningJob.text=Running Copy Job +CopyFilesPanel.lbCompletedJobs.text=Completed Copy Jobs +CopyFilesPanel.Source=Source +CopyFilesPanel.Destination=Destination +CopyFilesPanel.Initiated=Initiated +CopyFilesPanel.Started=Started +CopyFilesPanel.RunTime=Run Time +CopyFilesPanel.Status=State +CopyFilesPanel.tableCompletedJobs.toolTipText=Shows completed copy jobs and status +CopyFilesPanel.toolTipText=Shows the currently running copy job. +CopyFilesPanel.lbWait.text=Wait +CopyFilesPanel.lbWaitMinutesText.text=minutes before retrying +CopyFilesPanel.bnUp.text= +CopyFilesPanel.bnDown.text= +CopyFilesPanel.bnShowLog.text=&Show Log +CopyFilesPanel.tablePendingJobs.toolTipText=Scheduled copy jobs in the order they will complete, starting from the top. +CopyFilesPanel.lbPendingJobs.text=Scheduled Copy Jobs +CopyFilesPanel.chooseOrCreate=Please choose or create a Case +CopyFilesPanel.invalidCasePath=Invalid case path +CopyFilesPanel.empty= +CopyFilesPanel.failedToDeleteSome=Failed to delete some files in cancelled job +CopyFilesPanel.noRunningJob=No running copy job to cancel +CopyFilesPanel.destinationEmpty=Destination cannot be within Source +CopyFilesPanel.copyAborted=Copy aborted. No copying occurred. +CopyFilesPanel.copying=Copying... +CopyFilesPanel.copySuccess=Files successfully copied. +CopyFilesPanel.errorCopying=Error copying. +CopyFilesPanel.permissionDenied=Permission denied. Check permissions on source and destination directories. +CopyFilesPanel.retriesLeft=retries left. Waiting to retry. +CopyFilesPanel.noRetriesLeft=Error copying. All retries used. Skipping. +CopyFilesPanel.cancelled=Cancelled running copy job +CopyFilesPanel.deleteConfirmed=Copy job cancelled +CopyFilesPanel.deleteNotConfirmed=Copy job not cancelled +CopyFilesPanel.deleteUnable=Unable to cancel copy job +CopyFilesPanel.unableToOpenLogfile=Unable to open logfile +CopyFilesPanel.invalidSourcePath=Invalid source path +CopyFilesPanel.lbScheduledJobs.text=Scheduled Copy Jobs (Placeholder) +CopyFilesPanel.ConfirmCancellation=Do you really want to cancel the copy operation? +CopyFilesPanel.ConfirmCopyHeader=Confirm Copy to Existing Case +CopyFilesPanel.ConfirmCopyAdd=exists. Do you really want to copy more files to this Case? +CopyFilesPanel.ConfirmCopyYes=Copy +CopyFilesPanel.ConfirmCopyNo=Do not copy +ConfirmationDialog.ConfirmUnlockHeader=Confirm Case Unlock +ReviewModeCasePanel.bnShowLog.text=&Show Log +AutoIngestDashboard.bnPrioritizeCase.toolTipText=Move all images associated with a case to top of Pending queue. +AutoIngestDashboard.bnPrioritizeCase.text=Prioriti&ze Case +AutoIngestDashboard.bnShowCaseLog.toolTipText=Display case log file for selected case +AutoIngestDashboard.bnShowCaseLog.text=Show Case &Log +ReviewModeCasePanel.bnShowLog.toolTipText=Display case log file for selected case +CopyFilesPanel.bnCancelPendingJob.text=Ca&ncel +CopyFilesPanel.tbDestinationCase.text= +CopyFilesPanel.cbThrottleNetwork.text=&Throttle Network +CopyFilesPanel.cbThrottleNetwork.toolTipText=Select this box if a low-bandwidth network connection is involved in this copy job.
\nSelecting this box will artificially limit the transfer speed by inserting strategic delays.
\nThis helps copy files across low-bandwidth networks where the transfer would
\notherwise fail. Only select this if you are having problems copying across the network. +CopyFilesPanel.bnShowCurrentLog.text=Show &Log +CopyFilesPanel.bnShowCurrentLog.text=Show &Log +AutoIngestDashboard.bnCancelJob.toolTipText=Cancel processing of the current Job and move on to the next Job. This functionality is only available for jobs running on current AIM node. +AutoIngestDashboard.bnCancelJob.text=&Cancel Job +AutoIngestDashboard.bnDeleteCase.toolTipText=Delete the selected Case in its entirety +AutoIngestDashboard.bnDeleteCase.text=&Delete Case +CopyFilesPanel.lbCaseName.text=Case Name +CaseStatusIconCellRenderer.tooltiptext.ok=Images processed successfully +CaseStatusIconCellRenderer.tooltiptext.warning=An error occurred or processing was canceled for at least one image - please check the log +OptionsCategory_Name_Case_Import=Case Import +OptionsCategory_Keywords_Case_Import=Case Import Settings +CaseImportPanel.validationErrMsg.MUdisabled=Multi user settings must be enabled and saved +CaseImportPanel.validationErrMsg.AIMdisabled=Automated ingest cluster must be joined in Automated ingest mode +CaseImportPanel.ChooseCase=Choose a case to import +CaseImportPanel.ChooseSource=Choose source images +CaseImportPanel.DatabaseConnected=Database connected +CaseImportPanel.DatabaseNotConnected=Verify Multi-user database settings. +CaseImportPanel.ImportingCases=Importing case(s)... +CaseImportPanel.Cancelling=Cancelling +CaseImportPanel.CannotOpenLog=Unable to open log file +CaseImportPanel.BadCaseSourceFolder=Bad case source folder name +CaseImportPanel.BadCaseDestinationFolder=Bad case destination folder name +CaseImportPanel.BadImageSourceFolder=Bad image source folder name +CaseImportPanel.BadImageDestinationFolder=Bad image destination folder name +CaseImportPanel.Error=Error +CaseImportPanel.Complete=Complete +CaseImportPanel.Blank= +CaseImportPanel.DeleteWarning=Make sure no important files are in the case source directory +AutoIngestDashboard.lbStatus.text=Status: +SingleUserCaseImporter.NonUniqueOutputFolder=Output folder not unique. Skipping +SingleUserCaseImporter.WillImport=Will import: +SingleUserCaseImporter.None=None +SingleUserCaseImporter.WillNotImport=Will not import: +SingleUserCaseImporter.ContinueWithImport=Continue with import? +SingleUserCaseImporter.Cancelled=Cancelled +SingleUserCaseImporter.ImportedAsMultiUser=\nThis case was imported as a multi-user collaborative case on +SingleUserCaseImporter.StartingBatch=Starting batch processing of +SingleUserCaseImporter.to=to +SingleUserCaseImporter.ErrorFindingAutFiles=Error trying to find .aut files to import +SingleUserCaseImporter.StartedProcessing=Started processing +SingleUserCaseImporter.FinishedProcessing=Finished processing +SingleUserCaseImporter.FailedToComplete=Failed to complete processing of +SingleUserCaseImporter.CompletedBatch=Completed batch processing of +SingleUserCaseImporter.AbortingBatch=Aborting batch processing of +SingleUserCaseImporter.SourceImageMissing=. Source image missing for +ReviewModeCasePanel.CaseHeaderText=Case +ReviewModeCasePanel.CreatedTimeHeaderText=Created Time +ReviewModeCasePanel.StatusIconHeaderText=Status +ReviewModeCasePanel.OutputFolderHeaderText=Output Folder +ReviewModeCasePanel.LastAccessedTimeHeaderText=Last Accessed Time +CopyFilesPanel.bnOptions.text=&Options +AutoIngestDashboard.lbServicesStatus.text=Services Status: +AutoIngestDashboard.tbServicesStatusMessage.text= +AutoIngestDashboard.tbStatusMessage.text= +FileExporterSettingsPanel.ChooseRootDirectory=Choose a root directory for file output +FileExporterSettingsPanel.ChooseReportDirectory=Choose a report directory +FileExporterSettingsPanel.RuleName=Rule Name +FileExporterSettingsPanel.RootDirectory=Root Directory +FileExporterSettingsPanel.ReportDirectory=Report Directory +FileExporterSettingsPanel.AttributeValue=Value to compare against +FileExporterSettingsPanel.RuleListTooltip=Shows a list of rules. +FileExporterSettingsPanel.RuleOutputTooltip=This is the root output folder for saved output. You must have read and write access for this folder. +FileExporterSettingsPanel.MimetypeTooltip=Select the MIME Type to compare against. +FileExporterSettingsPanel.MimetypeText=MIME Type +FileExporterSettingsPanel.MimetypeCheckboxTooltip=Check this to filter based on MIME Type. +FileExporterSettingsPanel.FileSizeValueToolTip=Select the value to compare against. +FileExporterSettingsPanel.FileSizeUnitToolTip=Select the units of the file size value. +FileExporterSettingsPanel.FileSize=File Size +FileExporterSettingsPanel.FileSizeComparisonTooltip=Select the file size comparison operator. +FileExporterSettingsPanel.MimeTypeComparisonTooltip=Select the MIME Type comparison operator. +FileExporterSettingsPanel.CurrentlySelectedRuleNameTooltip=Shows the currently selected rule's name. +FileExporterSettingsPanel.SaveText=Save Rule +FileExporterSettingsPanel.SaveTooltip=Click to save the rule. There must be a rule name and at least one conditional (checkbox) enabled. +FileExporterSettingsPanel.BrowseText=Browse +FileExporterSettingsPanel.BrowseRootOutputFolder=Browse to set the root output folder. +FileExporterSettingsPanel.NewText=New Rule +FileExporterSettingsPanel.NewRuleTooltip=Clears the rule editor, allowing the user to enter a new rule. +FileExporterSettingsPanel.BrowseReportTooltip=Browse to set the report output folder. +FileExporterSettingsPanel.ReportOutputFolderTooltip=This is the report output folder. You must have read and write access for this folder. +FileExporterSettingsPanel.DeleteText=Delete Rule +FileExporterSettingsPanel.DeleteTooltip=Deletes the selected rule. +FileExporterSettingsPanel.UnsavedChangesLost=Unsaved rule changes will be lost. Continue? +FileExporterSettingsPanel.ChangesWillBeLost=Unsaved changes will be lost. +FileExporterSettingsPanel.DoYouWantToSave=Do you want to save? +FileExporterSettingsPanel.BadRootFolder=Bad root folder chosen. +FileExporterSettingsPanel.BadFolderForInterestingFileExport=Bad folder for file export +FileExporterSettingsPanel.BadReportFolder=Bad report folder chosen. +FileExporterSettingsPanel.ReallyDeleteRule=Really delete rule +FileExporterSettingsPanel.ReallyDeleteCondition=Really delete condition +FileExporterSettingsPanel.QuestionMark=? +FileExporterSettingsPanel.ConfirmRuleDeletion=Confirm rule deletion +FileExporterSettingsPanel.ConfirmClauseDeletion=Confirm clause deletion +FileExporterSettingsPanel.Title=File Export Rule Generator +FileExporterSettingsPanel.UnableToFindDirectory=Unable to find directory. +FileExporterSettingsPanel.PermissionsInvalid=Invalid folder permissions. Can not read or write. +FileExporterSettingsPanel.RuleNotSaved=Malformed rule. Rule not saved. +FileExporterSettingsPanel.MalformedRule=Malformed rule. +FileExporterSettingsPanel.FileSize_1=Select to include file size in the rule +FileExporterSettingsPanel.MimetypeCheckboxTooltip_1=Select to include MIME type in the rule +FileExporterSettingsPanel.MimeTypeComparisonTooltip_1=Select the conditional operator +FileExporterSettingsPanel.FileSizeComparisonTooltip_1=Select the conditional operator +FileExporterSettingsPanel.FileSizeValueToolTip_1=Select the desired file size +FileExporterSettingsPanel.FileSizeUnitToolTip_1=Select the desired file size units +FileExporterSettingsPanel.MimetypeTooltip_1=Select the desired MIME type +FileExporterSettingsPanel.CurrentlySelectedRuleNameTooltip_1=The name of the rule +FileExporterSettingsPanel.ReportOutputFolderTooltip_1=The Reports folder where a JSON report is written out for each exported file. +FileExporterSettingsPanel.RuleOutputTooltip_1=The Files folder where each matching file is written out +FileExporterSettingsPanel.BrowseRootOutputFolder_1=Browse for the Files Folder +FileExporterSettingsPanel.BrowseReportTooltip_1=Browse for the Reports Folder +FileExporterSettingsPanel.NewRuleTooltip_1=Clear the rule editor to begin a new rule +FileExporterSettingsPanel.DeleteTooltip_1=Delete the selected rule +FileExporterSettingsPanel.SaveTooltip_1=Save the current rule +AutoIngestDashboard.bnOpenLogDir.text=Open System Logs Directory +AutoIngestDashboard.bnPrioritizeJob.text=Prioritize Job +AutoIngestDashboard.bnPrioritizeJob.toolTipText=Move this folder to the top of the Pending queue. +AutoIngestDashboard.bnReprocessJob.text=Reprocess Job +AutoIngestDashboard.bnPrioritizeFolder.label= +AutoIngestDashboard.bnPrioritizeJob.actionCommand= diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CaseImportPanel.form b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CaseImportPanel.form new file mode 100644 index 0000000000..4a708e067e --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CaseImportPanel.form @@ -0,0 +1,476 @@ + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CaseImportPanel.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CaseImportPanel.java new file mode 100644 index 0000000000..d1ee4c3865 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CaseImportPanel.java @@ -0,0 +1,787 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.Color; +import java.awt.Cursor; +import java.awt.Desktop; +import java.awt.EventQueue; +import java.awt.Font; +import java.io.File; +import java.nio.file.Paths; +import javax.swing.ImageIcon; +import javax.swing.JFileChooser; +import javax.swing.JTextField; +import org.openide.util.ImageUtilities; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.core.RuntimeProperties; +import org.sleuthkit.autopsy.core.UserPreferences; +import org.sleuthkit.autopsy.core.UserPreferencesException; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.CaseDbConnectionInfo; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; +import java.util.logging.Level; +import org.sleuthkit.autopsy.experimental.configuration.AutoIngestUserPreferences; +import static org.sleuthkit.autopsy.experimental.configuration.AutoIngestUserPreferences.SelectedMode.AUTOMATED; + +/** + * This panel shows up in a tab pane next to the copy files panel for the + * automated ingest copy node. + * + */ +public class CaseImportPanel extends javax.swing.JPanel implements ImportDoneCallback { + + private final CaseImportPanelController controller; + private static final long serialVersionUID = 1L; + private static final Logger logger = Logger.getLogger(CaseImportPanel.class.getName()); + private Thread ongoingImport; // used to interrupt thread if we need to + private final JFileChooser caseSourceFolderChooser = new JFileChooser(); + private final JFileChooser imageSourceFolderChooser = new JFileChooser(); + private CaseDbConnectionInfo db; + private final ImageIcon goodDatabaseCredentials; + private final ImageIcon badDatabaseCredentials; + private boolean canTalkToDb = false; + private boolean copyImagesState = true; + private boolean deleteImagesState = false; + private static final String MULTI_USER_SETTINGS_MUST_BE_ENABLED = NbBundle.getMessage(CaseImportPanel.class, "CaseImportPanel.validationErrMsg.MUdisabled"); + private static final String AIM_MUST_BE_ENABLED = NbBundle.getMessage(CaseImportPanel.class, "CaseImportPanel.validationErrMsg.AIMdisabled"); + + // Used to specify which notification area should be upated + private enum NotificationLabel { + + INPUT, + OUTPUT, + BOTTOM, + PROGRESS + } + + /** + * Creates new panel CaseImportPanel + */ + public CaseImportPanel(CaseImportPanelController theController) { + controller = theController; + initComponents(); + badDatabaseCredentials = new ImageIcon(ImageUtilities.loadImage("org/sleuthkit/autopsy/experimental/images/warning16.png", false)); //NON-NLS + goodDatabaseCredentials = new ImageIcon(ImageUtilities.loadImage("org/sleuthkit/autopsy/experimental/images/tick.png", false)); //NON-NLS + } + + /** + * Validate current panel settings. + */ + boolean valid() { + // Nothing to validate for case import panel as far as Netbeans Tools/Options controller is concerned + return true; + } + + /** + * Store current panel settings. + */ + void store() { + // Nothing to store for case import panel as far as Netbeans Tools/Options controller is concerned + } + + /** + * Load data. + */ + final void load() { + + // Multi user mode must be enabled. This is required to make sure database credentials are set. + // Also, "join auto ingest cluster" must be selected and we need to be in automated ingest mode. + // This is required to make sure "shared images" and "shared results" folders are set + if (!UserPreferences.getIsMultiUserModeEnabled()) { + tbOops.setText(MULTI_USER_SETTINGS_MUST_BE_ENABLED); + return; + } else if (RuntimeProperties.coreComponentsAreActive()) { + tbOops.setText(AIM_MUST_BE_ENABLED); + return; + } else { + tbOops.setText(""); + } + + // Note: we used to store input folders in persistent storage but it is not done any more for some reason... + caseSourceFolderChooser.setCurrentDirectory(caseSourceFolderChooser.getFileSystemView().getParentDirectory(new File("C:\\"))); //NON-NLS + caseSourceFolderChooser.setAcceptAllFileFilterUsed(false); + caseSourceFolderChooser.setDialogTitle(NbBundle.getMessage(CaseImportPanel.class, "CaseImportPanel.ChooseCase")); + caseSourceFolderChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + imageSourceFolderChooser.setCurrentDirectory(imageSourceFolderChooser.getFileSystemView().getParentDirectory(new File("C:\\"))); //NON-NLS + imageSourceFolderChooser.setAcceptAllFileFilterUsed(false); + imageSourceFolderChooser.setDialogTitle(NbBundle.getMessage(CaseImportPanel.class, "CaseImportPanel.ChooseSource")); + imageSourceFolderChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + cbCopyImages.setSelected(true); + cbDeleteCase.setSelected(false); + picDbStatus.setText(""); //NON-NLS + tbDeleteWarning.setText(""); //NON-NLS + tbInputNotification.setText(""); //NON-NLS + tbOutputNotification.setText(""); //NON-NLS + tbBottomNotification.setText(""); //NON-NLS + pbShowProgress.setStringPainted(true); + pbShowProgress.setForeground(new Color(51, 153, 255)); + pbShowProgress.setString(""); //NON-NLS + showDbStatus(); + handleAutoModeInputs(); + } + + void handleAutoModeInputs() { + String caseDestinationResult = ""; + String output = AutoIngestUserPreferences.getAutoModeResultsFolder(); + if (output.isEmpty() || !(new File(output).exists())) { + setNotificationText(NotificationLabel.OUTPUT, NbBundle.getMessage(CaseImportPanel.class, "CaseImportPanel.BadCaseDestinationFolder"), false); + } else { + tbCaseDestination.setText(output); + caseDestinationResult = ""; + } + + String imageDestinationResult = ""; + String imageFolder = AutoIngestUserPreferences.getAutoModeImageFolder(); + if (imageFolder.isEmpty() || !(new File(imageFolder).exists())) { + setNotificationText(NotificationLabel.OUTPUT, NbBundle.getMessage(CaseImportPanel.class, "CaseImportPanel.BadImageDestinationFolder"), false); + } else { + tbImageDestination.setText(imageFolder); + imageDestinationResult = ""; + } + + String result = caseDestinationResult; + if (result.isEmpty()) { + result = imageDestinationResult; + } + setNotificationText(NotificationLabel.OUTPUT, result, false); + } + + /** + * Set status pictures to show if the database credentials are good or bad + */ + private void showDbStatus() { + try { + db = UserPreferences.getDatabaseConnectionInfo(); + } catch (UserPreferencesException ex) { + logger.log(Level.SEVERE, "Error accessing case database connection info", ex); //NON-NLS + setDbConnectionStatus(false, badDatabaseCredentials, NbBundle.getMessage(CaseImportPanel.class, "CaseImportPanel.DatabaseNotConnected")); + return; + } + try { + SleuthkitCase.tryConnect(db); + setDbConnectionStatus(true, goodDatabaseCredentials, NbBundle.getMessage(CaseImportPanel.class, "CaseImportPanel.DatabaseConnected")); + } catch (TskCoreException ex) { + setDbConnectionStatus(false, badDatabaseCredentials, NbBundle.getMessage(CaseImportPanel.class, "CaseImportPanel.DatabaseNotConnected")); + logger.log(Level.SEVERE, "Unable to communicate with PostgreSQL: {0}", ex.getMessage()); + } + } + + private void setDbConnectionStatus(boolean canConnect, ImageIcon credentials, String text) { + canTalkToDb = canConnect; + picDbStatus.setIcon(credentials); + picDbStatus.setText(text); + } + + /** + * 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() { + + lbDbConnection = new javax.swing.JLabel(); + picDbStatus = new javax.swing.JLabel(); + lbCaseDestination = new javax.swing.JLabel(); + lbCaseSource = new javax.swing.JLabel(); + lbCaption = new javax.swing.JLabel(); + lbImageDestination = new javax.swing.JLabel(); + lbImageSource = new javax.swing.JLabel(); + bnStart = new javax.swing.JButton(); + bnCancel = new javax.swing.JButton(); + bnShowLog = new javax.swing.JButton(); + bnBrowseCaseSource = new javax.swing.JButton(); + bnBrowseImageSource = new javax.swing.JButton(); + pbShowProgress = new javax.swing.JProgressBar(); + tbCaseSource = new javax.swing.JTextField(); + tbCaseDestination = new javax.swing.JTextField(); + tbImageDestination = new javax.swing.JTextField(); + tbBottomNotification = new javax.swing.JTextField(); + tbImageSource = new javax.swing.JTextField(); + cbCopyImages = new javax.swing.JCheckBox(); + cbDeleteCase = new javax.swing.JCheckBox(); + lbProgressBar = new javax.swing.JLabel(); + tbInputNotification = new javax.swing.JTextField(); + tbOutputNotification = new javax.swing.JTextField(); + tbDeleteWarning = new javax.swing.JTextField(); + tbOops = new javax.swing.JTextField(); + + setMinimumSize(new java.awt.Dimension(830, 240)); + + lbDbConnection.setFont(new java.awt.Font("Tahoma", 0, 12)); // NOI18N + lbDbConnection.setText("Database"); + lbDbConnection.setToolTipText("Set database credentials via 'Options'"); + lbDbConnection.setVerticalAlignment(javax.swing.SwingConstants.BOTTOM); + lbDbConnection.setFocusable(false); + + picDbStatus.setFont(new java.awt.Font("Tahoma", 0, 12)); // NOI18N + picDbStatus.setLabelFor(lbDbConnection); + picDbStatus.setText("Database Status"); + picDbStatus.setVerticalAlignment(javax.swing.SwingConstants.BOTTOM); + picDbStatus.setFocusable(false); + + lbCaseDestination.setFont(new java.awt.Font("Tahoma", 0, 12)); // NOI18N + lbCaseDestination.setText("Case Destination"); + lbCaseDestination.setFocusable(false); + + lbCaseSource.setFont(new java.awt.Font("Tahoma", 0, 12)); // NOI18N + lbCaseSource.setLabelFor(lbCaseSource); + lbCaseSource.setText("Case Source"); + lbCaseSource.setVerticalAlignment(javax.swing.SwingConstants.BOTTOM); + lbCaseSource.setFocusable(false); + + lbCaption.setFont(new java.awt.Font("Tahoma", 1, 12)); // NOI18N + lbCaption.setText("Import single-user cases to multi-user cases"); + + lbImageDestination.setFont(new java.awt.Font("Tahoma", 0, 12)); // NOI18N + lbImageDestination.setText("Image Destination"); + lbImageDestination.setFocusable(false); + + lbImageSource.setFont(new java.awt.Font("Tahoma", 0, 12)); // NOI18N + lbImageSource.setText("Image Source"); + lbImageSource.setFocusable(false); + + bnStart.setText("Start"); + bnStart.setEnabled(false); + bnStart.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnStartActionPerformed(evt); + } + }); + + bnCancel.setText("Cancel"); + bnCancel.setEnabled(false); + bnCancel.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnCancelActionPerformed(evt); + } + }); + + bnShowLog.setText("Show Log"); + bnShowLog.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnShowLogActionPerformed(evt); + } + }); + + bnBrowseCaseSource.setText("Browse"); + bnBrowseCaseSource.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnBrowseCaseSourceActionPerformed(evt); + } + }); + + bnBrowseImageSource.setText("Browse"); + bnBrowseImageSource.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnBrowseImageSourceActionPerformed(evt); + } + }); + + pbShowProgress.setFont(new java.awt.Font("Tahoma", 1, 12)); // NOI18N + pbShowProgress.setToolTipText(""); + pbShowProgress.setFocusable(false); + pbShowProgress.setMaximumSize(new java.awt.Dimension(32767, 16)); + pbShowProgress.setPreferredSize(new java.awt.Dimension(146, 16)); + + tbCaseSource.setEditable(false); + tbCaseSource.setFont(new java.awt.Font("Tahoma", 0, 12)); // NOI18N + tbCaseSource.setToolTipText("Press \"Browse\" to select the case source folder."); + tbCaseSource.setBorder(new javax.swing.border.LineBorder(new java.awt.Color(153, 153, 153), 1, true)); + tbCaseSource.setFocusable(false); + + tbCaseDestination.setEditable(false); + tbCaseDestination.setFont(new java.awt.Font("Tahoma", 0, 12)); // NOI18N + tbCaseDestination.setToolTipText("The case destination folder. Press \"Options\" and edit \"Shared Results Folder\" to change this. Any imported cases will be stored in this folder."); + tbCaseDestination.setBorder(new javax.swing.border.LineBorder(new java.awt.Color(153, 153, 153), 1, true)); + tbCaseDestination.setFocusable(false); + + tbImageDestination.setEditable(false); + tbImageDestination.setFont(new java.awt.Font("Tahoma", 0, 12)); // NOI18N + tbImageDestination.setToolTipText("This is the Image folder. Press \"Options\" and edit \"Shared Images Folder\" to change this. Any input images will be copied to this folder during import."); + tbImageDestination.setBorder(new javax.swing.border.LineBorder(new java.awt.Color(153, 153, 153), 1, true)); + tbImageDestination.setFocusable(false); + + tbBottomNotification.setEditable(false); + tbBottomNotification.setFont(new java.awt.Font("Tahoma", 0, 12)); // NOI18N + tbBottomNotification.setHorizontalAlignment(javax.swing.JTextField.RIGHT); + tbBottomNotification.setText("tbNotification"); + tbBottomNotification.setToolTipText("Shows notifications"); + tbBottomNotification.setBorder(null); + tbBottomNotification.setFocusable(false); + + tbImageSource.setEditable(false); + tbImageSource.setFont(new java.awt.Font("Tahoma", 0, 12)); // NOI18N + tbImageSource.setToolTipText("Press \"Browse\" to select the image source folder."); + tbImageSource.setBorder(new javax.swing.border.LineBorder(new java.awt.Color(153, 153, 153), 1, true)); + tbImageSource.setFocusable(false); + + cbCopyImages.setFont(new java.awt.Font("Tahoma", 0, 12)); // NOI18N + cbCopyImages.setText("Copy images"); + cbCopyImages.addChangeListener(new javax.swing.event.ChangeListener() { + public void stateChanged(javax.swing.event.ChangeEvent evt) { + cbCopyImagesStateChanged(evt); + } + }); + cbCopyImages.addPropertyChangeListener(new java.beans.PropertyChangeListener() { + public void propertyChange(java.beans.PropertyChangeEvent evt) { + cbCopyImagesPropertyChange(evt); + } + }); + + cbDeleteCase.setFont(new java.awt.Font("Tahoma", 0, 12)); // NOI18N + cbDeleteCase.setText("Delete original case"); + cbDeleteCase.addPropertyChangeListener(new java.beans.PropertyChangeListener() { + public void propertyChange(java.beans.PropertyChangeEvent evt) { + cbDeleteCasePropertyChange(evt); + } + }); + + lbProgressBar.setFont(new java.awt.Font("Tahoma", 0, 12)); // NOI18N + lbProgressBar.setText("Progress"); + + tbInputNotification.setEditable(false); + tbInputNotification.setFont(new java.awt.Font("Tahoma", 0, 12)); // NOI18N + tbInputNotification.setHorizontalAlignment(javax.swing.JTextField.RIGHT); + tbInputNotification.setText("Input box"); + tbInputNotification.setBorder(null); + + tbOutputNotification.setEditable(false); + tbOutputNotification.setFont(new java.awt.Font("Tahoma", 0, 12)); // NOI18N + tbOutputNotification.setHorizontalAlignment(javax.swing.JTextField.RIGHT); + tbOutputNotification.setText("Output box"); + tbOutputNotification.setBorder(null); + + tbDeleteWarning.setEditable(false); + tbDeleteWarning.setText("delete warning"); + tbDeleteWarning.setBorder(null); + + tbOops.setEditable(false); + tbOops.setFont(tbOops.getFont().deriveFont(tbOops.getFont().getStyle() | java.awt.Font.BOLD, 12)); + tbOops.setForeground(new java.awt.Color(255, 0, 0)); + tbOops.setBorder(null); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGap(41, 41, 41) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(lbImageSource, javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(lbCaseSource, javax.swing.GroupLayout.Alignment.TRAILING)) + .addGap(18, 18, 18) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(tbCaseSource) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(bnBrowseCaseSource, javax.swing.GroupLayout.PREFERRED_SIZE, 99, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(tbImageSource) + .addGroup(layout.createSequentialGroup() + .addGap(0, 0, Short.MAX_VALUE) + .addComponent(tbInputNotification, javax.swing.GroupLayout.PREFERRED_SIZE, 527, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(bnBrowseImageSource, javax.swing.GroupLayout.PREFERRED_SIZE, 99, javax.swing.GroupLayout.PREFERRED_SIZE)))) + .addGroup(layout.createSequentialGroup() + .addGap(37, 37, 37) + .addComponent(lbCaption) + .addGap(35, 35, 35) + .addComponent(tbOops, javax.swing.GroupLayout.PREFERRED_SIZE, 465, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(0, 0, Short.MAX_VALUE)) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addGroup(layout.createSequentialGroup() + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(tbOutputNotification, javax.swing.GroupLayout.PREFERRED_SIZE, 495, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(tbBottomNotification, javax.swing.GroupLayout.PREFERRED_SIZE, 391, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup() + .addGap(18, 18, 18) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(lbCaseDestination, javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(lbImageDestination, javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(lbDbConnection, javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(lbProgressBar, javax.swing.GroupLayout.Alignment.TRAILING)) + .addGap(18, 18, 18) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(tbCaseDestination) + .addComponent(tbImageDestination) + .addComponent(pbShowProgress, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(layout.createSequentialGroup() + .addComponent(bnStart, javax.swing.GroupLayout.PREFERRED_SIZE, 99, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(bnCancel, javax.swing.GroupLayout.PREFERRED_SIZE, 99, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(bnShowLog, javax.swing.GroupLayout.PREFERRED_SIZE, 99, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(cbCopyImages) + .addComponent(picDbStatus) + .addGroup(layout.createSequentialGroup() + .addComponent(cbDeleteCase) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(tbDeleteWarning, javax.swing.GroupLayout.PREFERRED_SIZE, 478, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addGap(0, 11, Short.MAX_VALUE))))) + .addGap(105, 105, 105))) + .addContainerGap()) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(lbCaption) + .addComponent(tbOops, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGap(18, 18, 18) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.CENTER) + .addComponent(bnBrowseCaseSource) + .addComponent(tbCaseSource, javax.swing.GroupLayout.PREFERRED_SIZE, 30, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(lbCaseSource)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.CENTER) + .addComponent(bnBrowseImageSource) + .addComponent(tbImageSource, javax.swing.GroupLayout.PREFERRED_SIZE, 30, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(lbImageSource)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(cbCopyImages) + .addComponent(tbInputNotification, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(cbDeleteCase) + .addComponent(tbDeleteWarning, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGap(35, 35, 35) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.CENTER) + .addComponent(tbCaseDestination, javax.swing.GroupLayout.PREFERRED_SIZE, 30, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(lbCaseDestination)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.CENTER) + .addComponent(tbImageDestination, javax.swing.GroupLayout.PREFERRED_SIZE, 30, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(lbImageDestination)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(tbOutputNotification, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(9, 9, 9) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(lbDbConnection) + .addComponent(picDbStatus)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.CENTER) + .addComponent(pbShowProgress, javax.swing.GroupLayout.PREFERRED_SIZE, 26, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(lbProgressBar)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(bnShowLog) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(bnStart) + .addComponent(bnCancel))) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(tbBottomNotification, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap(172, Short.MAX_VALUE)) + ); + }// //GEN-END:initComponents + + /** + * Handles pressing the "Start" button + * + * @param evt + */ + private void bnStartActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnStartActionPerformed + showDbStatus(); + if (canTalkToDb) { + setNotificationText(NotificationLabel.PROGRESS, NbBundle.getMessage(CaseImportPanel.class, "CaseImportPanel.ImportingCases"), true); + SingleUserCaseImporter caseImporter = new SingleUserCaseImporter( + tbImageSource.getText(), + tbCaseSource.getText(), + tbImageDestination.getText(), + tbCaseDestination.getText(), + cbCopyImages.isSelected(), + cbDeleteCase.isSelected(), + this); + pbShowProgress.setIndeterminate(true); + ongoingImport = new Thread(caseImporter); + setButtonsForJobRunning(true); + ongoingImport.start(); + } else { + bnStart.setEnabled(false); + } + }//GEN-LAST:event_bnStartActionPerformed + + /** + * Allows bulk-setting the button enabled states. + * + * @param setting true if we are currently processing an import job, false + * otherwise + */ + void setButtonsForJobRunning(boolean setting) { + bnBrowseCaseSource.setEnabled(!setting); + bnBrowseImageSource.setEnabled(!setting); + cbCopyImages.setEnabled(!setting); + cbDeleteCase.setEnabled(!setting); + bnStart.setEnabled(!setting); + bnCancel.setEnabled(setting); + } + + /** + * Handles pressing the Cancel button + * + * @param evt + */ + private void bnCancelActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnCancelActionPerformed + if (ongoingImport != null) { + setNotificationText(NotificationLabel.PROGRESS, NbBundle.getMessage(CaseImportPanel.class, "CaseImportPanel.Cancelling"), false); + ongoingImport.interrupt(); + } + }//GEN-LAST:event_bnCancelActionPerformed + + /** + * Handles pressing the Browse for case source folder button + * + * @param evt + */ + private void bnBrowseCaseSourceActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnBrowseCaseSourceActionPerformed + int returnVal = caseSourceFolderChooser.showOpenDialog(this); + if (returnVal == JFileChooser.APPROVE_OPTION) { + validateSourceFields(); + } + }//GEN-LAST:event_bnBrowseCaseSourceActionPerformed + + /** + * Show user information about status of source fields, hierarchically. + */ + private void validateSourceFields() { + String caseSourceResult = ""; + File selectedFolder = caseSourceFolderChooser.getSelectedFile(); + if (selectedFolder == null || !selectedFolder.exists()) { + caseSourceResult = NbBundle.getMessage(CaseImportPanel.class, "CaseImportPanel.BadCaseSourceFolder"); + tbCaseSource.setText(""); + } else { + caseSourceResult = NbBundle.getMessage(CaseImportPanel.class, "CaseImportPanel.Blank"); + caseSourceFolderChooser.setCurrentDirectory(selectedFolder); + tbCaseSource.setText(selectedFolder.toString()); + } + + String caseImagesResult = ""; + if (cbCopyImages.isSelected()) { + selectedFolder = imageSourceFolderChooser.getSelectedFile(); + if (selectedFolder == null || !selectedFolder.exists()) { + caseImagesResult = NbBundle.getMessage(CaseImportPanel.class, "CaseImportPanel.BadImageSourceFolder"); + tbImageSource.setText(""); //NON-NLS + } else { + if (tbInputNotification.getText().isEmpty()) { + caseImagesResult = NbBundle.getMessage(CaseImportPanel.class, "CaseImportPanel.Blank"); + } + imageSourceFolderChooser.setCurrentDirectory(selectedFolder); + tbImageSource.setText(selectedFolder.toString()); + } + } + String result = caseSourceResult; + if (result.isEmpty()) { + result = caseImagesResult; + } + setNotificationText(NotificationLabel.INPUT, result, false); + enableStartButton(); + } + + /** + * Handles pressing the Show Log button + * + * @param evt + */ + private void bnShowLogActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnShowLogActionPerformed + try { + File logFile = Paths.get(tbCaseDestination.getText(), SingleUserCaseImporter.CASE_IMPORT_LOG_FILE).toFile(); + setNotificationText(NotificationLabel.BOTTOM, "", false); //NON-NLS + Desktop.getDesktop().edit(logFile); + } catch (Exception ex) { + setNotificationText(NotificationLabel.BOTTOM, NbBundle.getMessage(CaseImportPanel.class, "CaseImportPanel.CannotOpenLog"), false); + } + }//GEN-LAST:event_bnShowLogActionPerformed + + /** + * Handles pressing the Browse for image source folder button + * + * @param evt + */ + private void bnBrowseImageSourceActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnBrowseImageSourceActionPerformed + int returnVal = imageSourceFolderChooser.showOpenDialog(this); + if (returnVal == JFileChooser.APPROVE_OPTION) { + validateSourceFields(); + } + }//GEN-LAST:event_bnBrowseImageSourceActionPerformed + + private void cbCopyImagesPropertyChange(java.beans.PropertyChangeEvent evt) {//GEN-FIRST:event_cbCopyImagesPropertyChange + + }//GEN-LAST:event_cbCopyImagesPropertyChange + + private void cbDeleteCasePropertyChange(java.beans.PropertyChangeEvent evt) {//GEN-FIRST:event_cbDeleteCasePropertyChange + if (deleteImagesState != cbDeleteCase.isSelected()) { + deleteImagesState = cbDeleteCase.isSelected(); + if (cbDeleteCase.isSelected()) { + tbDeleteWarning.setForeground(Color.RED); + tbDeleteWarning.setText(NbBundle.getMessage(CaseImportPanel.class, "CaseImportPanel.DeleteWarning")); + } else { + // USE BUNDLE + tbDeleteWarning.setText(""); + } + } + }//GEN-LAST:event_cbDeleteCasePropertyChange + + private void cbCopyImagesStateChanged(javax.swing.event.ChangeEvent evt) {//GEN-FIRST:event_cbCopyImagesStateChanged + // Enable or disable the image folder entries + // this gets notified of mouseovers and such, so check that it actually + // changed before changing the state of UI components + if (copyImagesState != cbCopyImages.isSelected()) { + copyImagesState = cbCopyImages.isSelected(); + if (copyImagesState) { + tbImageSource.setEnabled(true); + tbImageDestination.setEnabled(true); + bnBrowseImageSource.setEnabled(true); + } else { + tbImageSource.setEnabled(false); + tbImageDestination.setEnabled(false); + bnBrowseImageSource.setEnabled(false); + } + validateSourceFields(); + } + }//GEN-LAST:event_cbCopyImagesStateChanged + + /** + * Enables the start button if all input is in order, disables it otherwise + */ + private void enableStartButton() { + if (UserPreferences.getIsMultiUserModeEnabled() + && AutoIngestUserPreferences.getJoinAutoModeCluster() + && (! RuntimeProperties.coreComponentsAreActive()) + && !tbCaseSource.getText().isEmpty() + && !tbCaseDestination.getText().isEmpty() + && canTalkToDb == true + && (!cbCopyImages.isSelected() || (!tbImageSource.getText().isEmpty() && !tbImageDestination.getText().isEmpty()))) { + bnStart.setEnabled(true); + } else { + bnStart.setEnabled(false); + } + } + + /** + * Allows setting the notification text outside the EDT. + * + * @param position the label we intend to set + * @param text The text to set + * @param okay True if there was no issue, false otherwise. Sets text + * color. + */ + private void setNotificationText(final NotificationLabel position, final String text, final boolean okay) { + EventQueue.invokeLater(() -> { + if (position != NotificationLabel.PROGRESS) { + JTextField textField; + if (position == NotificationLabel.INPUT) { + textField = tbInputNotification; + } else if (position == NotificationLabel.OUTPUT) { + textField = tbOutputNotification; + } else { + textField = tbBottomNotification; + } + + textField.setText(text); + if (okay) { + Font font = textField.getFont(); + textField.setFont(font.deriveFont(Font.BOLD)); + textField.setForeground(Color.BLACK); + } else { + Font font = textField.getFont(); + textField.setFont(font.deriveFont(Font.PLAIN)); + textField.setForeground(Color.RED); + } + } else { + pbShowProgress.setString(text); + if (okay) { + pbShowProgress.setForeground(new Color(51, 153, 255)); + } else { + pbShowProgress.setForeground(Color.RED); + } + } + }); + } + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton bnBrowseCaseSource; + private javax.swing.JButton bnBrowseImageSource; + private javax.swing.JButton bnCancel; + private javax.swing.JButton bnShowLog; + private javax.swing.JButton bnStart; + private javax.swing.JCheckBox cbCopyImages; + private javax.swing.JCheckBox cbDeleteCase; + private javax.swing.JLabel lbCaption; + private javax.swing.JLabel lbCaseDestination; + private javax.swing.JLabel lbCaseSource; + private javax.swing.JLabel lbDbConnection; + private javax.swing.JLabel lbImageDestination; + private javax.swing.JLabel lbImageSource; + private javax.swing.JLabel lbProgressBar; + private javax.swing.JProgressBar pbShowProgress; + private javax.swing.JLabel picDbStatus; + private javax.swing.JTextField tbBottomNotification; + private javax.swing.JTextField tbCaseDestination; + private javax.swing.JTextField tbCaseSource; + private javax.swing.JTextField tbDeleteWarning; + private javax.swing.JTextField tbImageDestination; + private javax.swing.JTextField tbImageSource; + private javax.swing.JTextField tbInputNotification; + private javax.swing.JTextField tbOops; + private javax.swing.JTextField tbOutputNotification; + // End of variables declaration//GEN-END:variables + + /** + * This method is called by the import thread as it is finishing. + * + * @param result true if the entire import was successful, false + * otherwise + * @param resultString the text string to show the user + */ + @Override + public void importDoneCallback(boolean result, String resultString) { + if (resultString == null || resultString.isEmpty()) { + pbShowProgress.setIndeterminate(false); + pbShowProgress.setValue(100); + if (result) { + setNotificationText(NotificationLabel.PROGRESS, NbBundle.getMessage(CaseImportPanel.class, "CaseImportPanel.Complete"), true); + } else { + setNotificationText(NotificationLabel.PROGRESS, NbBundle.getMessage(CaseImportPanel.class, "CaseImportPanel.Error"), result); + } + } else { + pbShowProgress.setIndeterminate(false); + if (result == true) { + pbShowProgress.setValue(0); + } else { + pbShowProgress.setValue(100); + } + setNotificationText(NotificationLabel.PROGRESS, resultString, result); + } + setButtonsForJobRunning(false); + ongoingImport = null; + showDbStatus(); + } +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CaseImportPanelController.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CaseImportPanelController.java new file mode 100644 index 0000000000..8432ae4446 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CaseImportPanelController.java @@ -0,0 +1,134 @@ + +/* + * Autopsy Forensic Browser + * + * Copyright 2013-2014 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.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import javax.swing.JComponent; +import org.netbeans.spi.options.OptionsPanelController; +import org.openide.util.HelpCtx; +import org.openide.util.Lookup; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import java.util.logging.Level; +import org.sleuthkit.autopsy.coreutils.Logger; + +// Commenting this out to remove from options display. Case import is currently broken +// (needs update to the database queries) and will be moving to the tools menu +/*@OptionsPanelController.TopLevelRegistration(categoryName = "#OptionsCategory_Name_Case_Import", + iconBase = "org/sleuthkit/autopsy/experimental/images/import32.png", + position = 4, + keywords = "#OptionsCategory_Keywords_Case_Import", + keywordsCategory = "Case Import")*/ +public final class CaseImportPanelController extends OptionsPanelController { + + private CaseImportPanel panel; + private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); + private boolean changed; + private static final Logger logger = Logger.getLogger(CaseImportPanelController.class.getName()); + + @Override + public void update() { + getPanel().load(); + changed = false; + } + + @Override + public void applyChanges() { + getPanel().store(); + changed = false; + } + + @Override + public void cancel() { + } + + @Override + public boolean isValid() { + return getPanel().valid(); + } + + @Override + public boolean isChanged() { + return changed; + } + + @Override + public HelpCtx getHelpCtx() { + return null; + } + + @Override + public JComponent getComponent(Lookup masterLookup) { + return getPanel(); + } + + @Override + public void addPropertyChangeListener(PropertyChangeListener l) { + if (pcs.getPropertyChangeListeners().length == 0) { + pcs.addPropertyChangeListener(l); + } + } + + @Override + public void removePropertyChangeListener(PropertyChangeListener l) { + /** + * Note the NetBeans Framework does not appear to call this at all. We + * are using NetBeans 7.3.1 Build 201306052037. Perhaps in a future + * version of the Framework this will be resolved, but for now, simply + * don't unregister anything and add one time only in the + * addPropertyChangeListener() method above. + */ + } + + private CaseImportPanel getPanel() { + if (panel == null) { + panel = new CaseImportPanel(this); + } + return panel; + } + + void changed() { + if (!changed) { + changed = true; + + try { + pcs.firePropertyChange(OptionsPanelController.PROP_CHANGED, false, true); + } catch (Exception e) { + logger.log(Level.SEVERE, "GeneralOptionsPanelController listener threw exception", e); //NON-NLS + MessageNotifyUtil.Notify.show( + NbBundle.getMessage(this.getClass(), "GeneralOptionsPanelController.moduleErr"), + NbBundle.getMessage(this.getClass(), "GeneralOptionsPanelController.moduleErr.msg"), + MessageNotifyUtil.MessageType.ERROR); + } + } + + try { + pcs.firePropertyChange(OptionsPanelController.PROP_VALID, null, null); + } catch (Exception e) { + logger.log(Level.SEVERE, "GeneralOptionsPanelController listener threw exception", e); //NON-NLS + MessageNotifyUtil.Notify.show( + NbBundle.getMessage(this.getClass(), "GeneralOptionsPanelController.moduleErr"), + NbBundle.getMessage(this.getClass(), "GeneralOptionsPanelController.moduleErr.msg"), + MessageNotifyUtil.MessageType.ERROR); + } + } +} + diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CaseStatusIconCellRenderer.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CaseStatusIconCellRenderer.java new file mode 100644 index 0000000000..0010cc90ee --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CaseStatusIconCellRenderer.java @@ -0,0 +1,53 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.Component; +import javax.swing.ImageIcon; +import javax.swing.JTable; +import static javax.swing.SwingConstants.CENTER; +import org.openide.util.ImageUtilities; + +/** + * A JTable cell renderer that represents an auto ingest alert file exists flag + * as a center-aligned icon, and grays out the cell if the table is disabled. + */ +class CaseStatusIconCellRenderer extends GrayableCellRenderer { + + private static final long serialVersionUID = 1L; + static final ImageIcon checkedIcon = new ImageIcon(ImageUtilities.loadImage("org/sleuthkit/autopsy/experimental/images/tick.png", false)); + static final ImageIcon warningIcon = new ImageIcon(ImageUtilities.loadImage("org/sleuthkit/autopsy/experimental/images/warning16.png", false)); + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { + setHorizontalAlignment(CENTER); + if ((value instanceof Boolean)) { + if (true == (Boolean) value) { + setIcon(warningIcon); + setToolTipText(org.openide.util.NbBundle.getMessage(CaseStatusIconCellRenderer.class, "CaseStatusIconCellRenderer.tooltiptext.warning")); + } else { + setIcon(checkedIcon); + setToolTipText(org.openide.util.NbBundle.getMessage(CaseStatusIconCellRenderer.class, "CaseStatusIconCellRenderer.tooltiptext.ok")); + } + } + grayCellIfTableNotEnabled(table, isSelected); + + return this; + } +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CenteredGrayableCellRenderer.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CenteredGrayableCellRenderer.java new file mode 100644 index 0000000000..b9040b674b --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CenteredGrayableCellRenderer.java @@ -0,0 +1,34 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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 static javax.swing.SwingConstants.CENTER; + +/** + * A JTable cell renderer that center-aligns cell content and grays out the cell + * if the table is disabled. + */ +class CenteredGrayableCellRenderer extends GrayableCellRenderer { + + private static final long serialVersionUID = 1L; + + public CenteredGrayableCellRenderer() { + setHorizontalAlignment(CENTER); + } +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CoordinationServiceNamespace.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CoordinationServiceNamespace.java new file mode 100644 index 0000000000..1b1c4a1227 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CoordinationServiceNamespace.java @@ -0,0 +1,33 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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; + +/** + * Namespace elements for auto ingest coordination service nodes. + */ +final class CoordinationServiceNamespace { + private static final String ROOT = "autopsy"; + + static String getRoot() { + return ROOT; + } + + private CoordinationServiceNamespace() { + } +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DurationCellRenderer.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DurationCellRenderer.java new file mode 100644 index 0000000000..6bac0a996b --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DurationCellRenderer.java @@ -0,0 +1,74 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.Component; +import java.time.Duration; +import javax.swing.JTable; +import static javax.swing.SwingConstants.CENTER; + +/** + * A JTable cell renderer that renders a duration represented as a long as a + * string with days, hours, minutes, and seconds components. It center-aligns + * cell content and grays out the cell if the table is disabled. + */ +class DurationCellRenderer extends GrayableCellRenderer { + + private static final long serialVersionUID = 1L; + + DurationCellRenderer() { + setHorizontalAlignment(CENTER); + } + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { + if (value instanceof Long) { + { + Duration d = Duration.ofMillis((long) value); + if (d.isNegative()) { + d = Duration.ofMillis(-(long) value); + } + + String result; + long days = d.toDays(); + long hours = d.minusDays(days).toHours(); + long minutes = d.minusDays(days).minusHours(hours).toMinutes(); + long seconds = d.minusDays(days).minusHours(hours).minusMinutes(minutes).getSeconds(); + + if (minutes > 0) { + if (hours > 0) { + if (days > 0) { + result = days + " d " + hours + " h " + minutes + " m " + seconds + " s"; + } else { + result = hours + " h " + minutes + " m " + seconds + " s"; + } + } else { + result = minutes + " m " + seconds + " s"; + } + } else { + result = seconds + " s"; + } + + setText(result); + } + } + grayCellIfTableNotEnabled(table, isSelected); + return this; + } +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/FileExportRuleSet.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/FileExportRuleSet.java new file mode 100644 index 0000000000..d4616036f7 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/FileExportRuleSet.java @@ -0,0 +1,1134 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.Serializable; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.Objects; +import java.util.TreeMap; +import javax.annotation.concurrent.Immutable; +import org.apache.commons.codec.DecoderException; +import org.joda.time.DateTime; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; +import org.apache.commons.codec.binary.Hex; + +/** + * Uniquely named file export rules organized into uniquely named rule sets. + */ +final class FileExportRuleSet implements Serializable, Comparable { + + private static final long serialVersionUID = 1L; + private String name; + private final TreeMap rules; + + /** + * Constructs an empty named set of uniquely named rules. + * + * @param name The name of the set. + */ + FileExportRuleSet(String name) { + this.name = name; + rules = new TreeMap<>(); + } + + /** + * Gets the name of the rule set. + * + * @return The rules set name. + */ + String getName() { + return name; + } + + /** + * Sets the name of the rule set. + * + * @param setName The name of the rule set + */ + public void setName(String setName) { + this.name = setName; + } + + /** + * Gets the uniquely named rules in the rule set. + * + * @return A map of rules with name keys, sorted by name. + */ + NavigableMap getRules() { + return Collections.unmodifiableNavigableMap(rules); + } + + /** + * Gets a rule by name. + * + * @return A rule if found, null otherwise. + */ + Rule getRule(String ruleName) { + return rules.get(ruleName); + } + + /** + * Adds a rule to this set. If there is a rule in the set with the same + * name, the existing rule is replaced by the new rule. + * + * @param rule The rule to be added to the set. + */ + void addRule(Rule rule) { + this.rules.put(rule.getName(), rule); + } + + /** + * Removes a rule from a set, if it is present. + * + * @param rule The rule to be removed from the set. + */ + void removeRule(Rule rule) { + this.rules.remove(rule.getName()); + } + + /** + * Removes a rule from a set, if it is present. + * + * @param ruleName The rule to be removed from the set. + */ + void removeRule(String ruleName) { + this.rules.remove(ruleName); + } + + /** + * @inheritDoc + */ + @Override + public boolean equals(Object that) { + if (this == that) { + return true; + } else if (!(that instanceof FileExportRuleSet)) { + return false; + } else { + FileExportRuleSet thatSet = (FileExportRuleSet) that; + return this.name.equals(thatSet.getName()); + } + } + + /** + * @inheritDoc + */ + @Override + public int hashCode() { + return this.name.hashCode(); + } + + /** + * @inheritDoc + */ + @Override + public int compareTo(FileExportRuleSet that) { + return this.name.compareTo(that.getName()); + } + + /** + * A named file export rule consisting of zero to many conditions. + */ + static final class Rule implements Serializable, Comparable { + + private static final long serialVersionUID = 1L; + private final String name; + private FileMIMETypeCondition fileTypeCondition; + private final List fileSizeConditions; + private final List artifactConditions; + + /** + * Constructs a named file export rule consisting of zero to many + * conditions. + * + * @param name The name of the rule. + */ + Rule(String name) { + this.name = name; + this.fileSizeConditions = new ArrayList<>(); + this.artifactConditions = new ArrayList<>(); + } + + /** + * Gets the name of the rule. + * + * @return The rule name. + */ + String getName() { + return this.name; + } + + /** + * Adds a file MIME type condition to the rule. If the rule already has + * a file MIME type condition, the existing condition is replaced by the + * new condition. + * + * @param condition The new file MIME type condition. + */ + void addFileMIMETypeCondition(FileMIMETypeCondition condition) { + this.fileTypeCondition = condition; + } + + /** + * Removes a file MIME type condition from the rule. + * + * @param condition The new file MIME type condition. + */ + void removeFileMIMETypeCondition() { + this.fileTypeCondition = null; + } + + /** + * Gets the file MIME type condition of a rule. + * + * @return The file MIME type condition, possibly null. + */ + FileMIMETypeCondition getFileMIMETypeCondition() { + return this.fileTypeCondition; + } + + /** + * Adds a file size condition to the rule. If the rule already has a + * file size or file size range condition, the existing condition is + * replaced by the new condition. + * + * A rule may have either a file size condition or a file size range + * condition, but not both. + * + * @param condition The new file size condition. + */ + void addFileSizeCondition(FileSizeCondition condition) { + this.fileSizeConditions.clear(); + this.fileSizeConditions.add(condition); + } + + /** + * Removes a file size condition from the rule A rule may have either a + * file size condition or a file size range condition, but not both. + * + */ + void removeFileSizeCondition() { + this.fileSizeConditions.clear(); + } + + /** + * Adds a file size range condition to the rule. If the rule already has + * a file size or file size range condition, the existing condition is + * replaced by the new condition. + * + * The file size conditions that make up the file size range condition + * are not validated. + * + * A rule may have either a file size condition or a file size range + * condtion, but not both. + * + * @param conditionOne One part of the new size range condition. + * @param conditionTwo The other part of the new size range conditon. + */ + void addFileSizeRangeCondition(FileSizeCondition conditionOne, FileSizeCondition conditionTwo) { + this.fileSizeConditions.clear(); + this.fileSizeConditions.add(conditionOne); + this.fileSizeConditions.add(conditionTwo); + } + + /** + * Gets the file size conditions of a rule. + * + * @return A list of zero to two file size conditions. + */ + List getFileSizeConditions() { + return Collections.unmodifiableList(this.fileSizeConditions); + } + + /** + * Adds a condition that requires a file to have an artifact of a given + * type with an attribute of a given type with a value comparable to a + * specified value. + * + * @param condition The new artifact condition. + */ + void addArtfactCondition(ArtifactCondition condition) { + for (ArtifactCondition ac : artifactConditions) { + if (ac.equals(condition)) { + // already exists, do not re-add + return; + } + } + this.artifactConditions.add(condition); + } + + /** + * Removes a condition that requires a file to have an artifact of a + * given type with an attribute of a given type with a value comparable + * to a specified value. + * + * @param condition The new artifact condition. + */ + void removeArtifactCondition(ArtifactCondition condition) { + this.artifactConditions.remove(condition); + } + + /** + * Removes all artifact condition that requires a file to have an + * artifact of a given type with an attribute of a given type with a + * value comparable to a specified value. + * + */ + void removeArtifactConditions() { + this.artifactConditions.clear(); + } + + /** + * Gets the artifact conditions of a rule. + * + * @return A list of artifact conditions, possibly empty. + */ + List getArtifactConditions() { + return Collections.unmodifiableList(this.artifactConditions); + } + + /** + * @inheritDoc + */ + @Override + public boolean equals(Object that) { + if (this == that) { + return true; + } else if (!(that instanceof Rule)) { + return false; + } else { + Rule thatRule = (Rule) that; + return this.name.equals(thatRule.getName()) + && conditionsAreEqual(thatRule); + } + } + + boolean conditionsAreEqual(Rule that) { + if (!Objects.equals(this.fileTypeCondition, that.getFileMIMETypeCondition())) { + return false; + } + this.fileSizeConditions.sort(null); + that.fileSizeConditions.sort(null); + if (!this.fileSizeConditions.equals(that.getFileSizeConditions())) { + return false; + } + this.artifactConditions.sort(null); + that.artifactConditions.sort(null); + return this.artifactConditions.equals(that.getArtifactConditions()); + } + + /** + * @inheritDoc + */ + @Override + public int hashCode() { + return this.name.hashCode(); + } + + /** + * @inheritDoc + */ + @Override + public int compareTo(Rule that) { + return this.name.compareTo(that.getName()); + } + + /** + * Evaluates a rule to determine if there are any files that satisfy the + * rule. + * + * @param dataSourceId The data source id of the files. + * + * @return A list of file ids, possibly empty. + * + * @throws + * org.sleuthkit.autopsy.autoingest.fileexporter.ExportRuleSet.ExportRulesException + */ + List evaluate(long dataSourceId) throws ExportRulesException { + try { + SleuthkitCase db = Case.getCurrentCase().getSleuthkitCase(); + try (SleuthkitCase.CaseDbQuery queryResult = db.executeQuery(getQuery(dataSourceId))) { + ResultSet resultSet = queryResult.getResultSet(); + List fileIds = new ArrayList<>(); + while (resultSet.next()) { + fileIds.add(resultSet.getLong("obj_id")); + } + return fileIds; + } + } catch (IllegalStateException ex) { + throw new ExportRulesException("No current case", ex); + } catch (TskCoreException ex) { + throw new ExportRulesException("Error querying case database", ex); + } catch (SQLException ex) { + throw new ExportRulesException("Error processing result set", ex); + } + } + + /** + * Gets an SQL query statement that returns the object ids (column name + * is files.obj_id) of the files that satisfy the rule. + * + * @param dataSourceId The data source id of the files. + * + * @return The SQL query. + * + * @throws ExportRulesException If the artifact type or attribute type + * for a condition does not exist. + */ + private String getQuery(long dataSourceId) throws ExportRulesException { + String query = "SELECT DISTINCT files.obj_id FROM tsk_files AS files"; + if (!this.artifactConditions.isEmpty()) { + for (int i = 0; i < this.artifactConditions.size(); ++i) { + query += String.format(", blackboard_artifacts AS arts%d, blackboard_attributes AS attrs%d", i, i); + } + } + query += (" WHERE meta_type=1 AND mime_type IS NOT NULL AND md5 IS NOT NULL AND files.data_source_obj_id = " + dataSourceId); + + List conditions = this.getConditionClauses(); + if (!conditions.isEmpty()) { + for (int i = 0; i < conditions.size(); ++i) { + query += " AND " + conditions.get(i); + } + } + return query; + } + + /** + * Gets the SQL condition clauses for all the conditions. + * + * @return A collection of SQL condition clauses. + * + * @throws ExportRulesException If the artifact type or attribute type + * for a condition does not exist. + */ + private List getConditionClauses() throws ExportRulesException { + List conditions = new ArrayList<>(); + if (null != this.fileTypeCondition) { + conditions.add(fileTypeCondition.getConditionClause()); + } + if (!this.fileSizeConditions.isEmpty()) { + for (FileSizeCondition condition : this.fileSizeConditions) { + conditions.add(condition.getConditionClause()); + } + } + if (!this.artifactConditions.isEmpty()) { + for (int i = 0; i < this.artifactConditions.size(); ++i) { + conditions.add(this.artifactConditions.get(i).getConditionClause(i)); + } + } + return conditions; + } + + /** + * Relational operators that can be used to define rule conditions. + */ + enum RelationalOp { + + Equals("="), + LessThanEquals("<="), + LessThan("<"), + GreaterThanEquals(">="), + GreaterThan(">"), + NotEquals("!="); + + private String symbol; + private static final Map symbolToEnum = new HashMap<>(); + + static { + for (RelationalOp op : RelationalOp.values()) { + symbolToEnum.put(op.getSymbol(), op); + } + } + + /** + * Constructs a relational operator enum member that can are used to + * define rule conditions. + * + * @param symbol The symbolic form of the operator. + */ + private RelationalOp(String symbol) { + this.symbol = symbol; + } + + /** + * Gets the symbolic form of the operator. + * + * @return The operator symbol. + */ + String getSymbol() { + return this.symbol; + } + + /** + * Looks up the relational operator with a given symbol. + * + * @return The relational operator or null if there is no operator + * for the symbol. + */ + static RelationalOp fromSymbol(String symbol) { + return symbolToEnum.get(symbol); + } + + } + + /** + * A condition that requires a file to be of a specified MIME type. + */ + @Immutable + static final class FileMIMETypeCondition implements Serializable, Comparable { + + private static final long serialVersionUID = 1L; + private final String mimeType; + private final RelationalOp operator; + + /** + * Constructs a condition that requires a file to be of a specified + * MIME type. + * + * @param mimeType The MIME type. + */ + FileMIMETypeCondition(String mimeType, RelationalOp operator) { + this.mimeType = mimeType; + this.operator = operator; + } + + /** + * Gets the MIME type required by the condition. + * + * @return The MIME type. + */ + String getMIMEType() { + return mimeType; + } + + /** + * Gets the operator required by the condition. + * + * @return the operator. + */ + public RelationalOp getRelationalOp() { + return operator; + } + + /** + * @inheritDoc + */ + @Override + public boolean equals(Object that) { + if (this == that) { + return true; + } else if (!(that instanceof FileMIMETypeCondition)) { + return false; + } else { + FileMIMETypeCondition thatCondition = (FileMIMETypeCondition) that; + return ((this.mimeType.equals(thatCondition.getMIMEType())) + && (this.operator == thatCondition.getRelationalOp())); + } + } + + /** + * @inheritDoc + */ + @Override + public int hashCode() { + return this.mimeType.hashCode(); + } + + @Override + public int compareTo(FileMIMETypeCondition that) { + return this.mimeType.compareTo(that.getMIMEType()); + } + + /** + * Gets an SQL condition clause for the condition. + * + * @return The SQL condition clause. + */ + private String getConditionClause() { + return String.format("files.mime_type = '%s'", this.mimeType); + } + + } + + /** + * A condition that requires a file to have a size in bytes comparable + * to a specified size. + */ + @Immutable + static final class FileSizeCondition implements Serializable, Comparable { + + private static final long serialVersionUID = 1L; + private final int size; + private final SizeUnit unit; + private final Rule.RelationalOp op; + + /** + * Constructs a condition that requires a file to have a size in + * bytes comparable to a specified size. + * + * @param sizeinBytes The specified size. + * @param op The relational operator for the comparison. + */ + FileSizeCondition(int size, SizeUnit unit, Rule.RelationalOp op) { + this.size = size; + this.unit = unit; + this.op = op; + } + + /** + * Gets the size required by the condition. + * + * @return The size. + */ + int getSize() { + return size; + } + + /** + * Gets the size unit for the size required by the condition. + * + * @return The size unit. + */ + SizeUnit getUnit() { + return unit; + } + + /** + * Gets the relational operator for the condition. + * + * @return The operator. + */ + RelationalOp getRelationalOperator() { + return this.op; + } + + /** + * @inheritDoc + */ + @Override + public boolean equals(Object that) { + if (this == that) { + return true; + } else if (!(that instanceof FileSizeCondition)) { + return false; + } else { + FileSizeCondition thatCondition = (FileSizeCondition) that; + return this.size == thatCondition.getSize() + && this.unit == thatCondition.getUnit() + && this.op == thatCondition.getRelationalOperator(); + } + } + + /** + * @inheritDoc + */ + @Override + public int hashCode() { + int hash = 7; + hash = 9 * hash + this.size; + hash = 11 * hash + this.unit.hashCode(); + hash = 13 * hash + this.op.hashCode(); + return hash; + } + + @Override + public int compareTo(FileSizeCondition that) { + int retVal = this.unit.compareTo(that.getUnit()); + if (0 != retVal) { + return retVal; + } + retVal = new Long(this.size).compareTo(new Long(that.getSize())); + if (0 != retVal) { + return retVal; + } + return this.op.compareTo(that.getRelationalOperator()); + } + + /** + * Gets an SQL condition clause for the condition. + * + * @return The SQL condition clause. + */ + private String getConditionClause() { + return String.format("files.size %s %d", op.getSymbol(), size * unit.getMultiplier()); + } + + /** + * Size units used to define file size conditions. + */ + enum SizeUnit { + + Bytes(1L), + Kilobytes(1024L), + Megabytes(1024L * 1024), + Gigabytes(1024L * 1024 * 1024), + Terabytes(1024L * 1024 * 1024 * 1024), + Petabytes(1024L * 1024 * 1024 * 1024 * 1024); + private final long multiplier; + + /** + * Constructs a member of this enum. + * + * @param multiplier A multiplier for the size field of a file + * size condition. + */ + private SizeUnit(long multiplier) { + this.multiplier = multiplier; + } + + /** + * Gets the multiplier for the size field of a file size + * condition. + * + * @return The multiplier. + */ + long getMultiplier() { + return this.multiplier; + } + } + } + + /** + * A condition that requires a file to have an artifact of a given type + * with an attribute of a given type with a value comparable to a + * specified value. + */ + @Immutable + static final class ArtifactCondition implements Serializable, Comparable { + + private static final long serialVersionUID = 1L; + private final String artifactTypeName; + private final String attributeTypeName; + private final BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE attributeValueType; + private Integer intValue; + private Long longValue; + private Double doubleValue; + private String stringValue; + private DateTime dateTimeValue; + private byte[] byteValue; + private final RelationalOp op; + private String treeDisplayName; + + /** + * Constructs a condition that requires a file to have an artifact + * of a given type. + * + * @param treeDisplayName The name to display in the tree + * @param artifactTypeName The name of the artifact type. + * @param attributeTypeName The name of the attribute type. + * @param value The String representation of the value. + * @param attributeValueType The type of the value being passed in. + * @param op The relational operator for the + * comparison. + */ + ArtifactCondition(String artifactTypeName, String attributeTypeName, String value, + BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE attributeValueType, RelationalOp op) throws IllegalArgumentException { + this.artifactTypeName = artifactTypeName; + this.attributeTypeName = attributeTypeName; + this.attributeValueType = attributeValueType; + this.treeDisplayName = artifactTypeName; + this.intValue = null; + this.longValue = null; + this.doubleValue = null; + this.stringValue = null; + this.byteValue = null; + this.op = op; + try { + switch (this.attributeValueType) { + case STRING: + this.stringValue = value; + break; + case INTEGER: + this.intValue = Integer.parseInt(value); + break; + case LONG: + this.longValue = Long.parseLong(value); + break; + case DOUBLE: + this.doubleValue = Double.parseDouble(value); + break; + case BYTE: + try { + this.byteValue = Hex.decodeHex(value.toCharArray()); + } catch (DecoderException ex) { + this.byteValue = null; + throw new IllegalArgumentException("Bad hex decode"); //NON-NLS + } + break; + case DATETIME: + long result = Long.parseLong(value); + this.dateTimeValue = new DateTime(result); + break; + default: + throw new NumberFormatException("Bad type chosen"); //NON-NLS + } + } catch (NumberFormatException ex) { + this.intValue = null; + this.longValue = null; + this.doubleValue = null; + this.stringValue = null; + this.byteValue = null; + this.dateTimeValue = null; + throw new IllegalArgumentException(ex); + } + } + + /** + * Gets the artifact type name for this condition. + * + * @return The type name. + */ + String getArtifactTypeName() { + return this.artifactTypeName; + } + + /** + * Gets the tree display name for this condition. + * + * @return The tree display name for this condition. + */ + String getTreeDisplayName() { + return this.treeDisplayName; + } + + /** + * Sets the tree display name for this condition. + * + * @param name The tree display name for this condition. + */ + void setTreeDisplayName(String name) { + this.treeDisplayName = name; + } + + /** + * Gets the attribute type name for this condition. + * + * @return The type name. + */ + String getAttributeTypeName() { + return this.attributeTypeName; + } + + /** + * Gets the value type for this condition. + * + * @return The value type. + */ + BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE getAttributeValueType() { + return this.attributeValueType; + } + + /** + * Gets the integer value for this condition. + * + * @return The value, may be null. + */ + Integer getIntegerValue() { + return this.intValue; + } + + /** + * Gets the long value for this condition. + * + * @return The value, may be null. + */ + Long getLongValue() { + return this.longValue; + } + + /** + * Gets the double value for this condition. + * + * @return The value, may be null. + */ + Double getDoubleValue() { + return this.doubleValue; + } + + /** + * Gets the string value for this condition. + * + * @return The value, may be null. + */ + String getStringValue() { + return this.stringValue; + } + + /** + * Gets the byte value for this condition. + * + * @return The value, may be null. + */ + byte[] getByteValue() { + return this.byteValue; + } + + /** + * Gets the DateTime value for this condition. + * + * @return The value, may be null. + */ + DateTime getDateTimeValue() { + return this.dateTimeValue; + } + + /** + * Gets the string representation of the value, regardless of the + * data type + * + * @return The value, may be null. + */ + String getStringRepresentationOfValue() { + String valueText = ""; + switch (this.attributeValueType) { + case BYTE: + valueText = new String(Hex.encodeHex(getByteValue())); + break; + case DATETIME: + valueText = ""; + break; + case DOUBLE: + valueText = getDoubleValue().toString(); + break; + case INTEGER: + valueText = getIntegerValue().toString(); + break; + case LONG: + valueText = getLongValue().toString(); + break; + case STRING: + valueText = getStringValue(); + break; + default: + valueText = "Undefined"; + break; + } + return valueText; + } + + /** + * Gets the relational operator for the condition. + * + * @return The operator. + */ + RelationalOp getRelationalOperator() { + return this.op; + } + + /** + * @inheritDoc + */ + @Override + public boolean equals(Object that) { + if (this == that) { + return true; + } else if (!(that instanceof ArtifactCondition)) { + return false; + } else { + ArtifactCondition thatCondition = (ArtifactCondition) that; + return this.artifactTypeName.equals(thatCondition.getArtifactTypeName()) + && this.attributeTypeName.equals(thatCondition.getAttributeTypeName()) + && this.attributeValueType == thatCondition.getAttributeValueType() + && this.op == thatCondition.getRelationalOperator() + && Objects.equals(this.intValue, thatCondition.getIntegerValue()) + && Objects.equals(this.longValue, thatCondition.getLongValue()) + && Objects.equals(this.doubleValue, thatCondition.getDoubleValue()) + && Objects.equals(this.stringValue, thatCondition.getStringValue()) + && Arrays.equals(this.byteValue, thatCondition.getByteValue()) + && Objects.equals(this.dateTimeValue, thatCondition.getDateTimeValue()); + } + } + + /** + * @inheritDoc + */ + @Override + public int hashCode() { + int hash = 7; + hash = 9 * hash + this.artifactTypeName.hashCode(); + hash = 13 * hash + this.attributeTypeName.hashCode(); + hash = 11 * hash + this.attributeValueType.hashCode(); + hash = 13 * hash + this.op.hashCode(); + hash = 15 * hash + Objects.hashCode(this.intValue); + hash = 7 * hash + Objects.hashCode(this.longValue); + hash = 17 * hash + Objects.hashCode(this.doubleValue); + hash = 8 * hash + Objects.hashCode(this.stringValue); + hash = 27 * hash + Objects.hashCode(this.byteValue); + hash = 3 * hash + Objects.hashCode(this.dateTimeValue); + return hash; + } + + /** + * @inheritDoc + */ + @Override + public int compareTo(ArtifactCondition that) { + int retVal = this.artifactTypeName.compareTo(that.getArtifactTypeName()); + if (0 != retVal) { + return retVal; + } + retVal = this.attributeTypeName.compareTo(that.getAttributeTypeName()); + if (0 != retVal) { + return retVal; + } + retVal = this.attributeValueType.compareTo(that.getAttributeValueType()); + if (0 != retVal) { + return retVal; + } else { + switch (this.attributeValueType) { + case STRING: + retVal = this.stringValue.compareTo(that.getStringValue()); + if (0 != retVal) { + return retVal; + } + break; + case INTEGER: + retVal = this.intValue.compareTo(that.getIntegerValue()); + if (0 != retVal) { + return retVal; + } + break; + case LONG: + retVal = this.longValue.compareTo(that.getLongValue()); + if (0 != retVal) { + return retVal; + } + break; + case DOUBLE: + retVal = this.doubleValue.compareTo(that.getDoubleValue()); + if (0 != retVal) { + return retVal; + } + break; + case BYTE: + if (Arrays.equals(this.byteValue, that.getByteValue())) { + return 0; + } else { + return 1; + } + case DATETIME: + retVal = this.dateTimeValue.compareTo(that.getDateTimeValue()); + if (0 != retVal) { + return retVal; + } + break; + } + } + return this.op.compareTo(that.getRelationalOperator()); + } + + /** + * Gets the SQL condition clause for the condition. + * + * @param index The index of the condition within the collection of + * conditions that make up a rule. It is used for table + * name aliasing. + * + * @return The SQL clause as a string, without leading or trailing + * spaces. + * + * @throws ExportRulesException If the artifact type or attribute + * type for the condition does not + * exist. + */ + private String getConditionClause(int index) throws ExportRulesException { + Case currentCase = Case.getCurrentCase(); + SleuthkitCase caseDb = currentCase.getSleuthkitCase(); + BlackboardArtifact.Type artifactType; + BlackboardAttribute.Type attributeType; + try { + artifactType = caseDb.getArtifactType(artifactTypeName); + } catch (TskCoreException ex) { + throw new ExportRulesException(String.format("The specified %s artifact type does not exist in case database for %s", artifactTypeName, currentCase.getCaseDirectory()), ex); + } + try { + attributeType = caseDb.getAttributeType(attributeTypeName); + } catch (TskCoreException ex) { + throw new ExportRulesException(String.format("The specified %s attribute type does not exist in case database for %s", attributeTypeName, currentCase.getCaseDirectory()), ex); + } + + String clause = String.format("files.obj_id = arts%d.obj_id AND arts%d.artifact_type_id = %d AND attrs%d.artifact_id = arts%d.artifact_id AND attrs%d.attribute_type_id = %d AND ", + index, index, artifactType.getTypeID(), index, index, index, attributeType.getTypeID()); + switch (this.attributeValueType) { + case INTEGER: + clause += String.format("attrs%d.value_int32 %s %d", index, this.op.getSymbol(), this.intValue); + break; + case LONG: + clause += String.format("attrs%d.value_int64 %s %d", index, this.op.getSymbol(), this.longValue); + break; + case DOUBLE: + clause += String.format("attrs%d.value_double %s %f", index, this.op.getSymbol(), this.doubleValue); + break; + case STRING: + clause += String.format("attrs%d.value_text %s '%s'", index, this.op.getSymbol(), this.stringValue); + break; + case BYTE: + clause += String.format("attrs%d.value_byte %s decode('%s', 'hex')", index, this.op.getSymbol(), new String(Hex.encodeHex(getByteValue()))); + break; + case DATETIME: + clause += String.format("attrs%d.value_int64 %s '%s'", index, this.op.getSymbol(), this.dateTimeValue.getMillis()/1000); + break; + } + return clause; + } + + } + + } + + /** + * Exception type thrown by the export rules class. + */ + public final static class ExportRulesException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Constructs an exception. + * + * @param message The exception message. + */ + private ExportRulesException(String message) { + super(message); + } + + /** + * Constructs an exception. + * + * @param message The exception message. + * @param cause The exception cause. + */ + private ExportRulesException(String message, Throwable cause) { + super(message, cause); + } + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/FileExportSettings.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/FileExportSettings.java new file mode 100644 index 0000000000..86e5bd768f --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/FileExportSettings.java @@ -0,0 +1,282 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.Serializable; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.TreeMap; +import org.openide.util.io.NbObjectInputStream; +import org.openide.util.io.NbObjectOutputStream; +import org.sleuthkit.autopsy.coreutils.PlatformUtil; + +/** + * Settings for the export of files based on user-defined export rules. + */ +final class FileExportSettings implements Serializable { + + private static final long serialVersionUID = 1L; + private static final String DEFAULT_MASTER_CATALOG_NAME = "interim"; + private static final String DEFAULT_EXPORT_COMPLETED_FILE_NAME = "EXTRACTION_FINISHED"; + private static final String DEFAULT_RULES_EVALUATED_NAME = "SORTER_FINISHED"; + private static final String SETTINGS_DIRECTORY = org.sleuthkit.autopsy.experimental.configuration.SharedConfiguration.FILE_EXPORTER_FOLDER; + private static final String SETTINGS_FILE_NAME = org.sleuthkit.autopsy.experimental.configuration.SharedConfiguration.FILE_EXPORTER_SETTINGS_FILE; + private TreeMap ruleSets; + private String filesRootDirectory; + private String reportsRootDirectory; + private String masterCatalogName; + private String exportCompletedFlagFileName; + private String rulesEvaluatedFlagFileName; + private boolean enabled; + + /** + * Saves the file export settings to secondary storage. Existing settings + * are overwritten. + * + * @param settings The settings to save. + * + * @throws + * org.sleuthkit.autopsy.autoingest.FileExportSettings.PersistenceException + */ + static synchronized void store(FileExportSettings settings) throws PersistenceException { + Path folderPath = Paths.get(PlatformUtil.getUserConfigDirectory(), SETTINGS_DIRECTORY); + Path filePath = Paths.get(folderPath.toString(), SETTINGS_FILE_NAME); + try { + Files.createDirectories(folderPath); + try (NbObjectOutputStream out = new NbObjectOutputStream(new FileOutputStream(filePath.toString()))) { + out.writeObject(settings); + } + } catch (IOException ex) { + throw new PersistenceException(String.format("Failed to write settings to %s", filePath), ex); + } + } + + /** + * Reads the file export settings from secondary storage. + * + * @return The settings. + * + * @throws + * org.sleuthkit.autopsy.autoingest.FileExportSettings.PersistenceException + */ + static synchronized FileExportSettings load() throws PersistenceException { + Path filePath = Paths.get(PlatformUtil.getUserConfigDirectory(), SETTINGS_DIRECTORY, SETTINGS_FILE_NAME); + try { + // if the File Exporter settings file doesn't exist, return default settings + if (!filePath.toFile().exists()) { + return new FileExportSettings(); + } + + try (NbObjectInputStream in = new NbObjectInputStream(new FileInputStream(filePath.toString()))) { + FileExportSettings settings = (FileExportSettings) in.readObject(); + return settings; + } + } catch (IOException | ClassNotFoundException ex) { + throw new PersistenceException(String.format("Failed to read settings from %s", filePath), ex); + + } + } + + /** + * Constructs an instance of the settings for the export of files based on + * user-defined export rules. + */ + FileExportSettings() { + enabled = false; + ruleSets = new TreeMap<>(); + filesRootDirectory = null; + reportsRootDirectory = null; + masterCatalogName = DEFAULT_MASTER_CATALOG_NAME; + exportCompletedFlagFileName = DEFAULT_EXPORT_COMPLETED_FILE_NAME; + rulesEvaluatedFlagFileName = DEFAULT_RULES_EVALUATED_NAME; + } + + /** + * Sets file export enabled state. + * + * @param state The state to set. + */ + void setFileExportEnabledState(boolean state) { + this.enabled = state; + } + + /** + * Gets file export enabled state. + * + * @return The enabled state. + */ + boolean getFileExportEnabledState() { + return this.enabled; + } + + + /** + * Sets the file export rules. + * + * @param ruleSets A map of rules with name keys, sorted by name. + * + */ + void setRuleSets(TreeMap ruleSets) { + this.ruleSets = ruleSets; + } + + /** + * Gets the file export rules. + * + * @return A map of rules with name keys, sorted by name. + */ + TreeMap getRuleSets() { + return this.ruleSets; + } + + /** + * Sets the root file export directory. + * + * @param filesRootDirectory The path of the root directory for files + * export. + */ + void setFilesRootDirectory(Path filesRootDirectory) { + this.filesRootDirectory = filesRootDirectory.toString(); + } + + /** + * Gets the root file output directory. + * + * @return The path of the root directory for files export, may be null. + */ + Path getFilesRootDirectory() { + if (null != filesRootDirectory) { + return Paths.get(this.filesRootDirectory); + } else { + return null; + } + } + + /** + * Sets the root reports (file catalogs) directory. + * + * @param reportsRootDirectory The path of the root directory for reports + * (file catalogs). + */ + void setReportsRootDirectory(Path reportsRootDirectory) { + this.reportsRootDirectory = reportsRootDirectory.toString(); + } + + /** + * Gets the root file output directory. + * + * @return The path of the root directory for reports (file catalogs), may + * be null. + */ + Path getReportsRootDirectory() { + if (null != this.reportsRootDirectory) { + return Paths.get(this.reportsRootDirectory); + } else { + return null; + } + } + + /** + * Sets the name of the master catalog of exported files. + * + * @param name The catalog name. + */ + void setMasterCatalogName(String name) { + this.masterCatalogName = name; + } + + /** + * Gets the name of the master catalog of exported files. + * + * @return The catalog name. + */ + String getMasterCatalogName() { + return this.masterCatalogName; + } + + /** + * Sets the name of the file written to indicate file export is completed. + * + * @param fileName The file name. + */ + void setExportCompletedFlagFileName(String fileName) { + this.exportCompletedFlagFileName = fileName; + } + + /** + * Gets the name of the file written to indicate file export is completed. + * + * @return The file name. + */ + String getExportCompletedFlagFileName() { + return this.exportCompletedFlagFileName; + } + + /** + * Sets the name of the file written to indicate file export rule evaluation + * is completed. + * + * @param fileName The file name. + */ + void setRulesEvaluatedFlagFileName(String fileName) { + this.rulesEvaluatedFlagFileName = fileName; + } + + /** + * Gets the name of the file written to indicate file export rule evaluation + * is completed. + * + * @return The file name. + */ + String getRulesEvaluatedFlagFileName() { + return rulesEvaluatedFlagFileName; + } + + /** + * Exception thrown if there is a problem storing or loading the settings. + */ + public final static class PersistenceException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Constructs an exception. + * + * @param message The exception message. + */ + private PersistenceException(String message) { + super(message); + } + + /** + * Constructs an exception. + * + * @param message The exception message. + * @param cause The exception cause. + */ + private PersistenceException(String message, Throwable cause) { + super(message, cause); + } + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/FileExporter.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/FileExporter.java new file mode 100644 index 0000000000..65c9fc777f --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/FileExporter.java @@ -0,0 +1,645 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.fasterxml.jackson.core.JsonEncoding; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.Supplier; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.FileUtil; +import org.sleuthkit.autopsy.datamodel.ContentUtils; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.FileSystem; +import org.sleuthkit.datamodel.IngestJobInfo; +import org.sleuthkit.datamodel.IngestModuleInfo; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.TskData; +import org.sleuthkit.autopsy.experimental.autoingest.FileExportRuleSet.Rule; + +/** + * Exports the files that satisfy user-defined file export rules from a set of + * data sources associated with a device. + */ +final class FileExporter { + + private static final int FAT_NTFS_FLAGS = TskData.TSK_FS_TYPE_ENUM.TSK_FS_TYPE_FAT12.getValue() | TskData.TSK_FS_TYPE_ENUM.TSK_FS_TYPE_FAT16.getValue() | TskData.TSK_FS_TYPE_ENUM.TSK_FS_TYPE_FAT32.getValue() | TskData.TSK_FS_TYPE_ENUM.TSK_FS_TYPE_NTFS.getValue(); + // File Exporter requires File Type Identification and Hash Lookup modules to run beforehand. + private static final List REQUIRED_MODULE_CANONICAL_NAME = Arrays.asList("org.sleuthkit.autopsy.modules.filetypeid.FileTypeIdModuleFactory", "org.sleuthkit.autopsy.modules.hashdatabase.HashLookupModuleFactory"); + private String deviceId; + private FileExportSettings settings; + private Path filesDirPath; + private Path reportsDirPath; + private JsonFactory jsonGeneratorFactory; + private JsonGenerator masterCatalog; + private Map ruleNamesToCatalogs; + private List flagFilePaths; + + FileExporter() throws FileExportException { + try { + settings = FileExportSettings.load(); + } catch (FileExportSettings.PersistenceException ex) { + throw new FileExportException("Error initializing File Exporter", ex); + } + } + + boolean isEnabled() { + return settings.getFileExportEnabledState(); + } + + /** + * Exports the files that satisfy user-defined file export rules from a set + * of data sources associated with a device. + * + * @param deviceId The device id. + * @param dataSources The data sources. + * @param cancelCheck A function used to check if the file AutoInjectJob process + * should be terminated. + * @throws FileExportException if there is an error in the export process. + */ + void process(String deviceId, List dataSources, Supplier cancelCheck) throws FileExportException { + this.deviceId = deviceId; + try { + if (!isEnabled() || cancelCheck.get()) { + return; + } + // File Exporter requires several ingest modules to run beforehand. + // Verify that these modules were enabled for the current data source(s) + if (!verifyPrerequisites(dataSources)) { + // throw exception to pause auto ingest + throw new FileExportException("File Exporter prerequisite ingest modules were not turned on"); + } + + setUp(); + for (Content dataSource : dataSources) { + Map> fileIdsToRuleNames = evaluateRules(dataSource, cancelCheck); + if (cancelCheck.get()) { + break; + } + exportFiles(fileIdsToRuleNames, cancelCheck); + } + closeCatalogs(); + writeFlagFiles(); + } catch (FileExportSettings.PersistenceException | FileExportRuleSet.ExportRulesException | TskCoreException | IOException ex) { + throw new FileExportException("Error occurred during file export", ex); + } + } + + /** + * Verifies that all AFE prerequisite ingest modules were enabled for all + * input data sources + * + * @param dataSources List of data sources to check + * + * @return True if all AFE prerequisite ingest modules were enabled, + * false otherwise + * + * @throws org.sleuthkit.autopsy.autoingest.FileExporter.FileExportException + */ + private boolean verifyPrerequisites(List dataSources) throws FileExportException { + SleuthkitCase skCase = Case.getCurrentCase().getSleuthkitCase(); + List ingestJobs = new ArrayList<>(); + try { + // all ingest jobs that were processed as part of this case + ingestJobs = skCase.getIngestJobs(); + } catch (TskCoreException ex) { + throw new FileExportException("Failed to obtain ingest jobs", ex); + } + + // for each data source + for (Content dataSource : dataSources) { + // verify that all required modules were enabled + for (String requiredModuleCanonicalName : REQUIRED_MODULE_CANONICAL_NAME) { + boolean requiredModuleWasEnabled = false; + /* NOTE: there could have been multiple ingest jobs performed for + each data source. We have to loop over all ingest jobs for the data + source to check whether the required ingest module was enabled + in either one of them. */ + for (IngestJobInfo ingestJob : ingestJobs) { + if (ingestJob.getStatus() != IngestJobInfo.IngestJobStatusType.COMPLETED) { + continue; + } + + if (dataSource.getId() != ingestJob.getObjectId()) { + // ingest job was for a differnt data source + continue; + } + + // loop over job's ingest module list to check whether it contains the required module + if (isRequiredModuleEnabled(requiredModuleCanonicalName, ingestJob)) { + requiredModuleWasEnabled = true; + break; + } + } + if (!requiredModuleWasEnabled) { + // required module did not run + return false; + } + } + } + // if we are here then all requirements were met + return true; + } + + /** + * Loop over ingest job's ingest module list to check whether it contains + * the required module + * + * @param requiredModuleCanonicalName Canonical name of the ingest module to + * look for + * @param ingestJob Ingest job object + * + * @return True if ingest job contained required ingest module, false + * otherwise + */ + private boolean isRequiredModuleEnabled(String requiredModuleCanonicalName, IngestJobInfo ingestJob) { + for (IngestModuleInfo ingestModuleInfo : ingestJob.getIngestModuleInfo()) { + String canonicalName = ingestModuleInfo.getUniqueName().split("-")[0]; + if (canonicalName.equals(requiredModuleCanonicalName)) { + return true; + } + } + return false; + } + + /** + * Sets up the export process by loading user settings, creating output + * directories, creating the JSON generator for the master files catalog, + * and determining the flag file paths. + * + * @throws FileExportSettings.PersistenceException If there is a problem + * loading the settings. + * @throws IOException If there is a problem + * creating the export + * directories or the JSON + * generator. + */ + private void setUp() throws FileExportSettings.PersistenceException, IOException { + /* + * Create the file export and report root directory paths. + */ + filesDirPath = settings.getFilesRootDirectory(); + filesDirPath = filesDirPath.resolve(deviceId); + reportsDirPath = settings.getReportsRootDirectory(); + reportsDirPath = reportsDirPath.resolve(deviceId); + + /* + * Delete the results of a previous run, if any. Results deletion cleans + * up for a crashed auto ingest job. + */ + FileUtil.deleteDir(reportsDirPath.toFile()); + FileUtil.deleteDir(filesDirPath.toFile()); + + /* + * Create the file export and report root directories. + */ + Files.createDirectories(filesDirPath); + Files.createDirectories(reportsDirPath); + + /* + * Create the JSON generator for the master catalog of exported files + * and a map to hold the JSON generators for the rule catalogs. + */ + jsonGeneratorFactory = new JsonFactory(); + jsonGeneratorFactory.setRootValueSeparator("\r\n"); + String catalogName = settings.getMasterCatalogName(); + String catalogDir = catalogName.substring(0, 1).toUpperCase() + catalogName.substring(1); + Path catalogPath = Paths.get(reportsDirPath.toString(), catalogDir, catalogName + ".json"); + Files.createDirectories(catalogPath.getParent()); + File catalogFile = catalogPath.toFile(); + masterCatalog = jsonGeneratorFactory.createGenerator(catalogFile, JsonEncoding.UTF8); + ruleNamesToCatalogs = new HashMap<>(); + + /* + * Set up the paths for the flag files that are written to signal export + * is complete for thsi device. + */ + flagFilePaths = new ArrayList<>(); + flagFilePaths.add(Paths.get(filesDirPath.toString(), settings.getExportCompletedFlagFileName())); + flagFilePaths.add(Paths.get(reportsDirPath.toString(), catalogDir, settings.getExportCompletedFlagFileName())); + flagFilePaths.add(Paths.get(reportsDirPath.toString(), settings.getRulesEvaluatedFlagFileName())); + } + + /** + * Evaluates each file export rule to produce a map that associates the file + * id of each file to be exported with a list of the names of the rules + * satisfied by the file. + * + * @return The map of file ids to rule name lists. + */ + /** + * Evaluates each file export rule for a data source to produce a map that + * associates the file id of each file to be exported with a list of the + * names of the rules satisfied by the file. + * + * @param dataSource The data source. + * @param cancelCheck A function used to check if the file AutoInjectJob process + * should be terminated. + * + * @return The map of file ids to rule name lists. + * + * @throws FileExportRuleSet.ExportRulesException If there is a problem + * evaluating a rule. + */ + private Map> evaluateRules(Content dataSource, Supplier cancelCheck) throws FileExportRuleSet.ExportRulesException { + TreeMap ruleSets = settings.getRuleSets(); + Map> fileIdsToRuleNames = new HashMap<>(); + for (FileExportRuleSet ruleSet : ruleSets.values()) { + for (Rule rule : ruleSet.getRules().values()) { + if (cancelCheck.get()) { + return fileIdsToRuleNames; + } + List fileIds = rule.evaluate(dataSource.getId()); + for (Long fileId : fileIds) { + List ruleList; + if (!fileIdsToRuleNames.containsKey(fileId)) { + ruleList = new ArrayList<>(); + fileIdsToRuleNames.put(fileId, ruleList); + } else { + ruleList = fileIdsToRuleNames.get(fileId); + } + ruleList.add(rule.getName()); + } + } + } + return fileIdsToRuleNames; + } + + /** + * Writes each file to be exported to secondary storage and makes entries + * for the file in the master catalog and the catalogs of the export rules + * the file satisfied. + * + * @param fileIdsToRuleNames The map of file ids to rule name lists. + * @param cancelCheck A function used to check if the file write process + * should be terminated. + * + * @throws TskCoreException If there is a problem querying file metadata or + * getting file content. + * @throws IOException If there is a problem writing a file to + * secondary storage. + */ + private void exportFiles(Map> fileIdsToRuleNames, Supplier cancelCheck) throws TskCoreException, IOException { + for (Map.Entry> entry : fileIdsToRuleNames.entrySet()) { + if (cancelCheck.get()) { + return; + } + exportFile(entry.getKey(), entry.getValue(), cancelCheck); + } + } + + /** + * Writes a file to secondary storage and makes entries for the file in the + * master catalog and the catalogs of the export rules the file satisfied. + * + * @param fileId The id of the file to export. + * @param ruleNames The names of the export rules the file satisfied. + * @param progress The progress reporter for this module. + * @param cancelCheck A function used to check if the file write process + * should be terminated. + * + * @throws TskCoreException If there is a problem querying file metadata or + * getting file content. + * @throws IOException If there is a problem writing the file to + * storage. + */ + private void exportFile(Long fileId, List ruleNames, Supplier cancelCheck) throws TskCoreException, IOException { + AbstractFile file = Case.getCurrentCase().getSleuthkitCase().getAbstractFileById(fileId); + if (!shouldExportFile(file)) { + return; + } + Map> artifactsToAttributes = new HashMap<>(); + List artifacts = file.getAllArtifacts(); + for (BlackboardArtifact artifact : artifacts) { + artifactsToAttributes.put(artifact, artifact.getAttributes()); + } + Path filePath = exportFileToSecondaryStorage(file, cancelCheck); + if (filePath == null) { + return; + } + addFileToCatalog(file, artifactsToAttributes, filePath, masterCatalog); + for (String ruleName : ruleNames) { + JsonGenerator ruleCatalog = this.ruleNamesToCatalogs.get(ruleName); + if (null == ruleCatalog) { + Path catalogPath = Paths.get(reportsDirPath.toString(), ruleName, "catalog.json"); + Files.createDirectories(catalogPath.getParent()); + File catalogFile = catalogPath.toFile(); + ruleCatalog = jsonGeneratorFactory.createGenerator(catalogFile, JsonEncoding.UTF8); + ruleNamesToCatalogs.put(ruleName, ruleCatalog); + } + addFileToCatalog(file, artifactsToAttributes, filePath, ruleCatalog); + } + } + + /** + * Determines whether or not a file should be exported, even though it + * satisfies a file export rule. Unallocated space files, pseudo-files, and + * special NTFS or FAT file system files are not eligible for export. + * + * @param candidateFile The file. + * + * @return True or false. + * + * @throws TskCoreException If there is a problem querying file metadata. + */ + private static boolean shouldExportFile(AbstractFile candidateFile) throws TskCoreException { + if (candidateFile instanceof org.sleuthkit.datamodel.File) { + /* + * Is the file in an NTFS or FAT file system? + */ + org.sleuthkit.datamodel.File file = (org.sleuthkit.datamodel.File) candidateFile; + TskData.TSK_FS_TYPE_ENUM fileSystemType = TskData.TSK_FS_TYPE_ENUM.TSK_FS_TYPE_UNSUPP; + FileSystem fileSystem = file.getFileSystem(); + if (null != fileSystem) { + fileSystemType = fileSystem.getFsType(); + } + if ((fileSystemType.getValue() & FAT_NTFS_FLAGS) == 0) { + return true; + } + + /* + * Is the NTFS or FAT file in the root directory? + */ + AbstractFile parent = file.getParentDirectory(); + boolean isInRootDir = parent.isRoot(); + + /* + * Check its meta-address and check its name for the '$' character + * and a ':' character (not a default attribute). + */ + if (isInRootDir && file.getMetaAddr() < 32) { + String name = file.getName(); + if (name.length() > 0 && name.charAt(0) == '$' && name.contains(":")) { + return false; + } + } + } + return true; + } + + /** + * Writes a file to a "hash tree" in secondary storage. For example, a file + * with MD5 hash d131dd02c5e6eec46e4bc422aef54eb4 and MIME type text/html + * would be written to the following location: + * + * outputDir/text-html/D1/31/DD/02/D131DD02C5E6EEC4 + * + * @param file The file to export. + * @param cancelCheck A function used to check if the file write process + * should be terminated. + * + * @return The path to the exported file. + * + * @throws TskCoreException If there is a problem reading from the case + * database. + * @throws IOException If the file cannot be written. + */ + private Path exportFileToSecondaryStorage(AbstractFile file, Supplier cancelCheck) throws TskCoreException, IOException { + /* + * Get the MIME type of the file to be used as a path component. + */ + String mimeType = file.getMIMEType(); + + /* + * Get the MD5 hash of the file to be used for path components. + */ + String md5 = file.getMd5Hash().toUpperCase(); + + /* + * Construct the export path and export the file. + */ + Path exportFilePath = Paths.get(this.filesDirPath.toString(), + mimeType.replace('/', '-'), + md5.substring(0, 2), + md5.substring(2, 4), + md5.substring(4, 6), + md5.substring(6, 8), + md5); + File exportFile = exportFilePath.toFile(); + if (!exportFile.exists()) { + Files.createDirectories(exportFilePath.getParent()); + ContentUtils.writeToFile(file, exportFile, cancelCheck); + if (cancelCheck.get()) { + Files.delete(exportFilePath); + return null; + } + } + + return exportFilePath; + } + + /** + * Adds an exported file to a catalog. + * + * @param file The file. + * @param artifactsToAttributes The artifacts associated with the file. + * @param filePath The path to the exported file. + * @param catalog The catalog. + * + * @throws IOException If there is an error while writing to the + * catalog. + * @throws TskCoreExceptiion If there is a problem querying the case + * database. + */ + private void addFileToCatalog(AbstractFile file, Map> artifactsToAttributes, Path filePath, JsonGenerator catalog) throws IOException, TskCoreException { + catalog.writeStartObject(); + String md5 = file.getMd5Hash().toUpperCase(); + catalog.writeFieldName(md5); + catalog.writeStartObject(); + catalog.writeStringField("Filename", filePath.toString()); + catalog.writeStringField("Type", file.getMIMEType()); + catalog.writeStringField("MD5", md5); + catalog.writeFieldName("File data"); + catalog.writeStartObject(); + + catalog.writeFieldName("Modified"); + catalog.writeStartArray(); + catalog.writeString(file.getMtimeAsDate()); + catalog.writeEndArray(); + + catalog.writeFieldName("Changed"); + catalog.writeStartArray(); + catalog.writeString(file.getCtimeAsDate()); + catalog.writeEndArray(); + + catalog.writeFieldName("Accessed"); + catalog.writeStartArray(); + catalog.writeString(file.getAtimeAsDate()); + catalog.writeEndArray(); + + catalog.writeFieldName("Created"); + catalog.writeStartArray(); + catalog.writeString(file.getCrtimeAsDate()); + catalog.writeEndArray(); + + catalog.writeFieldName("Extension"); + catalog.writeStartArray(); + catalog.writeString(file.getNameExtension()); + catalog.writeEndArray(); + + catalog.writeFieldName("Filename"); + catalog.writeStartArray(); + catalog.writeString(file.getName()); + catalog.writeEndArray(); + + catalog.writeFieldName("Size"); + catalog.writeStartArray(); + catalog.writeString(Long.toString(file.getSize())); + catalog.writeEndArray(); + + catalog.writeFieldName("Source Path"); + catalog.writeStartArray(); + catalog.writeString(file.getParentPath() + "/" + file.getName()); + catalog.writeEndArray(); + + catalog.writeFieldName("Flags (Dir)"); + catalog.writeStartArray(); + catalog.writeString(file.getDirFlagAsString()); + catalog.writeEndArray(); + + catalog.writeFieldName("Flags (Meta)"); + catalog.writeStartArray(); + catalog.writeString(file.getMetaFlagsAsString()); + catalog.writeEndArray(); + + catalog.writeFieldName("Mode"); + catalog.writeStartArray(); + catalog.writeString(file.getModesAsString()); + catalog.writeEndArray(); + + catalog.writeFieldName("User ID"); + catalog.writeStartArray(); + catalog.writeString(Integer.toString(file.getUid())); + catalog.writeEndArray(); + + catalog.writeFieldName("Group ID"); + catalog.writeStartArray(); + catalog.writeString(Integer.toString(file.getGid())); + catalog.writeEndArray(); + + catalog.writeFieldName("Meta Addr"); + catalog.writeStartArray(); + catalog.writeString(Long.toString(file.getMetaAddr())); + catalog.writeEndArray(); + + catalog.writeFieldName("Attr Addr"); + catalog.writeStartArray(); + catalog.writeString(Long.toString(file.getAttrType().getValue()) + "-" + file.getAttributeId()); + catalog.writeEndArray(); + + catalog.writeFieldName("Dir Type"); + catalog.writeStartArray(); + catalog.writeString(file.getDirType().getLabel()); + catalog.writeEndArray(); + + catalog.writeFieldName("Meta Type"); + catalog.writeStartArray(); + catalog.writeString(file.getMetaType().toString()); + catalog.writeEndArray(); + + catalog.writeFieldName("Known"); + catalog.writeStartArray(); + catalog.writeString(file.getKnown().getName()); + catalog.writeEndArray(); + + catalog.writeEndObject(); + + for (Map.Entry> entry : artifactsToAttributes.entrySet()) { + for (BlackboardAttribute attr : entry.getValue()) { + catalog.writeFieldName(entry.getKey().getArtifactTypeName()); + catalog.writeStartObject(); + catalog.writeFieldName(attr.getAttributeType().getTypeName()); + catalog.writeStartArray(); + catalog.writeString(attr.getDisplayString()); + catalog.writeEndArray(); + catalog.writeEndObject(); + } + } + + catalog.writeEndObject(); + catalog.writeEndObject(); + } + + /** + * Closes all catalogs opened during export. + * + * @throws IOException If there is a problem closing a catalog. + */ + private void closeCatalogs() throws IOException { + masterCatalog.close(); + for (JsonGenerator catalog : this.ruleNamesToCatalogs.values()) { + catalog.close(); + } + + } + + /** + * Writes the flag files that signal export is completed. + * + * @throws IOException If there is a problem writing a flag file. + */ + private void writeFlagFiles() throws IOException { + for (Path flagFile : flagFilePaths) { + Files.createFile(flagFile); + } + } + + /** + * Exception thrown to clients if there is a problem exporting files. + */ + final static class FileExportException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Constructs an exception. + * + * @param message The exception message. + */ + private FileExportException(String message) { + super(message); + } + + /** + * Constructs an exception. + * + * @param message The exception message. + * @param cause The exception cause. + */ + private FileExportException(String message, Throwable cause) { + super(message, cause); + } + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/FileExporterSettingsPanel.form b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/FileExporterSettingsPanel.form new file mode 100644 index 0000000000..7ec8ed3539 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/FileExporterSettingsPanel.form @@ -0,0 +1,727 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/FileExporterSettingsPanel.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/FileExporterSettingsPanel.java new file mode 100644 index 0000000000..545ca3ae6e --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/FileExporterSettingsPanel.java @@ -0,0 +1,2061 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.github.lgooddatepicker.datetimepicker.DateTimePicker; +import com.github.lgooddatepicker.optionalusertools.PickerUtilities; +import com.github.lgooddatepicker.timepicker.TimePickerSettings; +import java.awt.Color; +import java.awt.Component; +import java.awt.event.FocusEvent; +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.NavigableMap; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.HashMap; +import java.util.Map; +import java.util.Date; +import javax.swing.JFileChooser; +import javax.swing.JOptionPane; +import javax.swing.JTextField; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import org.sleuthkit.autopsy.corecomponents.TextPrompt; +import org.sleuthkit.autopsy.experimental.autoingest.FileExportRuleSet.Rule; +import org.sleuthkit.autopsy.experimental.autoingest.FileExportRuleSet.Rule.FileMIMETypeCondition; +import org.sleuthkit.autopsy.experimental.autoingest.FileExportRuleSet.Rule.FileSizeCondition; +import org.sleuthkit.autopsy.experimental.autoingest.FileExportRuleSet.Rule.FileSizeCondition.SizeUnit; +import org.sleuthkit.autopsy.experimental.autoingest.FileExportRuleSet.Rule.RelationalOp; +import org.sleuthkit.autopsy.coreutils.FileUtil; +import java.util.logging.Level; +import javax.swing.DefaultComboBoxModel; +import javax.swing.Icon; +import javax.swing.ImageIcon; +import javax.swing.JDialog; +import javax.swing.JPanel; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeCellRenderer; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; +import org.apache.tika.mime.MediaType; +import org.apache.tika.mime.MimeTypes; +import org.openide.util.ImageUtilities; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector; +import org.openide.util.NbBundle; +import org.openide.windows.WindowManager; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.autopsy.experimental.autoingest.FileExportRuleSet.Rule.ArtifactCondition; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import javax.swing.DefaultListModel; +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.binary.Hex; + +/** + * Global settings panel for data-source-level ingest modules that export and + * catalog files based on user-defined export rules. + */ +public final class FileExporterSettingsPanel extends JPanel { + + private static final long serialVersionUID = 1L; + private final JFileChooser rootDirectoryChooser = new JFileChooser(); + private final JFileChooser reportDirectoryChooser = new JFileChooser(); + private static final Logger logger = Logger.getLogger(FileExporterSettingsPanel.class.getName()); + private FileExportRuleSet exportRuleSet = new FileExportRuleSet("DefaultRuleSet"); //NON-NLS + private Rule localRule = null; // this is the rule to compare against to see if things have changed + private static final SortedSet mediaTypes = MimeTypes.getDefaultMimeTypes().getMediaTypeRegistry().getTypes(); + private static final String ROOTNODE = "Rules"; //NON-NLS + private final JDialog jDialog; + private DefaultMutableTreeNode rootNode; + private final TreeSelectionModel treeSelectionModel; + private final DefaultTreeModel defaultTreeModel; + private final DefaultListModel attributeListModel; + private List expandedNodes = null; + private Map attributeTypeMap = new HashMap<>(); + private static final String UNSET = "Unset"; //NON-NLS + private final javax.swing.event.ListSelectionListener listSelectionListener; + private final javax.swing.event.TreeSelectionListener treeSelectionListener; + private TimePickerSettings timeSettings = new TimePickerSettings(); + + private enum ItemType { + + RULE_SET, + RULE, + MIME_TYPE_CLAUSE, + SIZE_CLAUSE, + ARTIFACT_CLAUSE + } + + private class Item { + + private String name; + private String parentRuleName; + private ItemType itemType; + + Item(String name, String parentRuleName, ItemType itemType) { + this.name = name; + this.parentRuleName = parentRuleName; + this.itemType = itemType; + } + + /** + * @return the name of the rule containing this Item + */ + String getRuleName() { + if (this.itemType == ItemType.RULE) { + return this.name; + } else { + return this.parentRuleName; + } + } + + /** + * @return the name + */ + String getName() { + return name; + } + + /** + * @param name the name to set + */ + void setName(String name) { + this.name = name; + } + + /** + * @return the parentRUleName + */ + String getParentName() { + return parentRuleName; + } + + /** + * @param parentName the parentRUleName to set + */ + void setParentName(String parentName) { + this.parentRuleName = parentName; + } + + /** + * @return the itemType + */ + ItemType getItemType() { + return itemType; + } + + /** + * @param itemType the itemType to set + */ + void setItemType(ItemType itemType) { + this.itemType = itemType; + } + + @Override + public String toString() { + return this.getName(); + } + } + + public FileExporterSettingsPanel(JDialog jDialog) { + timeSettings.setFormatForDisplayTime(PickerUtilities.createFormatterFromPatternString("HH:mm:ss", timeSettings.getLocale())); + timeSettings.setFormatForMenuTimes(PickerUtilities.createFormatterFromPatternString("HH:mm", timeSettings.getLocale())); + + initComponents(); + rootNode = new DefaultMutableTreeNode(new Item(ROOTNODE, ROOTNODE, ItemType.RULE_SET)); + trRuleList.setModel(new DefaultTreeModel(rootNode)); + this.jDialog = jDialog; + + attributeListModel = (DefaultListModel) lsAttributeList.getModel(); + + rootDirectoryChooser.setCurrentDirectory(rootDirectoryChooser.getFileSystemView().getParentDirectory(new File("C:\\"))); //NON-NLS + rootDirectoryChooser.setAcceptAllFileFilterUsed(false); + rootDirectoryChooser.setDialogTitle(NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.ChooseRootDirectory")); + rootDirectoryChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + + reportDirectoryChooser.setCurrentDirectory(reportDirectoryChooser.getFileSystemView().getParentDirectory(new File("C:\\"))); //NON-NLS + reportDirectoryChooser.setAcceptAllFileFilterUsed(false); + reportDirectoryChooser.setDialogTitle(NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.ChooseReportDirectory")); + reportDirectoryChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + + // Add text prompt to the text box fields + TextPrompt textPromptRuleName = new TextPrompt(NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.RuleName"), tbRuleName); + textPromptRuleName.setForeground(Color.LIGHT_GRAY); + textPromptRuleName.changeAlpha(0.9f); + + TextPrompt textPromptRootDirectory = new TextPrompt(NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.RootDirectory"), tbRootDirectory); + textPromptRootDirectory.setForeground(Color.LIGHT_GRAY); + textPromptRootDirectory.changeAlpha(0.9f); + + TextPrompt textPromptReportDirectory = new TextPrompt(NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.ReportDirectory"), tbReportDirectory); + textPromptReportDirectory.setForeground(Color.LIGHT_GRAY); + textPromptReportDirectory.changeAlpha(0.9f); + + TextPrompt textPromptReportAttributeValue = new TextPrompt(NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.AttributeValue"), tbAttributeValue); + textPromptReportAttributeValue.setForeground(Color.LIGHT_GRAY); + textPromptReportAttributeValue.changeAlpha(0.9f); + + for (SizeUnit item : SizeUnit.values()) { + comboBoxFileSizeUnits.addItem(item.toString()); + } + + for (RelationalOp item : RelationalOp.values()) { + comboBoxFileSizeComparison.addItem(item.getSymbol()); + comboBoxAttributeComparison.addItem(item.getSymbol()); + } + + comboBoxValueType.addItem(BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.INTEGER.getLabel()); + comboBoxValueType.addItem(BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.LONG.getLabel()); + comboBoxValueType.addItem(BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DOUBLE.getLabel()); + comboBoxValueType.addItem(BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING.getLabel()); + comboBoxValueType.addItem(BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DATETIME.getLabel()); + comboBoxValueType.addItem(BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.BYTE.getLabel()); + comboBoxValueType.addItem(UNSET); + + load(); + trRuleList.setCellRenderer(new DefaultTreeCellRenderer() { + private static final long serialVersionUID = 1L; + private final ImageIcon ruleSetIcon = new ImageIcon(ImageUtilities.loadImage("org/sleuthkit/autopsy/experimental/images/ruleset-icon.png", false)); + private final ImageIcon ruleIcon = new ImageIcon(ImageUtilities.loadImage("org/sleuthkit/autopsy/experimental/images/extracted_content.png", false)); + private final ImageIcon sizeIcon = new ImageIcon(ImageUtilities.loadImage("org/sleuthkit/autopsy/experimental/images/file-size-16.png", false)); + private final ImageIcon artifactIcon = new ImageIcon(ImageUtilities.loadImage("org/sleuthkit/autopsy/experimental/images/artifact-icon.png", false)); + private final ImageIcon mimetypeIcon = new ImageIcon(ImageUtilities.loadImage("org/sleuthkit/autopsy/experimental/images/mime-icon.png", false)); + private final ImageIcon otherIcon = new ImageIcon(ImageUtilities.loadImage("org/sleuthkit/autopsy/experimental/images/knownbad-icon.png", false)); + + @Override + public Component getTreeCellRendererComponent(javax.swing.JTree tree, + Object value, boolean selected, boolean expanded, + boolean isLeaf, int row, boolean focused) { + Component component = super.getTreeCellRendererComponent(tree, value, + selected, expanded, isLeaf, row, focused); + Icon icon; + switch (((Item) ((DefaultMutableTreeNode) value).getUserObject()).getItemType()) { + case ARTIFACT_CLAUSE: + icon = artifactIcon; + break; + case MIME_TYPE_CLAUSE: + icon = mimetypeIcon; + break; + case RULE: + icon = ruleIcon; + break; + case SIZE_CLAUSE: + icon = sizeIcon; + break; + case RULE_SET: + icon = ruleSetIcon; + break; + default: + icon = otherIcon; + break; + } + setIcon(icon); + return component; + } + }); + populateMimeTypes(); + populateArtifacts(); + populateAttributes(); + populateRuleTree(); + + tbRuleName.getDocument().addDocumentListener(new DocumentListener() { + @Override + public void changedUpdate(DocumentEvent e) { + setSaveButton(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + setSaveButton(); + } + + @Override + public void insertUpdate(DocumentEvent e) { + setSaveButton(); + } + }); + + comboBoxMimeValue.getEditor().getEditorComponent().addFocusListener(new java.awt.event.FocusListener() { + @Override + public void focusGained(FocusEvent e) { + comboBoxMimeValue.showPopup(); + comboBoxMimeValue.getEditor().selectAll(); + } + + @Override + public void focusLost(FocusEvent e) { + // do nothing + } + }); + + comboBoxArtifactName.getEditor().getEditorComponent().addFocusListener(new java.awt.event.FocusListener() { + @Override + public void focusGained(FocusEvent e) { + comboBoxArtifactName.showPopup(); + comboBoxArtifactName.getEditor().selectAll(); + } + + @Override + public void focusLost(FocusEvent e) { + // do nothing + } + }); + + comboBoxAttributeName.getEditor().getEditorComponent().addFocusListener(new java.awt.event.FocusListener() { + @Override + public void focusGained(FocusEvent e) { + comboBoxAttributeName.showPopup(); + comboBoxAttributeName.getEditor().selectAll(); + } + + @Override + public void focusLost(FocusEvent e) { + // do nothing + } + }); + + comboBoxMimeTypeComparison.addItem(RelationalOp.Equals.getSymbol()); + comboBoxMimeTypeComparison.addItem(RelationalOp.NotEquals.getSymbol()); + treeSelectionModel = trRuleList.getSelectionModel(); + defaultTreeModel = (DefaultTreeModel) trRuleList.getModel(); + bnDeleteRule.setEnabled(false); + String selectedAttribute = comboBoxAttributeName.getSelectedItem().toString(); + comboBoxValueType.setSelectedItem(selectedAttribute); + localRule = makeRuleFromUserInput(); + + listSelectionListener = this::lsAttributeListValueChanged; + lsAttributeList.addListSelectionListener(listSelectionListener); + + treeSelectionListener = this::trRuleListValueChanged; + trRuleList.addTreeSelectionListener(treeSelectionListener); + setDeleteAttributeButton(); + setSaveButton(); + } + + /** + * Save the settings, issuing warnings if needed. + * + * @param tryValidation attempt to validate if set to true, otherwise do not + * + * @return true if form ready to close, false otherwise + */ + boolean saveAndValidateSettings(boolean tryValidation) { + if (tryValidation) { + try { + validateRootDirectory(); + } catch (FolderDidNotValidateException ex) { + JOptionPane.showMessageDialog(this, + NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.BadRootFolder") + " " + ex.getMessage(), + NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.BadFolderForInterestingFileExport"), + JOptionPane.OK_OPTION); + return false; + } + + try { + validateReportDirectory(); + } catch (FolderDidNotValidateException ex) { + JOptionPane.showMessageDialog(this, + NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.BadReportFolder") + " " + ex.getMessage(), + NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.BadFolderForInterestingFileExport"), + JOptionPane.OK_OPTION); + return false; + } + + if (hasRuleChanged()) { + // if rule has changed without saving ask if we should save or discard + if (JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(WindowManager.getDefault().getMainWindow(), + NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.ChangesWillBeLost") + " " + + NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.DoYouWantToSave"), + NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.ChangesWillBeLost"), + JOptionPane.YES_NO_OPTION)) { + saveOrUpdateRule(); + } + } + } + store(); + return true; + } + + private void setValueField() { + if (comboBoxValueType.getSelectedItem().toString().compareTo("DateTime") == 0) { + dateTimePicker.setVisible(true); + tbAttributeValue.setVisible(false); + } else { + dateTimePicker.setVisible(false); + tbAttributeValue.setVisible(true); + } + } + + private void setTypeColor() { + if (cbAttributeType.isSelected() && comboBoxValueType.getSelectedItem().toString().compareTo(UNSET) == 0) { + comboBoxValueType.setForeground(Color.RED); + } else { + comboBoxValueType.setForeground(Color.BLACK); + } + } + + /** + * Enable save button if we have a rule name and at least one condition in + * use. + */ + private void setSaveButton() { + String ruleName = tbRuleName.getText(); + String selectedString = comboBoxValueType.getSelectedItem().toString(); + if ((!cbEnableFileExport.isSelected()) || (cbAttributeType.isSelected() && selectedString.compareTo(UNSET) == 0)) { + bnSaveRule.setEnabled(false); + lbSaveRuleHelper.setVisible(true); + return; + } + if (ruleName != null && !ruleName.isEmpty()) { + boolean result = cbFileSize.isSelected() || cbMimeType.isSelected() || cbAttributeType.isSelected(); + bnSaveRule.setEnabled(result); + lbSaveRuleHelper.setVisible(!result); + } else { + bnSaveRule.setEnabled(false); + lbSaveRuleHelper.setVisible(true); + } + } + + /** + * Go through the ruleSet and populate the JList + */ + void populateRuleTree() { + populateRuleTree(null); + } + + /** + * Go through the ruleSet and populate the JList + * + * @param ruleToBeSelected The path to the rule that should be selected + * after populating the tree + */ + void populateRuleTree(String ruleToBeSelected) { + TreePath ttt = new TreePath(rootNode); + Enumeration expandedDescendants = trRuleList.getExpandedDescendants(ttt); + expandedNodes = (expandedDescendants == null ? new ArrayList<>() : Collections.list(expandedDescendants)); + if (rootNode != null) { + rootNode.removeAllChildren(); + } + + for (Rule rule : exportRuleSet.getRules().values()) { + String ruleName = rule.getName(); + DefaultMutableTreeNode ruleNode = new DefaultMutableTreeNode(new Item(ruleName, ROOTNODE, ItemType.RULE)); + rootNode.add(ruleNode); + + FileMIMETypeCondition fileMIMETypeCondition = rule.getFileMIMETypeCondition(); + if (fileMIMETypeCondition != null) { + ruleNode.add(new DefaultMutableTreeNode(new Item("MIME Type", ruleName, ItemType.MIME_TYPE_CLAUSE))); + } + + List fileSizeConditions = rule.getFileSizeConditions(); + for (FileSizeCondition fsc : fileSizeConditions) { + ruleNode.add(new DefaultMutableTreeNode(new Item("File Size", ruleName, ItemType.SIZE_CLAUSE))); + } + + for (Rule.ArtifactCondition artifact : rule.getArtifactConditions()) { + DefaultMutableTreeNode clauseNode = new DefaultMutableTreeNode( + new Item(artifact.getTreeDisplayName(), ruleName, ItemType.ARTIFACT_CLAUSE)); + ruleNode.add(clauseNode); + } + } + ((DefaultTreeModel) trRuleList.getModel()).reload(); + + // Re-expand any rules that were open previously and that still exist + for (TreePath e : expandedNodes) { + TreePath treePath = findTreePathByRuleName(e.getLastPathComponent().toString()); + trRuleList.expandPath(treePath); + } + expandedNodes.clear(); + + // select the rule to leave the cursor in a logical place + if (ruleToBeSelected != null) { + TreePath treePath = findTreePathByRuleName(ruleToBeSelected); + treeSelectionModel.setSelectionPath(treePath); + trRuleList.expandPath(treePath); + } + } + + /** + * Populate the MIME types in the combo box. + */ + void populateMimeTypes() { + Set mimeTypesCollated = scanRulesForMimetypes(); + for (MediaType mediaType : mediaTypes) { + mimeTypesCollated.add(mediaType.toString()); + } + + FileTypeDetector fileTypeDetector; + try { + fileTypeDetector = new FileTypeDetector(); + List userDefinedFileTypes = fileTypeDetector.getUserDefinedTypes(); + mimeTypesCollated.addAll(userDefinedFileTypes); + + } catch (FileTypeDetector.FileTypeDetectorInitException ex) { + logger.log(Level.SEVERE, "Unable to get user defined file types", ex); + } + List sorted = new ArrayList<>(mimeTypesCollated); + Collections.sort(sorted, String.CASE_INSENSITIVE_ORDER); + for (String mime : sorted) { + comboBoxMimeValue.addItem(mime); + } + } + + /** + * Populate the Artifact types in the combo box. + */ + void populateArtifacts() { + Set artifactTypes = scanRulesForArtifacts(); + try { + SleuthkitCase currentCase = Case.getCurrentCase().getSleuthkitCase(); + for (BlackboardArtifact.Type type : currentCase.getArtifactTypes()) { + artifactTypes.add(type.getTypeName()); + } + } catch (IllegalStateException | TskCoreException ex) { + // Unable to find and open case or cannot read the database. Use enum. + for (BlackboardArtifact.ARTIFACT_TYPE artifact : BlackboardArtifact.ARTIFACT_TYPE.values()) { + artifactTypes.add(artifact.toString()); + } + } + List sorted = new ArrayList<>(artifactTypes); + Collections.sort(sorted, String.CASE_INSENSITIVE_ORDER); + for (String artifact : sorted) { + comboBoxArtifactName.addItem(artifact); + } + } + + /** + * Find all MIME types from existing rules + * + * @return a Set of Strings with existing rule MIME types + */ + Set scanRulesForMimetypes() { + Set mimeTypes = new HashSet<>(); + NavigableMap nmp = exportRuleSet.getRules(); + for (Rule rule : nmp.values()) { + if (rule.getFileMIMETypeCondition() != null) { + mimeTypes.add(rule.getFileMIMETypeCondition().getMIMEType()); + } + } + return mimeTypes; + } + + /** + * Find all artifact types from existing rules + * + * @return a Set of Strings with existing rule artifact types + */ + Set scanRulesForArtifacts() { + Set artifacts = new HashSet<>(); + + NavigableMap nmp = exportRuleSet.getRules(); + for (Rule rule : nmp.values()) { + for (ArtifactCondition ac : rule.getArtifactConditions()) { + artifacts.add(ac.getArtifactTypeName()); + } + } + return artifacts; + } + + /** + * Find all attribute types from existing rules + * + * @return a Set of Strings with existing rule attribute types + */ + Set scanRulesForAttributes() { + Set attributes = new HashSet<>(); + NavigableMap nmp = exportRuleSet.getRules(); + for (Rule rule : nmp.values()) { + for (ArtifactCondition ac : rule.getArtifactConditions()) { + attributes.add(ac.getAttributeTypeName()); + attributeTypeMap.put(ac.getAttributeTypeName(), ac.getAttributeValueType()); + } + } + return attributes; + } + + /** + * Populate the Attribute types in the combo box. + */ + void populateAttributes() { + Set attributeTypes = scanRulesForAttributes(); + + try { + SleuthkitCase currentCase = Case.getCurrentCase().getSleuthkitCase(); + for (BlackboardAttribute.Type type : currentCase.getAttributeTypes()) { + attributeTypes.add(type.getTypeName()); + attributeTypeMap.put(type.getTypeName(), type.getValueType()); + } + } catch (IllegalStateException | TskCoreException ex) { + // Unable to find and open case or cannot read the database. Use enum. + for (BlackboardAttribute.ATTRIBUTE_TYPE type : BlackboardAttribute.ATTRIBUTE_TYPE.values()) { + attributeTypes.add(type.getLabel()); + attributeTypeMap.put(type.getLabel(), type.getValueType()); + } + } + + List sorted = new ArrayList<>(attributeTypes); + Collections.sort(sorted, String.CASE_INSENSITIVE_ORDER); + for (String attribute : sorted) { + comboBoxAttributeName.addItem(attribute); + } + } + + private void populateArtifactEditor(ArtifactCondition artifactConditionToPopulateWith) { + cbAttributeType.setSelected(true); + comboBoxArtifactName.setSelectedItem(artifactConditionToPopulateWith.getArtifactTypeName()); + comboBoxAttributeComparison.setSelectedItem(artifactConditionToPopulateWith.getRelationalOperator().getSymbol()); + comboBoxAttributeName.setSelectedItem(artifactConditionToPopulateWith.getAttributeTypeName()); + BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE valueType = artifactConditionToPopulateWith.getAttributeValueType(); + comboBoxValueType.setSelectedItem(valueType.getLabel()); + comboBoxValueType.setEnabled(null == attributeTypeMap.get(artifactConditionToPopulateWith.getAttributeTypeName())); + if (valueType == BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DATETIME) { + Instant instant = Instant.ofEpochMilli(artifactConditionToPopulateWith.getDateTimeValue().toDate().getTime()); + dateTimePicker.setDateTime(LocalDateTime.ofInstant(instant, ZoneId.systemDefault())); + } else { + tbAttributeValue.setText(artifactConditionToPopulateWith.getStringRepresentationOfValue()); + } + } + + /** + * 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() { + + mainPanel = new javax.swing.JPanel(); + tbRootDirectory = new javax.swing.JTextField(); + pnEditRule = new javax.swing.JPanel(); + comboBoxMimeValue = new javax.swing.JComboBox(); + cbMimeType = new javax.swing.JCheckBox(); + spFileSizeValue = new javax.swing.JSpinner(); + comboBoxFileSizeUnits = new javax.swing.JComboBox(); + cbFileSize = new javax.swing.JCheckBox(); + comboBoxFileSizeComparison = new javax.swing.JComboBox(); + comboBoxMimeTypeComparison = new javax.swing.JComboBox(); + tbRuleName = new javax.swing.JTextField(); + bnSaveRule = new javax.swing.JButton(); + comboBoxArtifactName = new javax.swing.JComboBox(); + comboBoxAttributeName = new javax.swing.JComboBox(); + comboBoxAttributeComparison = new javax.swing.JComboBox(); + tbAttributeValue = new javax.swing.JTextField(); + bnAddAttribute = new javax.swing.JButton(); + comboBoxValueType = new javax.swing.JComboBox(); + cbAttributeType = new javax.swing.JCheckBox(); + dateTimePicker = new DateTimePicker(null, timeSettings); + lbArtifact = new javax.swing.JLabel(); + lbAttribute = new javax.swing.JLabel(); + bnDeleteAttribute = new javax.swing.JButton(); + jScrollPane1 = new javax.swing.JScrollPane(); + lsAttributeList = new javax.swing.JList(); + lbRuleName = new javax.swing.JLabel(); + lbSaveRuleHelper = new javax.swing.JLabel(); + bnBrowseReportDirectory = new javax.swing.JButton(); + tbReportDirectory = new javax.swing.JTextField(); + ruleListScrollPane = new javax.swing.JScrollPane(); + trRuleList = new javax.swing.JTree(); + lbFiles = new javax.swing.JLabel(); + lbReports = new javax.swing.JLabel(); + bnBrowseRootDirectory = new javax.swing.JButton(); + cbEnableFileExport = new javax.swing.JCheckBox(); + bnNewRule = new javax.swing.JButton(); + bnDeleteRule = new javax.swing.JButton(); + bnClose = new javax.swing.JButton(); + lbExplanation = new javax.swing.JLabel(); + + setBorder(javax.swing.BorderFactory.createEtchedBorder()); + setName(org.openide.util.NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.Title")); // NOI18N + + mainPanel.setAutoscrolls(true); + mainPanel.setPreferredSize(new java.awt.Dimension(657, 425)); + + tbRootDirectory.setToolTipText(org.openide.util.NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.RuleOutputTooltip_1")); // NOI18N + tbRootDirectory.setMaximumSize(new java.awt.Dimension(2000, 2000)); + + pnEditRule.setBorder(javax.swing.BorderFactory.createEtchedBorder()); + pnEditRule.setAutoscrolls(true); + + comboBoxMimeValue.setEditable(true); + comboBoxMimeValue.setMaximumRowCount(30); + comboBoxMimeValue.setToolTipText(org.openide.util.NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.MimetypeTooltip_1")); // NOI18N + comboBoxMimeValue.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + comboBoxMimeValueActionPerformed(evt); + } + }); + + cbMimeType.setText(org.openide.util.NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.MimetypeText")); // NOI18N + cbMimeType.setToolTipText(org.openide.util.NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.MimetypeCheckboxTooltip_1")); // NOI18N + cbMimeType.addItemListener(new java.awt.event.ItemListener() { + public void itemStateChanged(java.awt.event.ItemEvent evt) { + checkButtonItemStateChanged(evt); + } + }); + + spFileSizeValue.setModel(new javax.swing.SpinnerNumberModel(Integer.valueOf(1024), Integer.valueOf(0), null, Integer.valueOf(1))); + spFileSizeValue.setToolTipText(org.openide.util.NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.FileSizeValueToolTip_1")); // NOI18N + + comboBoxFileSizeUnits.setToolTipText(org.openide.util.NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.FileSizeUnitToolTip_1")); // NOI18N + + cbFileSize.setText(org.openide.util.NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.FileSize")); // NOI18N + cbFileSize.setToolTipText(org.openide.util.NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.FileSize_1")); // NOI18N + cbFileSize.addItemListener(new java.awt.event.ItemListener() { + public void itemStateChanged(java.awt.event.ItemEvent evt) { + checkButtonItemStateChanged(evt); + } + }); + + comboBoxFileSizeComparison.setToolTipText(org.openide.util.NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.FileSizeComparisonTooltip_1")); // NOI18N + comboBoxFileSizeComparison.setMinimumSize(new java.awt.Dimension(32, 20)); + + comboBoxMimeTypeComparison.setToolTipText(org.openide.util.NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.MimeTypeComparisonTooltip_1")); // NOI18N + comboBoxMimeTypeComparison.setMinimumSize(new java.awt.Dimension(32, 20)); + + tbRuleName.setToolTipText(org.openide.util.NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.CurrentlySelectedRuleNameTooltip_1")); // NOI18N + tbRuleName.setMaximumSize(new java.awt.Dimension(10, 1000)); + tbRuleName.setPreferredSize(new java.awt.Dimension(733, 20)); + tbRuleName.addKeyListener(new java.awt.event.KeyAdapter() { + public void keyTyped(java.awt.event.KeyEvent evt) { + tbRuleNameKeyTyped(evt); + } + }); + + bnSaveRule.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/experimental/images/save-icon.png"))); // NOI18N + bnSaveRule.setText(org.openide.util.NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.SaveText")); // NOI18N + bnSaveRule.setToolTipText(org.openide.util.NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.SaveTooltip_1")); // NOI18N + bnSaveRule.setEnabled(false); + bnSaveRule.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnSaveRuleActionPerformed(evt); + } + }); + + comboBoxArtifactName.setEditable(true); + comboBoxArtifactName.setMaximumRowCount(30); + comboBoxArtifactName.setToolTipText("The Artifact to match"); + comboBoxArtifactName.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + comboBoxArtifactNameActionPerformed(evt); + } + }); + + comboBoxAttributeName.setEditable(true); + comboBoxAttributeName.setMaximumRowCount(30); + comboBoxAttributeName.setToolTipText("The attribute of the artifact to match"); + comboBoxAttributeName.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + comboBoxAttributeNameActionPerformed(evt); + } + }); + + comboBoxAttributeComparison.setToolTipText("Select the conditional operator"); + comboBoxAttributeComparison.setMinimumSize(new java.awt.Dimension(32, 23)); + + tbAttributeValue.setToolTipText("Type a value here"); + tbAttributeValue.setMinimumSize(new java.awt.Dimension(6, 23)); + tbAttributeValue.setPreferredSize(new java.awt.Dimension(6, 23)); + + bnAddAttribute.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/experimental/images/left-arrow-16-icon.png"))); // NOI18N + bnAddAttribute.setText("Add Attribute"); + bnAddAttribute.setToolTipText("Click to add an attribute to the current rule"); + bnAddAttribute.setEnabled(false); + bnAddAttribute.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnAddAttributeActionPerformed(evt); + } + }); + + comboBoxValueType.setToolTipText("Select the data type of attribute. This will affect how values are compared."); + comboBoxValueType.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + comboBoxValueTypeActionPerformed(evt); + } + }); + + cbAttributeType.setText("Attributes"); + cbAttributeType.setToolTipText("Select to include artifact/attribute pairs in the rule"); + cbAttributeType.addItemListener(new java.awt.event.ItemListener() { + public void itemStateChanged(java.awt.event.ItemEvent evt) { + cbAttributeTypeItemStateChanged(evt); + } + }); + + dateTimePicker.setToolTipText("Choose a date and time"); + + lbArtifact.setText("Artifact"); + + lbAttribute.setText("Attribute"); + + bnDeleteAttribute.setText("Delete Attribute"); + bnDeleteAttribute.setToolTipText("Click to remove the selected attribute"); + bnDeleteAttribute.setEnabled(false); + bnDeleteAttribute.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnDeleteAttributeActionPerformed(evt); + } + }); + + lsAttributeList.setModel(new DefaultListModel()); + lsAttributeList.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); + lsAttributeList.setToolTipText("The attributes for the selected rule"); + jScrollPane1.setViewportView(lsAttributeList); + + lbRuleName.setText("Rule Name"); + + lbSaveRuleHelper.setHorizontalAlignment(javax.swing.SwingConstants.RIGHT); + lbSaveRuleHelper.setText("To save, a rule must have a name and at least one condition."); + + javax.swing.GroupLayout pnEditRuleLayout = new javax.swing.GroupLayout(pnEditRule); + pnEditRule.setLayout(pnEditRuleLayout); + pnEditRuleLayout.setHorizontalGroup( + pnEditRuleLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(pnEditRuleLayout.createSequentialGroup() + .addContainerGap() + .addGroup(pnEditRuleLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(pnEditRuleLayout.createSequentialGroup() + .addGap(21, 21, 21) + .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 198, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(10, 10, 10) + .addGroup(pnEditRuleLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, pnEditRuleLayout.createSequentialGroup() + .addComponent(lbSaveRuleHelper, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(bnSaveRule, javax.swing.GroupLayout.PREFERRED_SIZE, 101, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(pnEditRuleLayout.createSequentialGroup() + .addComponent(comboBoxAttributeComparison, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(dateTimePicker, javax.swing.GroupLayout.PREFERRED_SIZE, 306, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(tbAttributeValue, javax.swing.GroupLayout.DEFAULT_SIZE, 158, Short.MAX_VALUE)) + .addGroup(pnEditRuleLayout.createSequentialGroup() + .addGroup(pnEditRuleLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(pnEditRuleLayout.createSequentialGroup() + .addGroup(pnEditRuleLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(comboBoxArtifactName, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(lbArtifact)) + .addGap(18, 18, 18) + .addGroup(pnEditRuleLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(pnEditRuleLayout.createSequentialGroup() + .addComponent(comboBoxAttributeName, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(comboBoxValueType, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(lbAttribute))) + .addGroup(pnEditRuleLayout.createSequentialGroup() + .addComponent(bnAddAttribute, javax.swing.GroupLayout.PREFERRED_SIZE, 117, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(bnDeleteAttribute))) + .addGap(0, 0, Short.MAX_VALUE)))) + .addGroup(pnEditRuleLayout.createSequentialGroup() + .addGroup(pnEditRuleLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(cbMimeType) + .addComponent(cbFileSize) + .addComponent(cbAttributeType)) + .addGap(89, 89, 89) + .addGroup(pnEditRuleLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(pnEditRuleLayout.createSequentialGroup() + .addComponent(comboBoxMimeTypeComparison, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(18, 18, 18) + .addComponent(comboBoxMimeValue, javax.swing.GroupLayout.PREFERRED_SIZE, 240, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(pnEditRuleLayout.createSequentialGroup() + .addComponent(comboBoxFileSizeComparison, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(18, 18, 18) + .addComponent(spFileSizeValue, javax.swing.GroupLayout.PREFERRED_SIZE, 105, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(18, 18, 18) + .addComponent(comboBoxFileSizeUnits, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addGap(0, 0, Short.MAX_VALUE)) + .addGroup(pnEditRuleLayout.createSequentialGroup() + .addComponent(lbRuleName) + .addGap(18, 18, 18) + .addComponent(tbRuleName, javax.swing.GroupLayout.PREFERRED_SIZE, 1, Short.MAX_VALUE))) + .addContainerGap()) + ); + pnEditRuleLayout.setVerticalGroup( + pnEditRuleLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(pnEditRuleLayout.createSequentialGroup() + .addGap(14, 14, 14) + .addGroup(pnEditRuleLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(tbRuleName, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(lbRuleName)) + .addGap(26, 26, 26) + .addGroup(pnEditRuleLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(cbMimeType) + .addComponent(comboBoxMimeTypeComparison, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(comboBoxMimeValue, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(pnEditRuleLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(pnEditRuleLayout.createSequentialGroup() + .addGap(78, 78, 78) + .addComponent(lbAttribute) + .addGap(8, 8, 8) + .addGroup(pnEditRuleLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addGroup(pnEditRuleLayout.createSequentialGroup() + .addGroup(pnEditRuleLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(comboBoxAttributeName, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(comboBoxArtifactName, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(comboBoxValueType, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(pnEditRuleLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(tbAttributeValue, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(dateTimePicker, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addComponent(comboBoxAttributeComparison, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(pnEditRuleLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(bnAddAttribute) + .addComponent(bnDeleteAttribute)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(pnEditRuleLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(bnSaveRule) + .addComponent(lbSaveRuleHelper))) + .addGroup(pnEditRuleLayout.createSequentialGroup() + .addGap(26, 26, 26) + .addGroup(pnEditRuleLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(cbFileSize) + .addComponent(spFileSizeValue, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(comboBoxFileSizeUnits, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(comboBoxFileSizeComparison, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGap(26, 26, 26) + .addGroup(pnEditRuleLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(cbAttributeType) + .addComponent(lbArtifact)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 187, Short.MAX_VALUE))) + .addContainerGap()) + ); + + bnBrowseReportDirectory.setText(org.openide.util.NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.BrowseText")); // NOI18N + bnBrowseReportDirectory.setToolTipText(org.openide.util.NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.BrowseReportTooltip_1")); // NOI18N + bnBrowseReportDirectory.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnBrowseReportDirectoryActionPerformed(evt); + } + }); + + tbReportDirectory.setToolTipText(org.openide.util.NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.ReportOutputFolderTooltip_1")); // NOI18N + tbReportDirectory.setMaximumSize(new java.awt.Dimension(2000, 2000)); + + javax.swing.tree.DefaultMutableTreeNode treeNode1 = new javax.swing.tree.DefaultMutableTreeNode("root"); + trRuleList.setModel(new javax.swing.tree.DefaultTreeModel(treeNode1)); + trRuleList.setToolTipText("This tree shows the rules to collect files for automatic file export"); + trRuleList.setName("trRuleList"); // NOI18N + trRuleList.setShowsRootHandles(true); + ruleListScrollPane.setViewportView(trRuleList); + trRuleList.getAccessibleContext().setAccessibleParent(ruleListScrollPane); + + lbFiles.setText("Files Folder"); + + lbReports.setText("Reports Folder"); + + bnBrowseRootDirectory.setText(org.openide.util.NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.BrowseText")); // NOI18N + bnBrowseRootDirectory.setToolTipText(org.openide.util.NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.BrowseRootOutputFolder_1")); // NOI18N + bnBrowseRootDirectory.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnBrowseRootDirectoryActionPerformed(evt); + } + }); + + javax.swing.GroupLayout mainPanelLayout = new javax.swing.GroupLayout(mainPanel); + mainPanel.setLayout(mainPanelLayout); + mainPanelLayout.setHorizontalGroup( + mainPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, mainPanelLayout.createSequentialGroup() + .addContainerGap() + .addGroup(mainPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(mainPanelLayout.createSequentialGroup() + .addGroup(mainPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(lbFiles) + .addComponent(lbReports)) + .addGap(18, 18, 18) + .addGroup(mainPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(tbRootDirectory, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(tbReportDirectory, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addGroup(mainPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(bnBrowseReportDirectory, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.PREFERRED_SIZE, 93, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(bnBrowseRootDirectory, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.PREFERRED_SIZE, 93, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addGroup(mainPanelLayout.createSequentialGroup() + .addComponent(ruleListScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 278, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(pnEditRule, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)))) + ); + mainPanelLayout.setVerticalGroup( + mainPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, mainPanelLayout.createSequentialGroup() + .addGap(1, 1, 1) + .addGroup(mainPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(tbRootDirectory, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(lbFiles) + .addComponent(bnBrowseRootDirectory)) + .addGap(6, 6, 6) + .addGroup(mainPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(bnBrowseReportDirectory) + .addComponent(tbReportDirectory, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(lbReports)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(mainPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(ruleListScrollPane) + .addComponent(pnEditRule, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addContainerGap()) + ); + + cbEnableFileExport.setText("Enable File Export"); + cbEnableFileExport.setToolTipText("Select to enable File Export"); + cbEnableFileExport.addItemListener(new java.awt.event.ItemListener() { + public void itemStateChanged(java.awt.event.ItemEvent evt) { + cbEnableFileExportItemStateChanged(evt); + } + }); + + bnNewRule.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/experimental/images/plus-icon.png"))); // NOI18N + bnNewRule.setText(org.openide.util.NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.NewText")); // NOI18N + bnNewRule.setToolTipText(org.openide.util.NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.NewRuleTooltip_1")); // NOI18N + bnNewRule.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnNewRuleActionPerformed(evt); + } + }); + + bnDeleteRule.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/experimental/images/minus-icon.png"))); // NOI18N + bnDeleteRule.setText(org.openide.util.NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.DeleteText")); // NOI18N + bnDeleteRule.setToolTipText(org.openide.util.NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.DeleteTooltip_1")); // NOI18N + bnDeleteRule.setEnabled(false); + bnDeleteRule.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnDeleteRuleActionPerformed(evt); + } + }); + + bnClose.setText("Close"); + bnClose.setToolTipText("Close the settings panel"); + bnClose.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnCloseActionPerformed(evt); + } + }); + + lbExplanation.setText("File Export occurs after ingest has completed, automatically exporting files matching the rules specified below. Individual components of the rule are ANDed together."); + + 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(cbEnableFileExport) + .addGap(39, 39, 39) + .addComponent(lbExplanation) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addGroup(layout.createSequentialGroup() + .addGap(36, 36, 36) + .addComponent(bnNewRule, javax.swing.GroupLayout.PREFERRED_SIZE, 101, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(bnDeleteRule) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(bnClose, javax.swing.GroupLayout.PREFERRED_SIZE, 101, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(21, 21, 21)) + .addGroup(layout.createSequentialGroup() + .addComponent(mainPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 1051, Short.MAX_VALUE) + .addContainerGap()) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(cbEnableFileExport) + .addComponent(lbExplanation)) + .addGap(7, 7, 7) + .addComponent(mainPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 455, Short.MAX_VALUE) + .addGap(1, 1, 1) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(bnDeleteRule) + .addComponent(bnNewRule) + .addComponent(bnClose)) + .addContainerGap()) + ); + }// //GEN-END:initComponents + + /** + * Handles clicking the "New Rule" button. + * + * @param evt The event which caused this call. + */ + private void bnNewRuleActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnNewRuleActionPerformed + if (hasRuleChanged()) { + // if rule has changed without saving + if (JOptionPane.showConfirmDialog(this, + NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.UnsavedChangesLost"), + NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.ChangesWillBeLost"), + JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE) == JOptionPane.CANCEL_OPTION) { + // they were not quite ready to navigate away yet, so clear the selection + treeSelectionModel.clearSelection(); + return; + } + } + clearRuleEditor(); + localRule = makeRuleFromUserInput(); + }//GEN-LAST:event_bnNewRuleActionPerformed + + /** + * Handles clicking the "Delete Rule" button. + * + * @param evt The event which caused this call. + */ + private void bnDeleteRuleActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnDeleteRuleActionPerformed + Item item = (Item) ((DefaultMutableTreeNode) trRuleList.getLastSelectedPathComponent()).getUserObject(); + if (item != null) { + if (item.getItemType() == ItemType.RULE) { + String ruleName = item.getName(); + if (JOptionPane.showConfirmDialog(this, NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.ReallyDeleteRule") + + " " + ruleName + NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.QuestionMark"), + NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.ConfirmRuleDeletion"), + JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE) == JOptionPane.OK_OPTION) { + exportRuleSet.removeRule(ruleName); + clearRuleEditor(); + localRule = makeRuleFromUserInput(); + populateRuleTree(ruleName); + } + } + } else { + logger.log(Level.WARNING, "Nothing selected to delete"); + } + }//GEN-LAST:event_bnDeleteRuleActionPerformed + + /** + * Deconflict artifact names in the tree view. + * + * @param initalName The base name to start with + * @param rule The rule we are deconflicting + * + * @return The new name, or throw IllegalArgumentException + */ + String getArtifactClauseNameAndNumber(String initialName, Rule rule) { + int number = 1; + + if (rule != null) { + for (ArtifactCondition ac : rule.getArtifactConditions()) { + int location = ac.getTreeDisplayName().lastIndexOf('_'); + if (ac.getTreeDisplayName().startsWith(initialName)) { + int temp = Integer.parseInt(ac.getTreeDisplayName().substring(location + 1)); + if (temp >= number) { + number = temp + 1; + } + } + } + if (number == Integer.MAX_VALUE) { + // It never became unique, so give up. + throw new IllegalArgumentException("Too many attributes named " + initialName); //NON-NLS + } + } + return (initialName + "_" + Integer.toString(number)); + } + + /** + * Creates a rule from the rule editor components on the right side of the + * panel. + * + * @return the rule created from the user's inputs. + */ + private Rule makeRuleFromUserInput() { + return makeRuleFromUserInput(false); + } + + /** + * Creates a rule from the rule editor components on the right side of the + * panel. + * + * @param showFailures If there is a failure, shows the user a dialog box + * with an explanation of why it failed. + * + * @return the rule created from the user's inputs. + */ + private Rule makeRuleFromUserInput(boolean showFailures) { + String ruleName = tbRuleName.getText(); + Rule rule = new Rule(ruleName); + + FileSizeCondition fileSizeCondition = null; + if (cbFileSize.isSelected()) { + try { + spFileSizeValue.commitEdit(); + fileSizeCondition = new Rule.FileSizeCondition((Integer) spFileSizeValue.getValue(), + SizeUnit.valueOf(comboBoxFileSizeUnits.getSelectedItem().toString()), + RelationalOp.fromSymbol(comboBoxFileSizeComparison.getSelectedItem().toString())); + } catch (ParseException ex) { + fileSizeCondition = null; + logger.log(Level.WARNING, "Could not create size condition for rule %s: " + ruleName, ex); //NON-NLS + } + } + + FileMIMETypeCondition fileMIMETypeCondition = null; + if (cbMimeType.isSelected()) { + fileMIMETypeCondition = new Rule.FileMIMETypeCondition(comboBoxMimeValue.getSelectedItem().toString(), + RelationalOp.fromSymbol(comboBoxMimeTypeComparison.getSelectedItem().toString())); + } + + try { + ArtifactCondition artifactCondition = getArtifactConditionFromInput(rule); + + if (fileSizeCondition != null) { + rule.addFileSizeCondition(fileSizeCondition); + } + + if (fileMIMETypeCondition != null) { + rule.addFileMIMETypeCondition(fileMIMETypeCondition); + } + + if (artifactCondition != null) { + rule.addArtfactCondition(artifactCondition); + } + + return rule; + + } catch (IllegalArgumentException ex) { + String message = "Attribute value '" + tbAttributeValue.getText() + "' is not of type " + comboBoxValueType.getSelectedItem().toString() + ". Ignoring invalid Attribute Type clause."; + logger.log(Level.INFO, message); + if (showFailures) { + JOptionPane.showMessageDialog(this, message, "Invalid Type Conversion", JOptionPane.OK_OPTION); + } + cbAttributeType.setSelected(false); + return null; + } + } + + /** + * Get the artifact condition from the user's input + * + * @return the ArtifactCondition, or null if there isn't one. + */ + ArtifactCondition getArtifactConditionFromInput(Rule rule) throws IllegalArgumentException { + ArtifactCondition artifactCondition = null; + if (cbAttributeType.isSelected()) { + String selectedAttribute = comboBoxAttributeName.getSelectedItem().toString(); + BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE typeFromComboBox = BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.fromLabel(comboBoxValueType.getSelectedItem().toString()); + BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE intrinsicType = attributeTypeMap.get(comboBoxAttributeName.getSelectedItem().toString()); + + // if we don't have a type in the map, but they have set the combobox, put it in the map + if (intrinsicType == null && typeFromComboBox != null) { + intrinsicType = typeFromComboBox; + attributeTypeMap.put(selectedAttribute, typeFromComboBox); + } + + if (intrinsicType == BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DATETIME) { + LocalDateTime localDateTime = dateTimePicker.getDateTime(); + if (localDateTime == null) { + throw new IllegalArgumentException("Bad date/time combination"); + } + Instant instant = localDateTime.atZone(ZoneId.systemDefault()).toInstant(); + String stringValue = Long.toString(Date.from(instant).getTime()); + artifactCondition = new Rule.ArtifactCondition( + comboBoxArtifactName.getSelectedItem().toString(), + comboBoxAttributeName.getSelectedItem().toString(), + stringValue, + intrinsicType, + RelationalOp.fromSymbol(comboBoxAttributeComparison.getSelectedItem().toString())); + } else if (intrinsicType == BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.BYTE) { + try { + String stringValue = tbAttributeValue.getText(); + byte[] hexValue = Hex.decodeHex(stringValue.toCharArray()); + String finalValue = new String(Hex.encodeHex(hexValue)); + artifactCondition = new Rule.ArtifactCondition( + comboBoxArtifactName.getSelectedItem().toString(), + comboBoxAttributeName.getSelectedItem().toString(), + finalValue, + intrinsicType, + RelationalOp.fromSymbol(comboBoxAttributeComparison.getSelectedItem().toString())); + } catch (DecoderException ex) { + throw new IllegalArgumentException(ex); + } + } else if (intrinsicType != null) { + artifactCondition = new Rule.ArtifactCondition( + comboBoxArtifactName.getSelectedItem().toString(), + comboBoxAttributeName.getSelectedItem().toString(), + tbAttributeValue.getText(), + intrinsicType, + RelationalOp.fromSymbol(comboBoxAttributeComparison.getSelectedItem().toString())); + } else { + throw new IllegalArgumentException(); + } + } + return artifactCondition; + } + + /** + * Handles clicking the "Save Rule" button. + * + * @param evt The event which caused this call. + */ + private void bnSaveRuleActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnSaveRuleActionPerformed + saveOrUpdateRule(); + }//GEN-LAST:event_bnSaveRuleActionPerformed + + /** + * Get the TreePath to this Artifact clause given a String rule and clause + * name + * + * @param ruleName the rule name to find + * @param clauseName the clause name to find + * + * @return + */ + private TreePath findTreePathByRuleAndArtifactClauseName(String ruleName, String clauseName) { + @SuppressWarnings("unchecked") + Enumeration enumeration = rootNode.preorderEnumeration(); + boolean insideRule = false; + while (enumeration.hasMoreElements()) { + DefaultMutableTreeNode node = enumeration.nextElement(); + Item item = (Item) node.getUserObject(); + if (item.getItemType() == ItemType.RULE) { + insideRule = node.toString().equalsIgnoreCase(ruleName); + } + + if ((insideRule == true) + && (item.getItemType() == ItemType.ARTIFACT_CLAUSE) + && (item.getName().compareTo(clauseName) == 0)) { + return new TreePath(node.getPath()); + } + } + return null; + } + + /** + * Get the TreePath to this rule, given a String rule name. + * + * @param ruleName the name of the rule to find + * + * @return the TreePath to this rule, given a String rule name. + */ + private TreePath findTreePathByRuleName(String ruleName) { + @SuppressWarnings("unchecked") + Enumeration enumeration = rootNode.depthFirstEnumeration(); + while (enumeration.hasMoreElements()) { + DefaultMutableTreeNode node = enumeration.nextElement(); + if (node.toString().equalsIgnoreCase(ruleName)) { + return new TreePath(node.getPath()); + } + } + return null; + } + + /** + * This method saves the rule from the editor to the tree, if the rule is + * not malformed. This means the rule must have a name, and at least one + * condition is selected. It updates any existing rule in the tree with the + * same name. + * + */ + void saveOrUpdateRule() { + String ruleName = tbRuleName.getText(); + Rule userInputRule = makeRuleFromUserInput(true); + if (userInputRule == null) { // we had bad input. do not continue. + return; + } + if ((ruleName != null && !ruleName.isEmpty()) + && (cbFileSize.isSelected() || cbMimeType.isSelected() + || cbAttributeType.isSelected())) { + localRule = userInputRule; + Rule existingRule = exportRuleSet.getRule(ruleName); + if (existingRule == null) { + // This is a new rule. Store it in the list and put it in the tree. + List userRuleArtifactConditions = userInputRule.getArtifactConditions(); + for (ArtifactCondition artifactCondition : userRuleArtifactConditions) { + String displayName = artifactCondition.getTreeDisplayName(); + artifactCondition.setTreeDisplayName(getArtifactClauseNameAndNumber(displayName, null)); + } + exportRuleSet.addRule(userInputRule); + } else { + // Update an existing rule. + exportRuleSet.removeRule(existingRule); // remove rule if it exists already, does nothing if it does not exist + + if (cbMimeType.isSelected()) { + FileMIMETypeCondition fileMIMETypeCondition = userInputRule.getFileMIMETypeCondition(); + if (fileMIMETypeCondition != null) { + existingRule.addFileMIMETypeCondition(fileMIMETypeCondition); + } + } else { + existingRule.removeFileMIMETypeCondition(); + } + + if (cbFileSize.isSelected()) { + List fileSizeConditions = userInputRule.getFileSizeConditions(); + for (FileSizeCondition fileSizeCondition : fileSizeConditions) { + // Do not need to de-dupliate, as currently implmented in FileExportRuleSet + existingRule.addFileSizeCondition(fileSizeCondition); + } + } else { + existingRule.removeFileSizeCondition(); + } + + if (cbAttributeType.isSelected()) { + // for every artifact condition in the new rule, disambiguate the name + List userRuleArtifactConditions = userInputRule.getArtifactConditions(); + for (ArtifactCondition artifactCondition : userRuleArtifactConditions) { + String displayName = artifactCondition.getTreeDisplayName(); + String newName = getArtifactClauseNameAndNumber(displayName, existingRule); + artifactCondition.setTreeDisplayName(newName); + existingRule.addArtfactCondition(artifactCondition); + } + exportRuleSet.addRule(existingRule); + + } else { + existingRule.removeArtifactConditions(); + } + exportRuleSet.addRule(existingRule); + } + populateRuleTree(ruleName); + } else { + JOptionPane.showMessageDialog(null, + NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.RuleNotSaved"), + NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.MalformedRule"), + JOptionPane.OK_OPTION); + } + } + + /** + * Handles clicking the "Browse Root Directory" button. + * + * @param evt The event which caused this call. + */ + private void bnBrowseRootDirectoryActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnBrowseRootDirectoryActionPerformed + int returnVal = rootDirectoryChooser.showOpenDialog(this); + if (returnVal == JFileChooser.APPROVE_OPTION) { + try { + validateAndSanitizeBrowsedDirectory(rootDirectoryChooser, tbRootDirectory); + } catch (FolderDidNotValidateException ex) { + JOptionPane.showMessageDialog(this, + NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.BadRootFolder") + " " + ex.getMessage(), + NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.BadFolderForInterestingFileExport"), + JOptionPane.OK_OPTION); + } + } + }//GEN-LAST:event_bnBrowseRootDirectoryActionPerformed + + /** + * Handles when any 'condition' check boxes change. + * + * @param evt The event which caused this call. + */ + private void checkButtonItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_checkButtonItemStateChanged + setSaveButton(); + }//GEN-LAST:event_checkButtonItemStateChanged + + /** + * Handles clicking the "Browse Report Directory" button. + * + * @param evt The event which caused this call. + */ + private void bnBrowseReportDirectoryActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnBrowseReportDirectoryActionPerformed + int returnVal = reportDirectoryChooser.showOpenDialog(this); + if (returnVal == JFileChooser.APPROVE_OPTION) { + try { + validateAndSanitizeBrowsedDirectory(reportDirectoryChooser, tbReportDirectory); + } catch (FolderDidNotValidateException ex) { + JOptionPane.showMessageDialog(this, + NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.BadReportFolder") + " " + ex.getMessage(), + NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.BadFolderForInterestingFileExport"), + JOptionPane.OK_OPTION); + } + } + }//GEN-LAST:event_bnBrowseReportDirectoryActionPerformed + + /** + * Handles when the MIME value combo box changes + * + * @param evt + */ + private void comboBoxMimeValueActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_comboBoxMimeValueActionPerformed + if (-1 == comboBoxMimeValue.getSelectedIndex()) { + String item = comboBoxMimeValue.getSelectedItem().toString().trim(); + if (-1 == ((DefaultComboBoxModel) comboBoxMimeValue.getModel()).getIndexOf(item)) { + comboBoxMimeValue.addItem(item); + } + } + }//GEN-LAST:event_comboBoxMimeValueActionPerformed + + /** + * Set the state of the dialog based upon the input + * + * @param state the input state + */ + private void setEnabledState(boolean state) { + bnBrowseReportDirectory.setEnabled(state); + bnBrowseRootDirectory.setEnabled(state); + bnDeleteRule.setEnabled(state); + bnSaveRule.setEnabled(state); + lbSaveRuleHelper.setVisible(!state); + cbFileSize.setEnabled(state); + cbMimeType.setEnabled(state); + comboBoxFileSizeComparison.setEnabled(state); + comboBoxFileSizeUnits.setEnabled(state); + comboBoxMimeTypeComparison.setEnabled(state); + comboBoxMimeValue.setEnabled(state); + mainPanel.setEnabled(state); + pnEditRule.setEnabled(state); + spFileSizeValue.setEnabled(state); + tbReportDirectory.setEnabled(state); + tbRootDirectory.setEnabled(state); + tbRuleName.setEnabled(state); + tbAttributeValue.setEnabled(state); + cbAttributeType.setEnabled(state); + comboBoxArtifactName.setEnabled(state); + comboBoxAttributeComparison.setEnabled(state); + comboBoxAttributeName.setEnabled(state); + comboBoxValueType.setEnabled(state); + trRuleList.setEnabled(state); + bnAddAttribute.setEnabled(state); + dateTimePicker.setEnabled(state); + bnNewRule.setEnabled(state); + lbArtifact.setEnabled(state); + lbAttribute.setEnabled(state); + lbExplanation.setEnabled(state); + lbFiles.setEnabled(state); + lbReports.setEnabled(state); + lsAttributeList.setEnabled(state); + lbRuleName.setEnabled(state); + lbSaveRuleHelper.setEnabled(state); + } + + private void cbEnableFileExportItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_cbEnableFileExportItemStateChanged + setEnabledState(cbEnableFileExport.isSelected()); + setDeleteAttributeButton(); + setSaveButton(); + }//GEN-LAST:event_cbEnableFileExportItemStateChanged + + private void bnCloseActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnCloseActionPerformed + if (saveAndValidateSettings(cbEnableFileExport.isSelected())) { + jDialog.dispose(); + } + }//GEN-LAST:event_bnCloseActionPerformed + + private void cbAttributeTypeItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_cbAttributeTypeItemStateChanged + setSaveButton(); + setAddAttributeButton(); + setDeleteAttributeButton(); + setTypeColor(); + }//GEN-LAST:event_cbAttributeTypeItemStateChanged + + private void bnAddAttributeActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnAddAttributeActionPerformed + saveOrUpdateRule(); + }//GEN-LAST:event_bnAddAttributeActionPerformed + + private void tbRuleNameKeyTyped(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_tbRuleNameKeyTyped + setAddAttributeButton(); + }//GEN-LAST:event_tbRuleNameKeyTyped + + private void comboBoxArtifactNameActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_comboBoxArtifactNameActionPerformed + if (-1 == comboBoxArtifactName.getSelectedIndex()) { + // if the selected item is not in the drop down list + String item = comboBoxArtifactName.getSelectedItem().toString().trim(); + if (-1 == ((DefaultComboBoxModel) comboBoxArtifactName.getModel()).getIndexOf(item)) { + comboBoxArtifactName.addItem(item); + } + } + setSaveButton(); + setAddAttributeButton(); + }//GEN-LAST:event_comboBoxArtifactNameActionPerformed + + private void comboBoxAttributeNameActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_comboBoxAttributeNameActionPerformed + if (-1 == comboBoxAttributeName.getSelectedIndex()) { + // if the selected item is not in the drop down list + String item = comboBoxAttributeName.getSelectedItem().toString().trim(); + if (-1 == ((DefaultComboBoxModel) comboBoxAttributeName.getModel()).getIndexOf(item)) { + comboBoxAttributeName.addItem(item); + comboBoxValueType.setSelectedItem(UNSET); + comboBoxValueType.setEnabled(true); + } + } else { + BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE typeFromMap = attributeTypeMap.get(comboBoxAttributeName.getSelectedItem().toString()); + if (typeFromMap != null) { + comboBoxValueType.setSelectedItem(typeFromMap.getLabel()); + comboBoxValueType.setEnabled(false); + } else { + comboBoxValueType.setSelectedItem(UNSET); + comboBoxValueType.setEnabled(true); + } + } + + setSaveButton(); + setAddAttributeButton(); + setTypeColor(); + }//GEN-LAST:event_comboBoxAttributeNameActionPerformed + + private void comboBoxValueTypeActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_comboBoxValueTypeActionPerformed + setSaveButton(); + setAddAttributeButton(); + setTypeColor(); + setValueField(); + }//GEN-LAST:event_comboBoxValueTypeActionPerformed + + private void bnDeleteAttributeActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnDeleteAttributeActionPerformed + String selection = lsAttributeList.getSelectedValue(); + if (selection != null && !selection.isEmpty()) { + Item item = (Item) ((DefaultMutableTreeNode) trRuleList.getLastSelectedPathComponent()).getUserObject(); + if (item != null) { + if (JOptionPane.showConfirmDialog(this, NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.ReallyDeleteCondition") + + " " + selection + NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.QuestionMark"), + NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.ConfirmClauseDeletion"), + JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE) == JOptionPane.OK_OPTION) { + + String ruleName = item.getRuleName(); + + // get the rule + Rule rule = exportRuleSet.getRule(ruleName); + + // find the clause and remove it + for (ArtifactCondition ac : rule.getArtifactConditions()) { + if (selection.compareTo(ac.getTreeDisplayName()) == 0) { + rule.removeArtifactCondition(ac); + break; + } + } + if (isRuleEmpty(rule)) { + exportRuleSet.removeRule(rule); + } + populateRuleTree(ruleName); + } + } + } + }//GEN-LAST:event_bnDeleteAttributeActionPerformed + + private void lsAttributeListValueChanged(javax.swing.event.ListSelectionEvent evt) { + if (evt.getValueIsAdjusting() == false) { + // if this is the final iteration through the value changed handler + if (lsAttributeList.getSelectedIndex() >= 0) { + // and we have a selected entry + bnDeleteAttribute.setEnabled(true); + + DefaultMutableTreeNode node = (DefaultMutableTreeNode) trRuleList.getLastSelectedPathComponent(); + if (node == null) { // Nothing is selected + return; + } + Item ruleInfo = (Item) node.getUserObject(); + if (ruleInfo.getItemType() == ItemType.RULE_SET) { + return; + } + + Object listItem = lsAttributeList.getSelectedValue(); + if (listItem != null) { + Rule rule = exportRuleSet.getRules().get(ruleInfo.getRuleName()); + if (rule != null) { + // find the attribute to select + for (ArtifactCondition ac : rule.getArtifactConditions()) { + if (ac.getTreeDisplayName().compareTo(listItem.toString()) == 0) { + // set selected and expand it + TreePath shortPath = findTreePathByRuleName(rule.getName()); + TreePath treePath = findTreePathByRuleAndArtifactClauseName(rule.getName(), listItem.toString()); + trRuleList.expandPath(shortPath); + + // Don't let treeSelectionListener respond + trRuleList.removeTreeSelectionListener(treeSelectionListener); + treeSelectionModel.setSelectionPath(treePath); + populateArtifactEditor(ac); + setDeleteAttributeButton(); + trRuleList.addTreeSelectionListener(treeSelectionListener); + localRule = makeRuleFromUserInput(); + break; + } + } + } + } else { + bnDeleteAttribute.setEnabled(false); + } + } + } + } + + private void trRuleListValueChanged(javax.swing.event.TreeSelectionEvent evt) { + int selectionCount = treeSelectionModel.getSelectionCount(); + lsAttributeList.removeAll(); + attributeListModel.removeAllElements(); + + if (selectionCount > 0) { + if (hasRuleChanged()) { + // and the rule has changed without saving + if (JOptionPane.showConfirmDialog(this, NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.UnsavedChangesLost"), + NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.ChangesWillBeLost"), + JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE) == JOptionPane.CANCEL_OPTION) { + // they were not quite ready to navigate away yet, so clear the selection + trRuleList.clearSelection(); + return; + } + } + + DefaultMutableTreeNode node = (DefaultMutableTreeNode) trRuleList.getLastSelectedPathComponent(); + if (node == null) { // Nothing is selected + return; + } + + Item nodeInfo = (Item) node.getUserObject(); + bnDeleteRule.setEnabled(nodeInfo.getItemType() == ItemType.RULE); + + if (nodeInfo.getItemType() == ItemType.RULE_SET) { + tbRuleName.setText(null); + clearSizeCondition(); + clearMIMECondition(); + clearArtifactCondition(); + localRule = makeRuleFromUserInput(); + return; + } + + String selectedRuleName = nodeInfo.getRuleName(); + Rule rule = exportRuleSet.getRules().get(selectedRuleName); + if (rule != null) { + // Read values for this rule and display them in the UI + tbRuleName.setText(rule.getName()); + FileMIMETypeCondition fileMIMETypeCondition = rule.getFileMIMETypeCondition(); + if (fileMIMETypeCondition != null) { + // if there is a MIME type condition + cbMimeType.setSelected(true); + comboBoxMimeTypeComparison.setSelectedItem(fileMIMETypeCondition.getRelationalOp().getSymbol()); + comboBoxMimeValue.setSelectedItem(fileMIMETypeCondition.getMIMEType()); + } else { // Clear the selection + clearMIMECondition(); + } + + List fileSizeCondition = rule.getFileSizeConditions(); + if (fileSizeCondition != null && !fileSizeCondition.isEmpty()) { + // if there is a file size condition + FileSizeCondition condition = fileSizeCondition.get(0); + cbFileSize.setSelected(true); + spFileSizeValue.setValue(condition.getSize()); + comboBoxFileSizeUnits.setSelectedItem(condition.getUnit().toString()); + comboBoxFileSizeComparison.setSelectedItem(condition.getRelationalOperator().getSymbol()); + } else { + // Clear the selection + clearSizeCondition(); + } + + ArtifactCondition artifactConditionToPopulateWith = null; + List artifactConditions = rule.getArtifactConditions(); + if (nodeInfo.getItemType() != ItemType.ARTIFACT_CLAUSE) { + // if there are any attribute clauses, populate the first one, otherwise clear artifact editor + if (artifactConditions.isEmpty()) { + clearArtifactCondition(); + } else { + artifactConditionToPopulateWith = artifactConditions.get(0); + } + } else { // an artifact clause is selected. populate it. + for (ArtifactCondition artifact : artifactConditions) { + if (artifact.getTreeDisplayName().compareTo(nodeInfo.getName()) == 0) { + artifactConditionToPopulateWith = artifact; + break; + } + } + } + if (artifactConditionToPopulateWith != null) { + for (ArtifactCondition artifact : artifactConditions) { + attributeListModel.addElement(artifact.getTreeDisplayName()); + } + // Don't let listSelectionListener respond + lsAttributeList.removeListSelectionListener(listSelectionListener); + lsAttributeList.setSelectedValue(artifactConditionToPopulateWith.getTreeDisplayName(), true); + populateArtifactEditor(artifactConditionToPopulateWith); + setDeleteAttributeButton(); + lsAttributeList.addListSelectionListener(listSelectionListener); + } + } + localRule = makeRuleFromUserInput(); + } else { + bnDeleteRule.setEnabled(false); + } + } + + void setAddAttributeButton() { + if (!tbRuleName.getText().isEmpty()) { + bnAddAttribute.setEnabled((cbAttributeType.isSelected()) + && (comboBoxValueType.getSelectedItem().toString().compareTo(UNSET) != 0)); + } else { + bnAddAttribute.setEnabled(false); + } + } + + void setDeleteAttributeButton() { + String selection = lsAttributeList.getSelectedValue(); + bnDeleteAttribute.setEnabled(selection != null && !selection.isEmpty() && cbEnableFileExport.isSelected() != false); + } + + /** + * Clears out the fields of the file size condition, reseting them to the + * default value. + */ + private void clearSizeCondition() { + cbFileSize.setSelected(false); + spFileSizeValue.setValue(1024); + comboBoxFileSizeUnits.setSelectedIndex(0); + comboBoxFileSizeComparison.setSelectedIndex(0); + } + + /** + * Clears out the fields of the MIME Type condition, reseting them to the + * default value. + */ + private void clearMIMECondition() { + cbMimeType.setSelected(false); + comboBoxMimeValue.setSelectedIndex(0); + comboBoxMimeTypeComparison.setSelectedIndex(0); + } + + /** + * Clears out the fields of the Artifact condition, reseting them to the + * default value. + */ + private void clearArtifactCondition() { + tbAttributeValue.setText(""); + cbAttributeType.setSelected(false); + comboBoxArtifactName.setSelectedIndex(0); + comboBoxAttributeComparison.setSelectedIndex(0); + comboBoxAttributeName.setSelectedIndex(0); + comboBoxValueType.setSelectedIndex(0); + dateTimePicker.clear(); + } + + /** + * Clears out the fields of the entire rule editor, reseting them to the + * default value and clearing the selection in the rule list on the left. + */ + private void clearRuleEditor() { + tbRuleName.setText(null); + clearSizeCondition(); + clearMIMECondition(); + clearArtifactCondition(); + trRuleList.clearSelection(); + } + + /** + * Checks if a rule has changed since the last snapshot stored in localRule. + * + * @return True if the rule has changed, False otherwise + */ + boolean hasRuleChanged() { + return (!localRule.equals(makeRuleFromUserInput())); + } + + /** + * Check if this rule has no clauses left. + * + * @param rule The rule to check + * + * @return True if the rule is empty, false otherwise. + */ + boolean isRuleEmpty(Rule rule) { + + FileMIMETypeCondition mtc = rule.getFileMIMETypeCondition(); + List fsc = rule.getFileSizeConditions(); + List arc = rule.getArtifactConditions(); + + // if there are no clauses in the rule, it's empty. + return (mtc == null) && (fsc == null || fsc.isEmpty()) && (arc == null || arc.isEmpty()); + } + + /** + * Validates that the selected folder exists, and sets the passed-in + * textField to empty if it does not exist. Then checks if the sanitized + * folder is readable and writable with the current user's credentials, + * returning True if so and False if not. This has the desirable effect that + * a folder can show up in the text box (as chosen by the user while + * browsing), and still return false if the permissions are incorrect. This + * way the user can see the folder chosen and take appropriate action to fix + * the permissions. Use this only for files chosen by the file chooser. + * + * @param fileChooser The fileChooser we want to use. + * @param textField The text field to store the sanitized value in. + * + * Throws FolderDidNotValidateException + */ + private void validateAndSanitizeBrowsedDirectory(JFileChooser fileChooser, JTextField textField) throws FolderDidNotValidateException { + File selectedFile = fileChooser.getSelectedFile(); + if (selectedFile == null || !selectedFile.exists()) { + textField.setText(""); //NON-NLS + + throw new FolderDidNotValidateException(NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.UnableToFindDirectory")); + + } else { + textField.setText(selectedFile.toString()); + File file = new File(selectedFile.toString()); + fileChooser.setCurrentDirectory(file); + + if (!FileUtil.hasReadWriteAccess(file.toPath())) { + throw new FolderDidNotValidateException(NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.PermissionsInvalid")); + } + } + } + + /** + * Validates that the selected folder exists, then checks if the sanitized + * folder is readable and writable with the current user's credentials. + * Throws and exception with explanatory text if the folder is not valid. + * This has none of the side effects of the above method such as setting the + * JFileChooser and JTextFields. This method only checks if the passed-in + * String is a valid directory for our purposes. + * + * @param selectedString The String to validate + * + * @return Returns the validated String, if possible + * + * throws FolderDidNotValidateException + */ + private String validateDirectory(String selectedString) throws FolderDidNotValidateException { + File selectedFile = new File(selectedString); + + if (!selectedFile.exists()) { + throw new FolderDidNotValidateException(NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.UnableToFindDirectory")); + } else { + File file = new File(selectedFile.toString()); + + if (!FileUtil.hasReadWriteAccess(file.toPath())) { + throw new FolderDidNotValidateException(NbBundle.getMessage(FileExporterSettingsPanel.class, "FileExporterSettingsPanel.PermissionsInvalid")); + } + } + return selectedString; + } + + /** + * Allows Options Panel to tell if the root directory is valid. + * Throws an exception with explanatory text if it is not valid. + * + * throws FolderDidNotValidateException + */ + void validateRootDirectory() throws FolderDidNotValidateException { + tbRootDirectory.setText(validateDirectory(tbRootDirectory.getText())); + } + + /** + * Allows Options Panel to tell if the report directory is valid. + * Throws an exception with explanatory text if it is not valid. + * + * throws FolderDidNotValidateException + */ + void validateReportDirectory() throws FolderDidNotValidateException { + tbReportDirectory.setText(validateDirectory(tbReportDirectory.getText())); + } + + /** + * Store the settings to disk. + */ + public void store() { + FileExportSettings settings = new FileExportSettings(); + settings.setFilesRootDirectory(Paths.get(tbRootDirectory.getText())); + settings.setReportsRootDirectory(Paths.get(tbReportDirectory.getText())); + settings.setFileExportEnabledState(cbEnableFileExport.isSelected()); + TreeMap treeMap = new TreeMap<>(); + treeMap.put(exportRuleSet.getName(), exportRuleSet); + settings.setRuleSets(treeMap); + try { + FileExportSettings.store(settings); + } catch (FileExportSettings.PersistenceException ex) { + logger.log(Level.SEVERE, "Unable to save rules: ", ex); //NON-NLS + } + } + + /** + * Read the settings from disk. + */ + public void load() { + try { + FileExportSettings settings = FileExportSettings.load(); + if (settings != null) { + Path path = settings.getFilesRootDirectory(); + if (path != null) { + tbRootDirectory.setText(path.toString()); + } + path = settings.getReportsRootDirectory(); + if (path != null) { + tbReportDirectory.setText(path.toString()); + } + TreeMap treeMap = settings.getRuleSets(); + if (treeMap != null && !treeMap.isEmpty()) { + exportRuleSet = treeMap.firstEntry().getValue(); + } + boolean answer = settings.getFileExportEnabledState(); + setEnabledState(answer); + cbEnableFileExport.setSelected(answer); + } + return; + } catch (FileExportSettings.PersistenceException ex) { + logger.log(Level.INFO, "Unable to load rule settings: {0}", ex.getMessage()); //NON-NLS + } + setEnabledState(false); + cbEnableFileExport.setSelected(false); + } + + class FolderDidNotValidateException extends Exception { + + private static final long serialVersionUID = 1L; + + FolderDidNotValidateException(String message) { + super(message); + } + } + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton bnAddAttribute; + private javax.swing.JButton bnBrowseReportDirectory; + private javax.swing.JButton bnBrowseRootDirectory; + private javax.swing.JButton bnClose; + private javax.swing.JButton bnDeleteAttribute; + private javax.swing.JButton bnDeleteRule; + private javax.swing.JButton bnNewRule; + private javax.swing.JButton bnSaveRule; + private javax.swing.JCheckBox cbAttributeType; + private javax.swing.JCheckBox cbEnableFileExport; + private javax.swing.JCheckBox cbFileSize; + private javax.swing.JCheckBox cbMimeType; + private javax.swing.JComboBox comboBoxArtifactName; + private javax.swing.JComboBox comboBoxAttributeComparison; + private javax.swing.JComboBox comboBoxAttributeName; + private javax.swing.JComboBox comboBoxFileSizeComparison; + private javax.swing.JComboBox comboBoxFileSizeUnits; + private javax.swing.JComboBox comboBoxMimeTypeComparison; + private javax.swing.JComboBox comboBoxMimeValue; + private javax.swing.JComboBox comboBoxValueType; + private com.github.lgooddatepicker.datetimepicker.DateTimePicker dateTimePicker; + private javax.swing.JScrollPane jScrollPane1; + private javax.swing.JLabel lbArtifact; + private javax.swing.JLabel lbAttribute; + private javax.swing.JLabel lbExplanation; + private javax.swing.JLabel lbFiles; + private javax.swing.JLabel lbReports; + private javax.swing.JLabel lbRuleName; + private javax.swing.JLabel lbSaveRuleHelper; + private javax.swing.JList lsAttributeList; + private javax.swing.JPanel mainPanel; + private javax.swing.JPanel pnEditRule; + private javax.swing.JScrollPane ruleListScrollPane; + private javax.swing.JSpinner spFileSizeValue; + private javax.swing.JTextField tbAttributeValue; + private javax.swing.JTextField tbReportDirectory; + private javax.swing.JTextField tbRootDirectory; + private javax.swing.JTextField tbRuleName; + private javax.swing.JTree trRuleList; + // End of variables declaration//GEN-END:variables +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/GrayableCellRenderer.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/GrayableCellRenderer.java new file mode 100644 index 0000000000..60d3b77ccd --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/GrayableCellRenderer.java @@ -0,0 +1,70 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.Color; +import java.awt.Component; +import javax.swing.JTable; +import static javax.swing.SwingConstants.LEFT; +import javax.swing.table.DefaultTableCellRenderer; + +/** + * A JTable cell renderer that left-aligns cell content and grays out the cell + * if the table is disabled. + */ +class GrayableCellRenderer extends DefaultTableCellRenderer { + + private static final long serialVersionUID = 1L; + + GrayableCellRenderer() { + setHorizontalAlignment(LEFT); + } + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { + if (null != value) { + setText(value.toString()); + } + grayCellIfTableNotEnabled(table, isSelected); + return this; + } + + void grayCellIfTableNotEnabled(JTable table, boolean isSelected) { + if (table.isEnabled()) { + /* + * The table is enabled, make the foreground and background the + * normal selected or unselected color. + */ + if (isSelected) { + setBackground(table.getSelectionBackground()); + setForeground(table.getSelectionForeground()); + } else { + setBackground(table.getBackground()); + setForeground(table.getForeground()); + } + } else { + /* + * The table is disabled, make the foreground and background gray. + */ + setBackground(Color.lightGray); + setForeground(Color.darkGray); + } + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ImportDoneCallback.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ImportDoneCallback.java new file mode 100644 index 0000000000..dba1c080bb --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ImportDoneCallback.java @@ -0,0 +1,24 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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; + +public interface ImportDoneCallback { + + void importDoneCallback(boolean result, String resultString); +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/LongDateCellRenderer.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/LongDateCellRenderer.java new file mode 100644 index 0000000000..cfa2cedb14 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/LongDateCellRenderer.java @@ -0,0 +1,49 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.Component; +import java.text.SimpleDateFormat; +import javax.swing.JTable; +import static javax.swing.SwingConstants.CENTER; + +/** + * A JTable cell renderer that renders a date represented as a long as a + * center-aligned, long-format date string. It also grays out the cell if the + * table is disabled. + */ +class LongDateCellRenderer extends GrayableCellRenderer { + + private static final long serialVersionUID = 1L; + private static final String FORMAT_STRING = "yyyy/MM/dd HH:mm:ss"; //NON-NLS + private static final SimpleDateFormat dateFormat = new SimpleDateFormat(FORMAT_STRING); + + public LongDateCellRenderer() { + setHorizontalAlignment(CENTER); + } + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { + if (value != null) { + setText(dateFormat.format(value)); + } + grayCellIfTableNotEnabled(table, isSelected); + return this; + } +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Manifest.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Manifest.java new file mode 100644 index 0000000000..3acf895a9d --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Manifest.java @@ -0,0 +1,130 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.Serializable; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.concurrent.Immutable; + +/** + * RJCTODO + */ +@Immutable +public final class Manifest implements Serializable { + + private static final long serialVersionUID = 1L; + private final String filePath; + private final Date dateFileCreated; + private final String caseName; + private final String deviceId; + private final String dataSourcePath; + private final Map manifestProperties; + + /** + * RJCTODO + * + * @param manifestFilePath + * @param caseName + * @param deviceId + * @param dataSourcePath + * @param manifestProperties + * + * @throws IOException + */ + public Manifest(Path manifestFilePath, String caseName, String deviceId, Path dataSourcePath, Map manifestProperties) throws IOException { + this.filePath = manifestFilePath.toString(); + BasicFileAttributes attrs = Files.readAttributes(manifestFilePath, BasicFileAttributes.class); + this.dateFileCreated = new Date(attrs.creationTime().toMillis()); + this.caseName = caseName; + this.deviceId = deviceId; + this.dataSourcePath = dataSourcePath.toString(); + this.manifestProperties = new HashMap<>(manifestProperties); + } + + /** + * RJCTODO + * + * @return + */ + public Path getFilePath() { + return Paths.get(this.filePath); + } + + /** + * RJCTODO + * + * @return + * @throws IOException + */ + public Date getDateFileCreated() { + return this.dateFileCreated; + } + + /** + * RJCTODO + * + * @return + */ + public String getCaseName() { + return caseName; + } + + /** + * RJCTODO + * + * @return + */ + public String getDeviceId() { + return deviceId; + } + + /** + * RJCTODO + * + * @return + */ + public Path getDataSourcePath() { + return Paths.get(dataSourcePath); + } + + /** + * RJCTODO + * @return + */ + public String getDataSourceFileName() { + return Paths.get(dataSourcePath).getFileName().toString(); + } + + /** + * RJCTODO + * + * @return + */ + public Map getManifestProperties() { + return new HashMap<>(manifestProperties); + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ManifestFileParser.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ManifestFileParser.java new file mode 100644 index 0000000000..14111b0410 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ManifestFileParser.java @@ -0,0 +1,58 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.nio.file.Path; + +/** + * RJCTODO: + */ +public interface ManifestFileParser { + + boolean fileIsManifest(Path filePath); + Manifest parse(Path filePath) throws ManifestFileParserException; + + /** + * Exception thrown if a manifest file cannot be parsed. RJCTODO + */ + public final static class ManifestFileParserException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Constructs an exception to throw if a manifest file cannot be parsed. + * + * @param message The exception message. + */ + public ManifestFileParserException(String message) { + super(message); + } + + /** + * Constructs an exception to throw if a manifest file cannot be parsed. + * + * @param message The exception message. + * @param cause The exception cause, if it was a Throwable. + */ + public ManifestFileParserException(String message, Throwable cause) { + super(message, cause); + } + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ManifestNodeData.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ManifestNodeData.java new file mode 100644 index 0000000000..e4e272edd3 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ManifestNodeData.java @@ -0,0 +1,237 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.nio.ByteBuffer; +import java.util.Date; + +/** + * A coordination service node data transfer object for an auto ingest job + * manifest. The data include: processing status, priority, the number of times + * the auto ingest job for the manifest has crashed during processing, and the + * date the auto ingest job for the manifest was completed. + */ +final class ManifestNodeData { + + private static final int DEFAULT_PRIORITY = 0; + private final boolean coordSvcNodeDataWasSet; + private ProcessingStatus status; + private int priority; + private int numberOfCrashes; + private long completedDate; + private boolean errorsOccurred; + + /** + * Constructs a coordination service node data data transfer object for an + * auto ingest manifest from the raw bytes obtained from the coordination + * service. + * + * @param nodeData The raw bytes received from the coordination service. + */ + ManifestNodeData(byte[] nodeData) { + ByteBuffer buffer = ByteBuffer.wrap(nodeData); + this.coordSvcNodeDataWasSet = buffer.hasRemaining(); + if (this.coordSvcNodeDataWasSet) { + int rawStatus = buffer.getInt(); + if (ProcessingStatus.PENDING.ordinal() == rawStatus) { + this.status = ProcessingStatus.PENDING; + } else if (ProcessingStatus.PROCESSING.ordinal() == rawStatus) { + this.status = ProcessingStatus.PROCESSING; + } else if (ProcessingStatus.COMPLETED.ordinal() == rawStatus) { + this.status = ProcessingStatus.COMPLETED; + }else if (ProcessingStatus.DELETED.ordinal() == rawStatus) { + this.status = ProcessingStatus.DELETED; + } + this.priority = buffer.getInt(); + this.numberOfCrashes = buffer.getInt(); + this.completedDate = buffer.getLong(); + int errorFlag = buffer.getInt(); + this.errorsOccurred = (1 == errorFlag); + } else { + this.status = ProcessingStatus.PENDING; + this.priority = DEFAULT_PRIORITY; + this.numberOfCrashes = 0; + this.completedDate = 0L; + this.errorsOccurred = false; + } + } + + /** + * Constructs a coordination service node data data transfer object for an + * auto ingest manifest from values provided by the auto ingest system. + * + * @param status The processing status of the manifest. + * @param priority The priority of the manifest. + * @param numberOfCrashes The number of times auto ingest jobs for the + * manifest have crashed during processing. + * @param completedDate The date the auto ingest job for the manifest was + * completed. + */ + ManifestNodeData(ProcessingStatus status, int priority, int numberOfCrashes, Date completedDate, boolean errorOccurred) { + this.coordSvcNodeDataWasSet = false; + this.status = status; + this.priority = priority; + this.numberOfCrashes = numberOfCrashes; + this.completedDate = completedDate.getTime(); + this.errorsOccurred = errorOccurred; + } + + /** + * Indicates whether or not the coordination service node data was set, + * i.e., this object was constructed from raw bytes from the ccordination + * service node for the manifest. + * + * @return True or false. + */ + // RJCTODO: This is confusing, consider changing the API so that the use case is to + // check the length of the node data from the coordination service before + // constructing an instance of this object. That would be much more clear! + boolean coordSvcNodeDataWasSet() { + return this.coordSvcNodeDataWasSet; + } + + /** + * Gets the processing status of the manifest + * + * @return The processing status of the manifest. + */ + ProcessingStatus getStatus() { + return this.status; + } + + /** + * Sets the processing status of the manifest + * + * @param status The processing status of the manifest. + */ + void setStatus(ProcessingStatus status) { + this.status = status; + } + + /** + * Gets the priority of the manifest. + * + * @return The priority of the manifest. + */ + int getPriority() { + return this.priority; + } + + /** + * Sets the priority of the manifest. A higher number indicates a higheer + * priority. + * + * @param priority The priority of the manifest. + */ + void setPriority(int priority) { + this.priority = priority; + } + + /** + * Gets the number of times auto ingest jobs for the manifest have crashed + * during processing. + * + * @return The number of times auto ingest jobs for the manifest have + * crashed during processing. + */ + int getNumberOfCrashes() { + return this.numberOfCrashes; + } + + /** + * Sets the number of times auto ingest jobs for the manifest have crashed + * during processing. + * + * @param numberOfCrashes The number of times auto ingest jobs for the + * manifest have crashed during processing. + */ + void setNumberOfCrashes(int numberOfCrashes) { + this.numberOfCrashes = numberOfCrashes; + } + + /** + * Gets the date the auto ingest job for the manifest was completed. + * + * @return The date the auto ingest job for the manifest was completed. The + * epoch (January 1, 1970, 00:00:00 GMT) indicates the date is not + * set, i.e., Date.getTime() returns 0L. + */ + Date getCompletedDate() { + return new Date(this.completedDate); + } + + /** + * Sets the date the auto ingest job for the manifest was completed. + * + * @param completedDate The date the auto ingest job for the manifest was + * completed. Use the epoch (January 1, 1970, 00:00:00 + * GMT) to indicate the date is not set, i.e., new + * Date(0L). + */ + void setCompletedDate(Date completedDate) { + this.completedDate = completedDate.getTime(); + } + + /** + * Queries whether or not any errors occurred during the processing of the + * auto ingest job for the manifest. + * + * @return True or false. + */ + boolean getErrorsOccurred() { + return this.errorsOccurred; + } + + /** + * Sets whether or not any errors occurred during the processing of the auto + * ingest job for the manifest. + * + * @param errorsOccurred True or false. + */ + void setErrorsOccurred(boolean errorsOccurred) { + this.errorsOccurred = errorsOccurred; + } + + /** + * Gets the node data as raw bytes that can be sent to the coordination + * service. + * + * @return The manifest node data as a byte array. + */ + byte[] toArray() { + ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES * 4 + Long.BYTES); + buffer.putInt(this.status.ordinal()); + buffer.putInt(this.priority); + buffer.putInt(this.numberOfCrashes); + buffer.putLong(this.completedDate); + buffer.putInt(this.errorsOccurred ? 1 : 0); + return buffer.array(); + } + + /** + * Processing status for the auto ingest job for the manifest. + */ + enum ProcessingStatus { + PENDING, + PROCESSING, + COMPLETED, + DELETED + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/PathUtils.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/PathUtils.java new file mode 100644 index 0000000000..622912f95a --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/PathUtils.java @@ -0,0 +1,178 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.FilenameFilter; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.sleuthkit.autopsy.casemodule.CaseMetadata; +import org.sleuthkit.autopsy.casemodule.GeneralFilter; + +final class PathUtils { + + private static final List CASE_METADATA_FILE_EXTS = Arrays.asList(new String[]{CaseMetadata.getFileExtension()}); + private static final GeneralFilter caseMetadataFileFilter = new GeneralFilter(CASE_METADATA_FILE_EXTS, "Autopsy Case File"); + + /** + * Searches a given folder for the most recently modified case folder for a + * case. + * + * @param folderToSearch The folder to be searched. + * @param caseName The name of the case for which a case folder is to + * be found. + * + * @return The path of the case folder, or null if it is not found. + */ + static Path findCaseDirectory(Path folderToSearch, String caseName) { + File searchFolder = new File(folderToSearch.toString()); + if (!searchFolder.isDirectory()) { + return null; + } + Path caseFolderPath = null; + String[] candidateFolders = searchFolder.list(new CaseFolderFilter(caseName)); + long mostRecentModified = 0; + for (String candidateFolder : candidateFolders) { + File file = new File(candidateFolder); + if (file.lastModified() >= mostRecentModified) { + mostRecentModified = file.lastModified(); + caseFolderPath = Paths.get(folderToSearch.toString(), file.getPath()); + } + } + return caseFolderPath; + } + + /** + * Gets a listing of case folders in a given folder. + * + * @param folderToSearch The path of the folder to search. + * + * @return A list of the output case folder paths. + */ + static List findCaseFolders(Path folderToSearch) { // RJCTODO: Rename + File searchFolder = new File(folderToSearch.toString()); + if (!searchFolder.isDirectory()) { + return Collections.emptyList(); + } + String[] caseFolders = searchFolder.list(new CaseFolderFilter(null)); + List caseFolderPaths = new ArrayList<>(); + for (String path : caseFolders) { + caseFolderPaths.add(Paths.get(folderToSearch.toString(), path)); + } + return caseFolderPaths; + } + + /** + * Determines whether or not there is a case metadata file in a given + * folder. + * + * @param folderPath Path to the folder to search. + * + * @return True or false. + */ + static boolean hasCaseMetadataFile(Path folderPath) { + /** + * TODO: If need be, this can be rewritten without the FilenameFilter so + * that it does not necessarily visit every file in the folder. + */ + File folder = folderPath.toFile(); + if (!folder.isDirectory()) { + return false; + } + + String[] caseDataFiles = folder.list((File folder1, String fileName) -> { + File file = new File(folder1, fileName); + if (file.isFile()) { + return caseMetadataFileFilter.accept(file); + } + return false; + }); + return caseDataFiles.length != 0; + } + + /** + * Extracts the case name from a case folder path. + * + * @param caseFolderPath A case folder path. + * + * @return A case name, with the time stamp suffix removed. + */ + static String caseNameFromCaseDirectoryPath(Path caseFolderPath) { + String caseName = caseFolderPath.getFileName().toString(); + if (caseName.length() > TimeStampUtils.getTimeStampLength()) { + return caseName.substring(0, caseName.length() - TimeStampUtils.getTimeStampLength()); + } else { + return caseName; + } + } + + /** + * Creates a case folder path. Does not create the folder described by the + * path. + * + * @param caseFoldersPath The root case folders path. + * @param caseName The name of the case. + * + * @return A case folder path with a time stamp suffix. + */ + static Path createCaseFolderPath(Path caseFoldersPath, String caseName) { // RJCTODO: Rename + String folderName = caseName + "_" + TimeStampUtils.createTimeStamp(); + return Paths.get(caseFoldersPath.toString(), folderName); + } + + private static class CaseFolderFilter implements FilenameFilter { + + private final String caseName; + + CaseFolderFilter(String caseName) { + this.caseName = caseName; + } + + @Override + public boolean accept(File folder, String fileName) { + File file = new File(folder, fileName); + if (file.isDirectory() && fileName.length() > TimeStampUtils.getTimeStampLength()) { + Path filePath = Paths.get(file.getPath()); + if (TimeStampUtils.endsWithTimeStamp(fileName)) { + if (null != caseName) { + String fileNamePrefix = fileName.substring(0, fileName.length() - TimeStampUtils.getTimeStampLength()); + if (fileNamePrefix.equals(caseName)) { + return hasCaseMetadataFile(filePath); + } + } else { + return hasCaseMetadataFile(filePath); + } + } + } + return false; + } + + } + + /** + * Supress creation of instances of this class. + */ + private PathUtils() { + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ReviewModeCaseManager.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ReviewModeCaseManager.java new file mode 100644 index 0000000000..099ef36088 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ReviewModeCaseManager.java @@ -0,0 +1,240 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import javax.swing.SwingUtilities; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; +import org.openide.util.actions.CallableSystemAction; +import org.sleuthkit.autopsy.casemodule.AddImageAction; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.CaseActionException; +import org.sleuthkit.autopsy.casemodule.CaseNewAction; +import org.sleuthkit.autopsy.experimental.configuration.AutoIngestUserPreferences; +import org.sleuthkit.autopsy.experimental.coordinationservice.CoordinationService; +import org.sleuthkit.autopsy.experimental.coordinationservice.CoordinationService.CoordinationServiceException; + +/** + * Handles opening, locking, and unlocking cases in review mode. Instances of + * this class are tightly coupled to the Autopsy "current case" concept and the + * Autopsy UI, and cases must be opened by code executing in the event + * dispatch thread (EDT). Because of the tight coupling to the UI, exception + * messages are deliberately user-friendly. + */ +final class ReviewModeCaseManager implements PropertyChangeListener { + + /* + * Provides uniform exceptions with user-friendly error messages. + */ + final class ReviewModeCaseManagerException extends Exception { + + private static final long serialVersionUID = 1L; + + private ReviewModeCaseManagerException(String message) { + super(message); + } + + private ReviewModeCaseManagerException(String message, Throwable cause) { + super(message, cause); + } + + } + + private static final Logger logger = Logger.getLogger(ReviewModeCaseManager.class.getName()); + private static ReviewModeCaseManager instance; + private CoordinationService.Lock currentCaseLock; + + /** + * Gets the review mode case manager. + * + * @return The review mode case manager singleton. + */ + synchronized static ReviewModeCaseManager getInstance() { + if (instance == null) { + /* + * Two stage construction is used here to avoid allowing "this" + * reference to escape from the constructor via registering as an + * PropertyChangeListener. This is to ensure that a partially + * constructed manager is not published to other threads. + */ + instance = new ReviewModeCaseManager(); + Case.addPropertyChangeListener(instance); + } + return instance; + } + + /** + * Constructs a review mode case manager to handles opening, locking, and + * unlocking cases in review mode. Instances of this class are tightly + * coupled to the Autopsy "current case" concept and the Autopsy UI, + * and cases must be opened by code executing in the event dispatch thread + * (EDT). Because of the tight coupling to the UI, exception messages are + * deliberately user-friendly. + * + */ + private ReviewModeCaseManager() { + /* + * Disable the new case action because review mode is only for looking + * at cases created by automated ingest. + */ + CallableSystemAction.get(CaseNewAction.class).setEnabled(false); + + /* + * Permanently delete the "Open Recent Cases" item in the "File" menu. + * This is quite drastic, as it also affects Autopsy standalone mode on + * this machine, but review mode is only for looking at cases created by + * automated ingest. + */ + FileObject root = FileUtil.getConfigRoot(); + FileObject openRecentCasesMenu = root.getFileObject("Menu/Case/OpenRecentCase"); + if (openRecentCasesMenu != null) { + try { + openRecentCasesMenu.delete(); + } catch (IOException ex) { + ReviewModeCaseManager.logger.log(Level.WARNING, "Unable to remove Open Recent Cases file menu item", ex); + } + } + } + + /* + * Gets a list of the cases in the top level case folder used by automated + * ingest. + */ + List getCases() { + List cases = new ArrayList<>(); + List caseFolders = PathUtils.findCaseFolders(Paths.get(AutoIngestUserPreferences.getAutoModeResultsFolder())); + for (Path caseFolderPath : caseFolders) { + cases.add(new AutoIngestCase(caseFolderPath)); + } + return cases; + } + + /** + * Attempts to open a case as the current case. Assumes it is called by code + * executing in the event dispatch thread (EDT). + * + * @param caseMetadataFilePath Path to the case metadata file. + * + * @throws ReviewModeCaseManagerException + */ + /* + * TODO (RC): With a little work, the lock acquisition/release could be done + * by a thread in a single thread executor, removing the "do it in the EDT" + * requirement + */ + synchronized void openCaseInEDT(Path caseMetadataFilePath) throws ReviewModeCaseManagerException { + Path caseFolderPath = caseMetadataFilePath.getParent(); + try { + /* + * Acquire a lock on the case folder. If the lock cannot be + * acquired, the case cannot be opened. + */ + currentCaseLock = CoordinationService.getInstance(CoordinationServiceNamespace.getRoot()).tryGetSharedLock(CoordinationService.CategoryNode.CASES, caseFolderPath.toString()); + if (null == currentCaseLock) { + throw new ReviewModeCaseManagerException("Could not get shared access to multi-user case folder"); + } + + /* + * Open the case. + */ + Case.open(caseMetadataFilePath.toString()); + + /** + * Disable the add data source action in review mode. This has to be + * done here because Case.open() calls Case.doCaseChange() and the + * latter method enables the action. Since Case.doCaseChange() + * enables the menus on EDT by calling SwingUtilities.invokeLater(), + * we have to do the same thing here to maintain the order of + * execution. + */ + SwingUtilities.invokeLater(() -> { + CallableSystemAction.get(AddImageAction.class).setEnabled(false); + }); + + } catch (CoordinationServiceException | ReviewModeCaseManagerException | CaseActionException ex) { + /* + * Release the coordination service lock on the case folder. + */ + try { + if (currentCaseLock != null) { + currentCaseLock.release(); + currentCaseLock = null; + } + } catch (CoordinationService.CoordinationServiceException exx) { + logger.log(Level.SEVERE, String.format("Error deleting legacy LOCKED state file for case at %s", caseFolderPath), exx); + } + + if (ex instanceof CoordinationServiceException) { + throw new ReviewModeCaseManagerException("Could not get access to the case folder from the coordination service, contact administrator", ex); + } else if (ex instanceof IOException) { + throw new ReviewModeCaseManagerException("Could not write to the case folder, contact adminstrator", ex); + } else if (ex instanceof CaseActionException) { + /* + * CaseActionExceptions have user friendly error messages. + */ + throw new ReviewModeCaseManagerException(String.format("Could not open the case (%s), contract administrator", ex.getMessage()), ex); + } else if (ex instanceof ReviewModeCaseManagerException) { + throw (ReviewModeCaseManagerException) ex; + } + } + } + + /** + * @inheritDoc + */ + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (evt.getPropertyName().equals(Case.Events.CURRENT_CASE.toString()) + && null != evt.getOldValue() + && null == evt.getNewValue()) { + /* + * When a case is closed, release the coordination service lock on + * the case folder. This must be done in the EDT because it was + * acquired in the EDT via openCase(). + */ + if (null != currentCaseLock) { + try { + SwingUtilities.invokeAndWait(() -> { + try { + currentCaseLock.release(); + currentCaseLock = null; + } catch (CoordinationService.CoordinationServiceException ex) { + logger.log(Level.SEVERE, String.format("Failed to release the coordination service lock with path %s", currentCaseLock.getNodePath()), ex); + currentCaseLock = null; + } + }); + } catch (InterruptedException | InvocationTargetException ex) { + logger.log(Level.SEVERE, String.format("Failed to release the coordination service lock with path %s", currentCaseLock.getNodePath()), ex); + currentCaseLock = null; + } + } + } + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ReviewModeCasePanel.form b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ReviewModeCasePanel.form new file mode 100644 index 0000000000..2756fa8da0 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ReviewModeCasePanel.form @@ -0,0 +1,237 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ReviewModeCasePanel.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ReviewModeCasePanel.java new file mode 100644 index 0000000000..fc45308db7 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ReviewModeCasePanel.java @@ -0,0 +1,626 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.Desktop; +import java.nio.file.Paths; +import java.util.List; +import javax.swing.JPanel; +import java.awt.EventQueue; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.nio.file.Path; +import java.util.Date; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import org.sleuthkit.autopsy.coreutils.Logger; +import javax.swing.JOptionPane; +import javax.swing.JDialog; +import javax.swing.event.ListSelectionEvent; +import javax.swing.table.DefaultTableModel; +import javax.swing.table.TableColumn; +import org.sleuthkit.autopsy.casemodule.StartupWindowProvider; +import java.awt.Cursor; +import java.io.IOException; +import org.openide.windows.WindowManager; +import org.sleuthkit.autopsy.casemodule.CaseMetadata; +import org.sleuthkit.autopsy.experimental.autoingest.ReviewModeCaseManager.ReviewModeCaseManagerException; + +/** + * A panel that allows a user to open cases created by automated ingest. + */ +public final class ReviewModeCasePanel extends JPanel { + + private static final long serialVersionUID = 1L; + private static final Logger logger = Logger.getLogger(ReviewModeCasePanel.class.getName()); + private static final AutoIngestCase.LastAccessedDateDescendingComparator reverseDateModifiedComparator = new AutoIngestCase.LastAccessedDateDescendingComparator(); + private static final int CASE_COL_MIN_WIDTH = 30; + private static final int CASE_COL_MAX_WIDTH = 2000; + private static final int CASE_COL_PREFERRED_WIDTH = 300; + private static final int TIME_COL_MIN_WIDTH = 40; + private static final int TIME_COL_MAX_WIDTH = 250; + private static final int TIME_COL_PREFERRED_WIDTH = 160; + private static final int STATUS_COL_MIN_WIDTH = 55; + private static final int STATUS_COL_MAX_WIDTH = 250; + private static final int STATUS_COL_PREFERRED_WIDTH = 60; + private static final int MILLISECONDS_TO_WAIT_BEFORE_STARTING = 500; // RJCTODO: Shorten name + private static final int MILLISECONDS_TO_WAIT_BETWEEN_UPDATES = 30000; // RJCTODO: Shorten name + private ScheduledThreadPoolExecutor casesTableRefreshExecutor; + + /* + * The JTable table model for the cases table presented by this view is + * defined by the following string, enum, and array. + * + * TODO (RC): Consider unifying this stuff in an enum as in + * AutoIngestDashboard to make it less error prone. + */ + private static final String CASE_HEADER = org.openide.util.NbBundle.getMessage(ReviewModeCasePanel.class, "ReviewModeCasePanel.CaseHeaderText"); + private static final String CREATEDTIME_HEADER = org.openide.util.NbBundle.getMessage(ReviewModeCasePanel.class, "ReviewModeCasePanel.CreatedTimeHeaderText"); + private static final String COMPLETEDTIME_HEADER = org.openide.util.NbBundle.getMessage(ReviewModeCasePanel.class, "ReviewModeCasePanel.LastAccessedTimeHeaderText"); + private static final String STATUS_ICON_HEADER = org.openide.util.NbBundle.getMessage(ReviewModeCasePanel.class, "ReviewModeCasePanel.StatusIconHeaderText"); + private static final String OUTPUT_FOLDER_HEADER = org.openide.util.NbBundle.getMessage(ReviewModeCasePanel.class, "ReviewModeCasePanel.OutputFolderHeaderText"); + + enum COLUMN_HEADERS { + + CASE, + CREATEDTIME, + COMPLETEDTIME, + STATUS_ICON, + OUTPUTFOLDER // RJCTODO: Change name + } + private final String[] columnNames = {CASE_HEADER, CREATEDTIME_HEADER, COMPLETEDTIME_HEADER, STATUS_ICON_HEADER, OUTPUT_FOLDER_HEADER}; + private DefaultTableModel caseTableModel; + private Path currentlySelectedCase = null; + + /** + * Constructs a panel that allows a user to open cases created by automated + * ingest. + */ + public ReviewModeCasePanel(JDialog parent) { + caseTableModel = new DefaultTableModel(columnNames, 0) { + private static final long serialVersionUID = 1L; + + @Override + public boolean isCellEditable(int row, int column) { + return false; + } + }; + + initComponents(); + + /* + * Configure the columns of the cases table. + */ + TableColumn theColumn; + theColumn = casesTable.getColumn(CASE_HEADER); + theColumn.setCellRenderer(new GrayableCellRenderer()); + theColumn.setMinWidth(CASE_COL_MIN_WIDTH); + theColumn.setMaxWidth(CASE_COL_MAX_WIDTH); + theColumn.setPreferredWidth(CASE_COL_PREFERRED_WIDTH); + theColumn.setWidth(CASE_COL_PREFERRED_WIDTH); + + theColumn = casesTable.getColumn(CREATEDTIME_HEADER); + theColumn.setCellRenderer(new LongDateCellRenderer()); + theColumn.setMinWidth(TIME_COL_MIN_WIDTH); + theColumn.setMaxWidth(TIME_COL_MAX_WIDTH); + theColumn.setPreferredWidth(TIME_COL_PREFERRED_WIDTH); + theColumn.setWidth(TIME_COL_PREFERRED_WIDTH); + + theColumn = casesTable.getColumn(COMPLETEDTIME_HEADER); + theColumn.setCellRenderer(new LongDateCellRenderer()); + theColumn.setMinWidth(TIME_COL_MIN_WIDTH); + theColumn.setMaxWidth(TIME_COL_MAX_WIDTH); + theColumn.setPreferredWidth(TIME_COL_PREFERRED_WIDTH); + theColumn.setWidth(TIME_COL_PREFERRED_WIDTH); + + theColumn = casesTable.getColumn(STATUS_ICON_HEADER); + theColumn.setCellRenderer(new CaseStatusIconCellRenderer()); + theColumn.setMinWidth(STATUS_COL_MIN_WIDTH); + theColumn.setMaxWidth(STATUS_COL_MAX_WIDTH); + theColumn.setPreferredWidth(STATUS_COL_PREFERRED_WIDTH); + theColumn.setWidth(STATUS_COL_PREFERRED_WIDTH); + + casesTable.removeColumn(casesTable.getColumn(OUTPUT_FOLDER_HEADER)); + + /* + * Listen for row selection changes and set button state for the current + * selection. + */ + casesTable.getSelectionModel().addListSelectionListener((ListSelectionEvent e) -> { + //Ignore extra messages. + if (e.getValueIsAdjusting()) { + return; + } + setButtons(); + }); + + /* + * Add a window state listener that starts and stops refreshing of the + * cases table. + */ + if (parent != null) { + parent.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + stopCasesTableRefreshes(); + } + + @Override + public void windowActivated(WindowEvent e) { + startCasesTableRefreshes(); + } + + @Override + public void windowClosed(WindowEvent e) { + stopCasesTableRefreshes(); + } + }); + } + } + + /** + * Start doing periodic refreshes of the cases table. + */ + private void startCasesTableRefreshes() { + if (null == casesTableRefreshExecutor) { + casesTableRefreshExecutor = new ScheduledThreadPoolExecutor(1); + this.casesTableRefreshExecutor.scheduleAtFixedRate(() -> { + refreshCasesTable(); + }, MILLISECONDS_TO_WAIT_BEFORE_STARTING, MILLISECONDS_TO_WAIT_BETWEEN_UPDATES, TimeUnit.MILLISECONDS); + } + } + + /** + * Stop doing periodic refreshes of the cases table. + */ + private void stopCasesTableRefreshes() { + if (null != casesTableRefreshExecutor) { + casesTableRefreshExecutor.shutdown(); + } + this.casesTableRefreshExecutor = null; + } + + /* + * Updates the view presented by the panel. + */ + public void updateView() { + Thread thread = new Thread(() -> { + refreshCasesTable(); + }); + thread.start(); + } + + /** + * Gets the list of cases known to the review mode cases manager and + * refreshes the cases table. + */ + private void refreshCasesTable() { + try { + currentlySelectedCase = getSelectedCase(); + List theModel = ReviewModeCaseManager.getInstance().getCases(); + EventQueue.invokeLater(new CaseTableRefreshTask(theModel)); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Unexpected exception in refreshCasesTable", ex); //NON-NLS + } + } + + /** + * Gets the current selection in the cases table. + * + * @return A path representing the current selected case, null if there is + * no selection. + */ + private Path getSelectedCase() { + try { + int selectedRow = casesTable.getSelectedRow(); + if (selectedRow >= 0 && selectedRow < casesTable.getRowCount()) { + return Paths.get(caseTableModel.getValueAt(selectedRow, COLUMN_HEADERS.CASE.ordinal()).toString()); + } + } catch (Exception ignored) { + return null; + } + return null; + } + + /** + * Sets the current selection in the cases table. + * + * @param path The case folder path of the case to select. + */ + private void setSelectedCase(Path path) { + if (path != null) { + try { + for (int row = 0; row < casesTable.getRowCount(); ++row) { + Path temp = Paths.get(caseTableModel.getValueAt(row, COLUMN_HEADERS.CASE.ordinal()).toString()); + if (temp.compareTo(path) == 0) { // found it + casesTable.setRowSelectionInterval(row, row); + return; + } + } + } catch (Exception ignored) { + casesTable.clearSelection(); + } + } + casesTable.clearSelection(); + } + + /** + * Enables/disables the Open and Show Log buttons based on the case selected + * in the cases table. + */ + private void setButtons() { + boolean enabled = casesTable.getSelectedRow() >= 0 && casesTable.getSelectedRow() < casesTable.getRowCount(); + bnOpen.setEnabled(enabled); + bnShowLog.setEnabled(enabled); + } + + /** + * Opens a case. + * + * @param caseMetadataFilePath The path to the case metadata file. + */ + private void openCase(Path caseMetadataFilePath) { + setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + try { + ReviewModeCaseManager.getInstance().openCaseInEDT(caseMetadataFilePath); + stopCasesTableRefreshes(); + StartupWindowProvider.getInstance().close(); + } catch (ReviewModeCaseManagerException ex) { + logger.log(Level.SEVERE, String.format("Error while opening case with case metadata file path %s", caseMetadataFilePath), ex); + /* + * ReviewModeCaseManagerExceptions have user-friendly error + * messages. + */ + JOptionPane.showMessageDialog(WindowManager.getDefault().getMainWindow(), + ex.getMessage(), + org.openide.util.NbBundle.getMessage(ReviewModeCasePanel.class, "ReviewModeCasePanel.cannotOpenCase"), + JOptionPane.ERROR_MESSAGE); + + } finally { + setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } + + /** + * A task that refreshes the cases table using a list of auto ingest cases. + */ + private class CaseTableRefreshTask implements Runnable { + + private final List cases; + + CaseTableRefreshTask(List cases) { + setButtons(); + this.cases = cases; + } + + /** + * @inheritDoc + */ + @Override + public void run() { + cases.sort(reverseDateModifiedComparator); + caseTableModel.setRowCount(0); + long now = new Date().getTime(); + for (AutoIngestCase autoIngestCase : cases) { + if (passesTimeFilter(now, autoIngestCase.getLastAccessedDate().getTime())) { + caseTableModel.addRow(new Object[]{ + autoIngestCase.getCaseName(), + autoIngestCase.getCreationDate(), + autoIngestCase.getLastAccessedDate(), + (AutoIngestCase.CaseStatus.OK != autoIngestCase.getStatus()), + autoIngestCase.getCaseDirectoryPath().toString()}); + } + } + setSelectedCase(currentlySelectedCase); + } + + /** + * Indicates whether or not a time satisfies a time filter defined by + * this panel's time filter radio buttons. + * + * @param currentTime The current date and time in milliseconds from the + * Unix epoch. + * @param inputTime The date and time to be tested as milliseconds + * from the Unix epoch. + */ + private boolean passesTimeFilter(long currentTime, long inputTime) { + long numberOfUnits = 10; + long multiplier = 1; + if (rbAllCases.isSelected()) { + return true; + } else { + if (rbMonths.isSelected()) { + multiplier = 31; + } else { + if (rbWeeks.isSelected()) { + multiplier = 7; + } else { + if (rbDays.isSelected()) { + multiplier = 1; + } + } + } + } + return ((currentTime - inputTime) / (1000 * 60 * 60 * 24)) < (numberOfUnits * multiplier); + } + + } + + /** + * 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() { + + rbGroupHistoryLength = new javax.swing.ButtonGroup(); + bnOpen = new javax.swing.JButton(); + scrollPaneTable = new javax.swing.JScrollPane(); + casesTable = new javax.swing.JTable(); + bnRefresh = new javax.swing.JButton(); + panelFilter = new javax.swing.JPanel(); + rbAllCases = new javax.swing.JRadioButton(); + rbMonths = new javax.swing.JRadioButton(); + rbWeeks = new javax.swing.JRadioButton(); + rbDays = new javax.swing.JRadioButton(); + rbGroupLabel = new javax.swing.JLabel(); + bnShowLog = new javax.swing.JButton(); + + setName("Completed Cases"); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(bnOpen, org.openide.util.NbBundle.getMessage(ReviewModeCasePanel.class, "ReviewModeCasePanel.bnOpen.text")); // NOI18N + bnOpen.setEnabled(false); + bnOpen.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnOpenActionPerformed(evt); + } + }); + + casesTable.setAutoCreateRowSorter(true); + casesTable.setModel(caseTableModel); + casesTable.setAutoResizeMode(javax.swing.JTable.AUTO_RESIZE_ALL_COLUMNS); + casesTable.setRowHeight(20); + casesTable.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); + casesTable.addMouseListener(new java.awt.event.MouseAdapter() { + public void mouseClicked(java.awt.event.MouseEvent evt) { + casesTableMouseClicked(evt); + } + }); + scrollPaneTable.setViewportView(casesTable); + + org.openide.awt.Mnemonics.setLocalizedText(bnRefresh, org.openide.util.NbBundle.getMessage(ReviewModeCasePanel.class, "ReviewModeCasePanel.bnRefresh.text")); // NOI18N + bnRefresh.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnRefreshActionPerformed(evt); + } + }); + + rbGroupHistoryLength.add(rbAllCases); + rbAllCases.setSelected(true); + org.openide.awt.Mnemonics.setLocalizedText(rbAllCases, org.openide.util.NbBundle.getMessage(ReviewModeCasePanel.class, "ReviewModeCasePanel.rbAllCases.text")); // NOI18N + rbAllCases.addItemListener(new java.awt.event.ItemListener() { + public void itemStateChanged(java.awt.event.ItemEvent evt) { + rbAllCasesItemStateChanged(evt); + } + }); + + rbGroupHistoryLength.add(rbMonths); + org.openide.awt.Mnemonics.setLocalizedText(rbMonths, org.openide.util.NbBundle.getMessage(ReviewModeCasePanel.class, "ReviewModeCasePanel.rbMonths.text")); // NOI18N + rbMonths.addItemListener(new java.awt.event.ItemListener() { + public void itemStateChanged(java.awt.event.ItemEvent evt) { + rbMonthsItemStateChanged(evt); + } + }); + + rbGroupHistoryLength.add(rbWeeks); + org.openide.awt.Mnemonics.setLocalizedText(rbWeeks, org.openide.util.NbBundle.getMessage(ReviewModeCasePanel.class, "ReviewModeCasePanel.rbWeeks.text")); // NOI18N + rbWeeks.addItemListener(new java.awt.event.ItemListener() { + public void itemStateChanged(java.awt.event.ItemEvent evt) { + rbWeeksItemStateChanged(evt); + } + }); + + rbGroupHistoryLength.add(rbDays); + org.openide.awt.Mnemonics.setLocalizedText(rbDays, org.openide.util.NbBundle.getMessage(ReviewModeCasePanel.class, "ReviewModeCasePanel.rbDays.text")); // NOI18N + rbDays.setName(""); // NOI18N + rbDays.addItemListener(new java.awt.event.ItemListener() { + public void itemStateChanged(java.awt.event.ItemEvent evt) { + rbDaysItemStateChanged(evt); + } + }); + + rbGroupLabel.setFont(new java.awt.Font("Tahoma", 0, 12)); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(rbGroupLabel, org.openide.util.NbBundle.getMessage(ReviewModeCasePanel.class, "ReviewModeCasePanel.rbGroupLabel.text")); // NOI18N + + javax.swing.GroupLayout panelFilterLayout = new javax.swing.GroupLayout(panelFilter); + panelFilter.setLayout(panelFilterLayout); + panelFilterLayout.setHorizontalGroup( + panelFilterLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(panelFilterLayout.createSequentialGroup() + .addContainerGap() + .addGroup(panelFilterLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(rbGroupLabel) + .addComponent(rbAllCases) + .addComponent(rbMonths) + .addComponent(rbWeeks) + .addComponent(rbDays)) + .addContainerGap(34, Short.MAX_VALUE)) + ); + panelFilterLayout.setVerticalGroup( + panelFilterLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, panelFilterLayout.createSequentialGroup() + .addContainerGap() + .addComponent(rbGroupLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(rbDays) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(rbWeeks) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(rbMonths) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(rbAllCases) + .addContainerGap()) + ); + + org.openide.awt.Mnemonics.setLocalizedText(bnShowLog, org.openide.util.NbBundle.getMessage(ReviewModeCasePanel.class, "ReviewModeCasePanel.bnShowLog.text")); // NOI18N + bnShowLog.setToolTipText(org.openide.util.NbBundle.getMessage(ReviewModeCasePanel.class, "ReviewModeCasePanel.bnShowLog.toolTipText")); // NOI18N + bnShowLog.setEnabled(false); + bnShowLog.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnShowLogActionPerformed(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) + .addGroup(layout.createSequentialGroup() + .addGap(13, 13, 13) + .addComponent(bnOpen, javax.swing.GroupLayout.PREFERRED_SIZE, 80, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(18, 18, 18) + .addComponent(bnRefresh) + .addGap(18, 18, 18) + .addComponent(bnShowLog) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(panelFilter, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(20, 20, 20)) + .addGroup(layout.createSequentialGroup() + .addComponent(scrollPaneTable, javax.swing.GroupLayout.DEFAULT_SIZE, 1007, Short.MAX_VALUE) + .addContainerGap()))) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGap(43, 43, 43) + .addComponent(scrollPaneTable, javax.swing.GroupLayout.PREFERRED_SIZE, 450, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addGroup(layout.createSequentialGroup() + .addComponent(panelFilter, javax.swing.GroupLayout.PREFERRED_SIZE, 130, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap()) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(bnOpen) + .addComponent(bnRefresh) + .addComponent(bnShowLog)) + .addGap(36, 36, 36)))) + ); + }// //GEN-END:initComponents + + /** + * Open button action + * + * @param evt -- The event that caused this to be called + */ + private void bnOpenActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnOpenActionPerformed + Path caseMetadataFilePath = Paths.get((String) caseTableModel.getValueAt(casesTable.getSelectedRow(), + COLUMN_HEADERS.OUTPUTFOLDER.ordinal()), + caseTableModel.getValueAt(casesTable.getSelectedRow(), COLUMN_HEADERS.CASE.ordinal()) + CaseMetadata.getFileExtension()); + openCase(caseMetadataFilePath); + }//GEN-LAST:event_bnOpenActionPerformed + + /** + * Refresh button action + * + * @param evt -- The event that caused this to be called + */ + private void bnRefreshActionPerformed(java.awt.event.ActionEvent evt)//GEN-FIRST:event_bnRefreshActionPerformed + {//GEN-HEADEREND:event_bnRefreshActionPerformed + updateView(); + }//GEN-LAST:event_bnRefreshActionPerformed + + private void rbDaysItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_rbDaysItemStateChanged + if (rbDays.isSelected()) { + updateView(); + } + }//GEN-LAST:event_rbDaysItemStateChanged + + private void rbAllCasesItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_rbAllCasesItemStateChanged + if (rbAllCases.isSelected()) { + updateView(); + } + }//GEN-LAST:event_rbAllCasesItemStateChanged + + private void rbMonthsItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_rbMonthsItemStateChanged + if (rbMonths.isSelected()) { + updateView(); + } + }//GEN-LAST:event_rbMonthsItemStateChanged + + private void rbWeeksItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_rbWeeksItemStateChanged + if (rbWeeks.isSelected()) { + updateView(); + } + }//GEN-LAST:event_rbWeeksItemStateChanged + + private void bnShowLogActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnShowLogActionPerformed + int selectedRow = casesTable.getSelectedRow(); + int rowCount = casesTable.getRowCount(); + if (selectedRow >= 0 && selectedRow < rowCount) { + String thePath = (String) caseTableModel.getValueAt(selectedRow, COLUMN_HEADERS.OUTPUTFOLDER.ordinal()); + Path pathToLog = AutoIngestJobLogger.getLogPath(Paths.get(thePath)); + try { + if (pathToLog.toFile().exists()) { + Desktop.getDesktop().edit(pathToLog.toFile()); + } else { + JOptionPane.showMessageDialog(this, org.openide.util.NbBundle.getMessage(ReviewModeCasePanel.class, "DisplayLogDialog.cannotFindLog"), + org.openide.util.NbBundle.getMessage(ReviewModeCasePanel.class, "DisplayLogDialog.unableToShowLogFile"), JOptionPane.ERROR_MESSAGE); + } + } catch (IOException ex) { + logger.log(Level.SEVERE, String.format("Error attempting to open case auto ingest log file %s", pathToLog), ex); + JOptionPane.showMessageDialog(this, + org.openide.util.NbBundle.getMessage(ReviewModeCasePanel.class, "DisplayLogDialog.cannotOpenLog"), + org.openide.util.NbBundle.getMessage(ReviewModeCasePanel.class, "DisplayLogDialog.unableToShowLogFile"), + JOptionPane.PLAIN_MESSAGE); + } + } + }//GEN-LAST:event_bnShowLogActionPerformed + + private void casesTableMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_casesTableMouseClicked + if (evt.getClickCount() == 2) { + Path caseMetadataFilePath = Paths.get((String) caseTableModel.getValueAt(casesTable.getSelectedRow(), + COLUMN_HEADERS.OUTPUTFOLDER.ordinal()), + caseTableModel.getValueAt(casesTable.getSelectedRow(), COLUMN_HEADERS.CASE.ordinal()) + CaseMetadata.getFileExtension()); + openCase(caseMetadataFilePath); + } + }//GEN-LAST:event_casesTableMouseClicked + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton bnOpen; + private javax.swing.JButton bnRefresh; + private javax.swing.JButton bnShowLog; + private javax.swing.JTable casesTable; + private javax.swing.JPanel panelFilter; + private javax.swing.JRadioButton rbAllCases; + private javax.swing.JRadioButton rbDays; + private javax.swing.ButtonGroup rbGroupHistoryLength; + private javax.swing.JLabel rbGroupLabel; + private javax.swing.JRadioButton rbMonths; + private javax.swing.JRadioButton rbWeeks; + private javax.swing.JScrollPane scrollPaneTable; + // End of variables declaration//GEN-END:variables + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ShortDateCellRenderer.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ShortDateCellRenderer.java new file mode 100644 index 0000000000..713d177c0a --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ShortDateCellRenderer.java @@ -0,0 +1,49 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.Component; +import java.text.SimpleDateFormat; +import javax.swing.JTable; +import static javax.swing.SwingConstants.CENTER; + +/** + * A JTable cell renderer that renders a date represented as a long as a + * center-aligned, short-format date string. It also grays out the cell if the + * table is disabled. + */ +class ShortDateCellRenderer extends GrayableCellRenderer { + + private static final long serialVersionUID = 1L; + private static final String FORMAT_STRING = "MM/dd HH:mm"; //NON-NLS + private static final SimpleDateFormat dateFormat = new SimpleDateFormat(FORMAT_STRING); + + public ShortDateCellRenderer() { + setHorizontalAlignment(CENTER); + } + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { + if (value != null) { + setText(dateFormat.format(value)); + } + grayCellIfTableNotEnabled(table, isSelected); + return this; + } +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/SingleUserCaseImporter.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/SingleUserCaseImporter.java new file mode 100644 index 0000000000..e04399d321 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/SingleUserCaseImporter.java @@ -0,0 +1,478 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.Dimension; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Date; +import java.util.logging.Level; +import static javax.security.auth.callback.ConfirmationCallback.OK_CANCEL_OPTION; +import javax.swing.JOptionPane; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.SwingUtilities; +import org.apache.commons.io.FileUtils; +import org.openide.util.NbBundle; +import org.openide.windows.WindowManager; +import org.sleuthkit.autopsy.casemodule.SingleUserCaseConverter; +import org.sleuthkit.autopsy.casemodule.SingleUserCaseConverter.ImportCaseData; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.NetworkUtils; + +public class SingleUserCaseImporter implements Runnable { + + private static final String AIM_LOG_FILE_NAME = "auto_ingest_log.txt"; //NON-NLS + static final String CASE_IMPORT_LOG_FILE = "case_import_log.txt"; //NON-NLS + private static final String DOTAUT = ".aut"; //NON-NLS + private static final String SEP = System.getProperty("line.separator"); + private static final String logDateFormat = "yyyy/MM/dd HH:mm:ss"; //NON-NLS + private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat(logDateFormat); + private final Object threadWaitNotifyLock = new Object(); + private final ImportDoneCallback notifyOnComplete; + private final Path baseImageInput; + private final Path baseCaseInput; + private final Path baseImageOutput; + private final Path baseCaseOutput; + private final boolean copyImages; + private final boolean deleteCase; + private String oldCaseName = null; + private String newCaseName = null; + private int userAnswer = 0; + private PrintWriter writer; + + public SingleUserCaseImporter(String baseImageInput, String baseCaseInput, String baseImageOutput, String baseCaseOutput, boolean copyImages, boolean deleteCase, ImportDoneCallback callback) { + this.baseImageInput = Paths.get(baseImageInput); + this.baseCaseInput = Paths.get(baseCaseInput); + this.baseImageOutput = Paths.get(baseImageOutput); + this.baseCaseOutput = Paths.get(baseCaseOutput); + this.copyImages = copyImages; + this.deleteCase = deleteCase; + this.notifyOnComplete = callback; + } + + /** + * This causes iteration over all .aut files in the baseCaseInput path, + * calling SingleUserCaseConverter.importCase() for each one. + */ + public void importCases() throws Exception { + openLog(baseCaseOutput.toFile()); + log(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.StartingBatch") + + baseCaseInput.toString() + " " + + NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.to") + + " " + baseCaseOutput.toString()); //NON-NLS + + // iterate for .aut files + FindDotAutFolders dotAutFolders = new FindDotAutFolders(); + try { + Path walked = Files.walkFileTree(baseCaseInput, dotAutFolders); + } catch (IOException ex) { + log(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.ErrorFindingAutFiles") + " " + ex.getMessage()); //NON-NLS + } + + ArrayList ableToProcess = new ArrayList<>(); + ArrayList unableToProcess = new ArrayList<>(); + + SingleUserCaseConverter scc = new SingleUserCaseConverter(); + + // validate we can convert the .aut file, one by one + for (FoundAutFile f : dotAutFolders.getCandidateList()) { + this.oldCaseName = f.getPath().getFileName().toString(); + + // Test image output folder for uniqueness, find a unique folder for it if we can + File specificOutputFolder = baseImageOutput.resolve(oldCaseName).toFile(); + String newImageName = oldCaseName; + if (specificOutputFolder.exists()) { + // Not unique. add numbers before timestamp to specific image output name + String timeStamp = TimeStampUtils.getTimeStampOnly(oldCaseName); + newImageName = TimeStampUtils.removeTimeStamp(oldCaseName); + int number = 1; + String temp = ""; //NON-NLS + while (specificOutputFolder.exists()) { + if (number == Integer.MAX_VALUE) { + // It never became unique, so give up. + throw new Exception(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.NonUniqueOutputFolder") + newImageName); //NON-NLS + } + temp = newImageName + "_" + Integer.toString(number) + timeStamp; //NON-NLS + specificOutputFolder = baseImageOutput.resolve(temp).toFile(); + ++number; + } + newImageName = temp; + } + Path imageOutput = baseImageOutput.resolve(newImageName); + imageOutput.toFile().mkdirs(); // Create image output folder + + // Test case output folder for uniqueness, find a unique folder for it if we can + specificOutputFolder = baseCaseOutput.resolve(oldCaseName).toFile(); + newCaseName = oldCaseName; + if (specificOutputFolder.exists()) { + // not unique. add numbers before timestamp to specific case output name + String timeStamp = TimeStampUtils.getTimeStampOnly(oldCaseName); //NON-NLS + newCaseName = TimeStampUtils.removeTimeStamp(oldCaseName); + int number = 1; + String temp = ""; //NON-NLS + while (specificOutputFolder.exists()) { + if (number == Integer.MAX_VALUE) { + // It never became unique, so give up. + throw new Exception(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.NonUniqueOutputFolder") + newCaseName); //NON-NLS + } + temp = newCaseName + "_" + Integer.toString(number) + timeStamp; //NON-NLS + specificOutputFolder = baseCaseOutput.resolve(temp).toFile(); + ++number; + } + newCaseName = temp; + } + Path caseOutput = baseCaseOutput.resolve(newCaseName); + caseOutput.toFile().mkdirs(); // Create case output folder + + /** + * Test if the input path has a corresponding image input folder and + * no repeated case names in the path. If both of these conditions + * are true, we can process this case, otherwise not. + */ + // Check that there is an image folder if they are trying to copy it + boolean canProcess = true; + Path imageInput = null; + String relativeCaseName = TimeStampUtils.removeTimeStamp(baseCaseInput.relativize(f.getPath()).toString()); + Path testImageInputsFromOldCase = Paths.get(baseImageInput.toString(), relativeCaseName); + if (copyImages) { + if (!testImageInputsFromOldCase.toFile().isDirectory()) { + // Mark that we are unable to process this item + canProcess = false; + } else { + imageInput = testImageInputsFromOldCase; + } + if (imageInput == null) { + throw new Exception(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.SourceImageMissing") + " " + f.getPath()); //NON-NLS + } + + // If case name is in the image path, it causes bad things to happen with the parsing. Test for this. + for (int x = 0; x < imageInput.getNameCount(); ++x) { + if (oldCaseName.toLowerCase().equals(imageInput.getName(x).toString().toLowerCase())) { + // Mark that we are unable to process this item + canProcess = false; + } + } + } else { + imageInput = testImageInputsFromOldCase; + } + + // Create an Import Case Data object for this case + SingleUserCaseConverter.ImportCaseData icd = scc.new ImportCaseData( + imageInput, + f.getPath(), + imageOutput, + caseOutput, + oldCaseName, + newCaseName, + f.getAutFile().toString(), + f.getFolderName().toString(), + copyImages, + deleteCase); + + if (canProcess) { + ableToProcess.add(icd); + } else { + unableToProcess.add(icd); + } + } + + // Create text to be populated in the confirmation dialog + StringBuilder casesThatWillBeProcessed = new StringBuilder(); + StringBuilder casesThatWillNotBeProcessed = new StringBuilder(); + + casesThatWillBeProcessed.append(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.WillImport")).append(SEP); // NON-NLS + if (ableToProcess.isEmpty()) { + casesThatWillBeProcessed.append(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.None")).append(SEP); // NON-NLS + } else { + for (ImportCaseData i : ableToProcess) { + casesThatWillBeProcessed.append(i.getCaseInputFolder().toString()).append(SEP); + } + } + + if (!unableToProcess.isEmpty()) { + casesThatWillNotBeProcessed.append(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.WillNotImport")).append(SEP); // NON-NLS + for (ImportCaseData i : unableToProcess) { + casesThatWillNotBeProcessed.append(i.getCaseInputFolder().toString()).append(SEP); + } + } + + JTextArea jta = new JTextArea(casesThatWillBeProcessed.toString() + SEP + casesThatWillNotBeProcessed.toString()); + jta.setEditable(false); + JScrollPane jsp = new JScrollPane(jta) { + private static final long serialVersionUID = 1L; + + @Override + public Dimension getPreferredSize() { + return new Dimension(700, 480); + } + }; + + // Show confirmation dialog + SwingUtilities.invokeLater(() -> { + userAnswer = JOptionPane.showConfirmDialog(WindowManager.getDefault().getMainWindow(), + jsp, + NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.ContinueWithImport"), // NON-NLS + OK_CANCEL_OPTION); + synchronized (threadWaitNotifyLock) { + threadWaitNotifyLock.notify(); + } + }); + + // Wait while the user handles the confirmation dialog + synchronized (threadWaitNotifyLock) { + try { + threadWaitNotifyLock.wait(); + } catch (InterruptedException ex) { + Logger.getLogger(SingleUserCaseImporter.class.getName()).log(Level.SEVERE, "Threading Issue", ex); //NON-NLS + throw new Exception(ex); + } + } + + // If the user wants to proceed, do so. + if (userAnswer == JOptionPane.OK_OPTION) { + boolean result = true; // if anything went wrong, result becomes false. + // Feed .aut files in one by one for processing + for (ImportCaseData i : ableToProcess) { + try { + log(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.StartedProcessing") + + i.getCaseInputFolder() + + " " + NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.to") + " " + + i.getCaseOutputFolder()); //NON-NLS + SingleUserCaseConverter.importCase(i); + handleAutoIngestLog(i); + log(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.FinishedProcessing") + + i.getCaseInputFolder() + + " " + NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.to") + " " + + i.getCaseOutputFolder()); //NON-NLS + + } catch (Exception ex) { + log(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.FailedToComplete") + + i.getCaseInputFolder() + + " " + NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.to") + " " + + i.getCaseOutputFolder() + " " + ex.getMessage()); //NON-NLS + result = false; + } + } + + log(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.CompletedBatch") + + baseCaseInput.toString() + + " " + NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.to") + " " + + baseCaseOutput.toString()); //NON-NLS + + closeLog(); + if (notifyOnComplete != null) { + notifyOnComplete.importDoneCallback(result, ""); // NON-NLS + } + } else { + // The user clicked cancel. Abort. + log(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.AbortingBatch") + + baseCaseInput.toString() + + " " + NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.to") + " " + + baseCaseOutput.toString()); //NON-NLS + + closeLog(); + if (notifyOnComplete != null) { + notifyOnComplete.importDoneCallback(false, NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.Cancelled")); // NON-NLS + } + } + } + + @Override + public void run() { + try { + importCases(); + } catch (Exception ex) { + log(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.FailedToComplete") + + baseCaseInput.toString() + + " " + NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.to") + " " + + baseCaseOutput.toString() + + " " + ex.getMessage()); //NON-NLS + + closeLog(); + if (notifyOnComplete != null) { + notifyOnComplete.importDoneCallback(false, ex.getMessage()); // NON-NLS + } + } + } + + /** + * Move the Auto Ingest log if we can + * + * @param icd the Import Case Data structure detailing where the files are + */ + void handleAutoIngestLog(ImportCaseData icd) { + try { + Path source = icd.getCaseInputFolder().resolve(AIM_LOG_FILE_NAME); + Path destination = icd.getCaseOutputFolder().resolve(AIM_LOG_FILE_NAME); + + if (source.toFile().exists()) { + FileUtils.copyFile(source.toFile(), destination.toFile()); + } + + try (PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(destination.toString(), true)))) { + out.println(NbBundle.getMessage(SingleUserCaseImporter.class, "SingleUserCaseImporter.ImportedAsMultiUser") + new Date()); //NON-NLS + } catch (IOException e) { + // If unable to log it, no problem, move on + } + + File oldIngestLog = Paths.get(icd.getCaseOutputFolder().toString(), NetworkUtils.getLocalHostName(), AIM_LOG_FILE_NAME).toFile(); + if (oldIngestLog.exists()) { + oldIngestLog.delete(); + } + } catch (Exception ex) { + // If unable to copy Auto Ingest log, no problem, move on + } + } + + /** + * Open the case import log in the base output folder. + * + * @param location holds the path to the log file + */ + private void openLog(File location) { + location.mkdirs(); + File logFile = Paths.get(location.toString(), CASE_IMPORT_LOG_FILE).toFile(); + try { + writer = new PrintWriter(new BufferedWriter(new FileWriter(logFile, logFile.exists())), true); + } catch (IOException ex) { + writer = null; + Logger.getLogger(SingleUserCaseImporter.class.getName()).log(Level.WARNING, "Error opening log file " + logFile.toString(), ex); //NON-NLS + } + } + + /** + * Log a message to the case import log in the base output folder. + * + * @param message the message to log. + */ + private void log(String message) { + if (writer != null) { + writer.println(String.format("%s %s", simpleDateFormat.format((Date.from(Instant.now()).getTime())), message)); //NON-NLS + } + } + + /** + * Close the case import log + */ + private void closeLog() { + if (writer != null) { + writer.close(); + } + } + + /** + * Extend SimpleFileVisitor to find all the cases to process based upon + * presence of .aut files. + */ + private class FindDotAutFolders extends SimpleFileVisitor { + + private final ArrayList candidateList; + + public FindDotAutFolders() { + this.candidateList = new ArrayList<>(); + } + + /** + * Handle comparing .aut file and containing folder names without + * timestamps on either one. It strips them off if they exist. + * + * @param directory the directory we are currently visiting. + * @param attrs file attributes. + * + * @return CONTINUE if we want to carry on, SKIP_SUBTREE if we've found + * a .aut file, precluding searching any deeper into this + * folder. + * + * @throws IOException + */ + @Override + public FileVisitResult preVisitDirectory(Path directory, BasicFileAttributes attrs) throws IOException { + // Find all files that end in .aut + File[] dotAutFiles = directory.toFile().listFiles((File dir, String name) -> name.toLowerCase().endsWith(DOTAUT)); + for (File specificFile : dotAutFiles) { + // If the case name ends in a timestamp, strip it off + String sanitizedCaseName = specificFile.getName(); + sanitizedCaseName = TimeStampUtils.removeTimeStamp(sanitizedCaseName); + + // If the folder ends in a timestamp, strip it off + String sanitizedFolderName = TimeStampUtils.removeTimeStamp(directory.getFileName().toString()); + + // If file and folder match, found leaf node case + if (sanitizedCaseName.toLowerCase().startsWith(sanitizedFolderName.toLowerCase())) { + candidateList.add(new FoundAutFile(directory, Paths.get(sanitizedCaseName), Paths.get(sanitizedFolderName))); + return FileVisitResult.SKIP_SUBTREE; + } + } + // If no matching .aut files, continue to traverse subfolders + return FileVisitResult.CONTINUE; + } + + /** + * Returns the list of folders we've found that need to be looked at for + * possible import from single-user to multi-user cases. + * + * @return the candidateList + */ + public ArrayList getCandidateList() { + return candidateList; + } + } + + /** + * This class holds information about .aut files that have been found by the + * FileWalker. + */ + public class FoundAutFile { + + private final Path path; + private final Path autFile; + private final Path folderName; + + public FoundAutFile(Path path, Path autFile, Path folderName) { + this.path = path; + this.autFile = autFile; + this.folderName = folderName; + } + + Path getPath() { + return this.path; + } + + Path getAutFile() { + return this.autFile; + } + + Path getFolderName() { + return this.folderName; + } + } +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/StatusDatabaseLogger.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/StatusDatabaseLogger.java new file mode 100644 index 0000000000..2fa6db1457 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/StatusDatabaseLogger.java @@ -0,0 +1,104 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.logging.Level; +import org.sleuthkit.autopsy.core.UserPreferences; +import org.sleuthkit.autopsy.core.UserPreferencesException; +import org.sleuthkit.autopsy.coreutils.NetworkUtils; +import org.sleuthkit.autopsy.experimental.configuration.AutoIngestUserPreferences; + +/** + * Write auto-ingest status updates to a database. + */ +public class StatusDatabaseLogger { + /** + * Log the current status to the database using the database + * parameters saved in AutoIngestUserPreferences. + * @param message Current status message + * @param isError true if we're in an error state, false otherwise + * @throws SQLException + */ + public static void logToStatusDatabase(String message, boolean isError) throws SQLException, UserPreferencesException{ + + try{ + Class.forName("org.postgresql.Driver"); + } catch (ClassNotFoundException ex){ + java.util.logging.Logger SYS_LOGGER = AutoIngestSystemLogger.getLogger(); + SYS_LOGGER.log(Level.WARNING, "Error loading postgresql driver", ex); + } + try (Connection connection = DriverManager.getConnection("jdbc:postgresql://" + + AutoIngestUserPreferences.getLoggingDatabaseHostnameOrIP() + + ":" + AutoIngestUserPreferences.getLoggingPort() + + "/" + AutoIngestUserPreferences.getLoggingDatabaseName(), + AutoIngestUserPreferences.getLoggingUsername(), + AutoIngestUserPreferences.getLoggingPassword()); + Statement statement = connection.createStatement();) { + + logToStatusDatabase(statement, message, isError); + } + } + + /** + * Log the current status to the database using an already + * configured Statement. + * @param statement SQL statement (must have already been created) + * @param message Current status message + * @param isError true if we're in an error state, false otherwise + * @throws SQLException + */ + public static void logToStatusDatabase(Statement statement, String message, boolean isError) throws SQLException{ + if((statement == null) || statement.isClosed()){ + throw new SQLException("SQL Statement is null/closed"); + } + + int status; + if(isError){ + status = 1; + } else { + status = 0; + } + String timestamp = new java.text.SimpleDate‌​Format("yyyy-MM-dd HH:mm:ss").format(ne‌​w java.util.Date()); + + String checkForPreviousEntry = "SELECT * FROM statusUpdates WHERE tool='" + UserPreferences.getAppName() + "' AND " + + "node='" + NetworkUtils.getLocalHostName() + "'"; + + ResultSet resultSet = statement.executeQuery(checkForPreviousEntry); + String logMessage; + if(resultSet.next()){ + logMessage = "UPDATE statusUpdates SET reportTime='" + timestamp + + "', message='" + message + "', status=" + status + + " WHERE tool='" + UserPreferences.getAppName() + "' AND node='" + NetworkUtils.getLocalHostName() + "'"; + } else { + logMessage = "INSERT INTO statusUpdates (tool, node, reportTime, message, status) " + + "VALUES ('" + UserPreferences.getAppName() + + "', '" + NetworkUtils.getLocalHostName() + + "', '" + + timestamp + "', '" + message + "', '" + status + "')"; + + } + statement.execute(logMessage); + } + +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/TimeStampUtils.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/TimeStampUtils.java new file mode 100644 index 0000000000..fe53871dd5 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/TimeStampUtils.java @@ -0,0 +1,104 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Utility methods for working with strings with the time-stamp suffixes used by + * auto ingest. + */ +public final class TimeStampUtils { + + /* + * Sample time stamp suffix: 2015_02_02_12_10_31 + */ + private static final Pattern timeStampPattern = Pattern.compile("\\d{4}_\\d{2}_\\d{2}_\\d{2}_\\d{2}_\\d{2}$"); + private static final int LENGTH_OF_DATE_TIME_STAMP = 20; // length of the above time stamp + private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy_MM_dd_HH_mm_ss"); + + /** + * Checks whether a string ends with an auto ingest time stamp. + * + * @param inputString The string to check. + * + * @return True or false. + */ + public static boolean endsWithTimeStamp(String inputString) { + Matcher m = timeStampPattern.matcher(inputString); + return m.find(); + } + + /** + * Gets the fixed length of the auto-ingest time stamp suffix. + * + * @return The length. + */ + public static int getTimeStampLength() { + return LENGTH_OF_DATE_TIME_STAMP; + } + + /** + * Creates an auto ingest time stamp suffix using the current time. + * + * @return The suffix. + */ + public static String createTimeStamp() { + return dateFormat.format(Calendar.getInstance().getTime()); + } + + /** + * Removes an auto ingest timestamp suffix, if it present. + * + * @param inputString The string to trim. + * + * @return The trimmed string. + */ + public static String removeTimeStamp(String inputString) { + String trimmedString = inputString; + if (inputString != null && endsWithTimeStamp(inputString)) { + trimmedString = inputString.substring(0, inputString.length() - getTimeStampLength()); + } + return trimmedString; + } + + /** + * Gets the auto ingest time stamp suffix from a string, if it is present. + * + * @param inputString the name to check for a timestamp + * + * @return The time stamp, may be the empty. + */ + public static String getTimeStampOnly(String inputString) { + String timeStamp = ""; + if (inputString != null && endsWithTimeStamp(inputString)) { + timeStamp = inputString.substring(inputString.length() - getTimeStampLength(), inputString.length()); + } + return timeStamp; + } + + /* + * Private contructor to prevent instantiation. + */ + private TimeStampUtils() { + } +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/layer.xml b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/layer.xml new file mode 100644 index 0000000000..6711abd865 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/layer.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/AdvancedAutoIngestSettingsPanel.form b/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/AdvancedAutoIngestSettingsPanel.form new file mode 100644 index 0000000000..5e1c5c0a41 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/AdvancedAutoIngestSettingsPanel.form @@ -0,0 +1,375 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/AdvancedAutoIngestSettingsPanel.java b/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/AdvancedAutoIngestSettingsPanel.java new file mode 100644 index 0000000000..59e135c867 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/AdvancedAutoIngestSettingsPanel.java @@ -0,0 +1,357 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.configuration; + +import java.util.ArrayList; +import java.util.Collection; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JComponent; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.core.UserPreferences; + +class AdvancedAutoIngestSettingsPanel extends javax.swing.JPanel { + + private static final long serialVersionUID = 1L; + + AdvancedAutoIngestSettingsPanel(AutoIngestSettingsPanel.OptionsUiMode mode) { + initComponents(); + tbWarning.setLineWrap(true); + tbWarning.setWrapStyleWord(true); + load(mode); + } + + private void initThreadCount() { + int availableProcessors = Runtime.getRuntime().availableProcessors(); + Integer fileIngestThreadCountChoices[]; + if (availableProcessors >= 16) { + fileIngestThreadCountChoices = new Integer[]{1, 2, 4, 6, 8, 12, 16}; + } else if (availableProcessors >= 12 && availableProcessors <= 15) { + fileIngestThreadCountChoices = new Integer[]{1, 2, 4, 6, 8, 12}; + } else if (availableProcessors >= 8 && availableProcessors <= 11) { + fileIngestThreadCountChoices = new Integer[]{1, 2, 4, 6, 8}; + } else if (availableProcessors >= 6 && availableProcessors <= 7) { + fileIngestThreadCountChoices = new Integer[]{1, 2, 4, 6}; + } else if (availableProcessors >= 4 && availableProcessors <= 5) { + fileIngestThreadCountChoices = new Integer[]{1, 2, 4}; + } else if (availableProcessors >= 2 && availableProcessors <= 3) { + fileIngestThreadCountChoices = new Integer[]{1, 2}; + } else { + fileIngestThreadCountChoices = new Integer[]{1}; + } + numberOfFileIngestThreadsComboBox.setModel(new DefaultComboBoxModel<>(fileIngestThreadCountChoices)); + numberOfFileIngestThreadsComboBox.setSelectedItem(UserPreferences.numberOfFileIngestThreads()); + } + + private void load(AutoIngestSettingsPanel.OptionsUiMode mode) { + initThreadCount(); + spSecondsBetweenJobs.setValue(AutoIngestUserPreferences.getSecondsToSleepBetweenCases()); + spMaximumRetryAttempts.setValue(AutoIngestUserPreferences.getMaxNumTimesToProcessImage()); + int maxJobsPerCase = AutoIngestUserPreferences.getMaxConcurrentJobsForOneCase(); + spConcurrentJobsPerCase.setValue(maxJobsPerCase); + spInputScanInterval.setValue(AutoIngestUserPreferences.getMinutesOfInputScanInterval()); + spInputScanInterval.setEnabled(mode == AutoIngestSettingsPanel.OptionsUiMode.AIM); + spSecondsBetweenJobs.setEnabled(mode == AutoIngestSettingsPanel.OptionsUiMode.AIM); + spMaximumRetryAttempts.setEnabled(mode == AutoIngestSettingsPanel.OptionsUiMode.AIM); + cbTimeoutEnabled.setEnabled(mode == AutoIngestSettingsPanel.OptionsUiMode.AIM); + lbSecondsBetweenJobs.setEnabled(mode == AutoIngestSettingsPanel.OptionsUiMode.AIM); + lbInputScanInterval.setEnabled(mode == AutoIngestSettingsPanel.OptionsUiMode.AIM); + lbTimeoutText.setEnabled(mode == AutoIngestSettingsPanel.OptionsUiMode.AIM); + lbRetriesAllowed.setEnabled(mode == AutoIngestSettingsPanel.OptionsUiMode.AIM); + cbTimeoutEnabled.setSelected(UserPreferences.getIsTimeOutEnabled()); + int timeOutHrs = UserPreferences.getProcessTimeOutHrs(); + spTimeoutHours.setValue(timeOutHrs); + setCheckboxEnabledState(); + } + + void store() { + AutoIngestUserPreferences.setSecondsToSleepBetweenCases((int) spSecondsBetweenJobs.getValue()); + AutoIngestUserPreferences.setMaxNumTimesToProcessImage((int) spMaximumRetryAttempts.getValue()); + AutoIngestUserPreferences.setMaxConcurrentIngestNodesForOneCase((int) spConcurrentJobsPerCase.getValue()); + AutoIngestUserPreferences.setMinutesOfInputScanInterval((int) spInputScanInterval.getValue()); + UserPreferences.setNumberOfFileIngestThreads((Integer) numberOfFileIngestThreadsComboBox.getSelectedItem()); + boolean isChecked = cbTimeoutEnabled.isSelected(); + UserPreferences.setIsTimeOutEnabled(isChecked); + if (isChecked) { + // only store time out if it is enabled + int timeOutHrs = (int) spTimeoutHours.getValue(); + UserPreferences.setProcessTimeOutHrs(timeOutHrs); + } + } + + private void setCheckboxEnabledState() { + // Enable the timeout edit box iff the checkbox is checked and enabled + if (cbTimeoutEnabled.isEnabled() && cbTimeoutEnabled.isSelected()) { + spTimeoutHours.setEnabled(true); + lbTimeoutHours.setEnabled(true); + } else { + spTimeoutHours.setEnabled(false); + lbTimeoutHours.setEnabled(false); + } + } + + /** + * 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() { + + spMainScrollPane = new javax.swing.JScrollPane(); + tbWarning = new javax.swing.JTextArea(); + jPanelAutoIngestJobSettings = new javax.swing.JPanel(); + lbSecondsBetweenJobs = new javax.swing.JLabel(); + lbTimeoutText = new javax.swing.JLabel(); + lbInputScanInterval = new javax.swing.JLabel(); + lbRetriesAllowed = new javax.swing.JLabel(); + lbNumberOfThreads = new javax.swing.JLabel(); + lbConcurrentJobsPerCase = new javax.swing.JLabel(); + cbTimeoutEnabled = new javax.swing.JCheckBox(); + numberOfFileIngestThreadsComboBox = new javax.swing.JComboBox<>(); + lbRestartRequired = new javax.swing.JLabel(); + spConcurrentJobsPerCase = new javax.swing.JSpinner(); + spMaximumRetryAttempts = new javax.swing.JSpinner(); + spInputScanInterval = new javax.swing.JSpinner(); + spTimeoutHours = new javax.swing.JSpinner(); + spSecondsBetweenJobs = new javax.swing.JSpinner(); + lbSecondsBetweenJobsSeconds = new javax.swing.JLabel(); + lbTimeoutHours = new javax.swing.JLabel(); + lbInputScanIntervalMinutes = new javax.swing.JLabel(); + + tbWarning.setEditable(false); + tbWarning.setColumns(20); + tbWarning.setFont(new java.awt.Font("Monospaced", 1, 14)); // NOI18N + tbWarning.setRows(5); + tbWarning.setText(org.openide.util.NbBundle.getMessage(AdvancedAutoIngestSettingsPanel.class, "AdvancedAutoIngestSettingsPanel.tbWarning.text")); // NOI18N + tbWarning.setAutoscrolls(false); + spMainScrollPane.setViewportView(tbWarning); + + jPanelAutoIngestJobSettings.setBorder(javax.swing.BorderFactory.createTitledBorder(org.openide.util.NbBundle.getMessage(AdvancedAutoIngestSettingsPanel.class, "AdvancedAutoIngestSettingsPanel.jPanelAutoIngestJobSettings.border.title"))); // NOI18N + jPanelAutoIngestJobSettings.setName("Automated Ingest Job Settings"); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(lbSecondsBetweenJobs, org.openide.util.NbBundle.getMessage(AdvancedAutoIngestSettingsPanel.class, "AdvancedAutoIngestSettingsPanel.lbSecondsBetweenJobs.text")); // NOI18N + lbSecondsBetweenJobs.setToolTipText(org.openide.util.NbBundle.getMessage(AdvancedAutoIngestSettingsPanel.class, "AdvancedAutoIngestSettingsPanel.lbSecondsBetweenJobs.toolTipText_1")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(lbTimeoutText, org.openide.util.NbBundle.getMessage(AdvancedAutoIngestSettingsPanel.class, "AdvancedAutoIngestSettingsPanel.lbTimeoutText.text")); // NOI18N + lbTimeoutText.setToolTipText(org.openide.util.NbBundle.getMessage(AdvancedAutoIngestSettingsPanel.class, "AdvancedAutoIngestSettingsPanel.lbTimeoutText.toolTipText")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(lbInputScanInterval, org.openide.util.NbBundle.getMessage(AdvancedAutoIngestSettingsPanel.class, "AdvancedAutoIngestSettingsPanel.lbInputScanInterval.text")); // NOI18N + lbInputScanInterval.setToolTipText(org.openide.util.NbBundle.getMessage(AdvancedAutoIngestSettingsPanel.class, "AdvancedAutoIngestSettingsPanel.lbInputScanInterval.toolTipText_1")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(lbRetriesAllowed, org.openide.util.NbBundle.getMessage(AdvancedAutoIngestSettingsPanel.class, "AdvancedAutoIngestSettingsPanel.lbRetriesAllowed.text")); // NOI18N + lbRetriesAllowed.setToolTipText(org.openide.util.NbBundle.getMessage(AdvancedAutoIngestSettingsPanel.class, "AdvancedAutoIngestSettingsPanel.lbRetriesAllowed.toolTipText_1")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(lbNumberOfThreads, org.openide.util.NbBundle.getMessage(AdvancedAutoIngestSettingsPanel.class, "AdvancedAutoIngestSettingsPanel.lbNumberOfThreads.text")); // NOI18N + lbNumberOfThreads.setToolTipText(org.openide.util.NbBundle.getMessage(AdvancedAutoIngestSettingsPanel.class, "AdvancedAutoIngestSettingsPanel.lbNumberOfThreads.toolTipText_1")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(lbConcurrentJobsPerCase, org.openide.util.NbBundle.getMessage(AdvancedAutoIngestSettingsPanel.class, "AdvancedAutoIngestSettingsPanel.lbConcurrentJobsPerCase.text")); // NOI18N + lbConcurrentJobsPerCase.setToolTipText(org.openide.util.NbBundle.getMessage(AdvancedAutoIngestSettingsPanel.class, "AdvancedAutoIngestSettingsPanel.lbConcurrentJobsPerCase.toolTipText_1")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(cbTimeoutEnabled, org.openide.util.NbBundle.getMessage(AdvancedAutoIngestSettingsPanel.class, "AdvancedAutoIngestSettingsPanel.cbTimeoutEnabled.text")); // NOI18N + cbTimeoutEnabled.setToolTipText(org.openide.util.NbBundle.getMessage(AdvancedAutoIngestSettingsPanel.class, "AdvancedAutoIngestSettingsPanel.cbTimeoutEnabled.toolTipText")); // NOI18N + cbTimeoutEnabled.addItemListener(new java.awt.event.ItemListener() { + public void itemStateChanged(java.awt.event.ItemEvent evt) { + cbTimeoutEnabledItemStateChanged(evt); + } + }); + cbTimeoutEnabled.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + cbTimeoutEnabledActionPerformed(evt); + } + }); + + numberOfFileIngestThreadsComboBox.setToolTipText(org.openide.util.NbBundle.getMessage(AdvancedAutoIngestSettingsPanel.class, "AdvancedAutoIngestSettingsPanel.numberOfFileIngestThreadsComboBox.toolTipText")); // NOI18N + numberOfFileIngestThreadsComboBox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + numberOfFileIngestThreadsComboBoxActionPerformed(evt); + } + }); + + lbRestartRequired.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/corecomponents/warning16.png"))); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(lbRestartRequired, org.openide.util.NbBundle.getMessage(AdvancedAutoIngestSettingsPanel.class, "AdvancedAutoIngestSettingsPanel.lbRestartRequired.text")); // NOI18N + + spConcurrentJobsPerCase.setModel(new javax.swing.SpinnerNumberModel(3, 1, 100, 1)); + spConcurrentJobsPerCase.setToolTipText(org.openide.util.NbBundle.getMessage(AdvancedAutoIngestSettingsPanel.class, "AdvancedAutoIngestSettingsPanel.lbConcurrentJobsPerCase.toolTipText")); // NOI18N + + spMaximumRetryAttempts.setModel(new javax.swing.SpinnerNumberModel(2, 0, 9999999, 1)); + spMaximumRetryAttempts.setToolTipText(org.openide.util.NbBundle.getMessage(AdvancedAutoIngestSettingsPanel.class, "AdvancedAutoIngestSettingsPanel.lbRetriesAllowed.toolTipText_2")); // NOI18N + + spInputScanInterval.setModel(new javax.swing.SpinnerNumberModel(60, 1, 100000, 1)); + spInputScanInterval.setToolTipText(org.openide.util.NbBundle.getMessage(AdvancedAutoIngestSettingsPanel.class, "AdvancedAutoIngestSettingsPanel.spInputScanInterval.toolTipText")); // NOI18N + + spTimeoutHours.setModel(new javax.swing.SpinnerNumberModel(60, 1, 100000, 1)); + spTimeoutHours.setToolTipText(org.openide.util.NbBundle.getMessage(AdvancedAutoIngestSettingsPanel.class, "AdvancedAutoIngestSettingsPanel.spTimeoutHours.toolTipText")); // NOI18N + + spSecondsBetweenJobs.setModel(new javax.swing.SpinnerNumberModel(30, 30, 3600, 10)); + spSecondsBetweenJobs.setToolTipText(org.openide.util.NbBundle.getMessage(AdvancedAutoIngestSettingsPanel.class, "AdvancedAutoIngestSettingsPanel.spSecondsBetweenJobs.toolTipText")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(lbSecondsBetweenJobsSeconds, org.openide.util.NbBundle.getMessage(AdvancedAutoIngestSettingsPanel.class, "AdvancedAutoIngestSettingsPanel.lbSecondsBetweenJobsSeconds.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(lbTimeoutHours, org.openide.util.NbBundle.getMessage(AdvancedAutoIngestSettingsPanel.class, "AdvancedAutoIngestSettingsPanel.lbTimeoutHours.text")); // NOI18N + lbTimeoutHours.setToolTipText(org.openide.util.NbBundle.getMessage(AdvancedAutoIngestSettingsPanel.class, "AdvancedAutoIngestSettingsPanel.lbTimeoutHours.toolTipText")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(lbInputScanIntervalMinutes, org.openide.util.NbBundle.getMessage(AdvancedAutoIngestSettingsPanel.class, "AdvancedAutoIngestSettingsPanel.lbInputScanIntervalMinutes.text")); // NOI18N + lbInputScanIntervalMinutes.setToolTipText(org.openide.util.NbBundle.getMessage(AdvancedAutoIngestSettingsPanel.class, "AdvancedAutoIngestSettingsPanel.lbInputScanIntervalMinutes.toolTipText")); // NOI18N + + javax.swing.GroupLayout jPanelAutoIngestJobSettingsLayout = new javax.swing.GroupLayout(jPanelAutoIngestJobSettings); + jPanelAutoIngestJobSettings.setLayout(jPanelAutoIngestJobSettingsLayout); + jPanelAutoIngestJobSettingsLayout.setHorizontalGroup( + jPanelAutoIngestJobSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanelAutoIngestJobSettingsLayout.createSequentialGroup() + .addContainerGap() + .addComponent(cbTimeoutEnabled) + .addGap(5, 5, 5) + .addGroup(jPanelAutoIngestJobSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanelAutoIngestJobSettingsLayout.createSequentialGroup() + .addGroup(jPanelAutoIngestJobSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING, false) + .addGroup(jPanelAutoIngestJobSettingsLayout.createSequentialGroup() + .addComponent(lbInputScanInterval) + .addGap(49, 49, 49)) + .addGroup(jPanelAutoIngestJobSettingsLayout.createSequentialGroup() + .addComponent(lbRetriesAllowed) + .addGap(54, 54, 54)) + .addComponent(lbConcurrentJobsPerCase, javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(lbNumberOfThreads, javax.swing.GroupLayout.Alignment.LEADING)) + .addGroup(jPanelAutoIngestJobSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanelAutoIngestJobSettingsLayout.createSequentialGroup() + .addGap(0, 41, Short.MAX_VALUE) + .addGroup(jPanelAutoIngestJobSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(spInputScanInterval, javax.swing.GroupLayout.PREFERRED_SIZE, 90, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(spMaximumRetryAttempts, javax.swing.GroupLayout.PREFERRED_SIZE, 90, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(spConcurrentJobsPerCase, javax.swing.GroupLayout.PREFERRED_SIZE, 90, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addGroup(jPanelAutoIngestJobSettingsLayout.createSequentialGroup() + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(numberOfFileIngestThreadsComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 91, javax.swing.GroupLayout.PREFERRED_SIZE)))) + .addGroup(jPanelAutoIngestJobSettingsLayout.createSequentialGroup() + .addComponent(lbSecondsBetweenJobs) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(spSecondsBetweenJobs, javax.swing.GroupLayout.PREFERRED_SIZE, 90, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanelAutoIngestJobSettingsLayout.createSequentialGroup() + .addComponent(lbTimeoutText) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(spTimeoutHours, javax.swing.GroupLayout.PREFERRED_SIZE, 90, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(jPanelAutoIngestJobSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(lbRestartRequired) + .addComponent(lbSecondsBetweenJobsSeconds) + .addComponent(lbTimeoutHours) + .addComponent(lbInputScanIntervalMinutes)) + .addContainerGap(50, Short.MAX_VALUE)) + ); + jPanelAutoIngestJobSettingsLayout.setVerticalGroup( + jPanelAutoIngestJobSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanelAutoIngestJobSettingsLayout.createSequentialGroup() + .addContainerGap() + .addGroup(jPanelAutoIngestJobSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(lbRestartRequired) + .addGroup(jPanelAutoIngestJobSettingsLayout.createSequentialGroup() + .addGroup(jPanelAutoIngestJobSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(lbSecondsBetweenJobs) + .addComponent(spSecondsBetweenJobs, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(lbSecondsBetweenJobsSeconds)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(jPanelAutoIngestJobSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanelAutoIngestJobSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(lbTimeoutText) + .addComponent(spTimeoutHours, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(lbTimeoutHours)) + .addComponent(cbTimeoutEnabled)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(jPanelAutoIngestJobSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(lbInputScanInterval) + .addComponent(spInputScanInterval, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(lbInputScanIntervalMinutes)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(jPanelAutoIngestJobSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(lbRetriesAllowed) + .addComponent(spMaximumRetryAttempts, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(jPanelAutoIngestJobSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(lbConcurrentJobsPerCase) + .addComponent(spConcurrentJobsPerCase, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(jPanelAutoIngestJobSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(lbNumberOfThreads) + .addComponent(numberOfFileIngestThreadsComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)))) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + + spMaximumRetryAttempts.getAccessibleContext().setAccessibleDescription(org.openide.util.NbBundle.getMessage(AdvancedAutoIngestSettingsPanel.class, "AdvancedAutoIngestSettingsPanel.spMaximumRetryAttempts.AccessibleContext.accessibleDescription")); // NOI18N + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup() + .addContainerGap() + .addComponent(jPanelAutoIngestJobSettings, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup() + .addGap(16, 16, 16) + .addComponent(spMainScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 640, Short.MAX_VALUE))) + .addGap(16, 16, 16)) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGap(20, 20, 20) + .addComponent(spMainScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 106, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jPanelAutoIngestJobSettings, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGap(26, 26, 26)) + ); + }// //GEN-END:initComponents + + private void numberOfFileIngestThreadsComboBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_numberOfFileIngestThreadsComboBoxActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_numberOfFileIngestThreadsComboBoxActionPerformed + + private void cbTimeoutEnabledActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cbTimeoutEnabledActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_cbTimeoutEnabledActionPerformed + + private void cbTimeoutEnabledItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_cbTimeoutEnabledItemStateChanged + setCheckboxEnabledState(); + }//GEN-LAST:event_cbTimeoutEnabledItemStateChanged + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JCheckBox cbTimeoutEnabled; + private javax.swing.JPanel jPanelAutoIngestJobSettings; + private javax.swing.JLabel lbConcurrentJobsPerCase; + private javax.swing.JLabel lbInputScanInterval; + private javax.swing.JLabel lbInputScanIntervalMinutes; + private javax.swing.JLabel lbNumberOfThreads; + private javax.swing.JLabel lbRestartRequired; + private javax.swing.JLabel lbRetriesAllowed; + private javax.swing.JLabel lbSecondsBetweenJobs; + private javax.swing.JLabel lbSecondsBetweenJobsSeconds; + private javax.swing.JLabel lbTimeoutHours; + private javax.swing.JLabel lbTimeoutText; + private javax.swing.JComboBox numberOfFileIngestThreadsComboBox; + private javax.swing.JSpinner spConcurrentJobsPerCase; + private javax.swing.JSpinner spInputScanInterval; + private javax.swing.JScrollPane spMainScrollPane; + private javax.swing.JSpinner spMaximumRetryAttempts; + private javax.swing.JSpinner spSecondsBetweenJobs; + private javax.swing.JSpinner spTimeoutHours; + private javax.swing.JTextArea tbWarning; + // End of variables declaration//GEN-END:variables +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/AutoIngestSettingsPanel.form b/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/AutoIngestSettingsPanel.form new file mode 100644 index 0000000000..7d7a6f2972 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/AutoIngestSettingsPanel.form @@ -0,0 +1,659 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/AutoIngestSettingsPanel.java b/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/AutoIngestSettingsPanel.java new file mode 100644 index 0000000000..6650765635 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/AutoIngestSettingsPanel.java @@ -0,0 +1,1438 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.configuration; + +import java.awt.BorderLayout; +import java.io.File; +import java.nio.file.Files; +import java.util.List; +import javax.swing.JDialog; +import javax.swing.JFileChooser; +import javax.swing.JOptionPane; +import javax.swing.SwingWorker; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.core.UserPreferences; +import org.sleuthkit.autopsy.ingest.IngestJobSettings; +import org.sleuthkit.autopsy.ingest.IngestJobSettingsPanel; +import java.awt.Dimension; +import java.nio.file.Paths; +import org.openide.util.ImageUtilities; +import javax.swing.JScrollPane; +import javax.swing.SwingUtilities; +import org.sleuthkit.autopsy.coreutils.FileUtil; +import org.openide.windows.WindowManager; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.experimental.autoingest.FileExporterSettingsPanel; + +/** + * + */ +public class AutoIngestSettingsPanel extends javax.swing.JPanel { + + private final AutoIngestSettingsPanelController controller; + private final JFileChooser fc = new JFileChooser(); + private static final long serialVersionUID = 1L; + private static final Logger logger = Logger.getLogger(AutoIngestSettingsPanel.class.getName()); + private Integer oldIngestThreads; + private static final String MULTI_USER_SETTINGS_MUST_BE_ENABLED = NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.validationErrMsg.MUdisabled"); + + enum OptionsUiMode { + + STANDALONE, AIM, REVIEW, DOWNLOADING_CONFIGURATION + }; + + /** + * Creates new form AutoIngestSettingsPanel + * + * @param theController Controller to notify of changes. + */ + public AutoIngestSettingsPanel(AutoIngestSettingsPanelController theController) { + controller = theController; + initComponents(); + + cbJoinAutoIngestCluster.setVisible(true); + load(true); + sharedSettingsTextField.getDocument().addDocumentListener(new MyDocumentListener()); + inputPathTextField.getDocument().addDocumentListener(new MyDocumentListener()); + outputPathTextField.getDocument().addDocumentListener(new MyDocumentListener()); + + jLabelInvalidImageFolder.setText(""); + jLabelInvalidResultsFolder.setText(""); + sharedSettingsErrorTextField.setText(""); + jLabelTaskDescription.setText(""); + configButtonErrorTextField.setText(""); + + pbTaskInProgress.setEnabled(false); + jLabelCurrentTask.setEnabled(false); + jLabelTaskDescription.setEnabled(false); + + this.oldIngestThreads = UserPreferences.numberOfFileIngestThreads(); + } + + private class MyDocumentListener implements DocumentListener { + + @Override + public void changedUpdate(DocumentEvent e) { + validateSettings(); + controller.changed(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + validateSettings(); + controller.changed(); + } + + @Override + public void insertUpdate(DocumentEvent e) { + validateSettings(); + controller.changed(); + } + }; + + /** + * Load mode from persistent storage. + * + * @param inStartup True if we're doing the initial population of the UI + */ + final void load(boolean inStartup) { + + // multi user mode must be enabled + if (!UserPreferences.getIsMultiUserModeEnabled()) { + tbOops.setText(MULTI_USER_SETTINGS_MUST_BE_ENABLED); + } else { + tbOops.setText(""); + } + cbJoinAutoIngestCluster.setSelected(AutoIngestUserPreferences.getJoinAutoModeCluster()); + cbJoinAutoIngestCluster.setEnabled(UserPreferences.getIsMultiUserModeEnabled()); + + if (inStartup) { + AutoIngestUserPreferences.SelectedMode storedMode = AutoIngestUserPreferences.getMode(); + inputPathTextField.requestFocusInWindow(); + if (null != storedMode) switch (storedMode) { + case REVIEW: + jRadioButtonReview.setSelected(true); + enableOptionsBasedOnMode(OptionsUiMode.REVIEW); + break; + case AUTOMATED: + jRadioButtonAutomated.setSelected(true); + enableOptionsBasedOnMode(OptionsUiMode.AIM); + break; + default: + cbJoinAutoIngestCluster.setSelected(false); + enableOptionsBasedOnMode(OptionsUiMode.STANDALONE); + break; + } + } + + String images = AutoIngestUserPreferences.getAutoModeImageFolder(); + if (images != null) { + inputPathTextField.setText(images); + } else { + inputPathTextField.setText(""); + } + + String results = AutoIngestUserPreferences.getAutoModeResultsFolder(); + if (results != null) { + outputPathTextField.setText(results); + } else { + outputPathTextField.setText(""); + } + + if (inStartup) { + sharedConfigCheckbox.setSelected(AutoIngestUserPreferences.getSharedConfigEnabled()); + String sharedSettingsFolder = AutoIngestUserPreferences.getSharedConfigFolder(); + if (sharedSettingsFolder != null) { + sharedSettingsTextField.setText(sharedSettingsFolder); + } else { + String folder = getDefaultSharedFolder(); + sharedSettingsTextField.setText(folder); + } + + masterNodeCheckBox.setSelected(AutoIngestUserPreferences.getSharedConfigMaster()); + setEnabledStateForSharedConfiguration(); + } + + validateSettings(); + } + + /** + * Get the default location for the shared configuration folder. Currently + * this is a subfolder of the shared images folder. + * + * @return The default subfolder name, or an empty string if the base folder + * is not set + */ + private String getDefaultSharedFolder() { + + String images = inputPathTextField.getText().trim(); + if (images == null || images.isEmpty()) { + return ""; + } + File sharedFolder = new File(images, "sharedConfiguration"); + if (!sharedFolder.exists()) { + try { + sharedFolder.mkdir(); + return sharedFolder.getAbsolutePath(); + } catch (Exception ex) { + sharedSettingsErrorTextField.setText(NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.ErrorSettingDefaultFolder")); + return ""; + } + } + return sharedFolder.getAbsolutePath(); + } + + /** + * Save mode to persistent storage. + */ + void store() { + AutoIngestUserPreferences.setJoinAutoModeCluster(cbJoinAutoIngestCluster.isSelected()); + if (!cbJoinAutoIngestCluster.isSelected()) { + AutoIngestUserPreferences.setMode(AutoIngestUserPreferences.SelectedMode.STANDALONE); + return; + } + + if (jRadioButtonAutomated.isSelected()) { + boolean needsSaving = false; + String thePath = AutoIngestUserPreferences.getAutoModeImageFolder(); + if (thePath != null && 0 != inputPathTextField.getText().compareTo(thePath)) { + needsSaving = true; + } + thePath = AutoIngestUserPreferences.getAutoModeResultsFolder(); + if (thePath != null && 0 != outputPathTextField.getText().compareTo(thePath)) { + needsSaving = true; + } + if (needsSaving) { + SwingUtilities.invokeLater(() -> { + JOptionPane.showMessageDialog(null, + NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.MustRestart"), + NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.restartRequiredLabel.text"), + JOptionPane.WARNING_MESSAGE); + }); + } + + AutoIngestUserPreferences.setMode(AutoIngestUserPreferences.SelectedMode.AUTOMATED); + String imageFolderPath = getNormalizedFolderPath(inputPathTextField.getText().trim()); + AutoIngestUserPreferences.setAutoModeImageFolder(imageFolderPath); + String resultsFolderPath = getNormalizedFolderPath(outputPathTextField.getText().trim()); + AutoIngestUserPreferences.setAutoModeResultsFolder(resultsFolderPath); + AutoIngestUserPreferences.setSharedConfigEnabled(sharedConfigCheckbox.isSelected()); + if (sharedConfigCheckbox.isSelected()) { + String globalSettingsPath = getNormalizedFolderPath(sharedSettingsTextField.getText().trim()); + AutoIngestUserPreferences.setSharedConfigFolder(globalSettingsPath); + AutoIngestUserPreferences.setSharedConfigMaster(masterNodeCheckBox.isSelected()); + } + } else if (jRadioButtonReview.isSelected()) { + String thePath = AutoIngestUserPreferences.getAutoModeResultsFolder(); + if (thePath != null && 0 != outputPathTextField.getText().compareTo(thePath)) { + SwingUtilities.invokeLater(() -> { + JOptionPane.showMessageDialog(null, + NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.MustRestart"), + NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.restartRequiredLabel.text"), + JOptionPane.WARNING_MESSAGE); + }); + } + + AutoIngestUserPreferences.setMode(AutoIngestUserPreferences.SelectedMode.REVIEW); + String resultsFolderPath = getNormalizedFolderPath(outputPathTextField.getText().trim()); + AutoIngestUserPreferences.setAutoModeResultsFolder(resultsFolderPath); + } + } + + void validateSettings() { + if (valid()) { + if (validSharedConfigSettings()) { + downloadButton.setEnabled(true); + if (masterNodeCheckBox.isEnabled() && masterNodeCheckBox.isSelected()) { + uploadButton.setEnabled(true); + } else { + uploadButton.setEnabled(false); + } + } else { + downloadButton.setEnabled(false); + uploadButton.setEnabled(false); + } + displaySharedConfigButtonText(); + } else { + uploadButton.setEnabled(false); + if (validSharedConfigSettings()) { + downloadButton.setEnabled(true); + } else { + downloadButton.setEnabled(false); + } + displaySharedConfigButtonText(); + } + } + + /** + * Validate current panel settings. + */ + boolean valid() { + + if (!cbJoinAutoIngestCluster.isSelected()) { + return true; + } + + boolean isValidNodePanel = true; + + switch (getModeFromRadioButtons()) { + case AIM: + if (!validateImagePath()) { + isValidNodePanel = false; + } + if (!validateResultsPath()) { + isValidNodePanel = false; + } + if (!validateSharedSettingsPath()) { + isValidNodePanel = false; + configButtonErrorTextField.setText("Shared configuration folder is invalid"); + } + break; + case REVIEW: + if (!validateResultsPath()) { + isValidNodePanel = false; + } + break; + + case STANDALONE: + break; + default: + break; + } + + jLabelInvalidImageFolder.setVisible(false); + jLabelInvalidResultsFolder.setVisible(false); + + if (jRadioButtonAutomated.isSelected()) { + if (sharedConfigCheckbox.isEnabled() && sharedConfigCheckbox.isSelected() && !validSharedConfigSettings()) { + isValidNodePanel = false; + } + } + return isValidNodePanel; + } + + /** + * Check everything that is needed to enable the upload or download config + * buttons is set (except the master node checkbox for upload). + * + * @return + */ + boolean validSharedConfigSettings() { + // Check for: + // - shared config checkbox enabled and checked + // - valid shared config folder entered + // - mulit-user settings enabled + return (sharedConfigCheckbox.isEnabled() && sharedConfigCheckbox.isSelected() && UserPreferences.getIsMultiUserModeEnabled()); + } + + /** + * + */ + void displaySharedConfigButtonText() { + if (sharedConfigCheckbox.isEnabled() && sharedConfigCheckbox.isSelected()) { + if (!UserPreferences.getIsMultiUserModeEnabled()) { + configButtonErrorTextField.setText("Multi-user cases must be enabled"); + } else { + configButtonErrorTextField.setText(""); + } + } else { + configButtonErrorTextField.setText(""); + } + } + + /** + * Determines whether settings requiring a restart have changed, and also + * updates the UI. + * + * @return true if something has changed that will require a reset, false + * otherwise + */ + boolean isResetNeeded() { + + boolean resetNeeded = false; + + // Save the current values from the UI + String oldInputPath = inputPathTextField.getText().trim(); + String oldOutputPath = outputPathTextField.getText().trim(); + + // Refresh UI + load(false); + + if (!oldInputPath.equals(inputPathTextField.getText().trim())) { + resetNeeded = true; + } + if (!oldOutputPath.equals(outputPathTextField.getText().trim())) { + resetNeeded = true; + } + if (!this.oldIngestThreads.equals(UserPreferences.numberOfFileIngestThreads())) { + resetNeeded = true; + } + return resetNeeded; + } + + /** + * Normalizes a path to make sure there are no "space" characters at the end + * + * @param path Path to a directory + * + * @return Path without "space" characters at the end + */ + String normalizePath(String path) { + + while (path.length() > 0) { + if (path.charAt(path.length() - 1) == ' ') { + path = path.substring(0, path.length() - 1); + } else { + break; + } + } + return path; + } + + /** + * Validates that a path is valid and points to a folder. + * + * @param path A path to be validated + * + * @return boolean returns true if valid and points to a folder, false + * otherwise + */ + boolean isFolderPathValid(String path) { + try { + File file = new File(normalizePath(path)); + + // check if it's a symbolic link + if (Files.isSymbolicLink(file.toPath())) { + return true; + } + + // local folder + if (file.exists() && file.isDirectory()) { + return true; + } + } catch (Exception ex) { + // Files.isSymbolicLink (and other "files" methods) throw exceptions on seemingly innocent inputs. + // For example, it will throw an exception when either " " is last character in path or + // a path starting with ":". + // We can just ignore these exceptions as they occur in process of user typing in the path. + return false; + } + return false; + } + + /** + * Returns a path that was normalized by file system. + * + * @param path A path to be normalized. Normalization occurs inside a call + * to new File(). + * + * @return String returns normalized OS path + */ + String getNormalizedFolderPath(String path) { + // removes "/", "\", and " " characters at the end of path string. + // normalizePath() removes spaces at the end of path and a call to "new File()" + // internally formats the path string to remove "/" and "\" characters at the end of path. + File file = new File(normalizePath(path)); + return file.getPath(); + } + + /** + * Validate image path. Display warnings if invalid. + */ + boolean validateImagePath() { + + String inputPath = inputPathTextField.getText().trim(); + + if (inputPath.isEmpty()) { + jLabelInvalidImageFolder.setVisible(true); + jLabelInvalidImageFolder.setText(NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.ImageDirectoryUnspecified")); + return false; + } + + if (!isFolderPathValid(inputPath)) { + jLabelInvalidImageFolder.setVisible(true); + jLabelInvalidImageFolder.setText(NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.PathInvalid")); + return false; + } + + if (false == permissionsAppropriate(inputPath)) { + jLabelInvalidImageFolder.setVisible(true); + jLabelInvalidImageFolder.setText(NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.CannotAccess") + + " " + inputPath + " " + + NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.CheckPermissions")); + return false; + } + + jLabelInvalidImageFolder.setText(""); + return true; + } + + /** + * Validate results path. Display warnings if invalid. + */ + boolean validateResultsPath() { + + String outputPath = outputPathTextField.getText().trim(); + + if (outputPath.isEmpty()) { + jLabelInvalidResultsFolder.setVisible(true); + jLabelInvalidResultsFolder.setText(NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.ResultsDirectoryUnspecified")); + return false; + } + + if (!isFolderPathValid(outputPath)) { + jLabelInvalidResultsFolder.setVisible(true); + jLabelInvalidResultsFolder.setText(NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.PathInvalid")); + return false; + } + + if (false == permissionsAppropriate(outputPath)) { + jLabelInvalidResultsFolder.setVisible(true); + jLabelInvalidResultsFolder.setText(NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.CannotAccess") + + " " + outputPath + " " + + NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.CheckPermissions")); + return false; + } + + jLabelInvalidResultsFolder.setText(""); + return true; + } + + /** + * Validate shared settings path. Display warnings if invalid. + */ + boolean validateSharedSettingsPath() { + String sharedSettingsPath = sharedSettingsTextField.getText().trim(); + + // Automatically valid if shared settings aren't selected + if (!sharedConfigCheckbox.isSelected()) { + return true; + } + + if (sharedSettingsPath.isEmpty()) { + sharedSettingsErrorTextField.setVisible(true); + sharedSettingsErrorTextField.setText(NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.EmptySettingsDirectory")); + return false; + } + + if (!isFolderPathValid(sharedSettingsPath)) { + sharedSettingsErrorTextField.setVisible(true); + sharedSettingsErrorTextField.setText(NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.PathInvalid")); + return false; + } + + if (false == permissionsAppropriate(sharedSettingsPath)) { + sharedSettingsErrorTextField.setVisible(true); + sharedSettingsErrorTextField.setText(NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.CannotAccess") + + " " + sharedSettingsPath + " " + + NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.CheckPermissions")); + return false; + } + + sharedSettingsErrorTextField.setText(""); + return true; + } + + private void displayIngestJobSettingsPanel() { + + IngestJobSettings ingestJobSettings = new IngestJobSettings(AutoIngestUserPreferences.getAutoModeIngestModuleContextString()); + showWarnings(ingestJobSettings); + IngestJobSettingsPanel ingestJobSettingsPanel = new IngestJobSettingsPanel(ingestJobSettings); + + add(ingestJobSettingsPanel, BorderLayout.PAGE_START); + + if (JOptionPane.showConfirmDialog(null, ingestJobSettingsPanel, "Ingest Module Configuration", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE) == JOptionPane.OK_OPTION) { + // store the updated settings + ingestJobSettings = ingestJobSettingsPanel.getSettings(); + ingestJobSettings.save(); + showWarnings(ingestJobSettings); + } + } + + private static void showWarnings(IngestJobSettings ingestJobSettings) { + List warnings = ingestJobSettings.getWarnings(); + if (warnings.isEmpty() == false) { + StringBuilder warningMessage = new StringBuilder(); + for (String warning : warnings) { + warningMessage.append(warning).append("\n"); + } + JOptionPane.showMessageDialog(null, warningMessage.toString()); + } + } + + private void enableOptionsBasedOnMode(OptionsUiMode mode) { + if (mode != OptionsUiMode.DOWNLOADING_CONFIGURATION) { + jRadioButtonAutomated.setEnabled(cbJoinAutoIngestCluster.isSelected()); + jRadioButtonReview.setEnabled(cbJoinAutoIngestCluster.isSelected()); + + jLabelSelectInputFolder.setEnabled(mode == OptionsUiMode.AIM); + inputPathTextField.setEnabled(mode == OptionsUiMode.AIM); + browseInputFolderButton.setEnabled(mode == OptionsUiMode.AIM); + + jLabelSelectOutputFolder.setEnabled(mode == OptionsUiMode.AIM || mode == OptionsUiMode.REVIEW); + outputPathTextField.setEnabled(mode == OptionsUiMode.AIM || mode == OptionsUiMode.REVIEW); + browseOutputFolderButton.setEnabled(mode == OptionsUiMode.AIM || mode == OptionsUiMode.REVIEW); + + jPanelSharedConfig.setEnabled(mode == OptionsUiMode.AIM); + + jPanelIngestSettings.setEnabled(mode == OptionsUiMode.AIM); + bnEditIngestSettings.setEnabled(mode == OptionsUiMode.AIM); + bnAdvancedSettings.setEnabled(mode == OptionsUiMode.AIM); + bnLogging.setEnabled(mode == OptionsUiMode.AIM); + jPanelSharedConfig.setEnabled(mode == OptionsUiMode.AIM); + sharedConfigCheckbox.setEnabled(mode == OptionsUiMode.AIM); + masterNodeCheckBox.setEnabled(mode == OptionsUiMode.AIM && sharedConfigCheckbox.isSelected()); + bnFileExport.setEnabled(mode == OptionsUiMode.AIM); + sharedSettingsTextField.setEnabled(mode == OptionsUiMode.AIM && sharedConfigCheckbox.isSelected()); + downloadButton.setEnabled(mode == OptionsUiMode.AIM && sharedConfigCheckbox.isSelected()); + browseSharedSettingsButton.setEnabled(mode == OptionsUiMode.AIM && sharedConfigCheckbox.isSelected()); + uploadButton.setEnabled(mode == OptionsUiMode.AIM && sharedConfigCheckbox.isSelected() && masterNodeCheckBox.isSelected()); + } else { + setEnabledState(false); + } + + } + + private OptionsUiMode getModeFromRadioButtons() { + if (!cbJoinAutoIngestCluster.isSelected()) { + return OptionsUiMode.STANDALONE; + } + + if (jRadioButtonAutomated.isSelected()) { + return OptionsUiMode.AIM; + } else if (jRadioButtonReview.isSelected()) { + return OptionsUiMode.REVIEW; + } else { + return OptionsUiMode.STANDALONE; + } + } + + /** + * 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() { + + modeRadioButtons = new javax.swing.ButtonGroup(); + nodePanel = new javax.swing.JPanel(); + jPanelNodeType = new javax.swing.JPanel(); + jLabelSelectMode = new javax.swing.JLabel(); + restartRequiredNodeLabel = new javax.swing.JLabel(); + jRadioButtonAutomated = new javax.swing.JRadioButton(); + jRadioButtonReview = new javax.swing.JRadioButton(); + jLabelSelectInputFolder = new javax.swing.JLabel(); + inputPathTextField = new javax.swing.JTextField(); + browseInputFolderButton = new javax.swing.JButton(); + jLabelSelectOutputFolder = new javax.swing.JLabel(); + outputPathTextField = new javax.swing.JTextField(); + browseOutputFolderButton = new javax.swing.JButton(); + jLabelInvalidImageFolder = new javax.swing.JLabel(); + jLabelInvalidResultsFolder = new javax.swing.JLabel(); + jLabel1 = new javax.swing.JLabel(); + jPanelSharedConfig = new javax.swing.JPanel(); + sharedConfigCheckbox = new javax.swing.JCheckBox(); + sharedSettingsTextField = new javax.swing.JTextField(); + browseSharedSettingsButton = new javax.swing.JButton(); + sharedSettingsErrorTextField = new javax.swing.JTextField(); + masterNodeCheckBox = new javax.swing.JCheckBox(); + uploadButton = new javax.swing.JButton(); + downloadButton = new javax.swing.JButton(); + jLabelCurrentTask = new javax.swing.JLabel(); + pbTaskInProgress = new javax.swing.JProgressBar(); + jLabelTaskDescription = new javax.swing.JLabel(); + configButtonErrorTextField = new javax.swing.JTextField(); + jSeparator1 = new javax.swing.JSeparator(); + jPanelIngestSettings = new javax.swing.JPanel(); + bnEditIngestSettings = new javax.swing.JButton(); + bnAdvancedSettings = new javax.swing.JButton(); + bnFileExport = new javax.swing.JButton(); + bnLogging = new javax.swing.JButton(); + cbJoinAutoIngestCluster = new javax.swing.JCheckBox(); + tbOops = new javax.swing.JTextField(); + + jPanelNodeType.setBorder(javax.swing.BorderFactory.createTitledBorder(javax.swing.BorderFactory.createEtchedBorder(), org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.jPanelNodeType.border.title"))); // NOI18N + jPanelNodeType.setMinimumSize(new java.awt.Dimension(50, 50)); + + org.openide.awt.Mnemonics.setLocalizedText(jLabelSelectMode, org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.jLabelSelectMode.text")); // NOI18N + + restartRequiredNodeLabel.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/experimental/images/warning16.png"))); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(restartRequiredNodeLabel, org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.restartRequiredNodeLabel.text")); // NOI18N + + modeRadioButtons.add(jRadioButtonAutomated); + jRadioButtonAutomated.setSelected(true); + org.openide.awt.Mnemonics.setLocalizedText(jRadioButtonAutomated, org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.jRadioButtonAutomated.text")); // NOI18N + jRadioButtonAutomated.setToolTipText(org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.jRadioButtonAutomated.toolTipText")); // NOI18N + jRadioButtonAutomated.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + jRadioButtonAutomatedActionPerformed(evt); + } + }); + + modeRadioButtons.add(jRadioButtonReview); + org.openide.awt.Mnemonics.setLocalizedText(jRadioButtonReview, org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.jRadioButtonReview.text")); // NOI18N + jRadioButtonReview.setToolTipText(org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.jRadioButtonReview.toolTipText")); // NOI18N + jRadioButtonReview.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + jRadioButtonReviewActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(jLabelSelectInputFolder, org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.jLabelSelectInputFolder.text")); // NOI18N + jLabelSelectInputFolder.setVerticalAlignment(javax.swing.SwingConstants.BOTTOM); + + inputPathTextField.setText(org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.inputPathTextField.text")); // NOI18N + inputPathTextField.setToolTipText(org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.inputPathTextField.toolTipText")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(browseInputFolderButton, org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.browseInputFolderButton.text")); // NOI18N + browseInputFolderButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + browseInputFolderButtonActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(jLabelSelectOutputFolder, org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.jLabelSelectOutputFolder.text")); // NOI18N + jLabelSelectOutputFolder.setVerticalAlignment(javax.swing.SwingConstants.BOTTOM); + + outputPathTextField.setText(org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.outputPathTextField.text")); // NOI18N + outputPathTextField.setToolTipText(org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.outputPathTextField.toolTipText")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(browseOutputFolderButton, org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.browseOutputFolderButton.text")); // NOI18N + browseOutputFolderButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + browseOutputFolderButtonActionPerformed(evt); + } + }); + + jLabelInvalidImageFolder.setForeground(new java.awt.Color(255, 0, 0)); + org.openide.awt.Mnemonics.setLocalizedText(jLabelInvalidImageFolder, org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.jLabelInvalidImageFolder.text")); // NOI18N + + jLabelInvalidResultsFolder.setForeground(new java.awt.Color(255, 0, 0)); + org.openide.awt.Mnemonics.setLocalizedText(jLabelInvalidResultsFolder, org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.jLabelInvalidResultsFolder.text")); // NOI18N + + jLabel1.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/experimental/images/AIM.png"))); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(jLabel1, org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.jLabel1.text")); // NOI18N + + javax.swing.GroupLayout jPanelNodeTypeLayout = new javax.swing.GroupLayout(jPanelNodeType); + jPanelNodeType.setLayout(jPanelNodeTypeLayout); + jPanelNodeTypeLayout.setHorizontalGroup( + jPanelNodeTypeLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanelNodeTypeLayout.createSequentialGroup() + .addContainerGap() + .addGroup(jPanelNodeTypeLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanelNodeTypeLayout.createSequentialGroup() + .addGroup(jPanelNodeTypeLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(outputPathTextField, javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(inputPathTextField, javax.swing.GroupLayout.Alignment.LEADING)) + .addGap(10, 10, 10) + .addGroup(jPanelNodeTypeLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(browseInputFolderButton, javax.swing.GroupLayout.Alignment.TRAILING) + .addComponent(browseOutputFolderButton, javax.swing.GroupLayout.Alignment.TRAILING))) + .addGroup(jPanelNodeTypeLayout.createSequentialGroup() + .addGroup(jPanelNodeTypeLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanelNodeTypeLayout.createSequentialGroup() + .addComponent(jLabelSelectMode) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(restartRequiredNodeLabel)) + .addComponent(jRadioButtonReview) + .addComponent(jRadioButtonAutomated)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jLabel1)) + .addGroup(jPanelNodeTypeLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanelNodeTypeLayout.createSequentialGroup() + .addComponent(jLabelSelectInputFolder) + .addGap(18, 18, 18) + .addComponent(jLabelInvalidImageFolder, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addGroup(jPanelNodeTypeLayout.createSequentialGroup() + .addComponent(jLabelSelectOutputFolder) + .addGap(18, 18, 18) + .addComponent(jLabelInvalidResultsFolder, javax.swing.GroupLayout.PREFERRED_SIZE, 544, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(0, 0, Short.MAX_VALUE)))) + .addContainerGap()) + ); + jPanelNodeTypeLayout.setVerticalGroup( + jPanelNodeTypeLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanelNodeTypeLayout.createSequentialGroup() + .addGroup(jPanelNodeTypeLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanelNodeTypeLayout.createSequentialGroup() + .addContainerGap() + .addGroup(jPanelNodeTypeLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(jLabelSelectMode) + .addComponent(restartRequiredNodeLabel)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(jRadioButtonAutomated) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jRadioButtonReview)) + .addComponent(jLabel1)) + .addGap(1, 1, 1) + .addGroup(jPanelNodeTypeLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(jLabelSelectInputFolder) + .addComponent(jLabelInvalidImageFolder)) + .addGap(1, 1, 1) + .addGroup(jPanelNodeTypeLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(inputPathTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(browseInputFolderButton)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(jPanelNodeTypeLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(jLabelSelectOutputFolder, javax.swing.GroupLayout.PREFERRED_SIZE, 21, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(jLabelInvalidResultsFolder)) + .addGap(1, 1, 1) + .addGroup(jPanelNodeTypeLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(browseOutputFolderButton) + .addComponent(outputPathTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGap(0, 0, Short.MAX_VALUE)) + ); + + jPanelSharedConfig.setBorder(javax.swing.BorderFactory.createTitledBorder(javax.swing.BorderFactory.createEtchedBorder(), org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.jPanelSharedConfig.border.title"))); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(sharedConfigCheckbox, org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.sharedConfigCheckbox.text")); // NOI18N + sharedConfigCheckbox.setMaximumSize(new java.awt.Dimension(191, 14)); + sharedConfigCheckbox.setMinimumSize(new java.awt.Dimension(191, 14)); + sharedConfigCheckbox.setPreferredSize(new java.awt.Dimension(191, 14)); + sharedConfigCheckbox.addItemListener(new java.awt.event.ItemListener() { + public void itemStateChanged(java.awt.event.ItemEvent evt) { + sharedConfigCheckboxItemStateChanged(evt); + } + }); + + sharedSettingsTextField.setText(org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.sharedSettingsTextField.text")); // NOI18N + sharedSettingsTextField.setEnabled(false); + + org.openide.awt.Mnemonics.setLocalizedText(browseSharedSettingsButton, org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.browseSharedSettingsButton.text")); // NOI18N + browseSharedSettingsButton.setEnabled(false); + browseSharedSettingsButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + browseSharedSettingsButtonActionPerformed(evt); + } + }); + + sharedSettingsErrorTextField.setEditable(false); + sharedSettingsErrorTextField.setForeground(new java.awt.Color(255, 0, 0)); + sharedSettingsErrorTextField.setText(org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.sharedSettingsErrorTextField.text")); // NOI18N + sharedSettingsErrorTextField.setBorder(null); + + org.openide.awt.Mnemonics.setLocalizedText(masterNodeCheckBox, org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.masterNodeCheckBox.text")); // NOI18N + masterNodeCheckBox.setEnabled(false); + masterNodeCheckBox.addItemListener(new java.awt.event.ItemListener() { + public void itemStateChanged(java.awt.event.ItemEvent evt) { + masterNodeCheckBoxItemStateChanged(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(uploadButton, org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.uploadButton.text")); // NOI18N + uploadButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + uploadButtonActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(downloadButton, org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.downloadButton.text")); // NOI18N + downloadButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + downloadButtonActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(jLabelCurrentTask, org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.jLabelCurrentTask.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(jLabelTaskDescription, org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.jLabelTaskDescription.text")); // NOI18N + + configButtonErrorTextField.setEditable(false); + configButtonErrorTextField.setForeground(new java.awt.Color(255, 0, 0)); + configButtonErrorTextField.setText(org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.configButtonErrorTextField.text")); // NOI18N + configButtonErrorTextField.setBorder(null); + + javax.swing.GroupLayout jPanelSharedConfigLayout = new javax.swing.GroupLayout(jPanelSharedConfig); + jPanelSharedConfig.setLayout(jPanelSharedConfigLayout); + jPanelSharedConfigLayout.setHorizontalGroup( + jPanelSharedConfigLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanelSharedConfigLayout.createSequentialGroup() + .addGap(10, 10, 10) + .addComponent(jLabelCurrentTask) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(jLabelTaskDescription, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addContainerGap()) + .addGroup(jPanelSharedConfigLayout.createSequentialGroup() + .addContainerGap() + .addGroup(jPanelSharedConfigLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanelSharedConfigLayout.createSequentialGroup() + .addComponent(sharedSettingsTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 400, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(browseSharedSettingsButton, javax.swing.GroupLayout.PREFERRED_SIZE, 143, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(uploadButton, javax.swing.GroupLayout.PREFERRED_SIZE, 143, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGroup(jPanelSharedConfigLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING, false) + .addGroup(javax.swing.GroupLayout.Alignment.LEADING, jPanelSharedConfigLayout.createSequentialGroup() + .addComponent(downloadButton, javax.swing.GroupLayout.PREFERRED_SIZE, 143, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(configButtonErrorTextField)) + .addGroup(javax.swing.GroupLayout.Alignment.LEADING, jPanelSharedConfigLayout.createSequentialGroup() + .addComponent(sharedConfigCheckbox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(sharedSettingsErrorTextField)) + .addComponent(jSeparator1, javax.swing.GroupLayout.PREFERRED_SIZE, 692, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(pbTaskInProgress, javax.swing.GroupLayout.PREFERRED_SIZE, 695, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(masterNodeCheckBox)) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + jPanelSharedConfigLayout.setVerticalGroup( + jPanelSharedConfigLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanelSharedConfigLayout.createSequentialGroup() + .addContainerGap() + .addGroup(jPanelSharedConfigLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(sharedConfigCheckbox, javax.swing.GroupLayout.PREFERRED_SIZE, 21, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(sharedSettingsErrorTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 21, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(jPanelSharedConfigLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(sharedSettingsTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(browseSharedSettingsButton)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addGroup(jPanelSharedConfigLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(downloadButton) + .addComponent(configButtonErrorTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 21, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(jSeparator1, javax.swing.GroupLayout.PREFERRED_SIZE, 10, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(1, 1, 1) + .addComponent(masterNodeCheckBox, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(uploadButton) + .addGap(8, 8, 8) + .addGroup(jPanelSharedConfigLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(jLabelCurrentTask) + .addComponent(jLabelTaskDescription)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(pbTaskInProgress, javax.swing.GroupLayout.PREFERRED_SIZE, 22, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + + jPanelIngestSettings.setBorder(javax.swing.BorderFactory.createTitledBorder(javax.swing.BorderFactory.createEtchedBorder(), org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.jPanelIngestSettings.border.title"))); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(bnEditIngestSettings, org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.bnEditIngestSettings.text")); // NOI18N + bnEditIngestSettings.setToolTipText(org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.bnEditIngestSettings.toolTipText")); // NOI18N + bnEditIngestSettings.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnEditIngestSettingsActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(bnAdvancedSettings, org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.bnAdvancedSettings.text")); // NOI18N + bnAdvancedSettings.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnAdvancedSettingsActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(bnFileExport, org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.bnFileExport.text")); // NOI18N + bnFileExport.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnFileExportActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(bnLogging, org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.bnLogging.text")); // NOI18N + bnLogging.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnLoggingActionPerformed(evt); + } + }); + + javax.swing.GroupLayout jPanelIngestSettingsLayout = new javax.swing.GroupLayout(jPanelIngestSettings); + jPanelIngestSettings.setLayout(jPanelIngestSettingsLayout); + jPanelIngestSettingsLayout.setHorizontalGroup( + jPanelIngestSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanelIngestSettingsLayout.createSequentialGroup() + .addContainerGap() + .addComponent(bnEditIngestSettings, javax.swing.GroupLayout.PREFERRED_SIZE, 155, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(bnAdvancedSettings, javax.swing.GroupLayout.PREFERRED_SIZE, 155, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(bnFileExport, javax.swing.GroupLayout.PREFERRED_SIZE, 155, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(bnLogging, javax.swing.GroupLayout.PREFERRED_SIZE, 155, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + jPanelIngestSettingsLayout.setVerticalGroup( + jPanelIngestSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanelIngestSettingsLayout.createSequentialGroup() + .addContainerGap() + .addGroup(jPanelIngestSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(bnEditIngestSettings) + .addComponent(bnFileExport) + .addComponent(bnAdvancedSettings) + .addComponent(bnLogging)) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + + cbJoinAutoIngestCluster.setFont(cbJoinAutoIngestCluster.getFont().deriveFont(cbJoinAutoIngestCluster.getFont().getStyle() & ~java.awt.Font.BOLD, 11)); + org.openide.awt.Mnemonics.setLocalizedText(cbJoinAutoIngestCluster, org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.cbJoinAutoIngestCluster.text")); // NOI18N + cbJoinAutoIngestCluster.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + cbJoinAutoIngestClusterActionPerformed(evt); + } + }); + + tbOops.setEditable(false); + tbOops.setFont(tbOops.getFont().deriveFont(tbOops.getFont().getStyle() | java.awt.Font.BOLD, 12)); + tbOops.setForeground(new java.awt.Color(255, 0, 0)); + tbOops.setText(org.openide.util.NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.tbOops.text")); // NOI18N + tbOops.setBorder(null); + tbOops.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + tbOopsActionPerformed(evt); + } + }); + + javax.swing.GroupLayout nodePanelLayout = new javax.swing.GroupLayout(nodePanel); + nodePanel.setLayout(nodePanelLayout); + nodePanelLayout.setHorizontalGroup( + nodePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(nodePanelLayout.createSequentialGroup() + .addContainerGap() + .addGroup(nodePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(nodePanelLayout.createSequentialGroup() + .addComponent(cbJoinAutoIngestCluster, javax.swing.GroupLayout.PREFERRED_SIZE, 171, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(18, 18, 18) + .addComponent(tbOops, javax.swing.GroupLayout.PREFERRED_SIZE, 465, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(nodePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addComponent(jPanelNodeType, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jPanelSharedConfig, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(jPanelIngestSettings, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + nodePanelLayout.setVerticalGroup( + nodePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(nodePanelLayout.createSequentialGroup() + .addContainerGap() + .addGroup(nodePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(cbJoinAutoIngestCluster) + .addComponent(tbOops, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGap(3, 3, 3) + .addComponent(jPanelNodeType, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jPanelIngestSettings, javax.swing.GroupLayout.PREFERRED_SIZE, 62, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jPanelSharedConfig, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(nodePanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(nodePanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + ); + }// //GEN-END:initComponents + + private void browseSharedSettingsButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_browseSharedSettingsButtonActionPerformed + + String oldText = sharedSettingsTextField.getText().trim(); + // set the current directory of the FileChooser if the oldText is valid + File currentDir = new File(oldText); + if (currentDir.exists()) { + fc.setCurrentDirectory(currentDir); + } + + fc.setDialogTitle("Select shared configuration folder:"); + fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + + int retval = fc.showOpenDialog(this); + if (retval == JFileChooser.APPROVE_OPTION) { + String path = fc.getSelectedFile().getPath(); + sharedSettingsTextField.setText(path); + validateSettings(); + controller.changed(); + } + }//GEN-LAST:event_browseSharedSettingsButtonActionPerformed + + boolean permissionsAppropriate(String path) { + return FileUtil.hasReadWriteAccess(Paths.get(path)); + } + + private void downloadButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_downloadButtonActionPerformed + // First save the shared config folder and solr settings to the properties + String globalSettingsPath = getNormalizedFolderPath(sharedSettingsTextField.getText().trim()); + AutoIngestUserPreferences.setSharedConfigFolder(globalSettingsPath); + + enableUI(false); + jLabelCurrentTask.setEnabled(true); + jLabelTaskDescription.setEnabled(true); + pbTaskInProgress.setEnabled(true); + pbTaskInProgress.setIndeterminate(true); + + UpdateConfigSwingWorker worker = new UpdateConfigSwingWorker(ConfigTaskType.DOWNLOAD); + try { + worker.execute(); + } catch (Exception ex) { + jLabelTaskDescription.setText(ex.getLocalizedMessage()); + } + }//GEN-LAST:event_downloadButtonActionPerformed + + private void uploadButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_uploadButtonActionPerformed + store(); + + enableUI(false); + jLabelCurrentTask.setEnabled(true); + jLabelTaskDescription.setEnabled(true); + pbTaskInProgress.setEnabled(true); + pbTaskInProgress.setIndeterminate(true); + + UpdateConfigSwingWorker worker = new UpdateConfigSwingWorker(ConfigTaskType.UPLOAD); + try { + worker.execute(); + } catch (Exception ex) { + jLabelTaskDescription.setText(ex.getLocalizedMessage()); + } + }//GEN-LAST:event_uploadButtonActionPerformed + + private void sharedConfigCheckboxItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_sharedConfigCheckboxItemStateChanged + // Enable the global settings text box and browse button iff the checkbox is checked and enabled + setSharedConfigEnable(); + }//GEN-LAST:event_sharedConfigCheckboxItemStateChanged + + private void setSharedConfigEnable() { + setEnabledStateForSharedConfiguration(); + if (sharedConfigCheckbox.isEnabled() && sharedConfigCheckbox.isSelected() && jRadioButtonAutomated.isSelected()) { + sharedSettingsTextField.setEnabled(true); + browseSharedSettingsButton.setEnabled(true); + masterNodeCheckBox.setEnabled(true); + downloadButton.setEnabled(true); + validateSettings(); + controller.changed(); + } else { + sharedSettingsTextField.setEnabled(false); + browseSharedSettingsButton.setEnabled(false); + masterNodeCheckBox.setEnabled(false); + downloadButton.setEnabled(false); + sharedSettingsErrorTextField.setText(""); + validateSettings(); + controller.changed(); + } + } + + private void masterNodeCheckBoxItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_masterNodeCheckBoxItemStateChanged + // Enable the global settings text box and browse button iff the checkbox is checked and enabled + setEnabledStateForSharedConfiguration(); + if (masterNodeCheckBox.isEnabled() && masterNodeCheckBox.isSelected()) { + uploadButton.setEnabled(true); + validateSettings(); // This will disable the upload/save button if the settings aren't currently valid + controller.changed(); + } else { + uploadButton.setEnabled(false); + } + if (sharedConfigCheckbox.isEnabled()) { + jRadioButtonAutomated.setEnabled(false); + jRadioButtonReview.setEnabled(false); + } + }//GEN-LAST:event_masterNodeCheckBoxItemStateChanged + + private void cbJoinAutoIngestClusterActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cbJoinAutoIngestClusterActionPerformed + enableOptionsBasedOnMode(getModeFromRadioButtons()); + controller.changed(); + }//GEN-LAST:event_cbJoinAutoIngestClusterActionPerformed + + private void tbOopsActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_tbOopsActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_tbOopsActionPerformed + + private void browseOutputFolderButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_browseOutputFolderButtonActionPerformed + String oldText = outputPathTextField.getText().trim(); + // set the current directory of the FileChooser if the oldText is valid + File currentDir = new File(oldText); + if (currentDir.exists()) { + fc.setCurrentDirectory(currentDir); + } + + fc.setDialogTitle("Select case output folder:"); + fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + + int retval = fc.showOpenDialog(this); + if (retval == JFileChooser.APPROVE_OPTION) { + String path = fc.getSelectedFile().getPath(); + outputPathTextField.setText(path); + validateSettings(); + controller.changed(); + } + }//GEN-LAST:event_browseOutputFolderButtonActionPerformed + + private void browseInputFolderButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_browseInputFolderButtonActionPerformed + String oldText = inputPathTextField.getText().trim(); + // set the current directory of the FileChooser if the oldText is valid + File currentDir = new File(oldText); + if (currentDir.exists()) { + fc.setCurrentDirectory(currentDir); + } + + fc.setDialogTitle("Select case input folder:"); + fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + + int retval = fc.showOpenDialog(this); + if (retval == JFileChooser.APPROVE_OPTION) { + String path = fc.getSelectedFile().getPath(); + inputPathTextField.setText(path); + validateSettings(); + controller.changed(); + } + }//GEN-LAST:event_browseInputFolderButtonActionPerformed + + private void jRadioButtonReviewActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jRadioButtonReviewActionPerformed + if (cbJoinAutoIngestCluster.isSelected()) { + enableOptionsBasedOnMode(OptionsUiMode.REVIEW); + setSharedConfigEnable(); + validateSettings(); + controller.changed(); + } + }//GEN-LAST:event_jRadioButtonReviewActionPerformed + + private void jRadioButtonAutomatedActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jRadioButtonAutomatedActionPerformed + if (cbJoinAutoIngestCluster.isSelected()) { + enableOptionsBasedOnMode(OptionsUiMode.AIM); + setSharedConfigEnable(); + validateSettings(); + controller.changed(); + } + }//GEN-LAST:event_jRadioButtonAutomatedActionPerformed + + private void bnLoggingActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnLoggingActionPerformed + JDialog jDialog = new JDialog(); + NodeStatusLogPanel loggingPanel = new NodeStatusLogPanel(jDialog); + + JScrollPane jScrollPane = new JScrollPane(loggingPanel); + jScrollPane.setVerticalScrollBarPolicy(javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); + jScrollPane.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); + jScrollPane.setMinimumSize(new Dimension(100, 100)); + jDialog.add(jScrollPane); + jDialog.setTitle(NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.NodeStatusLogging.text")); + jDialog.setIconImage(ImageUtilities.loadImage("org/sleuthkit/autopsy/experimental/images/frame32.gif")); + jDialog.setModalityType(java.awt.Dialog.ModalityType.APPLICATION_MODAL); + jDialog.pack(); + jDialog.setLocationRelativeTo(this); + jDialog.setVisible(true); + }//GEN-LAST:event_bnLoggingActionPerformed + + private void bnFileExportActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnFileExportActionPerformed + JDialog jDialog = new JDialog(); + FileExporterSettingsPanel fileExporterSettingsPanel = new FileExporterSettingsPanel(jDialog); + jDialog.addWindowListener(new java.awt.event.WindowAdapter() { + @Override + public void windowClosing(java.awt.event.WindowEvent windowEvent) { + fileExporterSettingsPanel.store(); + } + }); + JScrollPane jScrollPane = new JScrollPane(fileExporterSettingsPanel); + jScrollPane.setVerticalScrollBarPolicy(javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); + jScrollPane.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); + jScrollPane.setMinimumSize(new Dimension(100, 100)); + jDialog.add(jScrollPane); + jDialog.setTitle(NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.FileExportRules.text")); + jDialog.setIconImage(ImageUtilities.loadImage("org/sleuthkit/autopsy/experimental/images/frame32.gif")); + jDialog.setModalityType(java.awt.Dialog.ModalityType.APPLICATION_MODAL); + jDialog.pack(); + jDialog.setLocationRelativeTo(this); + jDialog.setVisible(true); + }//GEN-LAST:event_bnFileExportActionPerformed + + private void bnAdvancedSettingsActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnAdvancedSettingsActionPerformed + AdvancedAutoIngestSettingsPanel advancedAutoIngestSettingsPanel = new AdvancedAutoIngestSettingsPanel(getModeFromRadioButtons()); + if (JOptionPane.showConfirmDialog(null, advancedAutoIngestSettingsPanel, + NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.AdvancedAutoIngestSettingsPanel.Title"), + JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE) == JOptionPane.OK_OPTION) { + advancedAutoIngestSettingsPanel.store(); + } + }//GEN-LAST:event_bnAdvancedSettingsActionPerformed + + private void bnEditIngestSettingsActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnEditIngestSettingsActionPerformed + displayIngestJobSettingsPanel(); + }//GEN-LAST:event_bnEditIngestSettingsActionPerformed + + private void enableUI(boolean state) { + enableOptionsBasedOnMode(OptionsUiMode.DOWNLOADING_CONFIGURATION); + downloadButton.setEnabled(state); + uploadButton.setEnabled(state); + browseSharedSettingsButton.setEnabled(state); + sharedConfigCheckbox.setEnabled(state); + masterNodeCheckBox.setEnabled(state); + sharedSettingsTextField.setEnabled(state); + } + + private void resetUI() { + load(true); + controller.changed(); + } + + public enum ConfigTaskType { + + DOWNLOAD, UPLOAD + } + + /** + * Handles running the upload configuration task. There's some added + * complexity here in order to re-enable the update button after the update + * completes. + * + */ + public class UpdateConfigSwingWorker extends SwingWorker { + + private String errorMessage = null; + private SharedConfiguration.SharedConfigResult uploadResult = SharedConfiguration.SharedConfigResult.SUCCESS; + private final SharedConfiguration sharedConfig = new SharedConfiguration(this); + private final ConfigTaskType task; + + protected UpdateConfigSwingWorker(ConfigTaskType task) { + this.task = task; + } + + @Override + protected Void doInBackground() throws InterruptedException { + try { + if (task == ConfigTaskType.UPLOAD) { + uploadResult = sharedConfig.uploadConfiguration(); + } else { + uploadResult = sharedConfig.downloadConfiguration(); + } + } catch (Exception ex) { + if (ex instanceof InterruptedException) { + throw (InterruptedException) ex; + } + errorMessage = ex.getLocalizedMessage(); + } + return null; + } + + public void publishStatus(String status) { + publish(status); + } + + @Override + protected void process(List messages) { + for (String status : messages) { + jLabelTaskDescription.setText(status); + } + } + + @Override + protected void done() { + // It would be nicer to hide the progress bar, but that seems to shrink the whole panel + pbTaskInProgress.setIndeterminate(false); + + if (uploadResult == SharedConfiguration.SharedConfigResult.LOCKED) { + jLabelTaskDescription.setText("Transfer of shared configuration incomplete"); + JOptionPane.showMessageDialog(null, "Shared configuration folder is currently locked by another node - try again in a few minutes", "Error", JOptionPane.ERROR_MESSAGE); + } else if (errorMessage != null) { + //MessageNotifyUtil.Message.info(errorMessage); + jLabelTaskDescription.setText("Transfer of shared configuration incomplete"); + JOptionPane.showMessageDialog(WindowManager.getDefault().getMainWindow(), errorMessage, "Error copying configuration", JOptionPane.ERROR_MESSAGE); + } else { + jLabelTaskDescription.setText("Shared configuration copied successfully"); + } + + // Check if anything requiring a reset has changed and update the UI + if (isResetNeeded()) { + JOptionPane.showMessageDialog(null, + NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.MustRestart"), + NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.restartRequiredLabel.text"), + JOptionPane.WARNING_MESSAGE); + } + + // Re-enable everything here + resetUI(); + setEnabledStateForSharedConfiguration(); + pbTaskInProgress.setEnabled(false); + } + } + + void setEnabledStateForSharedConfiguration() { + if (jRadioButtonAutomated.isSelected() && cbJoinAutoIngestCluster.isSelected()) { + if (sharedConfigCheckbox.isEnabled() && sharedConfigCheckbox.isSelected()) { + setEnabledState(masterNodeCheckBox.isSelected()); + } else { + // If we are in AIM mode and shared config is not enabled, allow this + setEnabledState(true); + } + } + } + + void setEnabledState(boolean enabled) { + bnAdvancedSettings.setEnabled(enabled); + bnEditIngestSettings.setEnabled(enabled); + bnFileExport.setEnabled(enabled); + bnLogging.setEnabled(enabled); + browseInputFolderButton.setEnabled(enabled); + browseOutputFolderButton.setEnabled(enabled); + browseSharedSettingsButton.setEnabled(sharedConfigCheckbox.isSelected() && jRadioButtonAutomated.isSelected()); + configButtonErrorTextField.setEnabled(enabled); + inputPathTextField.setEnabled(enabled); + jLabelInvalidImageFolder.setEnabled(enabled); + jLabelInvalidResultsFolder.setEnabled(enabled); + jLabelSelectInputFolder.setEnabled(enabled); + jLabelSelectMode.setEnabled(enabled); + jLabelSelectOutputFolder.setEnabled(enabled); + jPanelIngestSettings.setEnabled(enabled); + jPanelNodeType.setEnabled(enabled); + jPanelSharedConfig.setEnabled(enabled); + jRadioButtonAutomated.setEnabled(enabled); + jRadioButtonReview.setEnabled(enabled); + outputPathTextField.setEnabled(enabled); + restartRequiredNodeLabel.setEnabled(enabled); + } + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton bnAdvancedSettings; + private javax.swing.JButton bnEditIngestSettings; + private javax.swing.JButton bnFileExport; + private javax.swing.JButton bnLogging; + private javax.swing.JButton browseInputFolderButton; + private javax.swing.JButton browseOutputFolderButton; + private javax.swing.JButton browseSharedSettingsButton; + private javax.swing.JCheckBox cbJoinAutoIngestCluster; + private javax.swing.JTextField configButtonErrorTextField; + private javax.swing.JButton downloadButton; + private javax.swing.JTextField inputPathTextField; + private javax.swing.JLabel jLabel1; + private javax.swing.JLabel jLabelCurrentTask; + private javax.swing.JLabel jLabelInvalidImageFolder; + private javax.swing.JLabel jLabelInvalidResultsFolder; + private javax.swing.JLabel jLabelSelectInputFolder; + private javax.swing.JLabel jLabelSelectMode; + private javax.swing.JLabel jLabelSelectOutputFolder; + private javax.swing.JLabel jLabelTaskDescription; + private javax.swing.JPanel jPanelIngestSettings; + private javax.swing.JPanel jPanelNodeType; + private javax.swing.JPanel jPanelSharedConfig; + private javax.swing.JRadioButton jRadioButtonAutomated; + private javax.swing.JRadioButton jRadioButtonReview; + private javax.swing.JSeparator jSeparator1; + private javax.swing.JCheckBox masterNodeCheckBox; + private javax.swing.ButtonGroup modeRadioButtons; + private javax.swing.JPanel nodePanel; + private javax.swing.JTextField outputPathTextField; + private javax.swing.JProgressBar pbTaskInProgress; + private javax.swing.JLabel restartRequiredNodeLabel; + private javax.swing.JCheckBox sharedConfigCheckbox; + private javax.swing.JTextField sharedSettingsErrorTextField; + private javax.swing.JTextField sharedSettingsTextField; + private javax.swing.JTextField tbOops; + private javax.swing.JButton uploadButton; + // End of variables declaration//GEN-END:variables +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/AutoIngestSettingsPanelController.java b/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/AutoIngestSettingsPanelController.java new file mode 100644 index 0000000000..36b6050cf8 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/AutoIngestSettingsPanelController.java @@ -0,0 +1,131 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2013-2014 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.configuration; + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import javax.swing.JComponent; +import org.netbeans.spi.options.OptionsPanelController; +import org.openide.util.HelpCtx; +import org.openide.util.Lookup; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import java.util.logging.Level; +import org.sleuthkit.autopsy.coreutils.Logger; + +@OptionsPanelController.TopLevelRegistration(categoryName = "#OptionsCategory_Name_Auto_Ingest", + iconBase = "org/sleuthkit/autopsy/experimental/images/autoIngest32.png", + position = 3, + keywords = "#OptionsCategory_Keywords_Auto_Ingest_Settings", + keywordsCategory = "Auto Ingest") +public final class AutoIngestSettingsPanelController extends OptionsPanelController { + + private AutoIngestSettingsPanel panel; + private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); + private boolean changed; + private static final Logger logger = Logger.getLogger(AutoIngestSettingsPanelController.class.getName()); + + @Override + public void update() { + getPanel().load(false); + changed = false; + } + + @Override + public void applyChanges() { + getPanel().store(); + changed = false; + } + + @Override + public void cancel() { + } + + @Override + public boolean isValid() { + return getPanel().valid(); + } + + @Override + public boolean isChanged() { + return changed; + } + + @Override + public HelpCtx getHelpCtx() { + return null; + } + + @Override + public JComponent getComponent(Lookup masterLookup) { + return getPanel(); + } + + @Override + public void addPropertyChangeListener(PropertyChangeListener l) { + if (pcs.getPropertyChangeListeners().length == 0) { + pcs.addPropertyChangeListener(l); + } + } + + @Override + public void removePropertyChangeListener(PropertyChangeListener l) { + /** + * Note the NetBeans Framework does not appear to call this at all. We + * are using NetBeans 7.3.1 Build 201306052037. Perhaps in a future + * version of the Framework this will be resolved, but for now, simply + * don't unregister anything and add one time only in the + * addPropertyChangeListener() method above. + */ + } + + private AutoIngestSettingsPanel getPanel() { + if (panel == null) { + panel = new AutoIngestSettingsPanel(this); + } + return panel; + } + + void changed() { + if (!changed) { + changed = true; + + try { + pcs.firePropertyChange(OptionsPanelController.PROP_CHANGED, false, true); + } catch (Exception e) { + logger.log(Level.SEVERE, "GeneralOptionsPanelController listener threw exception", e); //NON-NLS + MessageNotifyUtil.Notify.show( + NbBundle.getMessage(this.getClass(), "GeneralOptionsPanelController.moduleErr"), + NbBundle.getMessage(this.getClass(), "GeneralOptionsPanelController.moduleErr.msg"), + MessageNotifyUtil.MessageType.ERROR); + } + } + + try { + pcs.firePropertyChange(OptionsPanelController.PROP_VALID, null, null); + } catch (Exception e) { + logger.log(Level.SEVERE, "GeneralOptionsPanelController listener threw exception", e); //NON-NLS + MessageNotifyUtil.Notify.show( + NbBundle.getMessage(this.getClass(), "GeneralOptionsPanelController.moduleErr"), + NbBundle.getMessage(this.getClass(), "GeneralOptionsPanelController.moduleErr.msg"), + MessageNotifyUtil.MessageType.ERROR); + } + } +} + diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/AutoIngestUserPreferences.java b/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/AutoIngestUserPreferences.java new file mode 100644 index 0000000000..56b2d844ce --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/AutoIngestUserPreferences.java @@ -0,0 +1,536 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.configuration; + +import java.util.Base64; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.PBEParameterSpec; +import org.sleuthkit.autopsy.core.UserPreferencesException; +import org.sleuthkit.autopsy.coreutils.ModuleSettings; + +/** + * Provides convenient access to a Preferences node for auto ingest user preferences + * with default values. + */ +public final class AutoIngestUserPreferences { + + + public enum SelectedMode { + + STANDALONE, + AUTOMATED, + REVIEW + }; + + private static final String SETTINGS_PROPERTIES = "AutoIngest"; + private static final String MODE = "AutopsyMode"; // NON-NLS + private static final String JOIN_AUTO_MODE_CLUSTER = "JoinAutoModeCluster"; // NON-NLS + private static final String AUTO_MODE_IMAGES_FOLDER = "AutoModeImageFolder"; // NON-NLS + private static final String AUTO_MODE_RESULTS_FOLDER = "AutoModeResultsFolder"; // NON-NLS + private static final String SHARED_CONFIG_FOLDER = "SharedSettingsFolder"; // NON-NLS + private static final String SHARED_CONFIG_ENABLED = "SharedSettingsEnabled"; // NON-NLS + private static final String SHARED_CONFIG_MASTER = "SharedSettingsMaster"; // NON-NLS + private static final String AUTO_MODE_CONTEXT_STRING = "AutoModeContext"; // NON-NLS + private static final String SLEEP_BETWEEN_CASES_TIME = "SleepBetweenCasesTime"; // NON-NLS + private static final String SHOW_TOOLS_WARNING = "ShowToolsWarning"; // NON-NLS + private static final String MAX_NUM_TIMES_TO_PROCESS_IMAGE = "MaxNumTimesToAttemptToProcessImage"; // NON-NLS + private static final String MAX_CONCURRENT_NODES_FOR_ONE_CASE = "MaxConcurrentNodesForOneCase"; // NON-NLS + private static final String STATUS_DATABASE_LOGGING_ENABLED = "StatusDatabaseLoggingEnabled"; // NON-NLS + private static final String LOGGING_DB_HOSTNAME_OR_IP = "LoggingHostnameOrIP"; // NON-NLS + private static final String LOGGING_PORT = "LoggingPort"; // NON-NLS + private static final String LOGGING_USERNAME = "LoggingUsername"; // NON-NLS + private static final String LOGGING_PASSWORD = "LoggingPassword"; // NON-NLS + private static final String LOGGING_DATABASE_NAME = "LoggingDatabaseName"; // NON-NLS + private static final String INPUT_SCAN_INTERVAL_TIME = "IntervalBetweenInputScan"; // NON-NLS + + // Prevent instantiation. + private AutoIngestUserPreferences() { + } + + /** + * Get mode from persistent storage. + * + * @return SelectedMode Selected mode. + */ + public static SelectedMode getMode() { + if (ModuleSettings.settingExists(SETTINGS_PROPERTIES, MODE)) { + int ordinal = Integer.parseInt(ModuleSettings.getConfigSetting(SETTINGS_PROPERTIES, MODE)); + return SelectedMode.values()[ordinal]; + } + return SelectedMode.STANDALONE; + } + + /** + * Set mode to persistent storage. + * + * @param mode Selected mode. + */ + public static void setMode(SelectedMode mode) { + ModuleSettings.setConfigSetting(SETTINGS_PROPERTIES, MODE, Integer.toString(mode.ordinal())); + } + + /** + * Get "Join Automated Ingest Cluster" setting from persistent storage. + * + * @return SelectedMode Selected setting. + */ + public static boolean getJoinAutoModeCluster() { + if (ModuleSettings.settingExists(SETTINGS_PROPERTIES, JOIN_AUTO_MODE_CLUSTER)) { + return Boolean.parseBoolean(ModuleSettings.getConfigSetting(SETTINGS_PROPERTIES, JOIN_AUTO_MODE_CLUSTER)); + } + return false; + } + + /** + * Set "Join Automated Ingest Cluster" setting to persistent storage. + * + * @param join boolean value of whether to join auto ingest cluster or not + */ + public static void setJoinAutoModeCluster(boolean join) { + ModuleSettings.setConfigSetting(SETTINGS_PROPERTIES, JOIN_AUTO_MODE_CLUSTER, Boolean.toString(join)); + } + + /** + * Get input folder for automated mode from persistent storage. + * + * @return String Selected input folder. + */ + public static String getAutoModeImageFolder() { + if (ModuleSettings.settingExists(SETTINGS_PROPERTIES, AUTO_MODE_IMAGES_FOLDER)) { + return ModuleSettings.getConfigSetting(SETTINGS_PROPERTIES, AUTO_MODE_IMAGES_FOLDER); + } + return ""; + } + + /** + * Set input image folder for automated mode from persistent storage. + * + * @param folder Selected input folder. + */ + public static void setAutoModeImageFolder(String folder) { + ModuleSettings.setConfigSetting(SETTINGS_PROPERTIES, AUTO_MODE_IMAGES_FOLDER, folder); + } + + /** + * Get results folder for automated mode from persistent storage. + * + * @return String Selected output folder. + */ + public static String getAutoModeResultsFolder() { + if (ModuleSettings.settingExists(SETTINGS_PROPERTIES, AUTO_MODE_RESULTS_FOLDER)) { + return ModuleSettings.getConfigSetting(SETTINGS_PROPERTIES, AUTO_MODE_RESULTS_FOLDER); + } + return ""; + } + + /** + * Set results folder for automated mode from persistent storage. + * + * @param folder Selected output folder. + */ + public static void setAutoModeResultsFolder(String folder) { + ModuleSettings.setConfigSetting(SETTINGS_PROPERTIES, AUTO_MODE_RESULTS_FOLDER, folder); + } + + /** + * Get shared config folder for automated mode from persistent + * storage. + * + * @return String Selected settings folder. + */ + public static String getSharedConfigFolder() { + if (ModuleSettings.settingExists(SETTINGS_PROPERTIES, SHARED_CONFIG_FOLDER)) { + return ModuleSettings.getConfigSetting(SETTINGS_PROPERTIES, SHARED_CONFIG_FOLDER); + } + return ""; + } + + /** + * Set shared config folder for automated mode from persistent + * storage. + */ + public static void setSharedConfigFolder(String folder) { + ModuleSettings.setConfigSetting(SETTINGS_PROPERTIES, SHARED_CONFIG_FOLDER, folder); + } + + /** + * Get shared config checkbox state for automated mode from + * persistent storage. + * + * @return Boolean true if shared settings are enabled. + */ + public static Boolean getSharedConfigEnabled() { + if (ModuleSettings.settingExists(SETTINGS_PROPERTIES, SHARED_CONFIG_ENABLED)) { + return Boolean.parseBoolean(ModuleSettings.getConfigSetting(SETTINGS_PROPERTIES, SHARED_CONFIG_ENABLED)); + } + return false; + } + + /** + * Save shared config checkbox state for automated mode to persistent + * storage. + * + * @param sharedSettingsEnabled true = use shared settings in auto-ingest + * mode + */ + public static void setSharedConfigEnabled(boolean sharedSettingsEnabled) { + ModuleSettings.setConfigSetting(SETTINGS_PROPERTIES, SHARED_CONFIG_ENABLED, Boolean.toString(sharedSettingsEnabled)); + } + + /** + * Get shared config master checkbox state for automated mode from + * persistent storage. + * + * @return true if this node is set as a shared configuration master + */ + public static Boolean getSharedConfigMaster() { + if (ModuleSettings.settingExists(SETTINGS_PROPERTIES, SHARED_CONFIG_MASTER)) { + return Boolean.parseBoolean(ModuleSettings.getConfigSetting(SETTINGS_PROPERTIES, SHARED_CONFIG_MASTER)); + } + return false; + } + + /** + * Save shared config master checkbox state to persistent storage. + * + * @param sharedSettingsMaster true = this node can upload configuration + */ + public static void setSharedConfigMaster(boolean sharedSettingsMaster) { + ModuleSettings.setConfigSetting(SETTINGS_PROPERTIES, SHARED_CONFIG_MASTER, Boolean.toString(sharedSettingsMaster)); + } + + /** + * Get context string for automated mode ingest module settings. + * + * @return String Context string for automated mode ingest module + * settings. + */ + public static String getAutoModeIngestModuleContextString() { + return AUTO_MODE_CONTEXT_STRING; + } + + /** + * Save whether tools warning dialog should be shown on startup. + * + * @param showToolsWarning true = show warning dialog, false = don't show + */ + public static void setShowToolsWarning(boolean showToolsWarning) { + ModuleSettings.setConfigSetting(SETTINGS_PROPERTIES, SHOW_TOOLS_WARNING, Boolean.toString(showToolsWarning)); + } + + /** + * Retrieve tools warning dialog setting. + * + * @return + */ + public static boolean getShowToolsWarning() { + if (ModuleSettings.settingExists(SETTINGS_PROPERTIES, SHOW_TOOLS_WARNING)) { + return Boolean.parseBoolean(ModuleSettings.getConfigSetting(SETTINGS_PROPERTIES, SHOW_TOOLS_WARNING)); + } + return true; + } + + /** + * Get the configured time to sleep between cases to prevent + * database locks + * + * @return int the value in seconds, default is 30 seconds. + */ + public static int getSecondsToSleepBetweenCases() { + if (ModuleSettings.settingExists(SETTINGS_PROPERTIES, SLEEP_BETWEEN_CASES_TIME)) { + return Integer.parseInt(ModuleSettings.getConfigSetting(SETTINGS_PROPERTIES, SLEEP_BETWEEN_CASES_TIME)); + } + return 30; + } + + /** + * Sets the wait time used by auto ingest nodes to ensure proper + * synchronization of node operations in circumstances where delays may + * occur, e.g., network file system latency effects on the visibility of + * newly created shared directories and files. + * + * @param value value the number of seconds to sleep between cases + */ + public static void setSecondsToSleepBetweenCases(int value) { + ModuleSettings.setConfigSetting(SETTINGS_PROPERTIES, SLEEP_BETWEEN_CASES_TIME, Integer.toString(value)); + } + + /** + * Get maximum number of times to attempt processing an image folder. This + * is used to avoid endless attempts to process an image folder with corrupt + * data that causes a crash. + * + * @return int maximum number of attempts, default is 2. + */ + public static int getMaxNumTimesToProcessImage() { + if (ModuleSettings.settingExists(SETTINGS_PROPERTIES, MAX_NUM_TIMES_TO_PROCESS_IMAGE)) { + return Integer.parseInt(ModuleSettings.getConfigSetting(SETTINGS_PROPERTIES, MAX_NUM_TIMES_TO_PROCESS_IMAGE)); + } + return 2; + } + + /** + * Set the maximum number of times to attempt to reprocess an image. This is + * used to avoid endless attempts to process an image folder with corrupt + * data that causes a crash. + * + * @param retries the number of retries to allow + */ + public static void setMaxNumTimesToProcessImage(int retries) { + ModuleSettings.setConfigSetting(SETTINGS_PROPERTIES, MAX_NUM_TIMES_TO_PROCESS_IMAGE, Integer.toString(retries)); + } + + /** + * Get maximum number of concurrent ingest nodes allowable for one case at a + * time. + * + * @return maximum number of concurrent nodes for one case. Default is 3. + */ + public static int getMaxConcurrentJobsForOneCase() { + if (ModuleSettings.settingExists(SETTINGS_PROPERTIES, MAX_CONCURRENT_NODES_FOR_ONE_CASE)) { + return Integer.parseInt(ModuleSettings.getConfigSetting(SETTINGS_PROPERTIES, MAX_CONCURRENT_NODES_FOR_ONE_CASE)); + } + return 3; + } + + /** + * Get maximum number of concurrent ingest nodes allowable for one case at a + * time. + * + * @param numberOfNodes the number of concurrent nodes to allow for one case + */ + public static void setMaxConcurrentIngestNodesForOneCase(int numberOfNodes) { + ModuleSettings.setConfigSetting(SETTINGS_PROPERTIES, MAX_CONCURRENT_NODES_FOR_ONE_CASE, Integer.toString(numberOfNodes)); + } + + /** + * Get status database logging checkbox state for automated ingest mode from + * persistent storage. + * + * @return Boolean true if database logging is enabled. + */ + public static Boolean getStatusDatabaseLoggingEnabled() { + if (ModuleSettings.settingExists(SETTINGS_PROPERTIES, STATUS_DATABASE_LOGGING_ENABLED)) { + return Boolean.parseBoolean(ModuleSettings.getConfigSetting(SETTINGS_PROPERTIES, STATUS_DATABASE_LOGGING_ENABLED)); + } + return false; + } + + /** + * Save status database logging checkbox state for automated ingest mode to + * persistent storage. + * + * @param databaseLoggingEnabled true = use database logging in auto-ingest + * mode + */ + public static void setStatusDatabaseLoggingEnabled(boolean databaseLoggingEnabled) { + ModuleSettings.setConfigSetting(SETTINGS_PROPERTIES, STATUS_DATABASE_LOGGING_ENABLED, Boolean.toString(databaseLoggingEnabled)); + } + + /** + * Get the logging database hostname from persistent storage. + * + * @return Logging database hostname or IP + */ + public static String getLoggingDatabaseHostnameOrIP() { + if (ModuleSettings.settingExists(SETTINGS_PROPERTIES, LOGGING_DB_HOSTNAME_OR_IP)) { + return ModuleSettings.getConfigSetting(SETTINGS_PROPERTIES, LOGGING_DB_HOSTNAME_OR_IP); + } + return ""; + } + + /** + * Save the logging database hostname to persistent storage. + * + * @param hostname Logging database hostname or IP + */ + public static void setLoggingDatabaseHostnameOrIP(String hostname) { + ModuleSettings.setConfigSetting(SETTINGS_PROPERTIES, LOGGING_DB_HOSTNAME_OR_IP, hostname); + } + + /** + * Get the logging database port from persistent storage. + * + * @return logging database port + */ + public static String getLoggingPort() { + if (ModuleSettings.settingExists(SETTINGS_PROPERTIES, LOGGING_PORT)) { + return ModuleSettings.getConfigSetting(SETTINGS_PROPERTIES, LOGGING_PORT); + } + return ""; + } + + /** + * Save the logging database port to persistent storage. + * + * @param port Logging database port + */ + public static void setLoggingPort(String port) { + ModuleSettings.setConfigSetting(SETTINGS_PROPERTIES, LOGGING_PORT, port); + } + + /** + * Get the logging database username from persistent storage. + * + * @return logging database username + */ + public static String getLoggingUsername() { + if (ModuleSettings.settingExists(SETTINGS_PROPERTIES, LOGGING_USERNAME)) { + return ModuleSettings.getConfigSetting(SETTINGS_PROPERTIES, LOGGING_USERNAME); + } + return ""; + } + + /** + * Save the logging database username to persistent storage. + * + * @param username Logging database username + */ + public static void setLoggingUsername(String username) { + ModuleSettings.setConfigSetting(SETTINGS_PROPERTIES, LOGGING_USERNAME, username); + } + + /** + * Get the logging database password from persistent storage. + * + * @return logging database password + */ + public static String getLoggingPassword() throws UserPreferencesException { + if (ModuleSettings.settingExists(SETTINGS_PROPERTIES, LOGGING_PASSWORD)) { + return TextConverter.convertHexTextToText(ModuleSettings.getConfigSetting(SETTINGS_PROPERTIES, LOGGING_PASSWORD)); + } + return ""; + } + + /** + * Save the logging database password to persistent storage. + * + * @param password Logging database password + */ + public static void setLoggingPassword(String password) throws UserPreferencesException { + ModuleSettings.setConfigSetting(SETTINGS_PROPERTIES, LOGGING_PASSWORD, TextConverter.convertTextToHexText(password)); + } + + /** + * Get the logging database name from persistent storage. + * + * @return logging database name + */ + public static String getLoggingDatabaseName() { + if (ModuleSettings.settingExists(SETTINGS_PROPERTIES, LOGGING_DATABASE_NAME)) { + return ModuleSettings.getConfigSetting(SETTINGS_PROPERTIES, LOGGING_DATABASE_NAME); + } + return ""; + } + + /** + * Save the logging database name to persistent storage. + * + * @param name Logging database name + */ + public static void setLoggingDatabaseName(String name) { + ModuleSettings.setConfigSetting(SETTINGS_PROPERTIES, LOGGING_DATABASE_NAME, name); + } + + /** + * Get the configured time for input scan interval + * + * @return int the value in minutes, default is 60 minutes. + */ + public static int getMinutesOfInputScanInterval() { + if (ModuleSettings.settingExists(SETTINGS_PROPERTIES, INPUT_SCAN_INTERVAL_TIME)) { + return Integer.parseInt(ModuleSettings.getConfigSetting(SETTINGS_PROPERTIES, INPUT_SCAN_INTERVAL_TIME)); + } + return 60; + } + + /** + * Set the configured time for input scan interval + * + * @param value the number of minutes for input interval + */ + public static void setMinutesOfInputScanInterval(int value) { + ModuleSettings.setConfigSetting(SETTINGS_PROPERTIES, INPUT_SCAN_INTERVAL_TIME, Integer.toString(value)); + } + + /** + * Copied from Autopsy UserPreferences - can be removed once everything is merged together. + * Provides ability to convert text to hex text. + */ + static final class TextConverter { + + private static final char[] TMP = "hgleri21auty84fwe".toCharArray(); //NON-NLS + private static final byte[] SALT = { + (byte) 0xde, (byte) 0x33, (byte) 0x10, (byte) 0x12, + (byte) 0xde, (byte) 0x33, (byte) 0x10, (byte) 0x12,}; + + /** + * Convert text to hex text. + * + * @param property Input text string. + * + * @return Converted hex string. + * + * @throws org.sleuthkit.autopsy.core.UserPreferencesException + */ + static String convertTextToHexText(String property) throws UserPreferencesException { + try { + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBEWithMD5AndDES"); //NON-NLS + SecretKey key = keyFactory.generateSecret(new PBEKeySpec(TMP)); + Cipher pbeCipher = Cipher.getInstance("PBEWithMD5AndDES"); //NON-NLS + pbeCipher.init(Cipher.ENCRYPT_MODE, key, new PBEParameterSpec(SALT, 20)); + return base64Encode(pbeCipher.doFinal(property.getBytes("UTF-8"))); + } catch (Exception ex) { + throw new UserPreferencesException("Error encrypting text"); + } + } + + private static String base64Encode(byte[] bytes) { + return Base64.getEncoder().encodeToString(bytes); + } + + /** + * Convert hex text back to text. + * + * @param property Input hex text string. + * + * @return Converted text string. + * + * @throws org.sleuthkit.autopsy.core.UserPreferencesException + */ + static String convertHexTextToText(String property) throws UserPreferencesException { + try { + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBEWithMD5AndDES"); //NON-NLS + SecretKey key = keyFactory.generateSecret(new PBEKeySpec(TMP)); + Cipher pbeCipher = Cipher.getInstance("PBEWithMD5AndDES"); //NON-NLS + pbeCipher.init(Cipher.DECRYPT_MODE, key, new PBEParameterSpec(SALT, 20)); + return new String(pbeCipher.doFinal(base64Decode(property)), "UTF-8"); + } catch (Exception ex) { + throw new UserPreferencesException("Error decrypting text"); + } + } + + private static byte[] base64Decode(String property) { + return Base64.getDecoder().decode(property); + } + } +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/Bundle.properties b/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/Bundle.properties new file mode 100644 index 0000000000..323fd5ee4a --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/Bundle.properties @@ -0,0 +1,149 @@ +AdvancedAutoIngestSettingsPanel.AccessibleContext.accessibleName= +AdvancedAutoIngestSettingsPanel.ItemDisabled.text=Item disabled based upon current Autopsy mode. +AdvancedAutoIngestSettingsPanel.jPanelFolderMaintenanceSettings.border.title=Folder Maintenance Settings +AdvancedAutoIngestSettingsPanel.lbConcurrentJobsPerCase.toolTipText=A soft limit on the number of concurrent jobs per case when multiple cases are processed simultaneously. +AdvancedAutoIngestSettingsPanel.lbInputScanInterval.AccessibleContext.accessibleDescription=The interval +AdvancedAutoIngestSettingsPanel.lbInputScanInterval.toolTipText=The interval between scans for new manifest files. +AdvancedAutoIngestSettingsPanel.lbInputScanIntervalMinutes1.AccessibleContext.accessibleName=minutes +AdvancedAutoIngestSettingsPanel.lbInputScanIntervalMinutes1.text=minutes +AdvancedAutoIngestSettingsPanel.lbNumberOfThreads.toolTipText=The number of threads running file level ingest modules. +AdvancedAutoIngestSettingsPanel.lbRetriesAllowed.toolTipText=The maximum number of retries for crashed jobs. +AdvancedAutoIngestSettingsPanel.lbSecondsBetweenJobs.toolTipText=A wait time used by auto ingest nodes to ensure proper synchronization of node operations. +AdvancedAutoIngestSettingsPanel.tbWarning.text=WARNING: Ensure you know what you are doing before modifying these values. Informed use can improve system performance. Misuse can cause system performance degradation and data loss. Please consult the user guide for details. +AdvancedAutoIngestSettingsPanel.threadCountLabel.text=For this computer, a maximum of {0} file ingest threads should be used. +AIMIngestSettingsPanel.browseGlobalSettingsButton.text=Browse +AIMIngestSettingsPanel.globalSettingsCheckbox.text=Use shared configuration in folder: +AIMIngestSettingsPanel.globalSettingsErrorTextField.text= +AIMIngestSettingsPanel.globalSettingsTextField.text= +AIMIngestSettingsPanel.jButton1.text=Download shared settings +AIMIngestSettingsPanel.jButtonEditIngestSettings.text=Ingest Module Settings +AIMIngestSettingsPanel.jButtonEditIngestSettings.toolTipText=Ingest job settings for the automated processing mode context. +AIMIngestSettingsPanel.jLabel1.text=Download the current shared setting (highly recommended to do this before editing) +AIMIngestSettingsPanel.lbSecondsBetweenJobs.text=Number of seconds to wait between jobs: +AIMIngestSettingsPanel.lbSecondsBetweenJobs.toolTipText=Increase this value if database locks cause problems. It gives a little more time for finalizing. +AIMIngestSettingsPanel.spSecondsBetweenJobs.toolTipText=Increase this value if database locks cause problems. It gives a little more time for finalizing. +AutoIngestSettingsPanel.AdvancedAutoIngestSettingsPanel.Title=Advanced Settings +AutoIngestSettingsPanel.browseGlobalSettingsButton.text=Browse +AutoIngestSettingsPanel.browseSharedSettingsButton.text=Browse +AutoIngestSettingsPanel.CannotAccess=Cannot access +AutoIngestSettingsPanel.cbJoinAutoIngestCluster.text=Join Automated Ingest Cluster +AutoIngestSettingsPanel.CheckPermissions=Check permissions. +AutoIngestSettingsPanel.configButtonErrorTextField.text=configButtonErrorTextField +AutoIngestSettingsPanel.downloadButton.text=Download Config +AutoIngestSettingsPanel.EmptySettingsDirectory=Enter path to settings directory +AutoIngestSettingsPanel.ErrorSettingDefaultFolder=Error creating default folder +AutoIngestSettingsPanel.FileExportRules.text=File Export Rules +AutoIngestSettingsPanel.globalSettingsCheckbox.text=Use shared configuration in folder: +AutoIngestSettingsPanel.globalSettingsErrorTextField.text= +AutoIngestSettingsPanel.globalSettingsTextField.text= +AutoIngestSettingsPanel.ImageDirectoryUnspecified=Shared images folder must be set +AutoIngestSettingsPanel.InvalidPortNumber=Invalid port number. +AutoIngestSettingsPanel.jLabelCurrentTask.text=Current task: +AutoIngestSettingsPanel.jLabelTaskDescription.text=jLabel1 +AutoIngestSettingsPanel.jPanelSharedConfig.border.title=Shared Configuration +AutoIngestSettingsPanel.jRadioButtonCopyFiles.text=File Copy mode +AutoIngestSettingsPanel.KeywordSearchNull=Cannot find Keyword Search service +AutoIngestSettingsPanel.masterNodeCheckBox.text=Use this node as a master node that can upload settings +AutoIngestSettingsPanel.MustRestart=Autopsy must be restarted for new configuration to take effect +AutoIngestSettingsPanel.nodePanel.TabConstraints.tabTitle=Node Configuration +AutoIngestSettingsPanel.NodeStatusLogging.text=Node Status Logging Settings +AutoIngestSettingsPanel.PathInvalid=Path is not valid +AutoIngestSettingsPanel.restartRequiredLabel.text=Application restart required to take effect. +AutoIngestSettingsPanel.restartRequiredLabel.text=Application restart required +AutoIngestSettingsPanel.ResultsDirectoryUnspecified=Shared results folder must be set +AutoIngestSettingsPanel.sharedConfigCheckbox.text=Use shared configuration in folder: +AutoIngestSettingsPanel.sharedSettingsErrorTextField.text=globalSettingsErrorTextField +AutoIngestSettingsPanel.sharedSettingsTextField.text= +AutoIngestSettingsPanel.tbOops.text= +AutoIngestSettingsPanel.uploadButton.text=Save & Upload Config +AutoIngestSettingsPanel.validationErrMsg.incomplete=Fill in all values +AutoIngestSettingsPanel.validationErrMsg.invalidDatabasePort=Invalid database port number +AutoIngestSettingsPanel.validationErrMsg.invalidIndexingServerPort=Invalid Solr server port number +AutoIngestSettingsPanel.validationErrMsg.invalidMessageServicePort=Invalid message service port number +AutoIngestSettingsPanel.validationErrMsg.MUdisabled=Multi user settings must be enabled and saved +GeneralOptionsPanelController.moduleErr.msg=A module caused an error listening to GeneralOptionsPanelController updates. See log to determine which module. Some data could be incomplete. +GeneralOptionsPanelController.moduleErr=Module Error +NodeStatusLogPanel.bnCancel.text=Cancel +NodeStatusLogPanel.bnOk.text=OK +NodeStatusLogPanel.bnTestDatabase.text=Test +NodeStatusLogPanel.cbEnableLogging.text=Enable Database Logging +NodeStatusLogPanel.lbDatabaseSettings.text=Database Settings +NodeStatusLogPanel.lbTestDatabase.text= +NodeStatusLogPanel.lbTestDbWarning.text= +NodeStatusLogPanel.lbTestDbWarning.text= +NodeStatusLogPanel.tbDbHostname.text= +NodeStatusLogPanel.tbDbHostname.toolTipText=Hostname or IP Address +NodeStatusLogPanel.tbDbName.text= +NodeStatusLogPanel.tbDbName.toolTipText=Database Name +NodeStatusLogPanel.tbDbPassword.text= +NodeStatusLogPanel.tbDbPassword.toolTipText=Password +NodeStatusLogPanel.tbDbPort.text= +NodeStatusLogPanel.tbDbPort.toolTipText=Port Number +NodeStatusLogPanel.tbDbUsername.text= +NodeStatusLogPanel.tbDbUsername.toolTipText=User Name +OpenOptionsPanelAction.name=Auto Ingest Options +OptionsCategory_Keywords_Auto_Ingest_Settings=Auto Ingest Settings +OptionsCategory_Keywords_General=Options +OptionsCategory_Name_Auto_Ingest=Auto Ingest +OptionsCategory_Name_General=Autopsy +OptionsDialog.jButton1.text=jButton1 +OptionsDialog.jCheckBox1.text=jCheckBox1 +OptionsDialog.jLabel1.text=jLabel1 +StartupWindow.AutoIngestMode=Automated Ingest Node +StartupWindow.CaseImportMode=Single User Case Import +StartupWindow.CopyAndImportMode=Utilities +StartupWindow.ReviewMode=Cases +StartupWindow.title.text=Welcome +AdvancedAutoIngestSettingsPanel.lbInputScanIntervalMinutes.text=minutes +AdvancedAutoIngestSettingsPanel.lbTimeoutHours.text=hour(s) +AdvancedAutoIngestSettingsPanel.lbSecondsBetweenJobsSeconds.text=seconds +AdvancedAutoIngestSettingsPanel.spSecondsBetweenJobs.toolTipText=Increase this value if database locks cause problems. It gives a little more time for finalizing. +AdvancedAutoIngestSettingsPanel.spTimeoutHours.toolTipText=Components that spawn potentially long-running processes optionally terminate those processes if the specified time out period has elapsed. +AdvancedAutoIngestSettingsPanel.spInputScanInterval.toolTipText=Increase this value to reduce the frequency of input scan. +AdvancedAutoIngestSettingsPanel.spMaximumRetryAttempts.AccessibleContext.accessibleDescription=Maximum number of retries allowed in Automated Ingest Mode +AdvancedAutoIngestSettingsPanel.spMaximumRetryAttempts.toolTipText=The maximum number of times Automated ingest will attempt to reprocess a job if processing fails. +AdvancedAutoIngestSettingsPanel.lbRestartRequired.text=Application restart required to take effect. +AdvancedAutoIngestSettingsPanel.cbTimeoutEnabled.toolTipText=Components that spawn potentially long-running processes optionally terminate those processes if the specified time out period has elapsed. +AdvancedAutoIngestSettingsPanel.cbTimeoutEnabled.text= +AdvancedAutoIngestSettingsPanel.lbConcurrentJobsPerCase.text=Target concurrent jobs per case: +AdvancedAutoIngestSettingsPanel.lbNumberOfThreads.text=Number of threads to use for file ingest: +AdvancedAutoIngestSettingsPanel.lbRetriesAllowed.text=Maximum job retries allowed: +AdvancedAutoIngestSettingsPanel.lbInputScanInterval.text=Interval between input scans: +AdvancedAutoIngestSettingsPanel.lbTimeoutText.text=External processes time out: +AdvancedAutoIngestSettingsPanel.lbSecondsBetweenJobs.text=System synchronization wait time: +AdvancedAutoIngestSettingsPanel.jPanelAutoIngestJobSettings.border.title=Automated Ingest Job Settings +AdvancedAutoIngestSettingsPanel.lbSecondsBetweenJobs.toolTipText_1=Increase this value if database locks cause problems. It gives a little more time for finalizing. +AdvancedAutoIngestSettingsPanel.lbTimeoutText.toolTipText=Components that spawn potentially long-running processes optionally terminate those processes if the specified time out period has elapsed. +AdvancedAutoIngestSettingsPanel.lbInputScanInterval.toolTipText_1=Increase this value to reduce the frequency of input scan. +AdvancedAutoIngestSettingsPanel.lbInputScanIntervalMinutes.toolTipText= +AdvancedAutoIngestSettingsPanel.lbTimeoutHours.toolTipText= +AdvancedAutoIngestSettingsPanel.lbRetriesAllowed.toolTipText_1=The maximum number of retries for crashed jobs. +AdvancedAutoIngestSettingsPanel.lbRetriesAllowed.toolTipText_2=The maximum number of retries for crashed jobs. +AdvancedAutoIngestSettingsPanel.lbConcurrentJobsPerCase.toolTipText_1=A soft limit on the number of concurrent jobs per case when multiple cases are processed simultaneously. +AdvancedAutoIngestSettingsPanel.lbNumberOfThreads.toolTipText_1=The number of threads running file level ingest modules. +AdvancedAutoIngestSettingsPanel.numberOfFileIngestThreadsComboBox.toolTipText=The number of threads running file level ingest modules. +NodeStatusLogPanel.tbDbName.toolTipText_1=Database name +AutoIngestSettingsPanel.jPanelNodeType.border.title=Node Type Setup +AutoIngestSettingsPanel.jLabel1.text= +AutoIngestSettingsPanel.jLabelInvalidResultsFolder.text=jLabelInvalidResultsFolder +AutoIngestSettingsPanel.jLabelInvalidImageFolder.text=jLabelInvalidImageFolder +AutoIngestSettingsPanel.browseOutputFolderButton.text=Browse +AutoIngestSettingsPanel.outputPathTextField.toolTipText=Results folder for automated processing, i.e., the location where results folders will be created by automated processing mode for presentation to the user in review mode. Also, cases that were single-user and were converted to multi-user cases will end up here. +AutoIngestSettingsPanel.outputPathTextField.text= +AutoIngestSettingsPanel.jLabelSelectOutputFolder.text=Select shared results folder: +AutoIngestSettingsPanel.browseInputFolderButton.text=Browse +AutoIngestSettingsPanel.inputPathTextField.toolTipText=Input folder for automated processing, i.e., the location where input case folders will be created for ingest by automated processing mode +AutoIngestSettingsPanel.inputPathTextField.text= +AutoIngestSettingsPanel.jLabelSelectInputFolder.text=Select shared images folder: +AutoIngestSettingsPanel.jRadioButtonReview.toolTipText=Review cases created in automated processing mode +AutoIngestSettingsPanel.jRadioButtonReview.text=Examiner +AutoIngestSettingsPanel.jRadioButtonAutomated.toolTipText=Automatically detect new data sources and create cases. +AutoIngestSettingsPanel.jRadioButtonAutomated.text=Automated ingest +AutoIngestSettingsPanel.restartRequiredNodeLabel.text=Application restart required +AutoIngestSettingsPanel.jLabelSelectMode.text=Select mode: +AutoIngestSettingsPanel.jPanelIngestSettings.border.title=Automated Ingest Settings +AutoIngestSettingsPanel.bnLogging.text=Node Status Logging +AutoIngestSettingsPanel.bnFileExport.text=File Export Settings +AutoIngestSettingsPanel.bnAdvancedSettings.text=Advanced Settings +AutoIngestSettingsPanel.bnEditIngestSettings.toolTipText=Ingest job settings for the automated processing mode context. +AutoIngestSettingsPanel.bnEditIngestSettings.text=Ingest Module Settings diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/Bundle_ja.properties b/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/Bundle_ja.properties new file mode 100644 index 0000000000..9fb3c7e4a1 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/Bundle_ja.properties @@ -0,0 +1,2 @@ + +AutoIngestSettingsPanel.cbJoinAutoIngestCluster.text= diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/NodeStatusLogPanel.form b/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/NodeStatusLogPanel.form new file mode 100644 index 0000000000..128044d113 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/NodeStatusLogPanel.form @@ -0,0 +1,277 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/NodeStatusLogPanel.java b/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/NodeStatusLogPanel.java new file mode 100644 index 0000000000..e17009feda --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/NodeStatusLogPanel.java @@ -0,0 +1,422 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.configuration; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.Collection; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.corecomponents.TextPrompt; +import java.awt.Cursor; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.logging.Level; +import javax.swing.ImageIcon; +import javax.swing.JDialog; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import org.openide.util.ImageUtilities; +import org.sleuthkit.autopsy.core.UserPreferencesException; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.experimental.autoingest.StatusDatabaseLogger; + +/** + * + */ +public class NodeStatusLogPanel extends javax.swing.JPanel { + + private static final String HOST_NAME_OR_IP_PROMPT = NbBundle.getMessage(NodeStatusLogPanel.class, "NodeStatusLogPanel.tbDbHostname.toolTipText"); + private static final String PORT_PROMPT = NbBundle.getMessage(NodeStatusLogPanel.class, "NodeStatusLogPanel.tbDbPort.toolTipText"); + private static final String USER_NAME_PROMPT = NbBundle.getMessage(NodeStatusLogPanel.class, "NodeStatusLogPanel.tbDbUsername.toolTipText"); + private static final String PASSWORD_PROMPT = NbBundle.getMessage(NodeStatusLogPanel.class, "NodeStatusLogPanel.tbDbPassword.toolTipText"); + private static final String DATABASE_NAME_PROMPT = NbBundle.getMessage(NodeStatusLogPanel.class, "NodeStatusLogPanel.tbDbName.toolTipText"); + + private final ImageIcon goodIcon; + private final ImageIcon badIcon; + + JDialog jDialog; + private static final Logger logger = Logger.getLogger(NodeStatusLogPanel.class.getName()); + + /** + * Creates new form DatabaseLogPanell + */ + public NodeStatusLogPanel(JDialog jDialog) { + initComponents(); + load(); + validateSettings(); + this.jDialog = jDialog; + + tbDbHostname.getDocument().addDocumentListener(new MyDocumentListener()); + tbDbPort.getDocument().addDocumentListener(new MyDocumentListener()); + tbDbPassword.getDocument().addDocumentListener(new MyDocumentListener()); + tbDbUsername.getDocument().addDocumentListener(new MyDocumentListener()); + tbDbName.getDocument().addDocumentListener(new MyDocumentListener()); + + /** + * Add text prompts to all of the text fields. + */ + Collection textPrompts = new ArrayList<>(); + textPrompts.add(new TextPrompt(HOST_NAME_OR_IP_PROMPT, tbDbHostname)); + textPrompts.add(new TextPrompt(PORT_PROMPT, tbDbPort)); + textPrompts.add(new TextPrompt(USER_NAME_PROMPT, tbDbUsername)); + textPrompts.add(new TextPrompt(PASSWORD_PROMPT, tbDbPassword)); + textPrompts.add(new TextPrompt(DATABASE_NAME_PROMPT, tbDbName)); + configureTextPrompts(textPrompts); + + goodIcon = new ImageIcon(ImageUtilities.loadImage("org/sleuthkit/autopsy/images/good.png", false)); + badIcon = new ImageIcon(ImageUtilities.loadImage("org/sleuthkit/autopsy/images/bad.png", false)); + } + + final void validateSettings(){ + if(valid()){ + bnOk.setEnabled(true); + if(cbEnableLogging.isSelected()){ + bnTestDatabase.setEnabled(true); + } else { + bnTestDatabase.setEnabled(false); + } + } else { + bnOk.setEnabled(false); + bnTestDatabase.setEnabled(false); + } + } + + private boolean valid(){ + if(cbEnableLogging.isSelected()){ + if(tbDbHostname.getText().isEmpty() + || tbDbPort.getText().isEmpty() + || tbDbUsername.getText().isEmpty() + || (tbDbPassword.getPassword().length == 0) + || tbDbName.getText().isEmpty()){ + return false; + } + } + return true; + } + + private void enableFields(boolean enable){ + tbDbHostname.setEnabled(enable); + tbDbPort.setEnabled(enable); + tbDbUsername.setEnabled(enable); + tbDbPassword.setEnabled(enable); + tbDbName.setEnabled(enable); + } + + final void load(){ + + try{ + cbEnableLogging.setSelected(AutoIngestUserPreferences.getStatusDatabaseLoggingEnabled()); + tbDbHostname.setText(AutoIngestUserPreferences.getLoggingDatabaseHostnameOrIP()); + tbDbPort.setText(AutoIngestUserPreferences.getLoggingPort()); + tbDbUsername.setText(AutoIngestUserPreferences.getLoggingUsername()); + tbDbPassword.setText(AutoIngestUserPreferences.getLoggingPassword()); + } catch (UserPreferencesException ex) { + logger.log(Level.SEVERE, "Error accessing status database connection info", ex); //NON-NLS + } + tbDbName.setText(AutoIngestUserPreferences.getLoggingDatabaseName()); + } + + void store(){ + AutoIngestUserPreferences.setStatusDatabaseLoggingEnabled(cbEnableLogging.isSelected()); + if(cbEnableLogging.isSelected()){ + try{ + AutoIngestUserPreferences.setLoggingDatabaseHostnameOrIP(tbDbHostname.getText().trim()); + AutoIngestUserPreferences.setLoggingPort(tbDbPort.getText().trim()); + AutoIngestUserPreferences.setLoggingUsername(tbDbUsername.getText().trim()); + AutoIngestUserPreferences.setLoggingPassword(new String(tbDbPassword.getPassword())); + AutoIngestUserPreferences.setLoggingDatabaseName(tbDbName.getText().trim()); + } catch (UserPreferencesException ex) { + logger.log(Level.SEVERE, "Error saving database connection info", ex); //NON-NLS + } + + } + } + + /** + * Sets the foreground color and transparency of a collection of text + * prompts. + * + * @param textPrompts The text prompts to configure. + */ + private static void configureTextPrompts(Collection textPrompts) { + float alpha = 0.9f; // Mostly opaque + for (TextPrompt textPrompt : textPrompts) { + textPrompt.setForeground(Color.LIGHT_GRAY); + textPrompt.changeAlpha(alpha); + } + } + + private void testDatabase(){ + String host = tbDbHostname.getText(); + String port = tbDbPort.getText(); + String username = tbDbUsername.getText(); + String password = new String(tbDbPassword.getPassword()); + String dbName = tbDbName.getText(); + + lbTestDatabase.setIcon(null); + lbTestDbWarning.setText(""); + setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + + // First test whether we can connect to the database + try{ + Class.forName("org.postgresql.Driver"); + } catch (ClassNotFoundException ex){ + // Continue on even if this fails + } + try (Connection connection = DriverManager.getConnection("jdbc:postgresql://" + host + ":" + port + "/" + dbName, + username, password); + Statement statement = connection.createStatement();) { + // Now make sure the database is set up for logging + try{ + StatusDatabaseLogger.logToStatusDatabase(statement, "Testing configuration", false); + lbTestDatabase.setIcon(goodIcon); + lbTestDbWarning.setText(""); + } catch (SQLException ex){ + lbTestDatabase.setIcon(badIcon); + lbTestDbWarning.setText("Database is not correctly initialized - " + ex.getMessage()); + } + } catch (SQLException ex) { + lbTestDatabase.setIcon(badIcon); + lbTestDbWarning.setText(ex.getMessage()); + } finally { + setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } + + /** + * 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() { + + pnDatabaseSettings = new javax.swing.JPanel(); + tbDbHostname = new javax.swing.JTextField(); + tbDbPort = new javax.swing.JTextField(); + tbDbUsername = new javax.swing.JTextField(); + tbDbPassword = new javax.swing.JPasswordField(); + lbDatabaseSettings = new javax.swing.JLabel(); + bnTestDatabase = new javax.swing.JButton(); + lbTestDatabase = new javax.swing.JLabel(); + lbTestDbWarning = new javax.swing.JLabel(); + tbDbName = new javax.swing.JTextField(); + cbEnableLogging = new javax.swing.JCheckBox(); + bnOk = new javax.swing.JButton(); + bnCancel = new javax.swing.JButton(); + + pnDatabaseSettings.setBorder(javax.swing.BorderFactory.createEtchedBorder()); + + tbDbHostname.setFont(tbDbHostname.getFont().deriveFont(tbDbHostname.getFont().getStyle() & ~java.awt.Font.BOLD, 12)); + tbDbHostname.setText(org.openide.util.NbBundle.getMessage(NodeStatusLogPanel.class, "NodeStatusLogPanel.tbDbHostname.text")); // NOI18N + tbDbHostname.setToolTipText(org.openide.util.NbBundle.getMessage(NodeStatusLogPanel.class, "NodeStatusLogPanel.tbDbHostname.toolTipText")); // NOI18N + + tbDbPort.setFont(tbDbPort.getFont().deriveFont(tbDbPort.getFont().getStyle() & ~java.awt.Font.BOLD, 12)); + tbDbPort.setText(org.openide.util.NbBundle.getMessage(NodeStatusLogPanel.class, "NodeStatusLogPanel.tbDbPort.text")); // NOI18N + tbDbPort.setToolTipText(org.openide.util.NbBundle.getMessage(NodeStatusLogPanel.class, "NodeStatusLogPanel.tbDbPort.toolTipText")); // NOI18N + + tbDbUsername.setFont(tbDbUsername.getFont().deriveFont(tbDbUsername.getFont().getStyle() & ~java.awt.Font.BOLD, 12)); + tbDbUsername.setText(org.openide.util.NbBundle.getMessage(NodeStatusLogPanel.class, "NodeStatusLogPanel.tbDbUsername.text")); // NOI18N + tbDbUsername.setToolTipText(org.openide.util.NbBundle.getMessage(NodeStatusLogPanel.class, "NodeStatusLogPanel.tbDbUsername.toolTipText")); // NOI18N + + tbDbPassword.setFont(tbDbPassword.getFont().deriveFont(tbDbPassword.getFont().getStyle() & ~java.awt.Font.BOLD, 12)); + tbDbPassword.setText(org.openide.util.NbBundle.getMessage(NodeStatusLogPanel.class, "NodeStatusLogPanel.tbDbPassword.text")); // NOI18N + tbDbPassword.setToolTipText(org.openide.util.NbBundle.getMessage(NodeStatusLogPanel.class, "NodeStatusLogPanel.tbDbPassword.toolTipText")); // NOI18N + + lbDatabaseSettings.setFont(lbDatabaseSettings.getFont().deriveFont(lbDatabaseSettings.getFont().getStyle() & ~java.awt.Font.BOLD, 12)); + org.openide.awt.Mnemonics.setLocalizedText(lbDatabaseSettings, org.openide.util.NbBundle.getMessage(NodeStatusLogPanel.class, "NodeStatusLogPanel.lbDatabaseSettings.text")); // NOI18N + lbDatabaseSettings.setVerticalAlignment(javax.swing.SwingConstants.TOP); + + bnTestDatabase.setFont(bnTestDatabase.getFont().deriveFont(bnTestDatabase.getFont().getStyle() & ~java.awt.Font.BOLD, 11)); + org.openide.awt.Mnemonics.setLocalizedText(bnTestDatabase, org.openide.util.NbBundle.getMessage(NodeStatusLogPanel.class, "NodeStatusLogPanel.bnTestDatabase.text")); // NOI18N + bnTestDatabase.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnTestDatabaseActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(lbTestDatabase, org.openide.util.NbBundle.getMessage(NodeStatusLogPanel.class, "NodeStatusLogPanel.lbTestDatabase.text")); // NOI18N + lbTestDatabase.setAutoscrolls(true); + + lbTestDbWarning.setForeground(new java.awt.Color(255, 0, 0)); + org.openide.awt.Mnemonics.setLocalizedText(lbTestDbWarning, org.openide.util.NbBundle.getMessage(NodeStatusLogPanel.class, "NodeStatusLogPanel.lbTestDbWarning.text")); // NOI18N + + tbDbName.setText(org.openide.util.NbBundle.getMessage(NodeStatusLogPanel.class, "NodeStatusLogPanel.tbDbName.text")); // NOI18N + tbDbName.setToolTipText(org.openide.util.NbBundle.getMessage(NodeStatusLogPanel.class, "NodeStatusLogPanel.tbDbName.toolTipText_1")); // NOI18N + + javax.swing.GroupLayout pnDatabaseSettingsLayout = new javax.swing.GroupLayout(pnDatabaseSettings); + pnDatabaseSettings.setLayout(pnDatabaseSettingsLayout); + pnDatabaseSettingsLayout.setHorizontalGroup( + pnDatabaseSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(pnDatabaseSettingsLayout.createSequentialGroup() + .addContainerGap() + .addGroup(pnDatabaseSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(pnDatabaseSettingsLayout.createSequentialGroup() + .addComponent(lbDatabaseSettings) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 344, Short.MAX_VALUE) + .addComponent(bnTestDatabase) + .addGap(18, 18, 18) + .addComponent(lbTestDatabase, javax.swing.GroupLayout.PREFERRED_SIZE, 16, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(tbDbHostname) + .addComponent(tbDbPort) + .addComponent(tbDbUsername) + .addComponent(tbDbPassword) + .addComponent(tbDbName) + .addComponent(lbTestDbWarning, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addContainerGap()) + ); + pnDatabaseSettingsLayout.setVerticalGroup( + pnDatabaseSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, pnDatabaseSettingsLayout.createSequentialGroup() + .addContainerGap() + .addGroup(pnDatabaseSettingsLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(bnTestDatabase) + .addComponent(lbTestDatabase, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(lbDatabaseSettings)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(tbDbHostname, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(tbDbPort, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(tbDbUsername, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(tbDbPassword, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(tbDbName, javax.swing.GroupLayout.PREFERRED_SIZE, 20, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(lbTestDbWarning, javax.swing.GroupLayout.PREFERRED_SIZE, 16, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(24, 24, 24)) + ); + + cbEnableLogging.setFont(cbEnableLogging.getFont().deriveFont(cbEnableLogging.getFont().getStyle() & ~java.awt.Font.BOLD, 11)); + org.openide.awt.Mnemonics.setLocalizedText(cbEnableLogging, org.openide.util.NbBundle.getMessage(NodeStatusLogPanel.class, "NodeStatusLogPanel.cbEnableLogging.text")); // NOI18N + cbEnableLogging.addItemListener(new java.awt.event.ItemListener() { + public void itemStateChanged(java.awt.event.ItemEvent evt) { + cbEnableLoggingItemStateChanged(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(bnOk, org.openide.util.NbBundle.getMessage(NodeStatusLogPanel.class, "NodeStatusLogPanel.bnOk.text")); // NOI18N + bnOk.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnOkActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(bnCancel, org.openide.util.NbBundle.getMessage(NodeStatusLogPanel.class, "NodeStatusLogPanel.bnCancel.text")); // NOI18N + bnCancel.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + bnCancelActionPerformed(evt); + } + }); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(pnDatabaseSettings, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap()) + .addGroup(layout.createSequentialGroup() + .addGap(232, 232, 232) + .addComponent(bnOk) + .addGap(18, 18, 18) + .addComponent(bnCancel) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(cbEnableLogging) + .addContainerGap(421, Short.MAX_VALUE))) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGap(79, 79, 79) + .addComponent(pnDatabaseSettings, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(bnOk) + .addComponent(bnCancel)) + .addContainerGap(69, Short.MAX_VALUE)) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGap(48, 48, 48) + .addComponent(cbEnableLogging) + .addContainerGap(324, Short.MAX_VALUE))) + ); + }// //GEN-END:initComponents + + private void cbEnableLoggingItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_cbEnableLoggingItemStateChanged + enableFields(cbEnableLogging.isSelected()); + validateSettings(); + }//GEN-LAST:event_cbEnableLoggingItemStateChanged + + private void bnTestDatabaseActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnTestDatabaseActionPerformed + testDatabase(); + }//GEN-LAST:event_bnTestDatabaseActionPerformed + + private void bnOkActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnOkActionPerformed + store(); + jDialog.dispose(); + }//GEN-LAST:event_bnOkActionPerformed + + private void bnCancelActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_bnCancelActionPerformed + jDialog.dispose(); + }//GEN-LAST:event_bnCancelActionPerformed + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton bnCancel; + private javax.swing.JButton bnOk; + private javax.swing.JButton bnTestDatabase; + private javax.swing.JCheckBox cbEnableLogging; + private javax.swing.JLabel lbDatabaseSettings; + private javax.swing.JLabel lbTestDatabase; + private javax.swing.JLabel lbTestDbWarning; + private javax.swing.JPanel pnDatabaseSettings; + private javax.swing.JTextField tbDbHostname; + private javax.swing.JTextField tbDbName; + private javax.swing.JPasswordField tbDbPassword; + private javax.swing.JTextField tbDbPort; + private javax.swing.JTextField tbDbUsername; + // End of variables declaration//GEN-END:variables + + private class MyDocumentListener implements DocumentListener { + + @Override + public void changedUpdate(DocumentEvent e) { + validateSettings(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + validateSettings(); + } + + @Override + public void insertUpdate(DocumentEvent e) { + validateSettings(); + } + }; +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/SharedConfiguration.java b/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/SharedConfiguration.java new file mode 100644 index 0000000000..0e6d38c903 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/SharedConfiguration.java @@ -0,0 +1,1207 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.configuration; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.UnsupportedEncodingException; +import java.util.zip.CRC32; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.logging.Level; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import java.util.concurrent.TimeUnit; +import java.util.prefs.BackingStoreException; +import org.apache.commons.io.FileUtils; +import org.sleuthkit.autopsy.core.UserPreferences; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.PlatformUtil; +import org.sleuthkit.autopsy.coreutils.FileUtil; +import org.sleuthkit.autopsy.ingest.IngestJobSettings; +import org.sleuthkit.autopsy.keywordsearch.KeywordListsManager; +import org.sleuthkit.autopsy.modules.hashdatabase.HashDbManager; +import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.autopsy.core.ServicesMonitor; +import org.sleuthkit.autopsy.modules.hashdatabase.HashDbManager.HashDb; +import org.sleuthkit.autopsy.experimental.configuration.AutoIngestSettingsPanel.UpdateConfigSwingWorker; +import org.sleuthkit.autopsy.experimental.coordinationservice.CoordinationService; +import org.sleuthkit.autopsy.experimental.coordinationservice.CoordinationService.Lock; +import org.sleuthkit.autopsy.experimental.coordinationservice.CoordinationService.CoordinationServiceException; + +/* + * A utility class for loading and saving shared configuration data + */ +public class SharedConfiguration { + + // Files + private static final String AUTO_MODE_CONTEXT_FILE = "AutoModeContext.properties"; //NON-NLS + private static final String USER_DEFINED_TYPE_DEFINITIONS_FILE = "UserFileTypeDefinitions.settings"; //NON-NLS + private static final String USER_DEFINED_TYPE_DEFINITIONS_FILE_LEGACY = "UserFileTypeDefinitions.xml"; //NON-NLS + private static final String INTERESTING_FILES_SET_DEFS_FILE = "InterestingFileSets.settings"; //NON-NLS + private static final String INTERESTING_FILES_SET_DEFS_FILE_LEGACY = "InterestingFilesSetDefs.xml"; //NON-NLS + private static final String KEYWORD_SEARCH_SETTINGS = "keywords.settings"; //NON-NLS + private static final String KEYWORD_SEARCH_SETTINGS_LEGACY = "keywords.xml"; //NON-NLS + private static final String KEYWORD_SEARCH_GENERAL_LEGACY = "KeywordSearch.properties"; //NON-NLS + private static final String KEYWORD_SEARCH_NSRL_LEGACY = "KeywordSearch_NSRL.properties"; //NON-NLS + private static final String KEYWORD_SEARCH_OPTIONS_LEGACY = "KeywordSearch_Options.properties"; //NON-NLS + private static final String KEYWORD_SEARCH_SCRIPTS_LEGACY = "KeywordSearch_Scripts.properties"; //NON-NLS + private static final String FILE_EXT_MISMATCH_SETTINGS = "mismatch_config.settings"; //NON-NLS + private static final String FILE_EXT_MISMATCH_SETTINGS_LEGACY = "mismatch_config.xml"; //NON-NLS + private static final String ANDROID_TRIAGE = "AndroidTriage_Options.properties"; //NON-NLS + private static final String GENERAL_PROPERTIES = "core.properties"; //NON-NLS + private static final String AUTO_INGEST_PROPERTIES = "AutoIngest.properties"; //NON-NLS + private static final String HASHDB_CONFIG_FILE_NAME = "hashLookup.settings"; //NON-NLS + private static final String HASHDB_CONFIG_FILE_NAME_LEGACY = "hashsets.xml"; //NON-NLS + public static final String FILE_EXPORTER_SETTINGS_FILE = "fileexporter.settings"; //NON-NLS + private static final String SHARED_CONFIG_VERSIONS = "SharedConfigVersions.txt"; //NON-NLS + + // Folders + private static final String AUTO_MODE_FOLDER = "AutoModeContext"; //NON-NLS + private static final String REMOTE_HASH_FOLDER = "hashDb"; //NON-NLS + private static final String PREFERENCES_FOLDER = "Preferences"; //NON-NLS + public static final String FILE_EXPORTER_FOLDER = "Automated File Exporter"; //NON-NLS + + private static final String LOCK_ROOT = "/autopsy"; // NON-NLS + private static final String UPLOAD_IN_PROGRESS_FILE = "uploadInProgress"; // NON-NLS + private static final String moduleDirPath = PlatformUtil.getUserConfigDirectory(); + private static final Logger logger = Logger.getLogger(SharedConfiguration.class.getName()); + + private final UpdateConfigSwingWorker swingWorker; + private AutoIngestUserPreferences.SelectedMode mode; + private String sharedConfigFolder; + private int fileIngestThreads; + private boolean sharedConfigMaster; + private boolean showToolsWarning; + private boolean displayLocalTime; + private boolean hideKnownFilesInDataSource; + private boolean hideKnownFilesInViews; + private boolean hideSlackFilesInDataSource; + private boolean hideSlackFilesInViews; + private boolean keepPreferredViewer; + + /** + * Exception type thrown by shared configuration. + */ + public final static class SharedConfigurationException extends Exception { + + private static final long serialVersionUID = 1L; + + private SharedConfigurationException(String message) { + super(message); + } + + private SharedConfigurationException(String message, Throwable cause) { + super(message, cause); + } + } + + // Using this so we can indicate whether a read/write failed because the lock file is present, + // which we need to know to display the correct message in the auto-ingest dashboard about why + // processing has been paused. + public enum SharedConfigResult { + + SUCCESS, LOCKED + } + + public SharedConfiguration() { + swingWorker = null; + } + + /** + * Construct with a SwingWorker reference to allow status update messages to + * go to the GUI + * + * @param worker + */ + public SharedConfiguration(UpdateConfigSwingWorker worker) { + this.swingWorker = worker; + } + + private void publishTask(String task) { + if (swingWorker != null) { + swingWorker.publishStatus(task); + } + } + + /** + * Upload the current multi-user ingest settings to a shared folder. + * + * @throws SharedConfigurationException + * @throws CoordinationServiceException + * @throws InterruptedException + */ + public SharedConfigResult uploadConfiguration() throws SharedConfigurationException, CoordinationServiceException, InterruptedException { + publishTask("Starting shared configuration upload"); + + File remoteFolder = getSharedFolder(); + + try (Lock writeLock = CoordinationService.getInstance(LOCK_ROOT).tryGetExclusiveLock(CoordinationService.CategoryNode.CONFIG, remoteFolder.getAbsolutePath(), 30, TimeUnit.MINUTES)) { + if (writeLock == null) { + logger.log(Level.INFO, String.format("Failed to lock %s - another node is currently uploading or downloading configuration", remoteFolder.getAbsolutePath())); + return SharedConfigResult.LOCKED; + } + + // Make sure the local settings have been initialized + if (!isConfigFolderPopulated(new File(moduleDirPath), false)) { + logger.log(Level.INFO, "Local configuration has not been initialized."); + throw new SharedConfigurationException("Local configuration has not been initialized. Please verify/update and save the settings using the Ingest Module Settings button and then retry the upload."); + } + + // Write a file to indicate that uploading is in progress. If we crash or + // have an error, this file will remain in the shared folder. + File uploadInProgress = new File(remoteFolder, UPLOAD_IN_PROGRESS_FILE); + if (!uploadInProgress.exists()) { + try { + Files.createFile(uploadInProgress.toPath()); + } catch (IOException ex) { + throw new SharedConfigurationException(String.format("Failed to create %s", uploadInProgress.toPath()), ex); + } + } + + // Make sure all recent changes are saved to the preference file + // Current testing suggests that we do not need to do this for the ingest settings + // because there is a longer delay between setting them and copying the files. + try { + // Make sure all recent changes are saved to the preference file + // Current testing suggests that we do not need to do this for the ingest settings + // because there is a longer delay between setting them and copying the files. + UserPreferences.saveToStorage(); + } catch (BackingStoreException ex) { + throw new SharedConfigurationException("Failed to save shared configuration settings", ex); + } + + uploadAutoModeContextSettings(remoteFolder); + uploadEnabledModulesSettings(remoteFolder); + uploadFileTypeSettings(remoteFolder); + uploadInterestingFilesSettings(remoteFolder); + uploadKeywordSearchSettings(remoteFolder); + uploadFileExtMismatchSettings(remoteFolder); + uploadAndroidTriageSettings(remoteFolder); + uploadMultiUserAndGeneralSettings(remoteFolder); + uploadHashDbSettings(remoteFolder); + uploadFileExporterSettings(remoteFolder); + + try { + Files.deleteIfExists(uploadInProgress.toPath()); + } catch (IOException ex) { + throw new SharedConfigurationException(String.format("Failed to delete %s", uploadInProgress.toPath()), ex); + } + } + + return SharedConfigResult.SUCCESS; + } + + /** + * Download the multi-user settings from a shared folder. + * + * @throws SharedConfigurationException + * @throws InterruptedException + */ + public synchronized SharedConfigResult downloadConfiguration() throws SharedConfigurationException, InterruptedException { + publishTask("Starting shared configuration download"); + + // Save local settings that should not get overwritten + saveNonSharedSettings(); + + File remoteFolder = getSharedFolder(); + + try (Lock readLock = CoordinationService.getInstance(LOCK_ROOT).tryGetSharedLock(CoordinationService.CategoryNode.CONFIG, remoteFolder.getAbsolutePath(), 30, TimeUnit.MINUTES)) { + if (readLock == null) { + return SharedConfigResult.LOCKED; + } + + // Make sure the shared configuration was last uploaded successfully + File uploadInProgress = new File(remoteFolder, UPLOAD_IN_PROGRESS_FILE); + if (uploadInProgress.exists()) { + logger.log(Level.INFO, String.format("Shared configuration folder %s is corrupt - re-upload configuration", remoteFolder.getAbsolutePath())); + throw new SharedConfigurationException(String.format("Shared configuration folder %s is corrupt - re-upload configuration", remoteFolder.getAbsolutePath())); + } + + // Make sure the shared configuration folder isn't empty + if (!isConfigFolderPopulated(remoteFolder, true)) { + logger.log(Level.INFO, String.format("Shared configuration folder %s is missing files / may be empty. Aborting download.", remoteFolder.getAbsolutePath())); + throw new SharedConfigurationException(String.format("Shared configuration folder %s is missing files / may be empty. Aborting download.", remoteFolder.getAbsolutePath())); + } + + try { + /* Make sure all recent changes are saved to the preference file. + This also releases open file handles to the preference files. If this + is not done, then occasionally downloading of shared configuration + fails silently, likely because Java/OS is still holding the file handle. + The problem manifests itself by some of the old/original configuration files + sticking around after shared configuration has seemingly been successfully + updated. */ + UserPreferences.saveToStorage(); + } catch (BackingStoreException ex) { + throw new SharedConfigurationException("Failed to save shared configuration settings", ex); + } + + downloadAutoModeContextSettings(remoteFolder); + downloadEnabledModuleSettings(remoteFolder); + downloadFileTypeSettings(remoteFolder); + downloadInterestingFilesSettings(remoteFolder); + downloadKeywordSearchSettings(remoteFolder); + downloadFileExtMismatchSettings(remoteFolder); + downloadAndroidTriageSettings(remoteFolder); + downloadFileExporterSettings(remoteFolder); + + // Download general settings, then restore the current + // values for the unshared fields + downloadMultiUserAndGeneralSettings(remoteFolder); + try { + UserPreferences.reloadFromStorage(); + } catch (BackingStoreException ex) { + throw new SharedConfigurationException("Failed to read shared configuration settings", ex); + } + + restoreNonSharedSettings(); + downloadHashDbSettings(remoteFolder); + } catch (CoordinationServiceException ex) { + throw new SharedConfigurationException(String.format("Coordination service error acquiring exclusive lock on shared configuration source %s", remoteFolder.getAbsolutePath()), ex); + } + + // Check Solr service + if (!isServiceUp(ServicesMonitor.Service.REMOTE_KEYWORD_SEARCH.toString())) { + throw new SharedConfigurationException("Keyword search service is down"); + } + + // Check PostgreSQL service + if (!isServiceUp(ServicesMonitor.Service.REMOTE_CASE_DATABASE.toString())) { + throw new SharedConfigurationException("Case database server is down"); + } + + // Check ActiveMQ service + if (!isServiceUp(ServicesMonitor.Service.MESSAGING.toString())) { + throw new SharedConfigurationException("Messaging service is down"); + } + + // Check input folder permissions + String inputFolder = AutoIngestUserPreferences.getAutoModeImageFolder(); + if (!FileUtil.hasReadWriteAccess(Paths.get(inputFolder))) { + throw new SharedConfigurationException("Cannot read input folder " + inputFolder + ". Check that the folder exists and that you have permissions to access it."); + } + + // Check output folder permissions + String outputFolder = AutoIngestUserPreferences.getAutoModeResultsFolder(); + if (!FileUtil.hasReadWriteAccess(Paths.get(outputFolder))) { + throw new SharedConfigurationException("Cannot read output folder " + outputFolder + ". Check that the folder exists and that you have permissions to access it."); + } + + return SharedConfigResult.SUCCESS; + } + + /** + * Tests service of interest to verify that it is running. + * + * @param serviceName Name of the service. + * + * @return True if the service is running, false otherwise. + */ + private boolean isServiceUp(String serviceName) { + try { + return (ServicesMonitor.getInstance().getServiceStatus(serviceName).equals(ServicesMonitor.ServiceStatus.UP.toString())); + } catch (ServicesMonitor.ServicesMonitorException ex) { + logger.log(Level.SEVERE, String.format("Problem checking service status for %s", serviceName), ex); + return false; + } + } + + /** + * Save any settings that should not be overwritten by the shared + * configuration. + */ + private void saveNonSharedSettings() { + sharedConfigMaster = AutoIngestUserPreferences.getSharedConfigMaster(); + sharedConfigFolder = AutoIngestUserPreferences.getSharedConfigFolder(); + showToolsWarning = AutoIngestUserPreferences.getShowToolsWarning(); + displayLocalTime = UserPreferences.displayTimesInLocalTime(); + hideKnownFilesInDataSource = UserPreferences.hideKnownFilesInDataSourcesTree(); + hideKnownFilesInViews = UserPreferences.hideKnownFilesInViewsTree(); + keepPreferredViewer = UserPreferences.keepPreferredContentViewer(); + fileIngestThreads = UserPreferences.numberOfFileIngestThreads(); + hideSlackFilesInDataSource = UserPreferences.hideSlackFilesInDataSourcesTree(); + hideSlackFilesInViews = UserPreferences.hideSlackFilesInViewsTree(); + } + + /** + * Restore the settings that may have been overwritten. + */ + private void restoreNonSharedSettings() { + AutoIngestUserPreferences.setSharedConfigFolder(sharedConfigFolder); + AutoIngestUserPreferences.setSharedConfigMaster(sharedConfigMaster); + AutoIngestUserPreferences.setShowToolsWarning(showToolsWarning); + UserPreferences.setDisplayTimesInLocalTime(displayLocalTime); + UserPreferences.setHideKnownFilesInDataSourcesTree(hideKnownFilesInDataSource); + UserPreferences.setHideKnownFilesInViewsTree(hideKnownFilesInViews); + UserPreferences.setKeepPreferredContentViewer(keepPreferredViewer); + UserPreferences.setNumberOfFileIngestThreads(fileIngestThreads); + UserPreferences.setHideSlackFilesInDataSourcesTree(hideSlackFilesInDataSource); + UserPreferences.setHideSlackFilesInViewsTree(hideSlackFilesInViews); + } + + /** + * Get the base folder being used to store the shared config settings. + * + * @return The shared configuration folder + * + * @throws SharedConfigurationException + */ + private static File getSharedFolder() throws SharedConfigurationException { + // Check that the shared folder is set and exists + String remoteConfigFolderPath = AutoIngestUserPreferences.getSharedConfigFolder(); + if (remoteConfigFolderPath.isEmpty()) { + logger.log(Level.SEVERE, "Shared configuration folder is not set."); + throw new SharedConfigurationException("Shared configuration folder is not set."); + } + File remoteFolder = new File(remoteConfigFolderPath); + if (!remoteFolder.exists()) { + logger.log(Level.SEVERE, "Shared configuration folder {0} does not exist", remoteConfigFolderPath); + throw new SharedConfigurationException("Shared configuration folder " + remoteConfigFolderPath + " does not exist"); + } + return remoteFolder; + } + + /** + * Do a basic check to determine whether settings have been stored to the + * given folder. There may still be missing files/errors, but this will stop + * empty/corrupt settings from overwriting local settings or being uploaded. + * + * Currently we check for: - A non-empty AutoModeContext folder - An + * AutoModeContext properties file + * + * @param folder Folder to check the contents of + * @param isSharedFolder True if the folder being tested is the shared + * folder, false if its the local folder + * + * @return true if the folder appears to have been initialized, false + * otherwise + */ + private static boolean isConfigFolderPopulated(File folder, boolean isSharedFolder) { + + if (!folder.exists()) { + return false; + } + + // Check that the context directory exists and is not empty + File contextDir; + if (isSharedFolder) { + contextDir = new File(folder, AUTO_MODE_FOLDER); + } else { + IngestJobSettings ingestJobSettings = new IngestJobSettings(AutoIngestUserPreferences.getAutoModeIngestModuleContextString()); + contextDir = ingestJobSettings.getSavedModuleSettingsFolder().toFile(); + } + + if ((!contextDir.exists()) || (!contextDir.isDirectory())) { + return false; + } + if (contextDir.listFiles().length == 0) { + return false; + } + + // Check that the automode context properties file exists + File contextProperties = new File(folder, AUTO_MODE_CONTEXT_FILE); + return contextProperties.exists(); + } + + /** + * Copy a local settings file to the remote folder. + * + * @param fileName Name of the file to copy + * @param localFolder Local settings folder + * @param remoteFolder Shared settings folder + * @param missingFileOk True if it's not an error if the source file is not + * found + * + * @throws SharedConfigurationException + */ + private static void copyToRemoteFolder(String fileName, String localFolder, File remoteFolder, boolean missingFileOk) throws SharedConfigurationException { + logger.log(Level.INFO, "Uploading {0} to {1}", new Object[]{fileName, remoteFolder.getAbsolutePath()}); + File localFile = new File(localFolder, fileName); + if (!localFile.exists()) { + Path deleteRemote = Paths.get(remoteFolder.toString(), fileName); + try { + if (deleteRemote.toFile().exists()) { + deleteRemote.toFile().delete(); + } + } catch (SecurityException ex) { + logger.log(Level.SEVERE, "Shared configuration {0} does not exist on local node, but unable to remove remote copy", fileName); + throw new SharedConfigurationException("Shared configuration file " + deleteRemote.toString() + " could not be deleted."); + } + if (!missingFileOk) { + logger.log(Level.SEVERE, "Local configuration file {0} does not exist", localFile.getAbsolutePath()); + throw new SharedConfigurationException("Local configuration file " + localFile.getAbsolutePath() + " does not exist"); + } else { + logger.log(Level.INFO, "Local configuration file {0} does not exist", localFile.getAbsolutePath()); + return; + } + } + + try { + FileUtils.copyFileToDirectory(localFile, remoteFolder); + } catch (IOException ex) { + throw new SharedConfigurationException(String.format("Failed to copy %s to %s", localFile.getAbsolutePath(), remoteFolder.getAbsolutePath()), ex); + } + } + + /** + * Copy a shared settings file to the local settings folder. + * + * @param fileName Name of the file to copy + * @param localFolder Local settings folder + * @param remoteFolder Shared settings folder + * @param missingFileOk True if it's not an error if the source file is not + * found + * + * @throws SharedConfigurationException + */ + private static void copyToLocalFolder(String fileName, String localFolder, File remoteFolder, boolean missingFileOk) throws SharedConfigurationException { + logger.log(Level.INFO, "Downloading {0} from {1}", new Object[]{fileName, remoteFolder.getAbsolutePath()}); + + File remoteFile = new File(remoteFolder, fileName); + if (!remoteFile.exists()) { + Path deleteLocal = Paths.get(localFolder, fileName); + try { + if (deleteLocal.toFile().exists()) { + deleteLocal.toFile().delete(); + } + } catch (SecurityException ex) { + logger.log(Level.SEVERE, "Shared configuration {0} does not exist on remote node, but unable to remove local copy", fileName); + throw new SharedConfigurationException("Shared configuration file " + deleteLocal.toString() + " could not be deleted."); + } + if (!missingFileOk) { + logger.log(Level.SEVERE, "Shared configuration file {0} does not exist", remoteFile.getAbsolutePath()); + throw new SharedConfigurationException("Shared configuration file " + remoteFile.getAbsolutePath() + " does not exist"); + } else { + logger.log(Level.INFO, "Shared configuration file {0} does not exist", remoteFile.getAbsolutePath()); + return; + } + } + + File localSettingsFolder = new File(localFolder); + try { + FileUtils.copyFileToDirectory(remoteFile, localSettingsFolder); + } catch (IOException ex) { + throw new SharedConfigurationException(String.format("Failed to copy %s to %s", remoteFile.getAbsolutePath(), localSettingsFolder.getAbsolutePath()), ex); + } + } + + /** + * Upload the basic set of auto-ingest settings to the shared folder. + * + * @param remoteFolder Shared settings folder + * + * @throws Exception + */ + private void uploadAutoModeContextSettings(File remoteFolder) throws SharedConfigurationException { + logger.log(Level.INFO, "Uploading shared configuration to {0}", remoteFolder.getAbsolutePath()); + publishTask("Uploading AutoModeContext configuration files"); + + // Make a subfolder + File remoteAutoConfFolder = new File(remoteFolder, AUTO_MODE_FOLDER); + try { + if (remoteAutoConfFolder.exists()) { + FileUtils.deleteDirectory(remoteAutoConfFolder); + } + Files.createDirectories(remoteAutoConfFolder.toPath()); + } catch (IOException | SecurityException ex) { + logger.log(Level.SEVERE, "Failed to create clean shared configuration subfolder " + remoteAutoConfFolder.getAbsolutePath(), ex); //NON-NLS + throw new SharedConfigurationException("Failed to create clean shared configuration subfolder " + remoteAutoConfFolder.getAbsolutePath()); + } + + IngestJobSettings ingestJobSettings = new IngestJobSettings(AutoIngestUserPreferences.getAutoModeIngestModuleContextString()); + File localFolder = ingestJobSettings.getSavedModuleSettingsFolder().toFile(); + + if (!localFolder.exists()) { + logger.log(Level.SEVERE, "Local configuration folder {0} does not exist", localFolder.getAbsolutePath()); + throw new SharedConfigurationException("Local configuration folder " + localFolder.getAbsolutePath() + " does not exist"); + } + + try { + FileUtils.copyDirectory(localFolder, remoteAutoConfFolder); + } catch (IOException ex) { + throw new SharedConfigurationException(String.format("Failed to copy %s to %s", localFolder.getAbsolutePath(), remoteAutoConfFolder.getAbsolutePath()), ex); + } + } + + /** + * Download the basic set of auto-ingest settings from the shared folder + * + * @param remoteFolder Shared settings folder + * + * @throws SharedConfigurationException + */ + private void downloadAutoModeContextSettings(File remoteFolder) throws SharedConfigurationException { + logger.log(Level.INFO, "Downloading shared configuration from {0}", remoteFolder.getAbsolutePath()); + publishTask("Downloading AutoModeContext configuration files"); + + // Check that the remote subfolder exists + File remoteAutoConfFolder = new File(remoteFolder, AUTO_MODE_FOLDER); + if (!remoteAutoConfFolder.exists()) { + logger.log(Level.SEVERE, "Shared configuration folder {0} does not exist", remoteAutoConfFolder.getAbsolutePath()); + throw new SharedConfigurationException("Shared configuration folder " + remoteAutoConfFolder.getAbsolutePath() + " does not exist"); + } + + // Get/create the local subfolder + IngestJobSettings ingestJobSettings = new IngestJobSettings(AutoIngestUserPreferences.getAutoModeIngestModuleContextString()); + File localFolder = ingestJobSettings.getSavedModuleSettingsFolder().toFile(); + + try { + if (localFolder.exists()) { + FileUtils.deleteDirectory(localFolder); + } + Files.createDirectories(localFolder.toPath()); + } catch (IOException | SecurityException ex) { + logger.log(Level.SEVERE, "Failed to create clean local configuration folder " + localFolder.getAbsolutePath(), ex); //NON-NLS + throw new SharedConfigurationException("Failed to create clean local configuration folder " + localFolder.getAbsolutePath()); + } + + try { + FileUtils.copyDirectory(remoteAutoConfFolder, localFolder); + } catch (IOException ex) { + throw new SharedConfigurationException(String.format("Failed to copy %s to %s", remoteFolder.getAbsolutePath(), localFolder.getAbsolutePath()), ex); + } + } + + /** + * Upload settings file containing enabled ingest modules. + * + * @param remoteFolder Shared settings folder + * + * @throws SharedConfigurationException + */ + private void uploadEnabledModulesSettings(File remoteFolder) throws SharedConfigurationException { + publishTask("Uploading enabled module configuration"); + copyToRemoteFolder(AUTO_MODE_CONTEXT_FILE, moduleDirPath, remoteFolder, false); + } + + /** + * Download settings file containing enabled ingest modules. + * + * @param remoteFolder Shared settings folder + * + * @throws SharedConfigurationException + */ + private void downloadEnabledModuleSettings(File remoteFolder) throws SharedConfigurationException { + publishTask("Downloading enabled module configuration"); + copyToLocalFolder(AUTO_MODE_CONTEXT_FILE, moduleDirPath, remoteFolder, false); + } + + /** + * Upload settings file containing file type settings. + * + * @param remoteFolder Shared settings folder + * + * @throws SharedConfigurationException + */ + private void uploadFileTypeSettings(File remoteFolder) throws SharedConfigurationException { + publishTask("Uploading FileType module configuration"); + copyToRemoteFolder(USER_DEFINED_TYPE_DEFINITIONS_FILE, moduleDirPath, remoteFolder, true); + copyToRemoteFolder(USER_DEFINED_TYPE_DEFINITIONS_FILE_LEGACY, moduleDirPath, remoteFolder, true); + } + + /** + * Download settings file containing file type settings. + * + * @param remoteFolder Shared settings folder + * + * @throws SharedConfigurationException + */ + private void downloadFileTypeSettings(File remoteFolder) throws SharedConfigurationException { + publishTask("Downloading FileType module configuration"); + copyToLocalFolder(USER_DEFINED_TYPE_DEFINITIONS_FILE, moduleDirPath, remoteFolder, true); + copyToLocalFolder(USER_DEFINED_TYPE_DEFINITIONS_FILE_LEGACY, moduleDirPath, remoteFolder, true); + } + + /** + * Upload settings for the interesting files module. + * + * @param remoteFolder Shared settings folder + * + * @throws SharedConfigurationException + */ + private void uploadInterestingFilesSettings(File remoteFolder) throws SharedConfigurationException { + publishTask("Uploading InterestingFiles module configuration"); + copyToRemoteFolder(INTERESTING_FILES_SET_DEFS_FILE_LEGACY, moduleDirPath, remoteFolder, true); + copyToRemoteFolder(INTERESTING_FILES_SET_DEFS_FILE, moduleDirPath, remoteFolder, true); + } + + /** + * Download settings for the interesting files module. + * + * @param remoteFolder Shared settings folder + * + * @throws SharedConfigurationException + */ + private void downloadInterestingFilesSettings(File remoteFolder) throws SharedConfigurationException { + publishTask("Downloading InterestingFiles module configuration"); + copyToLocalFolder(INTERESTING_FILES_SET_DEFS_FILE_LEGACY, moduleDirPath, remoteFolder, true); + copyToLocalFolder(INTERESTING_FILES_SET_DEFS_FILE, moduleDirPath, remoteFolder, true); + } + + /** + * Upload settings for the keyword search module. + * + * @param remoteFolder Shared settings folder + * + * @throws SharedConfigurationException + */ + private void uploadKeywordSearchSettings(File remoteFolder) throws SharedConfigurationException { + publishTask("Uploading KeywordSearch module configuration"); + copyToRemoteFolder(KEYWORD_SEARCH_SETTINGS, moduleDirPath, remoteFolder, true); + copyToRemoteFolder(KEYWORD_SEARCH_SETTINGS_LEGACY, moduleDirPath, remoteFolder, true); + copyToRemoteFolder(KEYWORD_SEARCH_GENERAL_LEGACY, moduleDirPath, remoteFolder, true); + copyToRemoteFolder(KEYWORD_SEARCH_NSRL_LEGACY, moduleDirPath, remoteFolder, true); + copyToRemoteFolder(KEYWORD_SEARCH_OPTIONS_LEGACY, moduleDirPath, remoteFolder, true); + copyToRemoteFolder(KEYWORD_SEARCH_SCRIPTS_LEGACY, moduleDirPath, remoteFolder, true); + } + + /** + * Download settings for the keyword search module. + * + * @param remoteFolder Shared settings folder + * + * @throws SharedConfigurationException + */ + private void downloadKeywordSearchSettings(File remoteFolder) throws SharedConfigurationException { + publishTask("Downloading KeywordSearch module configuration"); + copyToLocalFolder(KEYWORD_SEARCH_SETTINGS, moduleDirPath, remoteFolder, true); + copyToLocalFolder(KEYWORD_SEARCH_SETTINGS_LEGACY, moduleDirPath, remoteFolder, true); + copyToLocalFolder(KEYWORD_SEARCH_GENERAL_LEGACY, moduleDirPath, remoteFolder, true); + copyToLocalFolder(KEYWORD_SEARCH_NSRL_LEGACY, moduleDirPath, remoteFolder, true); + copyToLocalFolder(KEYWORD_SEARCH_OPTIONS_LEGACY, moduleDirPath, remoteFolder, true); + copyToLocalFolder(KEYWORD_SEARCH_SCRIPTS_LEGACY, moduleDirPath, remoteFolder, true); + KeywordListsManager.reloadKeywordLists(); + } + + /** + * Upload settings for the file extension mismatch module. + * + * @param remoteFolder Shared settings folder + * + * @throws SharedConfigurationException + */ + private void uploadFileExtMismatchSettings(File remoteFolder) throws SharedConfigurationException { + publishTask("Uploading File Extension Mismatch module configuration"); + copyToRemoteFolder(FILE_EXT_MISMATCH_SETTINGS, moduleDirPath, remoteFolder, true); + copyToRemoteFolder(FILE_EXT_MISMATCH_SETTINGS_LEGACY, moduleDirPath, remoteFolder, false); + } + + /** + * Download settings for the file extension mismatch module. + * + * @param remoteFolder Shared settings folder + * + * @throws SharedConfigurationException + */ + private void downloadFileExtMismatchSettings(File remoteFolder) throws SharedConfigurationException { + publishTask("Downloading File Extension Mismatch module configuration"); + copyToLocalFolder(FILE_EXT_MISMATCH_SETTINGS, moduleDirPath, remoteFolder, true); + copyToLocalFolder(FILE_EXT_MISMATCH_SETTINGS_LEGACY, moduleDirPath, remoteFolder, false); + } + + /** + * Upload settings for the android triage module. + * + * @param remoteFolder Shared settings folder + * + * @throws SharedConfigurationException + */ + private void uploadAndroidTriageSettings(File remoteFolder) throws SharedConfigurationException { + publishTask("Uploading Android Triage module configuration"); + copyToRemoteFolder(ANDROID_TRIAGE, moduleDirPath, remoteFolder, true); + } + + /** + * Download settings for the android triage module. + * + * @param remoteFolder Shared settings folder + * + * @throws SharedConfigurationException + */ + private void downloadAndroidTriageSettings(File remoteFolder) throws SharedConfigurationException { + publishTask("Downloading Android Triage module configuration"); + copyToLocalFolder(ANDROID_TRIAGE, moduleDirPath, remoteFolder, true); + } + + /** + * Upload File Exporter settings. + * + * @param remoteFolder Shared settings folder + * + * @throws SharedConfigurationException + */ + private void uploadFileExporterSettings(File remoteFolder) throws SharedConfigurationException { + publishTask("Uploading File Exporter configuration"); + File fileExporterFolder = new File(moduleDirPath, FILE_EXPORTER_FOLDER); + copyToRemoteFolder(FILE_EXPORTER_SETTINGS_FILE, fileExporterFolder.getAbsolutePath(), remoteFolder, true); + } + + /** + * Download File Exporter settings. + * + * @param remoteFolder Shared settings folder + * + * @throws SharedConfigurationException + */ + private void downloadFileExporterSettings(File remoteFolder) throws SharedConfigurationException { + publishTask("Downloading File Exporter configuration"); + File fileExporterFolder = new File(moduleDirPath, FILE_EXPORTER_FOLDER); + copyToLocalFolder(FILE_EXPORTER_SETTINGS_FILE, fileExporterFolder.getAbsolutePath(), remoteFolder, true); + } + + /** + * Upload multi-user settings and other general Autopsy settings + * + * @param remoteFolder Shared settings folder + * + * @throws SharedConfigurationException + */ + private void uploadMultiUserAndGeneralSettings(File remoteFolder) throws SharedConfigurationException { + publishTask("Uploading multi user configuration"); + File generalSettingsFolder = Paths.get(moduleDirPath, PREFERENCES_FOLDER, "org", "sleuthkit", "autopsy").toFile(); + copyToRemoteFolder(GENERAL_PROPERTIES, generalSettingsFolder.getAbsolutePath(), remoteFolder, false); + copyToRemoteFolder(AUTO_INGEST_PROPERTIES, moduleDirPath, remoteFolder, false); + } + + /** + * Download multi-user settings and other general Autopsy settings + * + * @param remoteFolder Shared settings folder + * + * @throws SharedConfigurationException + */ + private void downloadMultiUserAndGeneralSettings(File remoteFolder) throws SharedConfigurationException { + publishTask("Downloading multi user configuration"); + File generalSettingsFolder = Paths.get(moduleDirPath, PREFERENCES_FOLDER, "org", "sleuthkit", "autopsy").toFile(); + copyToLocalFolder(GENERAL_PROPERTIES, generalSettingsFolder.getAbsolutePath(), remoteFolder, false); + copyToLocalFolder(AUTO_INGEST_PROPERTIES, moduleDirPath, remoteFolder, false); + } + + /** + * Upload settings and hash databases to the shared folder. The general + * algorithm is: - Copy the general settings in hashsets.xml - For each hash + * database listed in hashsets.xml: - Calculate the CRC of the database - If + * the CRC does not match the one listed for that database in the shared + * folder, (or if no entry exists), copy the database - Store the CRCs for + * each database in the shared folder and locally + * + * @param remoteFolder Shared settings folder + * + * @throws SharedConfigurationException + */ + private void uploadHashDbSettings(File remoteFolder) throws SharedConfigurationException { + publishTask("Uploading HashDb module configuration"); + + // Keep track of everything being uploaded + File localVersionFile = new File(moduleDirPath, SHARED_CONFIG_VERSIONS); + File sharedVersionFile = new File(remoteFolder, SHARED_CONFIG_VERSIONS); + Map newVersions = new HashMap<>(); + Map sharedVersions = readVersionsFromFile(sharedVersionFile); + + // Copy the settings file + copyToRemoteFolder(HASHDB_CONFIG_FILE_NAME, moduleDirPath, remoteFolder, true); + copyToRemoteFolder(HASHDB_CONFIG_FILE_NAME_LEGACY, moduleDirPath, remoteFolder, true); + + // Get the list of databases from the file + List databases = getHashFileNamesFromSettingsFile(); + for (String fullPathToDbFile : databases) { + + // Compare the CRC of the local copy with what is stored in the shared folder + publishTask("Deciding whether to upload " + fullPathToDbFile); + String crc = calculateCRC(fullPathToDbFile); + + // Determine full path to db file in remote folder + String sharedName = convertLocalDbPathToShared(fullPathToDbFile); + File sharedDbBaseFolder = new File(remoteFolder, REMOTE_HASH_FOLDER); + File sharedDb = new File(sharedDbBaseFolder, sharedName); + + if (!(sharedVersions.containsKey(fullPathToDbFile) + && sharedVersions.get(fullPathToDbFile).equals(crc) + && sharedDb.exists())) { + + publishTask("Uploading " + fullPathToDbFile); + File sharedDbPath = sharedDb.getParentFile(); + + if (!sharedDbPath.exists()) { + if (!sharedDbPath.mkdirs()) { + throw new SharedConfigurationException("Error creating shared hash database directory " + sharedDbPath.getAbsolutePath()); + } + } + + File dbFile = new File(fullPathToDbFile); + // copy hash db file to the remote folder + copyFile(sharedDbPath, dbFile); + + // check whether the hash db has an index file (.idx) that should also be copied. + // NOTE: only text hash databases (.txt, .hash, .Hash) can have index file. + // it is possible that the hash db file itself is the index file + String fullPathToIndexFile = ""; + if (fullPathToDbFile.endsWith(".txt") || fullPathToDbFile.endsWith(".hash") || fullPathToDbFile.endsWith(".Hash")) { + // check whether index file for this text database is present + // For example, if text db name is "hash_db.txt" then index file name will be "hash_db.txt-md5.idx" + fullPathToIndexFile = fullPathToDbFile + "-md5.idx"; + + // if index file exists, copy it to the remote location + File dbIndexFile = new File(fullPathToIndexFile); + if (dbIndexFile.exists()) { + // copy index file to the remote folder + copyFile(sharedDbPath, dbIndexFile); + } else { + fullPathToIndexFile = ""; + } + } else if (fullPathToDbFile.endsWith(".idx")) { + // hash db file itself is the index file and it has already been copied to the remote location + fullPathToIndexFile = fullPathToDbFile; + } + + // check whether "index of the index" file exists for this hash DB's index file. + // NOTE: "index of the index" file may only exist + // for text hash database index files (i.e ".idx" extension). The index of the + // index file will always have the same name as the index file, + // distinguished only by the "2" in the extension. "index of the index" file + // is optional and may not be present. + if (fullPathToIndexFile.endsWith(".idx")) { + String fullPathToIndexOfIndexFile = fullPathToIndexFile + "2"; // "index of the index" file has same file name and extension ".idx2" + File dbIndexOfIndexFile = new File(fullPathToIndexOfIndexFile); + if (dbIndexOfIndexFile.exists()) { + // copy index of the index file to the remote folder + copyFile(sharedDbPath, dbIndexOfIndexFile); + } + } + } + + newVersions.put(fullPathToDbFile, crc); + } + + // Write the versions of all uploaded files to a file (make local and shared copies) + writeVersionsToFile(localVersionFile, newVersions); + writeVersionsToFile(sharedVersionFile, newVersions); + } + + /** + * Utility method to copy a file + * + * @param sharedDbPath File object of the folder to copy to + * @param dbFile File object of the file to copy + * + * @throws + * org.sleuthkit.autopsy.configuration.SharedConfiguration.SharedConfigurationException + */ + private void copyFile(File sharedDbPath, File dbFile) throws SharedConfigurationException { + try { + Path path = Paths.get(sharedDbPath.toString(), dbFile.getName()); + if (path.toFile().exists()) { + path.toFile().delete(); + } + FileUtils.copyFileToDirectory(dbFile, sharedDbPath); + } catch (IOException | SecurityException ex) { + throw new SharedConfigurationException(String.format("Failed to copy %s to %s", dbFile.getAbsolutePath(), sharedDbPath.getAbsolutePath()), ex); + } + } + + /** + * Upload settings and hash databases to the shared folder. The general + * algorithm is: - Copy the general settings in hashsets.xml - For each hash + * database listed in hashsets.xml: - Compare the recorded CRC in the shared + * directory with the one in the local directory - If different, download + * the database - Update the local list of database CRCs Note that databases + * are downloaded to the exact path they were uploaded from. + * + * @param remoteFolder Shared settings folder + * + * @throws SharedConfigurationException + */ + private void downloadHashDbSettings(File remoteFolder) throws SharedConfigurationException { + publishTask("Downloading HashDb module configuration"); + + // Read in the current local and shared database versions + File localVersionFile = new File(moduleDirPath, SHARED_CONFIG_VERSIONS); + File remoteVersionFile = new File(remoteFolder, SHARED_CONFIG_VERSIONS); + Map localVersions = readVersionsFromFile(localVersionFile); + Map remoteVersions = readVersionsFromFile(remoteVersionFile); + + /* + Iterate through remote list + If local needs it, download + + Download remote settings files to local + Download remote versions file to local + HashDbManager reload + */ + File localDb = new File(""); + File sharedDb = new File(""); + try { + for (String path : remoteVersions.keySet()) { + localDb = new File(path); + if ((!localVersions.containsKey(path)) + || (!localVersions.get(path).equals(remoteVersions.get(path))) + || !localDb.exists()) { + // Need to download a new copy if + // - We have no entry for the database in the local versions file + // - The CRC in the local versions file does not match the one in the shared file + // - Local copy of the database does not exist + + if (localDb.exists()) { + String crc = calculateCRC(path); + if (crc.equals(remoteVersions.get(path))) { + // Can skip the download if the local disk has it + // but it's just not in the versions file. This will + // be populated just before refreshing HashDbManager. + continue; + } + } + + publishTask("Downloading " + path); + String sharedName = convertLocalDbPathToShared(path); + File sharedDbBaseFolder = new File(remoteFolder, REMOTE_HASH_FOLDER); + sharedDb = new File(sharedDbBaseFolder, sharedName); + + if (!localDb.getParentFile().exists()) { + if (!localDb.getParentFile().mkdirs()) { + throw new SharedConfigurationException("Error creating hash database directory " + localDb.getParentFile().getAbsolutePath()); + } + } + + // If a copy of the database is loaded, close it before deleting and copying. + if (localDb.exists()) { + List hashDbs = HashDbManager.getInstance().getAllHashSets(); + HashDbManager.HashDb matchingDb = null; + for (HashDbManager.HashDb db : hashDbs) { + try { + if (localDb.getAbsolutePath().equals(db.getDatabasePath()) || localDb.getAbsolutePath().equals(db.getIndexPath())) { + matchingDb = db; + break; + } + } catch (TskCoreException ex) { + throw new SharedConfigurationException(String.format("Error getting hash database path info for %s", localDb.getParentFile().getAbsolutePath()), ex); + } + } + + if (matchingDb != null) { + try { + HashDbManager.getInstance().removeHashDatabase(matchingDb); + } catch (HashDbManager.HashDbManagerException ex) { + throw new SharedConfigurationException(String.format("Error updating hash database info for %s", localDb.getAbsolutePath()), ex); + } + + } + if (localDb.exists()) { + localDb.delete(); + } + } + FileUtils.copyFile(sharedDb, localDb); + + // check whether the hash db has an index file (.idx) that should also be copied. + // NOTE: only text hash databases (.txt, .hash, .Hash) can have index file. + // it is possible that the hash db file itself is the index file + String fullPathToRemoteDbFile = sharedDb.getPath(); + String fullPathToRemoteIndexFile = ""; + String fullPathToLocalIndexFile = ""; + if (fullPathToRemoteDbFile.endsWith(".txt") || fullPathToRemoteDbFile.toLowerCase().endsWith(".hash")) { + // check whether index file for this text database is present + // For example, if text db name is "hash_db.txt" then index file name will be "hash_db.txt-md5.idx" + fullPathToRemoteIndexFile = fullPathToRemoteDbFile + "-md5.idx"; + + // if index file exists, copy it to the remote location + File remoteDbIndexFile = new File(fullPathToRemoteIndexFile); + if (remoteDbIndexFile.exists()) { + // delete local copy of "index of the index" file if one exists + fullPathToLocalIndexFile = localDb.getPath() + "-md5.idx"; + File localIndexFile = new File(fullPathToLocalIndexFile); + if (localIndexFile.exists()) { + localIndexFile.delete(); + } + // copy index file to the remote folder + FileUtils.copyFile(remoteDbIndexFile, localIndexFile); + } else { + // index file doesn't exist at remote location + fullPathToRemoteIndexFile = ""; + } + } else if (fullPathToRemoteDbFile.endsWith(".idx")) { + // hash db file itself is the index file and it has already been copied to the remote location + fullPathToRemoteIndexFile = fullPathToRemoteDbFile; + fullPathToLocalIndexFile = localDb.getPath(); + } + + // check whether "index of the index" file exists for this hash DB index file. + // NOTE: "index of the index" file may only exist for hash database index files (.idx files). + // For example, hash_db.txt-md5.idx index file will have hash_db.txt-md5.idx2 "index of the index" file. + // "index of the index" file is optional and may not be present. + if (fullPathToRemoteIndexFile.endsWith(".idx")) { + // check if "index of the index" file exists in remote shared config folder + String fullPathToRemoteIndexOfIndexFile = fullPathToRemoteIndexFile + "2"; // "index of the index" file has same file name with extension ".idx2" + File remoteIndexOfIndexFile = new File(fullPathToRemoteIndexOfIndexFile); + if (remoteIndexOfIndexFile.exists()) { + + // delete local copy of "index of the index" file if one exists + String fullPathToLocalIndexOfIndexFile = fullPathToLocalIndexFile + "2"; // "index of the index" file has same file name with extension ".idx2" + File localIndexOfIndexFile = new File(fullPathToLocalIndexOfIndexFile); + if (localIndexOfIndexFile.exists()) { + localIndexOfIndexFile.delete(); + } + // copy index of the index file to the local folder + FileUtils.copyFile(remoteIndexOfIndexFile, localIndexOfIndexFile); + } + } + } + } + } catch (IOException | SecurityException ex) { + throw new SharedConfigurationException(String.format("Failed to copy %s to %s", sharedDb.getAbsolutePath(), localDb.getAbsolutePath()), ex); + } + + // Copy the settings filey + copyToLocalFolder(HASHDB_CONFIG_FILE_NAME, moduleDirPath, remoteFolder, true); + copyToLocalFolder(HASHDB_CONFIG_FILE_NAME_LEGACY, moduleDirPath, remoteFolder, true); + copyToLocalFolder(SHARED_CONFIG_VERSIONS, moduleDirPath, remoteFolder, true); + + // Refresh HashDbManager with the new settings + HashDbManager.getInstance().loadLastSavedConfiguration(); + } + + /** + * Read in the hashsets settings to pull out the names of the databases. + * + * @return List of all hash databases + * + * @throws SharedConfigurationException + */ + private static List getHashFileNamesFromSettingsFile() throws SharedConfigurationException { + List results = new ArrayList<>(); + try { + HashDbManager hashDbManager = HashDbManager.getInstance(); + hashDbManager.loadLastSavedConfiguration(); + for (HashDb hashDb : hashDbManager.getAllHashSets()) { + if (hashDb.hasIndexOnly()) { + results.add(hashDb.getIndexPath()); + } else { + results.add(hashDb.getDatabasePath()); + } + } + } catch (TskCoreException ex) { + throw new SharedConfigurationException("Unable to read hash databases", ex); + } + return results; + } + + /** + * Change the database path into a form that can be used to create + * subfolders in the shared folder. + * + * @param localName Database name from the XML file + * + * @return Path with the initial colon removed + */ + private static String convertLocalDbPathToShared(String localName) { + // Replace the colon + String sharedName = localName.replace(":", "__colon__"); + return sharedName; + } + + /** + * Write the list of database paths and versions to a file. + * + * @param versionFile File to write to + * @param versions Map of database name -> version (current using CRCs as + * versions) + * + * @throws SharedConfigurationException + */ + private static void writeVersionsToFile(File versionFile, Map versions) throws SharedConfigurationException { + try (PrintWriter writer = new PrintWriter(versionFile.getAbsoluteFile(), "UTF-8")) { + for (String filename : versions.keySet()) { + writer.println(versions.get(filename) + " " + filename); + } + } catch (FileNotFoundException | UnsupportedEncodingException ex) { + throw new SharedConfigurationException(String.format("Failed to write version info to %s", versionFile), ex); + } + } + + /** + * Read the map of database paths to versions from a file. + * + * @param versionFile File containing the version information + * + * @return Map of database name -> version + * + * @throws SharedConfigurationException + */ + private static Map readVersionsFromFile(File versionFile) throws SharedConfigurationException { + Map versions = new HashMap<>(); + + // If the file does not exist, return an empty map + if (!versionFile.exists()) { + return versions; + } + + // Read in and store each pair + try (BufferedReader reader = new BufferedReader(new FileReader(versionFile))) { + String currentLine = reader.readLine(); + while (null != currentLine) { + if (!currentLine.isEmpty()) { + int index = currentLine.indexOf(' '); // Find the first space + String crc = currentLine.substring(0, index); + String path = currentLine.substring(index + 1); + versions.put(path, crc); + } + currentLine = reader.readLine(); + } + } catch (FileNotFoundException ex) { + throw new SharedConfigurationException(String.format("Failed to find version file %s", versionFile), ex); + } catch (IOException ex) { + throw new SharedConfigurationException(String.format("Failed to read version info from %s", versionFile), ex); + } + + return versions; + } + + /** + * Calculate the CRC of a file to use to determine if it has changed. + * + * @param filePath File to get the CRC for + * + * @return String containing the CRC + * + * @throws SharedConfigurationException + */ + private static String calculateCRC(String filePath) throws SharedConfigurationException { + File file = new File(filePath); + try { + FileInputStream fileStream = new FileInputStream(file); + CRC32 crc = new CRC32(); + byte[] buffer = new byte[65536]; + int bytesRead = fileStream.read(buffer); + while (-1 != bytesRead) { + crc.update(buffer, 0, bytesRead); + bytesRead = fileStream.read(buffer); + } + return String.valueOf(crc.getValue()); + } catch (IOException ex) { + throw new SharedConfigurationException(String.format("Failed to calculate CRC for %s", file.getAbsolutePath()), ex); + } + } +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/StartupWindow.java b/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/StartupWindow.java new file mode 100644 index 0000000000..fbffcf7ece --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/StartupWindow.java @@ -0,0 +1,140 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2011 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.configuration; + +import java.awt.Cursor; +import java.awt.Dimension; +import java.awt.Toolkit; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import javax.swing.JDialog; +import javax.swing.WindowConstants; +import org.openide.util.ImageUtilities; +import org.openide.util.NbBundle; +import org.openide.util.lookup.ServiceProvider; +import org.openide.windows.WindowManager; +import org.sleuthkit.autopsy.casemodule.CueBannerPanel; +import org.sleuthkit.autopsy.casemodule.StartupWindowInterface; +import org.sleuthkit.autopsy.coreutils.NetworkUtils; +import org.sleuthkit.autopsy.experimental.autoingest.AutoIngestDashboard; +import org.sleuthkit.autopsy.experimental.autoingest.ReviewModeCasePanel; + +/** + * The default implementation of the Autopsy startup window + */ +@ServiceProvider(service = StartupWindowInterface.class) +public final class StartupWindow extends JDialog implements StartupWindowInterface { + + private static final String TITLE = NbBundle.getMessage(StartupWindow.class, "StartupWindow.title.text"); + private static Dimension DIMENSIONS = new Dimension(750, 400); + private static CueBannerPanel welcomeWindow; + private static final long serialVersionUID = 1L; + private ReviewModeCasePanel caseManagementPanel = null; + private static final String LOCAL_HOST_NAME = NetworkUtils.getLocalHostName(); + + public StartupWindow() { + super(WindowManager.getDefault().getMainWindow(), TITLE, false); + init(); + } + + /** + * Shows the startup window. + */ + private void init() { + Dimension screenDimension = Toolkit.getDefaultToolkit().getScreenSize(); + + // set the popUp window / JFrame + setSize(DIMENSIONS); + int w = getSize().width; + int h = getSize().height; + + // set the location of the popUp Window on the center of the screen + setLocation((screenDimension.width - w) / 2, (screenDimension.height - h) / 2); + setLocationRelativeTo(WindowManager.getDefault().getMainWindow()); + + addPanelForMode(); + pack(); + setResizable(false); + } + + @Override + public void open() { + + if (caseManagementPanel != null) { + caseManagementPanel.updateView(); + caseManagementPanel.setCursor(Cursor.getDefaultCursor()); + } + + if (welcomeWindow != null) { + welcomeWindow.refresh(); + } + this.setLocationRelativeTo(WindowManager.getDefault().getMainWindow()); + setVisible(true); + } + + /** + * Closes the startup window. + */ + @Override + public void close() { + this.setVisible(false); + } + + /** + * Adds a panel to the dialog based on operational mode selected by the + * user. + */ + private void addPanelForMode() { + AutoIngestUserPreferences.SelectedMode mode = AutoIngestUserPreferences.getMode(); + + switch (mode) { + case AUTOMATED: + this.setTitle(NbBundle.getMessage(StartupWindow.class, "StartupWindow.AutoIngestMode") + " (" + LOCAL_HOST_NAME + ")"); + setIconImage(ImageUtilities.loadImage("org/sleuthkit/autopsy/experimental/images/frame.gif", false)); //NON-NLS + this.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + AutoIngestDashboard.getInstance().shutdown(); + } + }); + setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); + add(AutoIngestDashboard.getInstance()); + break; + case REVIEW: + this.setTitle(NbBundle.getMessage(StartupWindow.class, "StartupWindow.ReviewMode") + " (" + LOCAL_HOST_NAME + ")"); + caseManagementPanel = new ReviewModeCasePanel(this); + setIconImage(ImageUtilities.loadImage("org/sleuthkit/autopsy/experimental/images/frame.gif", false)); //NON-NLS + add(caseManagementPanel); + break; + default: + welcomeWindow = new CueBannerPanel(); + // add the command to close the window to the button on the Volume Detail Panel + welcomeWindow.setCloseButtonActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + close(); + } + }); + add(welcomeWindow); + break; + } + } +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/coordinationservice/CoordinationService.java b/Experimental/src/org/sleuthkit/autopsy/experimental/coordinationservice/CoordinationService.java new file mode 100644 index 0000000000..8b0b293165 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/coordinationservice/CoordinationService.java @@ -0,0 +1,418 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2015 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.coordinationservice; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import org.apache.zookeeper.CreateMode; +import org.apache.zookeeper.ZooDefs; +import org.apache.curator.RetryPolicy; +import org.apache.curator.retry.ExponentialBackoffRetry; +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.CuratorFrameworkFactory; +import org.apache.curator.framework.recipes.locks.InterProcessMutex; +import org.apache.curator.framework.recipes.locks.InterProcessReadWriteLock; +import org.apache.zookeeper.KeeperException; +import org.sleuthkit.autopsy.core.UserPreferences; +import java.io.IOException; +import org.apache.zookeeper.WatchedEvent; +import org.apache.zookeeper.ZooKeeper; +import org.apache.zookeeper.KeeperException.NoNodeException; + +/** + * A centralized service for maintaining configuration information and providing + * distributed synchronization using a shared hierarchical namespace of nodes. + */ +public final class CoordinationService { + + /** + * Category nodes are the immediate children of the root node of a shared + * hierarchical namespace managed by the coordination service. + */ + public enum CategoryNode { // RJCTODO: Move this to CoordinationServiceNamespace + + CASES("cases"), + MANIFESTS("manifests"), + CONFIG("config"); + + private final String displayName; + + private CategoryNode(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } + } + + /** + * Exception type thrown by the coordination service. + */ + public final static class CoordinationServiceException extends Exception { + + private static final long serialVersionUID = 1L; + + private CoordinationServiceException(String message) { + super(message); + } + + private CoordinationServiceException(String message, Throwable cause) { + super(message, cause); + } + } + + /** + * An opaque encapsulation of a lock for use in distributed synchronization. + * Instances are obtained by calling a get lock method and must be passed to + * a release lock method. + */ + public static class Lock implements AutoCloseable { + + /** + * This implementation uses the Curator read/write lock. see + * http://curator.apache.org/curator-recipes/shared-reentrant-read-write-lock.html + */ + private final InterProcessMutex interProcessLock; + private final String nodePath; + + private Lock(String nodePath, InterProcessMutex lock) { + this.nodePath = nodePath; + this.interProcessLock = lock; + } + + public String getNodePath() { + return nodePath; + } + + public void release() throws CoordinationServiceException { + try { + this.interProcessLock.release(); + } catch (Exception ex) { + throw new CoordinationServiceException(String.format("Failed to release the lock on %s", nodePath), ex); + } + } + + @Override + public void close() throws CoordinationServiceException { + release(); + } + } + + private static CuratorFramework curator = null; + private static final Map rootNodesToServices = new HashMap<>(); + private final Map categoryNodeToPath = new HashMap<>(); + private static final int SESSION_TIMEOUT_MILLISECONDS = 300000; + private static final int CONNECTION_TIMEOUT_MILLISECONDS = 300000; + private static final int ZOOKEEPER_SESSION_TIMEOUT_MILLIS = 3000; + private static final int ZOOKEEPER_CONNECTION_TIMEOUT_MILLIS = 15000; + private static final int PORT_OFFSET = 1000; + + /** + * Gets an instance of the centralized coordination service for a specific + * namespace. + * + * @param rootNode The name of the root node that defines the namespace. + * + * @return The service for the namespace defined by the root node name. + * + * @throws CoordinationServiceException If an instaNce of the coordination + * service cannot be created. + */ + public static synchronized CoordinationService getInstance(String rootNode) throws CoordinationServiceException { + if (null == curator) { + RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); + // When run in Solr, ZooKeeper defaults to Solr port + 1000 + int zooKeeperServerPort = Integer.valueOf(UserPreferences.getIndexingServerPort()) + PORT_OFFSET; + String connectString = UserPreferences.getIndexingServerHost() + ":" + zooKeeperServerPort; + curator = CuratorFrameworkFactory.newClient(connectString, SESSION_TIMEOUT_MILLISECONDS, CONNECTION_TIMEOUT_MILLISECONDS, retryPolicy); + curator.start(); + } + + /* + * Get or create a coordination service for the namespace defined by the + * specified root node. + */ + if (rootNodesToServices.containsKey(rootNode)) { + return rootNodesToServices.get(rootNode); + } else { + CoordinationService service; + try { + service = new CoordinationService(rootNode); + } catch (Exception ex) { + curator = null; + throw new CoordinationServiceException("Failed to create coordination service", ex); + } + rootNodesToServices.put(rootNode, service); + return service; + } + } + + /** + * Constructs an instance of the centralized coordination service for a + * specific namespace. + * + * @param rootNodeName The name of the root node that defines the namespace. + */ + private CoordinationService(String rootNodeName) throws Exception { + + if (false == isZooKeeperAccessible()) { + throw new Exception("Unable to access ZooKeeper"); + } + + String rootNode = rootNodeName; + if (!rootNode.startsWith("/")) { + rootNode = "/" + rootNode; + } + + for (CategoryNode node : CategoryNode.values()) { + String nodePath = rootNode + "/" + node.getDisplayName(); + try { + curator.create().creatingParentsIfNeeded().withMode(CreateMode.PERSISTENT).withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE).forPath(nodePath); + } catch (KeeperException ex) { + if (ex.code() != KeeperException.Code.NODEEXISTS) { + throw ex; + } + } + categoryNodeToPath.put(node.getDisplayName(), nodePath); + } + } + + /** + * Tries to get an exclusive lock on a node path appended to a category path + * in the namespace managed by this coordination service. Blocks until the + * lock is obtained or the time out expires. + * + * @param category The desired category in the namespace. + * @param nodePath The node path to use as the basis for the lock. + * @param timeOut Length of the time out. + * @param timeUnit Time unit for the time out. + * + * @return The lock, or null if lock acquisition timed out. + * + * @throws CoordinationServiceException If there is an error during lock + * acquisition. + * @throws InterruptedException If interrupted while blocked during + * lock acquisition. + */ + public Lock tryGetExclusiveLock(CategoryNode category, String nodePath, int timeOut, TimeUnit timeUnit) throws CoordinationServiceException, InterruptedException { + String fullNodePath = getFullyQualifiedNodePath(category, nodePath); + try { + InterProcessReadWriteLock lock = new InterProcessReadWriteLock(curator, fullNodePath); + if (lock.writeLock().acquire(timeOut, timeUnit)) { + return new Lock(nodePath, lock.writeLock()); + } else { + return null; + } + } catch (Exception ex) { + if (ex instanceof InterruptedException) { + throw (InterruptedException) ex; + } else { + throw new CoordinationServiceException(String.format("Failed to get exclusive lock for %s", fullNodePath), ex); + } + } + } + + /** + * Tries to get an exclusive lock on a node path appended to a category path + * in the namespace managed by this coordination service. Returns + * immediately if the lock can not be acquired. + * + * @param category The desired category in the namespace. + * @param nodePath The node path to use as the basis for the lock. + * + * @return The lock, or null if the lock could not be obtained. + * + * @throws CoordinationServiceException If there is an error during lock + * acquisition. + */ + public Lock tryGetExclusiveLock(CategoryNode category, String nodePath) throws CoordinationServiceException { + String fullNodePath = getFullyQualifiedNodePath(category, nodePath); + try { + InterProcessReadWriteLock lock = new InterProcessReadWriteLock(curator, fullNodePath); + if (!lock.writeLock().acquire(0, TimeUnit.SECONDS)) { + return null; + } + return new Lock(nodePath, lock.writeLock()); + } catch (Exception ex) { + throw new CoordinationServiceException(String.format("Failed to get exclusive lock for %s", fullNodePath), ex); + } + } + + /** + * Tries to get a shared lock on a node path appended to a category path in + * the namespace managed by this coordination service. Blocks until the lock + * is obtained or the time out expires. + * + * @param category The desired category in the namespace. + * @param nodePath The node path to use as the basis for the lock. + * @param timeOut Length of the time out. + * @param timeUnit Time unit for the time out. + * + * @return The lock, or null if lock acquisition timed out. + * + * @throws CoordinationServiceException If there is an error during lock + * acquisition. + * @throws InterruptedException If interrupted while blocked during + * lock acquisition. + */ + public Lock tryGetSharedLock(CategoryNode category, String nodePath, int timeOut, TimeUnit timeUnit) throws CoordinationServiceException, InterruptedException { + String fullNodePath = getFullyQualifiedNodePath(category, nodePath); + try { + InterProcessReadWriteLock lock = new InterProcessReadWriteLock(curator, fullNodePath); + if (lock.readLock().acquire(timeOut, timeUnit)) { + return new Lock(nodePath, lock.readLock()); + } else { + return null; + } + } catch (Exception ex) { + if (ex instanceof InterruptedException) { + throw (InterruptedException) ex; + } else { + throw new CoordinationServiceException(String.format("Failed to get shared lock for %s", fullNodePath), ex); + } + } + } + + /** + * Tries to get a shared lock on a node path appended to a category path in + * the namespace managed by this coordination service. Returns immediately + * if the lock can not be acquired. + * + * @param category The desired category in the namespace. + * @param nodePath The node path to use as the basis for the lock. + * + * @return The lock, or null if the lock could not be obtained. + * + * @throws CoordinationServiceException If there is an error during lock + * acquisition. + */ + public Lock tryGetSharedLock(CategoryNode category, String nodePath) throws CoordinationServiceException { + String fullNodePath = getFullyQualifiedNodePath(category, nodePath); + try { + InterProcessReadWriteLock lock = new InterProcessReadWriteLock(curator, fullNodePath); + if (!lock.readLock().acquire(0, TimeUnit.SECONDS)) { + return null; + } + return new Lock(nodePath, lock.readLock()); + } catch (Exception ex) { + throw new CoordinationServiceException(String.format("Failed to get shared lock for %s", fullNodePath), ex); + } + } + + /** + * Retrieve the data associated with the specified node. + * + * @param category The desired category in the namespace. + * @param nodePath The node to retrieve the data for. + * + * @return The data associated with the node, if any, or null if the node + * has not been created yet. + * + * @throws CoordinationServiceException If there is an error setting the + * node data. + * @throws InterruptedException If interrupted while blocked during + * setting of node data. + */ + public byte[] getNodeData(CategoryNode category, String nodePath) throws CoordinationServiceException, InterruptedException { + String fullNodePath = getFullyQualifiedNodePath(category, nodePath); + try { + return curator.getData().forPath(fullNodePath); + } catch (NoNodeException ex) { + return null; + } catch (Exception ex) { + if (ex instanceof InterruptedException) { + throw (InterruptedException) ex; + } else { + throw new CoordinationServiceException(String.format("Failed to get data for %s", fullNodePath), ex); + } + } + } + + /** + * Store the given data with the specified node. + * + * @param category The desired category in the namespace. + * @param nodePath The node to associate the data with. + * @param data The data to store with the node. + * + * @throws CoordinationServiceException If there is an error setting the + * node data. + * @throws InterruptedException If interrupted while blocked during + * setting of node data. + */ + public void setNodeData(CategoryNode category, String nodePath, byte[] data) throws CoordinationServiceException, InterruptedException { + String fullNodePath = getFullyQualifiedNodePath(category, nodePath); + try { + curator.setData().forPath(fullNodePath, data); + } catch (Exception ex) { + if (ex instanceof InterruptedException) { + throw (InterruptedException) ex; + } else { + throw new CoordinationServiceException(String.format("Failed to set data for %s", fullNodePath), ex); + } + } + } + + /** + * Creates a node path within a given category. + * + * @param category A category node. + * @param nodePath A node path relative to a category node path. + * + * @return + */ + private String getFullyQualifiedNodePath(CategoryNode category, String nodePath) { + return categoryNodeToPath.get(category.getDisplayName()) + "/" + nodePath.toUpperCase(); + } + + /** + * Determines if ZooKeeper is accessible with the current settings. Closes + * the connection prior to returning. + * + * @return true if a connection was achieved, false otherwise + */ + private static boolean isZooKeeperAccessible() { + boolean result = false; + Object workerThreadWaitNotifyLock = new Object(); + int zooKeeperServerPort = Integer.valueOf(UserPreferences.getIndexingServerPort()) + PORT_OFFSET; + String connectString = UserPreferences.getIndexingServerHost() + ":" + zooKeeperServerPort; + + try { + ZooKeeper zooKeeper = new ZooKeeper(connectString, ZOOKEEPER_SESSION_TIMEOUT_MILLIS, + (WatchedEvent event) -> { + + synchronized (workerThreadWaitNotifyLock) { + workerThreadWaitNotifyLock.notify(); + } + }); + synchronized (workerThreadWaitNotifyLock) { + workerThreadWaitNotifyLock.wait(ZOOKEEPER_CONNECTION_TIMEOUT_MILLIS); + } + ZooKeeper.States state = zooKeeper.getState(); + if (state == ZooKeeper.States.CONNECTED || state == ZooKeeper.States.CONNECTEDREADONLY) { + result = true; + } + zooKeeper.close(); + } catch (InterruptedException | IOException ignored) { + } + return result; + } +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/images/AIM.png b/Experimental/src/org/sleuthkit/autopsy/experimental/images/AIM.png new file mode 100644 index 0000000000..7052b1419c Binary files /dev/null and b/Experimental/src/org/sleuthkit/autopsy/experimental/images/AIM.png differ diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/images/arrow-down-icon.png b/Experimental/src/org/sleuthkit/autopsy/experimental/images/arrow-down-icon.png new file mode 100644 index 0000000000..6729f70a43 Binary files /dev/null and b/Experimental/src/org/sleuthkit/autopsy/experimental/images/arrow-down-icon.png differ diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/images/arrow-up-icon.png b/Experimental/src/org/sleuthkit/autopsy/experimental/images/arrow-up-icon.png new file mode 100644 index 0000000000..f53dcc0a2a Binary files /dev/null and b/Experimental/src/org/sleuthkit/autopsy/experimental/images/arrow-up-icon.png differ diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/images/artifact-icon.png b/Experimental/src/org/sleuthkit/autopsy/experimental/images/artifact-icon.png new file mode 100644 index 0000000000..5b0c461f5c Binary files /dev/null and b/Experimental/src/org/sleuthkit/autopsy/experimental/images/artifact-icon.png differ diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/images/autoIngest32.png b/Experimental/src/org/sleuthkit/autopsy/experimental/images/autoIngest32.png new file mode 100644 index 0000000000..2e634806cc Binary files /dev/null and b/Experimental/src/org/sleuthkit/autopsy/experimental/images/autoIngest32.png differ diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/images/bad.png b/Experimental/src/org/sleuthkit/autopsy/experimental/images/bad.png new file mode 100644 index 0000000000..e8f8f55fc1 Binary files /dev/null and b/Experimental/src/org/sleuthkit/autopsy/experimental/images/bad.png differ diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/images/extracted_content.png b/Experimental/src/org/sleuthkit/autopsy/experimental/images/extracted_content.png new file mode 100644 index 0000000000..12dbfaccea Binary files /dev/null and b/Experimental/src/org/sleuthkit/autopsy/experimental/images/extracted_content.png differ diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/images/file-size-16.png b/Experimental/src/org/sleuthkit/autopsy/experimental/images/file-size-16.png new file mode 100644 index 0000000000..d7dfc85b75 Binary files /dev/null and b/Experimental/src/org/sleuthkit/autopsy/experimental/images/file-size-16.png differ diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/images/frame.gif b/Experimental/src/org/sleuthkit/autopsy/experimental/images/frame.gif new file mode 100644 index 0000000000..c1fc81ff1b Binary files /dev/null and b/Experimental/src/org/sleuthkit/autopsy/experimental/images/frame.gif differ diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/images/frame32.gif b/Experimental/src/org/sleuthkit/autopsy/experimental/images/frame32.gif new file mode 100644 index 0000000000..b77aa72faa Binary files /dev/null and b/Experimental/src/org/sleuthkit/autopsy/experimental/images/frame32.gif differ diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/images/good.png b/Experimental/src/org/sleuthkit/autopsy/experimental/images/good.png new file mode 100644 index 0000000000..210b1a6c3c Binary files /dev/null and b/Experimental/src/org/sleuthkit/autopsy/experimental/images/good.png differ diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/images/import32.png b/Experimental/src/org/sleuthkit/autopsy/experimental/images/import32.png new file mode 100644 index 0000000000..8d958f916a Binary files /dev/null and b/Experimental/src/org/sleuthkit/autopsy/experimental/images/import32.png differ diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/images/knownbad-icon.png b/Experimental/src/org/sleuthkit/autopsy/experimental/images/knownbad-icon.png new file mode 100644 index 0000000000..aeacad07ad Binary files /dev/null and b/Experimental/src/org/sleuthkit/autopsy/experimental/images/knownbad-icon.png differ diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/images/left-arrow-16-icon.png b/Experimental/src/org/sleuthkit/autopsy/experimental/images/left-arrow-16-icon.png new file mode 100644 index 0000000000..d5222c2a78 Binary files /dev/null and b/Experimental/src/org/sleuthkit/autopsy/experimental/images/left-arrow-16-icon.png differ diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/images/locked.png b/Experimental/src/org/sleuthkit/autopsy/experimental/images/locked.png new file mode 100644 index 0000000000..45079c95fa Binary files /dev/null and b/Experimental/src/org/sleuthkit/autopsy/experimental/images/locked.png differ diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/images/mime-icon.png b/Experimental/src/org/sleuthkit/autopsy/experimental/images/mime-icon.png new file mode 100644 index 0000000000..1d82d83ecb Binary files /dev/null and b/Experimental/src/org/sleuthkit/autopsy/experimental/images/mime-icon.png differ diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/images/minus-icon.png b/Experimental/src/org/sleuthkit/autopsy/experimental/images/minus-icon.png new file mode 100644 index 0000000000..f953427ffb Binary files /dev/null and b/Experimental/src/org/sleuthkit/autopsy/experimental/images/minus-icon.png differ diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/images/plus-icon.png b/Experimental/src/org/sleuthkit/autopsy/experimental/images/plus-icon.png new file mode 100644 index 0000000000..619e8d25d9 Binary files /dev/null and b/Experimental/src/org/sleuthkit/autopsy/experimental/images/plus-icon.png differ diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/images/ruleset-icon.png b/Experimental/src/org/sleuthkit/autopsy/experimental/images/ruleset-icon.png new file mode 100644 index 0000000000..8e48e5edf0 Binary files /dev/null and b/Experimental/src/org/sleuthkit/autopsy/experimental/images/ruleset-icon.png differ diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/images/save-icon.png b/Experimental/src/org/sleuthkit/autopsy/experimental/images/save-icon.png new file mode 100644 index 0000000000..b6595171d4 Binary files /dev/null and b/Experimental/src/org/sleuthkit/autopsy/experimental/images/save-icon.png differ diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/images/tick.png b/Experimental/src/org/sleuthkit/autopsy/experimental/images/tick.png new file mode 100644 index 0000000000..a7d7a96be3 Binary files /dev/null and b/Experimental/src/org/sleuthkit/autopsy/experimental/images/tick.png differ diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/images/warning16.png b/Experimental/src/org/sleuthkit/autopsy/experimental/images/warning16.png new file mode 100644 index 0000000000..f5ba881738 Binary files /dev/null and b/Experimental/src/org/sleuthkit/autopsy/experimental/images/warning16.png differ diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/images/yield16-icon.png b/Experimental/src/org/sleuthkit/autopsy/experimental/images/yield16-icon.png new file mode 100644 index 0000000000..e2a81f774e Binary files /dev/null and b/Experimental/src/org/sleuthkit/autopsy/experimental/images/yield16-icon.png differ diff --git a/nbproject/project.properties b/nbproject/project.properties index 023ce3ea6a..a513a165cd 100644 --- a/nbproject/project.properties +++ b/nbproject/project.properties @@ -9,6 +9,7 @@ app.version=4.2.0 #build.type=RELEASE build.type=DEVELOPMENT +project.org.sleuthkit.autopsy.experimental=Experimental project.org.sleuthkit.autopsy.imagegallery=ImageGallery update_versions=false #custom JVM options @@ -28,7 +29,8 @@ modules=\ ${project.org.sleuthkit.autopsy.thunderbirdparser}:\ ${project.org.sleuthkit.autopsy.core}:\ ${project.org.sleuthkit.autopsy.corelibs}:\ - ${project.org.sleuthkit.autopsy.imagegallery} + ${project.org.sleuthkit.autopsy.imagegallery}:\ + ${project.org.sleuthkit.autopsy.experimental} project.org.sleuthkit.autopsy.core=Core project.org.sleuthkit.autopsy.corelibs=CoreLibs project.org.sleuthkit.autopsy.keywordsearch=KeywordSearch