diff --git a/Core/manifest.mf b/Core/manifest.mf index 2070e22853..c93cac723e 100644 --- a/Core/manifest.mf +++ b/Core/manifest.mf @@ -2,7 +2,7 @@ Manifest-Version: 1.0 OpenIDE-Module: org.sleuthkit.autopsy.core/10 OpenIDE-Module-Localizing-Bundle: org/sleuthkit/autopsy/core/Bundle.properties OpenIDE-Module-Layer: org/sleuthkit/autopsy/core/layer.xml -OpenIDE-Module-Implementation-Version: 17 +OpenIDE-Module-Implementation-Version: 18 OpenIDE-Module-Requires: org.openide.windows.WindowManager AutoUpdate-Show-In-Client: true AutoUpdate-Essential-Module: true diff --git a/Core/nbproject/project.properties b/Core/nbproject/project.properties index f3cf2e1903..c3a40f5a77 100644 --- a/Core/nbproject/project.properties +++ b/Core/nbproject/project.properties @@ -23,5 +23,5 @@ nbm.homepage=http://www.sleuthkit.org/ nbm.module.author=Brian Carrier nbm.needs.restart=true source.reference.metadata-extractor-2.8.1.jar=release/modules/ext/metadata-extractor-2.8.1-src.zip!/Source/ -spec.version.base=10.6 +spec.version.base=10.7 diff --git a/Core/release/InternalPythonModules/README.txt b/Core/release/InternalPythonModules/README.txt index e71281b732..7f03c2fa24 100755 --- a/Core/release/InternalPythonModules/README.txt +++ b/Core/release/InternalPythonModules/README.txt @@ -1,4 +1,6 @@ -Place Jython modules HERE in their respective folders. Eg. - +Do not place user modules here. Place them in the folder at C:\Users\JDoe\AppData\Roaming\Autopsy\python_modules where JDoe is your Windows username. You can also access this folder by launching Autopsy and clicking Tools -> Python Plugins through the menu. + +Place Jython modules HERE in their respective folders. Eg. - InternalPythonModules/ - testModule1/ - testModule1.py diff --git a/Core/src/org/sleuthkit/autopsy/actions/AddBlackboardArtifactTagAction.java b/Core/src/org/sleuthkit/autopsy/actions/AddBlackboardArtifactTagAction.java index 28ce44b3c7..02c2891b88 100755 --- a/Core/src/org/sleuthkit/autopsy/actions/AddBlackboardArtifactTagAction.java +++ b/Core/src/org/sleuthkit/autopsy/actions/AddBlackboardArtifactTagAction.java @@ -87,6 +87,7 @@ public class AddBlackboardArtifactTagAction extends AddTagAction { "AddBlackboardArtifactTagAction.taggingErr"), JOptionPane.ERROR_MESSAGE); }); + break; } } }).start(); diff --git a/Core/src/org/sleuthkit/autopsy/actions/AddContentTagAction.java b/Core/src/org/sleuthkit/autopsy/actions/AddContentTagAction.java index e5999fd98c..c386858aa4 100755 --- a/Core/src/org/sleuthkit/autopsy/actions/AddContentTagAction.java +++ b/Core/src/org/sleuthkit/autopsy/actions/AddContentTagAction.java @@ -141,6 +141,7 @@ public class AddContentTagAction extends AddTagAction { NbBundle.getMessage(this.getClass(), "AddContentTagAction.taggingErr"), JOptionPane.ERROR_MESSAGE); }); + break; } } }).start(); diff --git a/Core/src/org/sleuthkit/autopsy/actions/DeleteBlackboardArtifactTagAction.java b/Core/src/org/sleuthkit/autopsy/actions/DeleteBlackboardArtifactTagAction.java index 66697cb89e..2ce4ebd666 100755 --- a/Core/src/org/sleuthkit/autopsy/actions/DeleteBlackboardArtifactTagAction.java +++ b/Core/src/org/sleuthkit/autopsy/actions/DeleteBlackboardArtifactTagAction.java @@ -75,6 +75,7 @@ public class DeleteBlackboardArtifactTagAction extends AbstractAction { "DeleteBlackboardArtifactTagAction.tagDelErr"), JOptionPane.ERROR_MESSAGE); }); + break; } } }).start(); diff --git a/Core/src/org/sleuthkit/autopsy/actions/DeleteContentTagAction.java b/Core/src/org/sleuthkit/autopsy/actions/DeleteContentTagAction.java index c37eedd024..aabc483247 100755 --- a/Core/src/org/sleuthkit/autopsy/actions/DeleteContentTagAction.java +++ b/Core/src/org/sleuthkit/autopsy/actions/DeleteContentTagAction.java @@ -73,6 +73,7 @@ public class DeleteContentTagAction extends AbstractAction { NbBundle.getMessage(this.getClass(), "DeleteContentTagAction.tagDelErr"), JOptionPane.ERROR_MESSAGE); }); + break; } } }).start(); diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardChooseDataSourceVisual.java b/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardChooseDataSourceVisual.java index ad02d666a9..adf61a8f32 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardChooseDataSourceVisual.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/AddImageWizardChooseDataSourceVisual.java @@ -39,6 +39,7 @@ import org.openide.util.Lookup; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.datasourceprocessors.RawDSProcessor; /** * visual component for the first panel of add image wizard. Allows the user to @@ -92,7 +93,8 @@ final class AddImageWizardChooseDataSourceVisual extends JPanel { datasourceProcessorsMap.remove(LocalDiskDSProcessor.getType()); } coreDSPTypes.add(LocalFilesDSProcessor.getType()); - + coreDSPTypes.add(RawDSProcessor.getType()); + for (String dspType : coreDSPTypes) { typeComboBox.addItem(dspType); } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties index b58268dce3..cbf9804b48 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties @@ -159,7 +159,7 @@ GeneralFilter.rawImageDesc.text=Raw Images (*.img, *.dd, *.001, *.aa, *.raw, *.b GeneralFilter.encaseImageDesc.text=Encase Images (*.e01) GeneralFilter.virtualMachineImageDesc.text=Virtual Machines (*.vmdk, *.vhd) GeneralFilter.executableDesc.text=Executables (*.exe) -ImageDSProcessor.dsType.text=Image or VM File +ImageDSProcessor.dsType.text=Disk Image or VM File ImageDSProcessor.allDesc.text=All Supported Types ImageFilePanel.moduleErr=Module Error ImageFilePanel.moduleErr.msg=A module caused an error listening to ImageFilePanel updates. See log to determine which module. Some data could be incomplete. 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..1cceb7ab12 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/ImageDSProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/ImageDSProcessor.java @@ -18,6 +18,8 @@ */ package org.sleuthkit.autopsy.casemodule; +import java.io.File; +import java.nio.file.Path; import javax.swing.JPanel; import java.util.ArrayList; import java.util.Calendar; @@ -26,9 +28,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.DataSourceProcessorProgressMonitor; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; +import org.sleuthkit.autopsy.coreutils.DataSourceUtils; +import org.sleuthkit.autopsy.corecomponentinterfaces.AutoIngestDataSourceProcessor; /** * A image file data source processor that implements the DataSourceProcessor @@ -36,16 +41,19 @@ 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=AutoIngestDataSourceProcessor.class)} +) +public class ImageDSProcessor implements DataSourceProcessor, AutoIngestDataSourceProcessor { private final static String DATA_SOURCE_TYPE = NbBundle.getMessage(ImageDSProcessor.class, "ImageDSProcessor.dsType.text"); private static final List allExt = new ArrayList<>(); private static final GeneralFilter rawFilter = new GeneralFilter(GeneralFilter.RAW_IMAGE_EXTS, GeneralFilter.RAW_IMAGE_DESC); private static final GeneralFilter encaseFilter = new GeneralFilter(GeneralFilter.ENCASE_IMAGE_EXTS, GeneralFilter.ENCASE_IMAGE_DESC); private static final GeneralFilter virtualMachineFilter = new GeneralFilter(GeneralFilter.VIRTUAL_MACHINE_EXTS, GeneralFilter.VIRTUAL_MACHINE_DESC); - private static final String allDesc = NbBundle.getMessage(ImageDSProcessor.class, "ImageDSProcessor.allDesc.text"); - private static final GeneralFilter allFilter = new GeneralFilter(allExt, allDesc); + private static final String ALL_DESC = NbBundle.getMessage(ImageDSProcessor.class, "ImageDSProcessor.allDesc.text"); + private static final GeneralFilter allFilter = new GeneralFilter(allExt, ALL_DESC); private static final List filtersList = new ArrayList<>(); private final ImageFilePanel configPanel; private AddImageTask addImageTask; @@ -209,6 +217,48 @@ public class ImageDSProcessor implements DataSourceProcessor { setDataSourceOptionsCalled = false; } + 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 AutoIngestDataSourceProcessorException { + + // 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 AutoIngestDataSourceProcessorException("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 AutoIngestDataSourceProcessorException { + 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); + } + /** * Sets the configuration of the data source processor without using the * selection and configuration panel. @@ -230,5 +280,5 @@ public class ImageDSProcessor implements DataSourceProcessor { this.ignoreFatOrphanFiles = ignoreFatOrphanFiles; setDataSourceOptionsCalled = true; } - + } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.java index 044559e0f8..4373959707 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2012 Basis Technology Corp. + * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -45,10 +45,10 @@ public class ImageFilePanel extends JPanel implements DocumentListener { private final String PROP_LASTIMAGE_PATH = "LBL_LastImage_PATH"; //NON-NLS private static final Logger logger = Logger.getLogger(ImageFilePanel.class.getName()); - private JFileChooser fc = new JFileChooser(); + private final JFileChooser fc = new JFileChooser(); // Externally supplied name is used to store settings - private String contextName; + private final String contextName; /** * Creates new form ImageFilePanel @@ -78,17 +78,17 @@ public class ImageFilePanel extends JPanel implements DocumentListener { this.contextName = context; - createTimeZoneList(); } /** * Creates and returns an instance of a ImageFilePanel. + * @return instance of the ImageFilePanel */ public static synchronized ImageFilePanel createInstance(String context, List fileChooserFilters) { ImageFilePanel instance = new ImageFilePanel(context, fileChooserFilters); - instance.postInit(); + instance.createTimeZoneList(); return instance; } @@ -234,6 +234,7 @@ public class ImageFilePanel extends JPanel implements DocumentListener { /** * Set the path of the image file. + * @param s path of the image file */ public void setContentPath(String s) { pathTextField.setText(s); @@ -242,7 +243,6 @@ public class ImageFilePanel extends JPanel implements DocumentListener { public String getTimeZone() { String tz = timeZoneComboBox.getSelectedItem().toString(); return tz.substring(tz.indexOf(")") + 2).trim(); - } public boolean getNoFatOrphans() { diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskDSProcessor.java b/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskDSProcessor.java index 02590894f2..015f7751ea 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.DataSourceProcessorCallback; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgressMonitor; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; +import org.sleuthkit.autopsy.coreutils.DriveUtils; +import org.sleuthkit.autopsy.corecomponentinterfaces.AutoIngestDataSourceProcessor; /** * 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=AutoIngestDataSourceProcessor.class)} +) +public class LocalDiskDSProcessor implements DataSourceProcessor, AutoIngestDataSourceProcessor { private static final String DATA_SOURCE_TYPE = NbBundle.getMessage(LocalDiskDSProcessor.class, "LocalDiskDSProcessor.dsType.text"); private final LocalDiskPanel configPanel; @@ -188,6 +196,37 @@ public class LocalDiskDSProcessor implements DataSourceProcessor { setDataSourceOptionsCalled = false; } + @Override + public int canProcess(Path dataSourcePath) throws AutoIngestDataSourceProcessorException { + + // 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 AutoIngestDataSourceProcessorException { + 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); + } + /** * Sets the configuration of the data source processor without using the * configuration panel. @@ -209,5 +248,5 @@ public class LocalDiskDSProcessor implements DataSourceProcessor { this.ignoreFatOrphanFiles = ignoreFatOrphanFiles; setDataSourceOptionsCalled = true; } - + } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskPanel.java index 1aeda4a6da..0d485b3392 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskPanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/LocalDiskPanel.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2012 Basis Technology Corp. + * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -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; @@ -66,7 +67,6 @@ final class LocalDiskPanel extends JPanel { customInit(); createTimeZoneList(); - } /** @@ -185,7 +185,6 @@ final class LocalDiskPanel extends JPanel { * * @return String selected disk path */ - //@Override public String getContentPaths() { if (disks.size() > 0) { LocalDisk selected = (LocalDisk) diskComboBox.getSelectedItem(); @@ -198,7 +197,6 @@ final class LocalDiskPanel extends JPanel { /** * Set the selected disk. */ - // @Override public void setContentPath(String s) { for (int i = 0; i < disks.size(); i++) { if (disks.get(i).getPath().equals(s)) { @@ -223,25 +221,20 @@ final class LocalDiskPanel extends JPanel { * * @return true */ - //@Override public boolean validatePanel() { return enableNext; } - //@Override public void reset() { //nothing to reset - } /** * Set the focus to the diskComboBox and refreshes the list of disks. */ - // @Override public void select() { diskComboBox.requestFocusInWindow(); model.loadDisks(); - } /** @@ -293,8 +286,8 @@ final class LocalDiskPanel extends JPanel { List partitions = new ArrayList<>(); //private String SELECT = "Select a local disk:"; - private String LOADING = NbBundle.getMessage(this.getClass(), "LocalDiskPanel.localDiskModel.loading.msg"); - private String NO_DRIVES = NbBundle.getMessage(this.getClass(), "LocalDiskPanel.localDiskModel.nodrives.msg"); + private final String LOADING = NbBundle.getMessage(this.getClass(), "LocalDiskPanel.localDiskModel.loading.msg"); + private final String NO_DRIVES = NbBundle.getMessage(this.getClass(), "LocalDiskPanel.localDiskModel.nodrives.msg"); LocalDiskThread worker = null; @@ -415,7 +408,7 @@ final class LocalDiskPanel extends JPanel { class LocalDiskThread extends SwingWorker { - private Logger logger = Logger.getLogger(LocalDiskThread.class.getName()); + private final Logger logger = Logger.getLogger(LocalDiskThread.class.getName()); @Override protected Object doInBackground() throws Exception { diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesDSProcessor.java b/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesDSProcessor.java index c7825b6458..9c19cb2cfc 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesDSProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesDSProcessor.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-2016 Basis Technology Corp. + * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,15 +18,18 @@ */ 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.DataSourceProcessorCallback; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgressMonitor; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; +import org.sleuthkit.autopsy.corecomponentinterfaces.AutoIngestDataSourceProcessor; /** * A local/logical files and/or directories data source processor that @@ -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=AutoIngestDataSourceProcessor.class)} +) +public class LocalFilesDSProcessor implements DataSourceProcessor, AutoIngestDataSourceProcessor { private static final String DATA_SOURCE_TYPE = NbBundle.getMessage(LocalFilesDSProcessor.class, "LocalFilesDSProcessor.dsType"); private final LocalFilesPanel configPanel; @@ -122,7 +128,7 @@ public class LocalFilesDSProcessor implements DataSourceProcessor { @Override public void run(DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) { if (!setDataSourceOptionsCalled) { - localFilePaths = Arrays.asList(configPanel.getContentPaths().split(LocalFilesPanel.FILES_SEP)); + localFilePaths = configPanel.getContentPaths(); } run(UUID.randomUUID().toString(), configPanel.getFileSetName(), localFilePaths, progressMonitor, callback); } @@ -179,6 +185,20 @@ public class LocalFilesDSProcessor implements DataSourceProcessor { setDataSourceOptionsCalled = false; } + @Override + public int canProcess(Path dataSourcePath) throws AutoIngestDataSourceProcessorException { + // 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 AutoIngestDataSourceProcessorException { + this.localFilePaths = Arrays.asList(new String[]{dataSourcePath.toString()}); + run(deviceId, deviceId, this.localFilePaths, progressMonitor, callBack); + } + /** * Sets the configuration of the data source processor without using the * configuration panel. The data source processor will assign a UUID to the @@ -192,9 +212,12 @@ public class LocalFilesDSProcessor implements DataSourceProcessor { */ @Deprecated public void setDataSourceOptions(String paths) { - //LocalFilesPanel.FILES_SEP is currently "," - this.localFilePaths = Arrays.asList(paths.split(LocalFilesPanel.FILES_SEP)); + // The LocalFilesPanel used to separate file paths with a comma and pass + // them as a string, but because file names are allowed to contain + // commas, this approach was buggy and replaced. We now pass a list of + // String paths. + this.localFilePaths = Arrays.asList(paths.split(",")); setDataSourceOptionsCalled = true; } - + } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesPanel.java index c04aaa5bbf..39485da30f 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesPanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesPanel.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013 Basis Technology Corp. + * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,9 +18,8 @@ */ package org.sleuthkit.autopsy.casemodule; -import java.awt.Dialog; import java.io.File; -import java.util.Arrays; +import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.TreeSet; @@ -32,9 +31,6 @@ import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import java.util.logging.Level; import javax.swing.JOptionPane; -import org.openide.DialogDescriptor; -import org.openide.DialogDisplayer; -import org.openide.NotifyDescriptor; import org.sleuthkit.autopsy.casemodule.Case.CaseType; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.PathValidator; @@ -42,12 +38,12 @@ import org.sleuthkit.autopsy.coreutils.PathValidator; /** * Add input wizard subpanel for adding local files / dirs to the case */ -class LocalFilesPanel extends JPanel { +final class LocalFilesPanel extends JPanel { - private Set currentFiles = new TreeSet(); //keep currents in a set to disallow duplicates per add + 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; - public static final String FILES_SEP = ","; private static final Logger logger = Logger.getLogger(LocalFilesPanel.class.getName()); private String displayName = ""; @@ -73,38 +69,24 @@ class LocalFilesPanel extends JPanel { this.displayNameLabel.setText(NbBundle.getMessage(this.getClass(), "LocalFilesPanel.displayNameLabel.text")); } - //@Override - public String getContentPaths() { - //TODO consider interface change to return list of paths instead - + public List getContentPaths() { + List pathsList = new ArrayList<>(); if (currentFiles == null) { - return ""; + return pathsList; } - StringBuilder b = new StringBuilder(); for (File f : currentFiles) { - b.append(f.getAbsolutePath()); - b.append(FILES_SEP); + pathsList.add(f.getAbsolutePath()); } - return b.toString(); + return pathsList; } - //@Override - public void setContentPath(String s) { - //for the local file panel we don't need to restore the last paths used - //when the wizard restarts - } - - //@Override public String getContentType() { return NbBundle.getMessage(this.getClass(), "LocalFilesPanel.contentType.text"); } - //@Override public boolean validatePanel() { - // display warning if there is one (but don't disable "next" button) warnIfPathIsInvalid(getContentPaths()); - return enableNext; } @@ -112,13 +94,11 @@ class LocalFilesPanel extends JPanel { * Validates path to selected data source and displays warning if it is * invalid. * - * @param path Absolute path to the selected data source + * @param paths Absolute paths to the selected data source */ - private void warnIfPathIsInvalid(String path) { + private void warnIfPathIsInvalid(List pathsList) { errorLabel.setVisible(false); - // Path variable for "Local files" module is a coma separated string containg multiple paths - List pathsList = Arrays.asList(path.split(",")); CaseType currentCaseType = Case.getCurrentCase().getCaseType(); for (String currentPath : pathsList) { @@ -130,12 +110,10 @@ class LocalFilesPanel extends JPanel { } } - //@Override public void select() { reset(); } - //@Override public void reset() { currentFiles.clear(); selectedPaths.setText(""); @@ -281,14 +259,9 @@ class LocalFilesPanel extends JPanel { } this.selectedPaths.setText(allPaths.toString()); this.selectedPaths.setToolTipText(allPaths.toString()); - } - if (!currentFiles.isEmpty()) { - enableNext = true; - } else { - enableNext = false; - } + enableNext = !currentFiles.isEmpty(); try { firePropertyChange(DataSourceProcessor.DSP_PANEL_EVENT.UPDATE_UI.toString(), false, true); 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/casemodule/events/TagAddedEvent.java b/Core/src/org/sleuthkit/autopsy/casemodule/events/TagAddedEvent.java index 365a3c0ff8..1f679c47a3 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/events/TagAddedEvent.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/events/TagAddedEvent.java @@ -39,7 +39,7 @@ abstract class TagAddedEvent extends AutopsyEvent implements Seri private transient T tag; /** - * The id of the tag that was added. This will bu used to re-load the + * The id of the tag that was added. This will be used to re-load the * transient tag from the database. */ private final Long tagID; diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/services/TagsManager.java b/Core/src/org/sleuthkit/autopsy/casemodule/services/TagsManager.java index 9d4d0eb112..89454ae07c 100755 --- a/Core/src/org/sleuthkit/autopsy/casemodule/services/TagsManager.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/services/TagsManager.java @@ -248,7 +248,7 @@ public class TagsManager implements Closeable { try { Case.getCurrentCase().notifyContentTagAdded(tag); } catch (IllegalStateException ex) { - LOGGER.log(Level.SEVERE, "Added a tag to a closed case", ex); + throw new TskCoreException("Added a tag to a closed case", ex); } return tag; } @@ -266,7 +266,7 @@ public class TagsManager implements Closeable { try { Case.getCurrentCase().notifyContentTagDeleted(tag); } catch (IllegalStateException ex) { - LOGGER.log(Level.SEVERE, "Deleted a tag from a closed case", ex); + throw new TskCoreException("Deleted a tag from a closed case", ex); } } @@ -378,7 +378,7 @@ public class TagsManager implements Closeable { try { Case.getCurrentCase().notifyBlackBoardArtifactTagAdded(tag); } catch (IllegalStateException ex) { - LOGGER.log(Level.SEVERE, "Added a tag to a closed case", ex); + throw new TskCoreException("Added a tag to a closed case", ex); } return tag; } @@ -396,7 +396,7 @@ public class TagsManager implements Closeable { try { Case.getCurrentCase().notifyBlackBoardArtifactTagDeleted(tag); } catch (IllegalStateException ex) { - LOGGER.log(Level.SEVERE, "Deleted a tag from a closed case", ex); + throw new TskCoreException("Deleted a tag from a closed case", ex); } } diff --git a/Core/src/org/sleuthkit/autopsy/core/Installer.java b/Core/src/org/sleuthkit/autopsy/core/Installer.java index bc14db3b7d..e2036e6f25 100644 --- a/Core/src/org/sleuthkit/autopsy/core/Installer.java +++ b/Core/src/org/sleuthkit/autopsy/core/Installer.java @@ -64,12 +64,54 @@ public class Installer extends ModuleInstall { //Note: if shipping with a different CRT version, this will only print a warning //and try to use linker mechanism to find the correct versions of libs. //We should update this if we officially switch to a new version of CRT/compiler - System.loadLibrary("msvcr100"); //NON-NLS - System.loadLibrary("msvcp100"); //NON-NLS + System.loadLibrary("api-ms-win-core-console-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-core-datetime-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-core-debug-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-core-errorhandling-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-core-file-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-core-file-l1-2-0"); //NON-NLS + System.loadLibrary("api-ms-win-core-file-l2-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-core-handle-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-core-heap-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-core-interlocked-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-core-libraryloader-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-core-localization-l1-2-0"); //NON-NLS + System.loadLibrary("api-ms-win-core-memory-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-core-namedpipe-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-core-processenvironment-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-core-processthreads-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-core-processthreads-l1-1-1"); //NON-NLS + System.loadLibrary("api-ms-win-core-profile-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-core-rtlsupport-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-core-string-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-core-synch-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-core-synch-l1-2-0"); //NON-NLS + System.loadLibrary("api-ms-win-core-sysinfo-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-core-timezone-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-core-util-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-crt-conio-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-crt-convert-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-crt-environment-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-crt-filesystem-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-crt-heap-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-crt-locale-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-crt-math-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-crt-multibyte-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-crt-private-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-crt-process-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-crt-runtime-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-crt-stdio-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-crt-string-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-crt-time-l1-1-0"); //NON-NLS + System.loadLibrary("api-ms-win-crt-utility-l1-1-0"); //NON-NLS - logger.log(Level.INFO, "MSVCR100 and MSVCP100 libraries loaded"); //NON-NLS + System.loadLibrary("ucrtbase"); //NON-NLS + System.loadLibrary("vcruntime140"); //NON-NLS + System.loadLibrary("msvcp140"); //NON-NLS + + logger.log(Level.INFO, "Visual C Runtime libraries loaded"); //NON-NLS } catch (UnsatisfiedLinkError e) { - logger.log(Level.SEVERE, "Error loading MSVCR100 and MSVCP100 libraries, ", e); //NON-NLS + logger.log(Level.SEVERE, "Error loading Visual C Runtime libraries, ", e); //NON-NLS } try { 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/core/UserPreferences.java b/Core/src/org/sleuthkit/autopsy/core/UserPreferences.java index 784556e7a4..519a1efda4 100755 --- a/Core/src/org/sleuthkit/autopsy/core/UserPreferences.java +++ b/Core/src/org/sleuthkit/autopsy/core/UserPreferences.java @@ -40,11 +40,13 @@ import org.sleuthkit.datamodel.TskData.DbType; */ public final class UserPreferences { - private static final boolean isWindowsOS = PlatformUtil.isWindowsOS(); + private static final boolean IS_WINDOWS_OS = PlatformUtil.isWindowsOS(); private static final Preferences preferences = NbPreferences.forModule(UserPreferences.class); public static final String KEEP_PREFERRED_VIEWER = "KeepPreferredViewer"; // NON-NLS - public static final String HIDE_KNOWN_FILES_IN_DATA_SOURCES_TREE = "HideKnownFilesInDataSourcesTree"; //NON-NLS + public static final String HIDE_KNOWN_FILES_IN_DATA_SRCS_TREE = "HideKnownFilesInDataSourcesTree"; //NON-NLS public static final String HIDE_KNOWN_FILES_IN_VIEWS_TREE = "HideKnownFilesInViewsTree"; //NON-NLS + public static final String HIDE_SLACK_FILES_IN_DATA_SRCS_TREE = "HideSlackFilesInDataSourcesTree"; //NON-NLS + public static final String HIDE_SLACK_FILES_IN_VIEWS_TREE = "HideSlackFilesInViewsTree"; //NON-NLS public static final String DISPLAY_TIMES_IN_LOCAL_TIME = "DisplayTimesInLocalTime"; //NON-NLS public static final String NUMBER_OF_FILE_INGEST_THREADS = "NumberOfFileIngestThreads"; //NON-NLS public static final String IS_MULTI_USER_MODE_ENABLED = "IsMultiUserModeEnabled"; //NON-NLS @@ -109,11 +111,11 @@ public final class UserPreferences { } public static boolean hideKnownFilesInDataSourcesTree() { - return preferences.getBoolean(HIDE_KNOWN_FILES_IN_DATA_SOURCES_TREE, false); + return preferences.getBoolean(HIDE_KNOWN_FILES_IN_DATA_SRCS_TREE, false); } public static void setHideKnownFilesInDataSourcesTree(boolean value) { - preferences.putBoolean(HIDE_KNOWN_FILES_IN_DATA_SOURCES_TREE, value); + preferences.putBoolean(HIDE_KNOWN_FILES_IN_DATA_SRCS_TREE, value); } public static boolean hideKnownFilesInViewsTree() { @@ -124,6 +126,22 @@ public final class UserPreferences { preferences.putBoolean(HIDE_KNOWN_FILES_IN_VIEWS_TREE, value); } + public static boolean hideSlackFilesInDataSourcesTree() { + return preferences.getBoolean(HIDE_SLACK_FILES_IN_DATA_SRCS_TREE, true); + } + + public static void setHideSlackFilesInDataSourcesTree(boolean value) { + preferences.putBoolean(HIDE_SLACK_FILES_IN_DATA_SRCS_TREE, value); + } + + public static boolean hideSlackFilesInViewsTree() { + return preferences.getBoolean(HIDE_SLACK_FILES_IN_VIEWS_TREE, true); + } + + public static void setHideSlackFilesInViewsTree(boolean value) { + preferences.putBoolean(HIDE_SLACK_FILES_IN_VIEWS_TREE, value); + } + public static boolean displayTimesInLocalTime() { return preferences.getBoolean(DISPLAY_TIMES_IN_LOCAL_TIME, true); } @@ -180,7 +198,7 @@ public final class UserPreferences { } public static boolean getIsMultiUserModeEnabled() { - if (!isWindowsOS) { + if (!IS_WINDOWS_OS) { return false; } return preferences.getBoolean(IS_MULTI_USER_MODE_ENABLED, false); diff --git a/Core/src/org/sleuthkit/autopsy/corecomponentinterfaces/AutoIngestDataSourceProcessor.java b/Core/src/org/sleuthkit/autopsy/corecomponentinterfaces/AutoIngestDataSourceProcessor.java new file mode 100644 index 0000000000..f7d50235da --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/corecomponentinterfaces/AutoIngestDataSourceProcessor.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 AutoIngestDataSourceProcessor 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 AutoIngestDataSourceProcessorException; + + /** + * 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 AutoIngestDataSourceProcessorException; + + /** + * A custom exception for the use of AutomatedIngestDataSourceProcessor. + */ + public class AutoIngestDataSourceProcessorException extends Exception { + + private static final long serialVersionUID = 1L; + + public AutoIngestDataSourceProcessorException(String message) { + super(message); + } + + public AutoIngestDataSourceProcessorException(String message, Throwable cause) { + super(message, cause); + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/AutopsyOptionsPanel.form b/Core/src/org/sleuthkit/autopsy/corecomponents/AutopsyOptionsPanel.form index f2c44eb4e1..5a169b7329 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/AutopsyOptionsPanel.form +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/AutopsyOptionsPanel.form @@ -22,12 +22,12 @@ - + - + @@ -49,47 +49,69 @@ - - + - - - - + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + + + - - + + + + + + - - - - - + + + + - - + - @@ -108,7 +130,13 @@ - + + + + + + + @@ -131,7 +159,7 @@ - + @@ -303,6 +331,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/AutopsyOptionsPanel.java b/Core/src/org/sleuthkit/autopsy/corecomponents/AutopsyOptionsPanel.java index 13ee9b0a1b..de9a6e5429 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/AutopsyOptionsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/AutopsyOptionsPanel.java @@ -101,6 +101,8 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { useBestViewerRB.setSelected(!keepPreferredViewer); dataSourcesHideKnownCB.setSelected(UserPreferences.hideKnownFilesInDataSourcesTree()); viewsHideKnownCB.setSelected(UserPreferences.hideKnownFilesInViewsTree()); + dataSourcesHideSlackCB.setSelected(UserPreferences.hideSlackFilesInDataSourcesTree()); + viewsHideSlackCB.setSelected(UserPreferences.hideSlackFilesInViewsTree()); boolean useLocalTime = UserPreferences.displayTimesInLocalTime(); useLocalTimeRB.setSelected(useLocalTime); useGMTTimeRB.setSelected(!useLocalTime); @@ -124,6 +126,8 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { UserPreferences.setKeepPreferredContentViewer(keepCurrentViewerRB.isSelected()); UserPreferences.setHideKnownFilesInDataSourcesTree(dataSourcesHideKnownCB.isSelected()); UserPreferences.setHideKnownFilesInViewsTree(viewsHideKnownCB.isSelected()); + UserPreferences.setHideSlackFilesInDataSourcesTree(dataSourcesHideSlackCB.isSelected()); + UserPreferences.setHideSlackFilesInViewsTree(viewsHideSlackCB.isSelected()); UserPreferences.setDisplayTimesInLocalTime(useLocalTimeRB.isSelected()); UserPreferences.setNumberOfFileIngestThreads((Integer) numberOfFileIngestThreadsComboBox.getSelectedItem()); @@ -167,6 +171,9 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { jCheckBoxEnableProcTimeout = new javax.swing.JCheckBox(); jLabelProcessTimeOutUnits = new javax.swing.JLabel(); jFormattedTextFieldProcTimeOutHrs = new JFormattedTextField(NumberFormat.getIntegerInstance()); + dataSourcesHideSlackCB = new javax.swing.JCheckBox(); + viewsHideSlackCB = new javax.swing.JCheckBox(); + jLabelHideSlackFiles = new javax.swing.JLabel(); jScrollPane1.setBorder(null); @@ -253,6 +260,22 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { } }); + org.openide.awt.Mnemonics.setLocalizedText(dataSourcesHideSlackCB, org.openide.util.NbBundle.getMessage(AutopsyOptionsPanel.class, "AutopsyOptionsPanel.dataSourcesHideSlackCB.text")); // NOI18N + dataSourcesHideSlackCB.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + dataSourcesHideSlackCBActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(viewsHideSlackCB, org.openide.util.NbBundle.getMessage(AutopsyOptionsPanel.class, "AutopsyOptionsPanel.viewsHideSlackCB.text")); // NOI18N + viewsHideSlackCB.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + viewsHideSlackCBActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(jLabelHideSlackFiles, org.openide.util.NbBundle.getMessage(AutopsyOptionsPanel.class, "AutopsyOptionsPanel.jLabelHideSlackFiles.text")); // NOI18N + javax.swing.GroupLayout jPanel1Layout = new javax.swing.GroupLayout(jPanel1); jPanel1.setLayout(jPanel1Layout); jPanel1Layout.setHorizontalGroup( @@ -260,38 +283,52 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { .addGroup(jPanel1Layout.createSequentialGroup() .addContainerGap() .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(jPanel1Layout.createSequentialGroup() - .addGap(10, 10, 10) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel1Layout.createSequentialGroup() .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(keepCurrentViewerRB) - .addComponent(useBestViewerRB) - .addComponent(dataSourcesHideKnownCB) - .addComponent(viewsHideKnownCB) .addGroup(jPanel1Layout.createSequentialGroup() + .addGap(10, 10, 10) .addComponent(numberOfFileIngestThreadsComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addGap(18, 18, 18) - .addComponent(restartRequiredLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)))) + .addComponent(restartRequiredLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addGroup(jPanel1Layout.createSequentialGroup() + .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(jLabelTimeDisplay) + .addComponent(jLabelNumThreads) + .addComponent(jLabelSetProcessTimeOut) + .addGroup(jPanel1Layout.createSequentialGroup() + .addGap(10, 10, 10) + .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(useLocalTimeRB) + .addComponent(useGMTTimeRB) + .addGroup(jPanel1Layout.createSequentialGroup() + .addComponent(jCheckBoxEnableProcTimeout) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jFormattedTextFieldProcTimeOutHrs, javax.swing.GroupLayout.PREFERRED_SIZE, 27, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabelProcessTimeOutUnits))))) + .addGap(213, 213, 213))) + .addContainerGap()) .addGroup(jPanel1Layout.createSequentialGroup() .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(jLabelHideKnownFiles) - .addComponent(jLabelTimeDisplay) + .addComponent(jLabelSelectFile) .addGroup(jPanel1Layout.createSequentialGroup() .addGap(10, 10, 10) .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(useLocalTimeRB) - .addComponent(useGMTTimeRB))) - .addComponent(jLabelSelectFile) - .addComponent(jLabelNumThreads) + .addComponent(keepCurrentViewerRB) + .addComponent(useBestViewerRB) + .addComponent(dataSourcesHideKnownCB) + .addComponent(viewsHideKnownCB)))) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addGroup(jPanel1Layout.createSequentialGroup() + .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(jLabelHideSlackFiles) .addGroup(jPanel1Layout.createSequentialGroup() .addGap(10, 10, 10) - .addComponent(jCheckBoxEnableProcTimeout) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(jFormattedTextFieldProcTimeOutHrs, javax.swing.GroupLayout.PREFERRED_SIZE, 27, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(jLabelProcessTimeOutUnits)) - .addComponent(jLabelSetProcessTimeOut)) - .addGap(213, 213, 213))) - .addContainerGap()) + .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(dataSourcesHideSlackCB) + .addComponent(viewsHideSlackCB)))) + .addGap(0, 0, Short.MAX_VALUE)))) ); jPanel1Layout.setVerticalGroup( jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) @@ -308,7 +345,13 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { .addComponent(dataSourcesHideKnownCB) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(viewsHideKnownCB) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jLabelHideSlackFiles) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(dataSourcesHideSlackCB) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(viewsHideSlackCB) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(jLabelTimeDisplay) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(useLocalTimeRB) @@ -328,7 +371,7 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(jFormattedTextFieldProcTimeOutHrs, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(jLabelProcessTimeOutUnits))) - .addContainerGap()) + .addContainerGap(49, Short.MAX_VALUE)) ); jScrollPane1.setViewportView(jPanel1); @@ -337,11 +380,11 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { this.setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(jScrollPane1, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, 657, Short.MAX_VALUE) + .addComponent(jScrollPane1, javax.swing.GroupLayout.Alignment.TRAILING) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 402, Short.MAX_VALUE) + .addComponent(jScrollPane1) ); }// //GEN-END:initComponents @@ -382,13 +425,23 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null); }//GEN-LAST:event_jFormattedTextFieldProcTimeOutHrsActionPerformed + private void dataSourcesHideSlackCBActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_dataSourcesHideSlackCBActionPerformed + firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null); + }//GEN-LAST:event_dataSourcesHideSlackCBActionPerformed + + private void viewsHideSlackCBActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_viewsHideSlackCBActionPerformed + firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null); + }//GEN-LAST:event_viewsHideSlackCBActionPerformed + // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.ButtonGroup buttonGroup1; private javax.swing.ButtonGroup buttonGroup3; private javax.swing.JCheckBox dataSourcesHideKnownCB; + private javax.swing.JCheckBox dataSourcesHideSlackCB; private javax.swing.JCheckBox jCheckBoxEnableProcTimeout; private javax.swing.JFormattedTextField jFormattedTextFieldProcTimeOutHrs; private javax.swing.JLabel jLabelHideKnownFiles; + private javax.swing.JLabel jLabelHideSlackFiles; private javax.swing.JLabel jLabelNumThreads; private javax.swing.JLabel jLabelProcessTimeOutUnits; private javax.swing.JLabel jLabelSelectFile; @@ -403,5 +456,6 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { private javax.swing.JRadioButton useGMTTimeRB; private javax.swing.JRadioButton useLocalTimeRB; private javax.swing.JCheckBox viewsHideKnownCB; + private javax.swing.JCheckBox viewsHideSlackCB; // End of variables declaration//GEN-END:variables } diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties index 5c705ac4a4..7f5cde3851 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties @@ -27,7 +27,7 @@ Format_OperatingSystem_Value={0} version {1} running on {2} LBL_Copyright=
Autopsy™ is a digital forensics platform based on The Sleuth Kit™ and other tools.
Copyright © 2003-2016.
URL_ON_IMG=http://www.sleuthkit.org/ -URL_ON_HELP=http://sleuthkit.org/autopsy/docs/user-docs/4.1/ +URL_ON_HELP=http://sleuthkit.org/autopsy/docs/user-docs/4.3/ FILE_FOR_LOCAL_HELP=file:/// INDEX_FOR_LOCAL_HELP=/docs/index.html @@ -197,3 +197,6 @@ MultiUserSettingsPanel.lbTestSolrWarning.text= MultiUserSettingsPanel.lbTestDbWarning.text= MultiUserSettingsPanel.KeywordSearchNull=Cannot find keyword search service MultiUserSettingsPanel.InvalidPortNumber=Invalid port number +AutopsyOptionsPanel.jLabelHideSlackFiles.text=Hide slack files in the: +AutopsyOptionsPanel.dataSourcesHideSlackCB.text=Data Sources area (the directory hierarchy) +AutopsyOptionsPanel.viewsHideSlackCB.text=Views area diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java index ccb562a3ec..a3e87e3008 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataResultViewerTable.java @@ -1,15 +1,15 @@ /* * Autopsy Forensic Browser - * - * Copyright 2013-2014 Basis Technology Corp. + * + * 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. @@ -18,22 +18,32 @@ */ package org.sleuthkit.autopsy.corecomponents; +import java.awt.Color; +import java.awt.Component; import java.awt.Cursor; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.dnd.DnDConstants; +import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; import java.beans.PropertyChangeEvent; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; +import java.util.Arrays; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; -import javax.swing.Action; +import java.util.TreeMap; import javax.swing.JTable; import javax.swing.ListSelectionModel; import javax.swing.SwingUtilities; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.TableColumnModelEvent; +import javax.swing.event.TableColumnModelListener; +import javax.swing.table.TableCellRenderer; +import org.netbeans.swing.outline.DefaultOutlineCellRenderer; import org.netbeans.swing.outline.DefaultOutlineModel; import org.openide.explorer.ExplorerManager; import org.openide.explorer.view.OutlineView; @@ -46,28 +56,43 @@ import org.openide.nodes.NodeEvent; import org.openide.nodes.NodeListener; import org.openide.nodes.NodeMemberEvent; import org.openide.nodes.NodeReorderEvent; -import org.openide.nodes.Sheet; import org.openide.util.NbBundle; +import org.openide.util.NbPreferences; import org.sleuthkit.autopsy.corecomponentinterfaces.DataResultViewer; /** * DataResult sortable table viewer */ -// @@@ Restore implementation of DataResultViewerTable as a DataResultViewer -// service provider when DataResultViewers can be made compatible with node +// @@@ Restore implementation of DataResultViewerTable as a DataResultViewer +// service provider when DataResultViewers can be made compatible with node // multiple selection actions. //@ServiceProvider(service = DataResultViewer.class) public class DataResultViewerTable extends AbstractDataResultViewer { - private String firstColumnLabel = NbBundle.getMessage(DataResultViewerTable.class, "DataResultViewerTable.firstColLbl"); - private Set> propertiesAcc = new LinkedHashSet<>(); + private static final long serialVersionUID = 1L; + + private final String firstColumnLabel = NbBundle.getMessage(DataResultViewerTable.class, "DataResultViewerTable.firstColLbl"); + /* The properties map maps + * key: stored value of column index -> value: property at that index + * We move around stored values instead of directly using the column indices + * in order to not override settings for a column that may not appear in the + * current table view due to its collection of its children's properties. + */ + private final Map> propertiesMap = new TreeMap<>(); private final DummyNodeListener dummyNodeListener = new DummyNodeListener(); private static final String DUMMY_NODE_DISPLAY_NAME = NbBundle.getMessage(DataResultViewerTable.class, "DataResultViewerTable.dummyNodeDisplayName"); + private static final Color TAGGED_COLOR = new Color(200, 210, 220); private Node currentRoot; + // When a column in the table is moved, these two variables keep track of where + // the column started and where it ended up. + private int startColumnIndex = -1; + private int endColumnIndex = -1; /** * Creates a DataResultViewerTable object that is compatible with node * multiple selection actions. + * + * @param explorerManager allow for explorer manager sharing */ public DataResultViewerTable(ExplorerManager explorerManager) { super(explorerManager); @@ -94,34 +119,97 @@ public class DataResultViewerTable extends AbstractDataResultViewer { ov.getOutline().setRootVisible(false); ov.getOutline().setDragEnabled(false); - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * The following lines of code were added for this feature. - */ -// ov.getOutline().getColumnModel().addColumnModelListener(new TableColumnModelListener() { -// @Override -// public void columnAdded(TableColumnModelEvent e) {} -// @Override -// public void columnRemoved(TableColumnModelEvent e) {} -// @Override -// public void columnMarginChanged(ChangeEvent e) {} -// @Override -// public void columnSelectionChanged(ListSelectionEvent e) {} -// -// @Override -// public void columnMoved(TableColumnModelEvent e) { -// // change the order of the column in the array/hashset -// List> props = new ArrayList<>(propertiesAcc); -// Node.Property prop = props.remove(e.getFromIndex()); -// props.add(e.getToIndex(), prop); -// -// propertiesAcc.clear(); -// for (int j = 0; j < props.size(); ++j) { -// propertiesAcc.add(props.get(j)); -// } -// } -// }); + // add a listener so that when columns are moved, the new order is stored + ov.getOutline().getColumnModel().addColumnModelListener(new TableColumnModelListener() { + @Override + public void columnAdded(TableColumnModelEvent e) { + } + @Override + public void columnRemoved(TableColumnModelEvent e) { + } + @Override + public void columnMarginChanged(ChangeEvent e) { + } + @Override + public void columnSelectionChanged(ListSelectionEvent e) { + } + @Override + public void columnMoved(TableColumnModelEvent e) { + int fromIndex = e.getFromIndex(); + int toIndex = e.getToIndex(); + if (fromIndex == toIndex) { + return; + } + + /* Because a column may be dragged to several different positions before + * the mouse is released (thus causing multiple TableColumnModelEvents to + * be fired), we want to keep track of the starting column index in this + * potential series of movements. Therefore we only keep track of the + * original fromIndex in startColumnIndex, but we always update + * endColumnIndex to know the final position of the moved column. + * See the MouseListener mouseReleased method. + */ + if (startColumnIndex == -1) { + startColumnIndex = fromIndex; + } + endColumnIndex = toIndex; + + // This array contains the keys of propertiesMap in order + int[] indicesList = new int[propertiesMap.size()]; + int pos = 0; + for (int key : propertiesMap.keySet()) { + indicesList[pos++] = key; + } + int leftIndex = Math.min(fromIndex, toIndex); + int rightIndex = Math.max(fromIndex, toIndex); + // Now we can copy the range of keys that have been affected by + // the column movement + int[] range = Arrays.copyOfRange(indicesList, leftIndex, rightIndex + 1); + int rangeSize = range.length; + + // column moved right, shift all properties left, put in moved + // property at the rightmost index + if (fromIndex < toIndex) { + Property movedProp = propertiesMap.get(range[0]); + for (int i = 0; i < rangeSize - 1; i++) { + propertiesMap.put(range[i], propertiesMap.get(range[i + 1])); + } + propertiesMap.put(range[rangeSize - 1], movedProp); + } + // column moved left, shift all properties right, put in moved + // property at the leftmost index + else { + Property movedProp = propertiesMap.get(range[rangeSize - 1]); + for (int i = rangeSize - 1; i > 0; i--) { + propertiesMap.put(range[i], propertiesMap.get(range[i - 1])); + } + propertiesMap.put(range[0], movedProp); + } + + storeState(); + } + }); + + // add a listener to move columns back if user tries to move the first column out of place + ov.getOutline().getTableHeader().addMouseListener(new MouseAdapter() { + @Override + public void mouseReleased(MouseEvent e) { + /* If the startColumnIndex is not -1 (which is the reset value), that + * means columns have been moved around. We then check to see if either + * the starting or end position is 0 (the first column), and then swap + * them back if that is the case because we don't want to allow movement + * of the first column. We then reset startColumnIndex to -1, the reset + * value. + * We check if startColumnIndex is at reset or not because it is + * possible for the mouse to be released and a MouseEvent to be fired + * without having moved any columns. + */ + if (startColumnIndex != -1 && (startColumnIndex == 0 || endColumnIndex == 0)) { + ov.getOutline().moveColumn(endColumnIndex, startColumnIndex); + } + startColumnIndex = -1; + } + }); } /** @@ -175,68 +263,6 @@ public class DataResultViewerTable extends AbstractDataResultViewer { private javax.swing.JScrollPane tableScrollPanel; // End of variables declaration//GEN-END:variables - /** - * Gets regular Bean property set properties from first child of Node. - * - * @param parent Node with at least one child to get properties from - * - * @return Properties, - */ - private Node.Property[] getChildPropertyHeaders(Node parent) { - Node firstChild = parent.getChildren().getNodeAt(0); - - if (firstChild == null) { - throw new IllegalArgumentException( - NbBundle.getMessage(this.getClass(), "DataResultViewerTable.illegalArgExc.noChildFromParent")); - } else { - for (PropertySet ps : firstChild.getPropertySets()) { - if (ps.getName().equals(Sheet.PROPERTIES)) { - return ps.getProperties(); - } - } - - throw new IllegalArgumentException( - NbBundle.getMessage(this.getClass(), "DataResultViewerTable.illegalArgExc.childWithoutPropertySet")); - } - } - - /** - * Gets regular Bean property set properties from all first children and, - * recursively, subchildren of Node. Note: won't work out the box for lazy - * load - you need to set all children props for the parent by hand - * - * @param parent Node with at least one child to get properties from - * - * @return Properties, - */ - @SuppressWarnings("rawtypes") - private Node.Property[] getAllChildPropertyHeaders(Node parent) { - Node firstChild = parent.getChildren().getNodeAt(0); - - Property[] properties = null; - - if (firstChild == null) { - throw new IllegalArgumentException( - NbBundle.getMessage(this.getClass(), "DataResultViewerTable.illegalArgExc.noChildFromParent")); - } else { - Set allProperties = new LinkedHashSet<>(); - while (firstChild != null) { - for (PropertySet ps : firstChild.getPropertySets()) { - final Property[] props = ps.getProperties(); - final int propsNum = props.length; - for (int i = 0; i < propsNum; ++i) { - allProperties.add(props[i]); - } - } - firstChild = firstChild.getChildren().getNodeAt(0); - } - - properties = allProperties.toArray(new Property[0]); - } - return properties; - - } - /** * Gets regular Bean property set properties from all children and, * recursively, subchildren of Node. Note: won't work out the box for lazy @@ -246,7 +272,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer { * @param rows max number of rows to retrieve properties for (can be used * for memory optimization) */ - private void getAllChildPropertyHeadersRec(Node parent, int rows) { + private void getAllChildPropertyHeadersRec(Node parent, int rows, Set> propertiesAcc) { Children children = parent.getChildren(); int childCount = 0; for (Node child : children.getNodes()) { @@ -260,7 +286,7 @@ public class DataResultViewerTable extends AbstractDataResultViewer { propertiesAcc.add(props[j]); } } - getAllChildPropertyHeadersRec(child, rows); + getAllChildPropertyHeadersRec(child, rows, propertiesAcc); } } @@ -277,11 +303,17 @@ public class DataResultViewerTable extends AbstractDataResultViewer { */ @Override public void setNode(Node selectedNode) { + final OutlineView ov = ((OutlineView) this.tableScrollPanel); + /* The quick filter must be reset because when determining column width, + * ETable.getRowCount is called, and the documentation states that quick + * filters must be unset for the method to work + * "If the quick-filter is applied the number of rows do not match the number of rows in the model." + */ + ov.getOutline().unsetQuickFilter(); // change the cursor to "waiting cursor" for this operation this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); try { boolean hasChildren = false; - if (selectedNode != null) { // @@@ This just did a DB round trip to get the count and the results were not saved... hasChildren = selectedNode.getChildren().getNodesCount() > 0; @@ -299,7 +331,6 @@ public class DataResultViewerTable extends AbstractDataResultViewer { root.addNodeListener(dummyNodeListener); setupTable(root); } else { - final OutlineView ov = ((OutlineView) this.tableScrollPanel); Node emptyNode = new AbstractNode(Children.LEAF); em.setRootContext(emptyNode); // make empty node ov.getOutline().setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS); @@ -319,37 +350,24 @@ public class DataResultViewerTable extends AbstractDataResultViewer { private void setupTable(final Node root) { em.setRootContext(root); - final OutlineView ov = ((OutlineView) this.tableScrollPanel); if (ov == null) { return; } + currentRoot = root; + List> props = loadState(); - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * The next three lines of code replaced the three lines of code that - * follow - */ -// storeState(); - // set the new root as current -// currentRoot = root; -// List> props = loadState(); - propertiesAcc.clear(); - DataResultViewerTable.this.getAllChildPropertyHeadersRec(root, 100); - List> props = new ArrayList<>(propertiesAcc); - - /* + /** * OutlineView makes the first column be the result of * node.getDisplayName with the icon. This duplicates our first column, * which is the file name, etc. So, pop that property off the list, but * use its display name as the header for the column so that the header * can change depending on the type of data being displayed. * - * NOTE: This assumes that the first property is always the one tha - * duplicates getDisplayName(). This seems like a big assumption and - * could be made more robust. + * NOTE: This assumes that the first property is always the one that + * duplicates getDisplayName(). The current implementation does not + * allow the first property column to be moved. */ if (props.size() > 0) { Node.Property prop = props.remove(0); @@ -372,142 +390,176 @@ public class DataResultViewerTable extends AbstractDataResultViewer { ov.setPropertyColumns(propStrings); // show the horizontal scroll panel and show all the content & header - int totalColumns = props.size(); - - //int scrollWidth = ttv.getWidth(); - int margin = 4; - int startColumn = 1; - // If there is only one column (which was removed from props above) // Just let the table resize itself. ov.getOutline().setAutoResizeMode((props.size() > 0) ? JTable.AUTO_RESIZE_OFF : JTable.AUTO_RESIZE_ALL_COLUMNS); - // get first 100 rows values for the table - Object[][] content; - content = getRowValues(root, 100); - - if (content != null) { - // get the fontmetrics + if (root.getChildren().getNodesCount() != 0) { final Graphics graphics = ov.getGraphics(); if (graphics != null) { final FontMetrics metrics = graphics.getFontMetrics(); - // for the "Name" column - int nodeColWidth = Math.min(getMaxColumnWidth(0, metrics, margin, 40, firstColumnLabel, content), 250); // Note: 40 is the width of the icon + node lines. Change this value if those values change! - ov.getOutline().getColumnModel().getColumn(0).setPreferredWidth(nodeColWidth); + int margin = 4; + int padding = 8; - // get the max for each other column - for (int colIndex = startColumn; colIndex <= totalColumns; colIndex++) { - int colWidth = Math.min(getMaxColumnWidth(colIndex, metrics, margin, 8, props, content), 350); - ov.getOutline().getColumnModel().getColumn(colIndex).setPreferredWidth(colWidth); + for (int column = 0; column < ov.getOutline().getModel().getColumnCount(); column++) { + int firstColumnPadding = (column == 0) ? 32 : 0; + int columnWidthLimit = (column == 0) ? 350 : 300; + int valuesWidth = 0; + + // find the maximum width needed to fit the values for the first 100 rows, at most + for (int row = 0; row < Math.min(100, ov.getOutline().getRowCount()); row++) { + TableCellRenderer renderer = ov.getOutline().getCellRenderer(row, column); + Component comp = ov.getOutline().prepareRenderer(renderer, row, column); + valuesWidth = Math.max(comp.getPreferredSize().width, valuesWidth); + } + + int headerWidth = metrics.stringWidth(ov.getOutline().getColumnName(column)); + valuesWidth += firstColumnPadding; // add extra padding for first column + + int columnWidth = Math.max(valuesWidth, headerWidth); + columnWidth += 2 * margin + padding; // add margin and regular padding + columnWidth = Math.min(columnWidth, columnWidthLimit); + + ov.getOutline().getColumnModel().getColumn(column).setPreferredWidth(columnWidth); } } - + } else { // if there's no content just auto resize all columns - if (content.length <= 0) { - // turn on the auto resize - ov.getOutline().setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS); + ov.getOutline().setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS); + } + + /** + * This custom renderer extends the renderer that was already being + * used by the outline table. This renderer colors a row if the + * tags property of the node is not empty. + */ + class ColorTagCustomRenderer extends DefaultOutlineCellRenderer { + private static final long serialVersionUID = 1L; + @Override + public Component getTableCellRendererComponent(JTable table, + Object value, boolean isSelected, boolean hasFocus, int row, int col) { + + Component component = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, col); + // only override the color if a node is not selected + if (!isSelected) { + Node node = currentRoot.getChildren().getNodeAt(table.convertRowIndexToModel(row)); + boolean tagFound = false; + if (node != null) { + Node.PropertySet[] propSets = node.getPropertySets(); + if (propSets.length != 0) { + // currently, a node has only one property set, named Sheet.PROPERTIES ("properties") + Node.Property[] props = propSets[0].getProperties(); + for (Property prop : props) { + if (prop.getName().equals("Tags")) { + try { + tagFound = !prop.getValue().equals(""); + } catch (IllegalAccessException | InvocationTargetException ignore) { + } + break; + } + } + } + } + //if the node does have associated tags, set its background color + if (tagFound) { + component.setBackground(TAGGED_COLOR); + } + } + return component; } } + ov.getOutline().setDefaultRenderer(Object.class, new ColorTagCustomRenderer()); + } + + /** + * Store the current column order into a preference file. + */ + private synchronized void storeState() { + if (currentRoot == null || propertiesMap.isEmpty()) { + return; + } + + TableFilterNode tfn; + if (currentRoot instanceof TableFilterNode) { + tfn = (TableFilterNode) currentRoot; + } else { + return; + } + + // Store the current order of the columns into settings + for (Map.Entry> entry : propertiesMap.entrySet()) { + Property prop = entry.getValue(); + int storeValue = entry.getKey(); + NbPreferences.forModule(this.getClass()).put(getColumnPreferenceKey(prop, tfn.getColumnOrderKey()), String.valueOf(storeValue)); + } } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * The following three methods were added for this feature + /** + * Loads the stored column order from the preference file. + * + * @return a List> of the preferences in order */ - // Store the state of current root Node. -// private void storeState() { -// if(currentRoot == null || propertiesAcc.isEmpty()) -// return; -// -// TableFilterNode tfn; -// if(currentRoot instanceof TableFilterNode) -// tfn = (TableFilterNode) currentRoot; -// else -// return; -// -// List> props = new ArrayList<>(propertiesAcc); -// for (int i = 0; i < props.size(); i++) { -// Property prop = props.get(i); -// NbPreferences.forModule(this.getClass()).put(getUniqueColName(prop, tfn.getItemType()), String.valueOf(i)); -// } -// } - // Load the state of current root Node if exists. -// private List> loadState() { -// propertiesAcc.clear(); -// this.getAllChildPropertyHeadersRec(currentRoot, 100); -// List> props = new ArrayList<>(propertiesAcc); -// -// // If node is not table filter node, use default order for columns -// TableFilterNode tfn; -// if (currentRoot instanceof TableFilterNode) { -// tfn = (TableFilterNode) currentRoot; -// } else { -// Logger.getLogger(DataResultViewerTable.class.getName()).log(Level.INFO, -// "Node {0} is not TableFilterNode, columns are going to be in default order", currentRoot.getName()); -// return props; -// } -// -// List> orderedProps = new ArrayList<>(propertiesAcc); -// for (Property prop : props) { -// Integer value = Integer.valueOf(NbPreferences.forModule(this.getClass()).get(getUniqueColName(prop, tfn.getItemType()), "-1")); -// if (value >= 0) { -// /** -// * The original contents of orderedProps do not matter when -// * setting the new ordered values. The reason we copy -// * propertiesAcc into it first is to give it the currect size so -// * we can set() in any index. -// */ -// orderedProps.set(value, prop); -// } -// } -// propertiesAcc.clear(); -// for (Property prop : orderedProps) { -// propertiesAcc.add(prop); -// } -// return orderedProps; -// } -// -// // Get unique name for node and it's property. -// private String getUniqueColName(Property prop, String type) { -// return Case.getCurrentCase().getName() + "." + type + "." -// + prop.getName().replaceAll("[^a-zA-Z0-9_]", "") + ".columnOrder"; -// } + private synchronized List> loadState() { + // This is a set because we add properties of up to 100 child nodes, and we want unique properties + Set> propertiesAcc = new LinkedHashSet<>(); + this.getAllChildPropertyHeadersRec(currentRoot, 100, propertiesAcc); - // Populate a two-dimensional array with rows of property values for up - // to maxRows children of the node passed in. - private static Object[][] getRowValues(Node node, int maxRows) { - int numRows = Math.min(maxRows, node.getChildren().getNodesCount()); - Object[][] rowValues = new Object[numRows][]; - int rowCount = 0; - for (Node child : node.getChildren().getNodes()) { - if (rowCount >= maxRows) { - break; - } - // BC: I got this once, I think it was because the table - // refreshed while we were in this method - // could be better synchronized. Or it was from - // the lazy nodes updating... Didn't have time - // to fully debug it. - if (rowCount > numRows) { - break; - } - PropertySet[] propertySets = child.getPropertySets(); - if (propertySets.length > 0) { - Property[] properties = propertySets[0].getProperties(); - rowValues[rowCount] = new Object[properties.length]; - for (int j = 0; j < properties.length; ++j) { - try { - rowValues[rowCount][j] = properties[j].getValue(); - } catch (IllegalAccessException | InvocationTargetException ignore) { - rowValues[rowCount][j] = "n/a"; //NON-NLS - } - } - } - ++rowCount; + List> props = new ArrayList<>(propertiesAcc); + + // If node is not table filter node, use default order for columns + TableFilterNode tfn; + if (currentRoot instanceof TableFilterNode) { + tfn = (TableFilterNode) currentRoot; + } else { + // The node is not a TableFilterNode, columns are going to be in default order + return props; } - return rowValues; + + propertiesMap.clear(); + /* + * We load column index values into the properties map. If a property's + * index is outside the range of the number of properties or the index + * has already appeared as the position of another property, we put that + * property at the end. + */ + int offset = props.size(); + boolean noPreviousSettings = true; + for (Property prop : props) { + Integer value = Integer.valueOf(NbPreferences.forModule(this.getClass()).get(getColumnPreferenceKey(prop, tfn.getColumnOrderKey()), "-1")); + if (value >= 0 && value < offset && !propertiesMap.containsKey(value)) { + propertiesMap.put(value, prop); + noPreviousSettings = false; + } else { + propertiesMap.put(offset, prop); + offset++; + } + } + + // If none of the properties had previous settings, we should decrement + // each value by the number of properties to make the values 0-indexed. + if (noPreviousSettings) { + Integer[] keys = propertiesMap.keySet().toArray(new Integer[propertiesMap.keySet().size()]); + for (int key : keys) { + propertiesMap.put(key - props.size(), propertiesMap.get(key)); + propertiesMap.remove(key); + } + } + + return new ArrayList<>(propertiesMap.values()); + } + + /** + * Gets a key for the current node and a property of its child nodes to + * store the column position into a preference file. + * + * @param prop Property of the column + * @param type The type of the current node + * @return A generated key for the preference file + */ + private String getColumnPreferenceKey(Property prop, String type) { + return type.replaceAll("[^a-zA-Z0-9_]", "") + "." + + prop.getName().replaceAll("[^a-zA-Z0-9_]", "") + ".column"; } @Override @@ -520,63 +572,6 @@ public class DataResultViewerTable extends AbstractDataResultViewer { return new DataResultViewerTable(); } - /** - * Gets the max width of the column from the given index, header, and table. - * - * @param index the index of the column on the table / header - * @param metrics the font metrics that this component use - * @param margin the left/right margin of the column - * @param padding the left/right padding of the column - * @param header the property headers of the table - * @param table the object table - * - * @return max the maximum width of the column - */ - @SuppressWarnings("rawtypes") - private int getMaxColumnWidth(int index, FontMetrics metrics, int margin, int padding, List> header, Object[][] table) { - // set the tree (the node / names column) width - String headerName = header.get(index - 1).getDisplayName(); - - return getMaxColumnWidth(index, metrics, margin, padding, headerName, table); - } - - /** - * Gets the max width of the column from the given index, header, and table. - * - * @param index the index of the column on the table / header - * @param metrics the font metrics that this component use - * @param margin the left/right margin of the column - * @param padding the left/right padding of the column - * @param header the column header for the comparison - * @param table the object table - * - * @return max the maximum width of the column - */ - private synchronized int getMaxColumnWidth(int index, FontMetrics metrics, int margin, int padding, String header, Object[][] table) { - // set the tree (the node / names column) width - String headerName = header; - int headerWidth = metrics.stringWidth(headerName); // length of the header - int colWidth = 0; - - // Get maximum width of column data - for (int i = 0; i < table.length; i++) { - if (table[i] == null || index >= table[i].length) { - continue; - } - String test = table[i][index].toString(); - colWidth = Math.max(colWidth, metrics.stringWidth(test)); - } - - colWidth += padding; // add the padding on the most left gap - headerWidth += 8; // add the padding to the header (change this value if the header padding value is changed) - - // Set the width - int width = Math.max(headerWidth, colWidth); - width += 2 * margin; // Add margin - - return width; - } - @Override public void clearComponent() { this.tableScrollPanel.removeAll(); @@ -601,11 +596,8 @@ public class DataResultViewerTable extends AbstractDataResultViewer { if (SwingUtilities.isEventDispatchThread()) { setupTable(nme.getNode()); } else { - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - setupTable(nme.getNode()); - } + SwingUtilities.invokeLater(() -> { + setupTable(nme.getNode()); }); } } diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/TableFilterNode.java b/Core/src/org/sleuthkit/autopsy/corecomponents/TableFilterNode.java index 9a524bcbd3..9c04a003ff 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/TableFilterNode.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/TableFilterNode.java @@ -25,10 +25,13 @@ import org.openide.util.NbBundle; /** * A filter node that creates at most one layer of child nodes for the node it * wraps. It is designed to be used for nodes displayed in Autopsy table views. + * This ensures that the table view for the node will not recursively display + * child nodes and display only the first layer of child nodes. */ public class TableFilterNode extends FilterNode { private final boolean createChildren; + private String columnOrderKey = "NONE"; /** * Constructs a filter node that creates at most one layer of child nodes @@ -38,12 +41,29 @@ public class TableFilterNode extends FilterNode { * @param wrappedNode The node to wrap in the filter node. * @param createChildren True if a children (child factory) object should be * created for the wrapped node. + * The constructor should include column order key. (See getColumnOrderKey) */ public TableFilterNode(Node wrappedNode, boolean createChildren) { super(wrappedNode, TableFilterChildren.createInstance(wrappedNode, createChildren)); this.createChildren = createChildren; } + /** + * Constructs a filter node that has information about the node's type. + * + * @param wrappedNode The node to wrap in the filter node. + * @param createChildren True if a children (child factory) object should be + * created for the wrapped node. + * @param columnOrderKey A key that represents the type of the original + * wrapped node and what is being displayed under that + * node. + */ + public TableFilterNode(Node wrappedNode, boolean createChildren, String columnOrderKey) { + super(wrappedNode, TableFilterChildren.createInstance(wrappedNode, createChildren)); + this.createChildren = createChildren; + this.columnOrderKey = columnOrderKey; + } + /** * Returns a display name for the wrapped node, for use in the first column * of an Autopsy table view. @@ -59,4 +79,14 @@ public class TableFilterNode extends FilterNode { } } + /** + * @return the column order key, which allows custom column ordering to be + * written into a properties file and be reloaded for future use in + * a table with the same root node or for different cases. This is + * done by DataResultViewerTable. The key should represent what + * kinds of items the table is showing. + */ + String getColumnOrderKey() { + return columnOrderKey; + } } 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/AbstractAbstractFileNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java index 0458ec9a59..e3c750826d 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java @@ -1,15 +1,15 @@ /* * Autopsy Forensic Browser - * - * Copyright 2011-2014 Basis Technology Corp. + * + * Copyright 2011-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. @@ -20,17 +20,24 @@ package org.sleuthkit.autopsy.datamodel; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; +import java.util.ArrayList; +import java.util.List; import org.openide.nodes.Children; import java.util.Map; import java.util.logging.Level; +import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; +import org.openide.nodes.Sheet; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; +import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.autopsy.ingest.ModuleContentEvent; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.ContentTag; import org.sleuthkit.datamodel.TskCoreException; /** @@ -103,9 +110,23 @@ public abstract class AbstractAbstractFileNode extends A // case was closed. Remove listeners so that we don't get called with a stale case handle removeListeners(); } + } else if (eventType.equals(Case.Events.CONTENT_TAG_ADDED.toString())) { + ContentTagAddedEvent event = (ContentTagAddedEvent) evt; + if (event.getAddedTag().getContent().equals(content)) { + updateSheet(); + } + } else if (eventType.equals(Case.Events.CONTENT_TAG_DELETED.toString())) { + ContentTagDeletedEvent event = (ContentTagDeletedEvent) evt; + if (event.getDeletedTagInfo().getContentID() == content.getId()) { + updateSheet(); + } } }; + private void updateSheet() { + this.setSheet(createSheet()); + } + // Note: this order matters for the search result, changed it if the order of property headers on the "KeywordSearchNode"changed public static enum AbstractFilePropertyType { @@ -278,6 +299,24 @@ public abstract class AbstractAbstractFileNode extends A map.put(AbstractFilePropertyType.MIMETYPE.toString(), content.getMIMEType() == null ? "" : content.getMIMEType()); } + /** + * Used by subclasses of AbstractAbstractFileNode to add the tags property + * to their sheets. + * @param ss the modifiable Sheet.Set returned by Sheet.get(Sheet.PROPERTIES) + */ + protected void addTagProperty(Sheet.Set ss) { + final String NO_DESCR = NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.addFileProperty.desc"); + List tags; + try { + tags = Case.getCurrentCase().getServices().getTagsManager().getContentTagsByContent(content); + } catch (TskCoreException ex) { + tags = new ArrayList<>(); + LOGGER.log(Level.SEVERE, "Failed to get tags for content " + content.getName(), ex); + } + ss.put(new NodeProperty<>("Tags", NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.addFileProperty.tags.displayName"), + NO_DESCR, tags.stream().map(t -> t.getName().getDisplayName()).collect(Collectors.joining(", ")))); + } + static String getContentDisplayName(AbstractFile file) { String name = file.getName(); switch (name) { diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractContentChildren.java b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractContentChildren.java index f2babbc88e..3559bb79d7 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractContentChildren.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractContentChildren.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2014 Basis Technology Corp. + * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,8 +22,7 @@ import org.openide.nodes.AbstractNode; import org.openide.nodes.Children.Keys; import org.openide.nodes.Node; import org.openide.util.NbBundle; -import org.sleuthkit.autopsy.datamodel.accounts.FileTypeExtensionFilters; -import org.sleuthkit.autopsy.datamodel.accounts.RecentFiles; +import org.sleuthkit.autopsy.datamodel.FileTypes.FileTypesNode; import org.sleuthkit.autopsy.datamodel.accounts.Accounts; import org.sleuthkit.autopsy.datamodel.accounts.Accounts.AccountsRootNode; import org.sleuthkit.datamodel.Content; @@ -33,6 +32,7 @@ import org.sleuthkit.datamodel.File; import org.sleuthkit.datamodel.Image; import org.sleuthkit.datamodel.LayoutFile; import org.sleuthkit.datamodel.LocalFile; +import org.sleuthkit.datamodel.SlackFile; import org.sleuthkit.datamodel.SleuthkitItemVisitor; import org.sleuthkit.datamodel.SleuthkitVisitableItem; import org.sleuthkit.datamodel.VirtualDirectory; @@ -108,6 +108,11 @@ abstract class AbstractContentChildren extends Keys { return new VirtualDirectoryNode(ld); } + @Override + public AbstractContentNode visit(SlackFile sf) { + return new SlackFileNode(sf); + } + @Override protected AbstractContentNode defaultVisit(SleuthkitVisitableItem di) { throw new UnsupportedOperationException(NbBundle.getMessage(this.getClass(), @@ -130,8 +135,8 @@ abstract class AbstractContentChildren extends Keys { } @Override - public AbstractNode visit(FileTypeExtensionFilters sf) { - return new FileTypesNode(sf.getSleuthkitCase(), null); + public AbstractNode visit(FileTypesByExtension sf) { + return new org.sleuthkit.autopsy.datamodel.FileTypesByExtension.FileTypesByExtNode(sf.getSleuthkitCase(), null); } @Override @@ -189,6 +194,11 @@ abstract class AbstractContentChildren extends Keys { return new ResultsNode(r.getSleuthkitCase()); } + @Override + public AbstractNode visit(FileTypes ft) { + return new FileTypesNode(ft.getSleuthkitCase()); + } + @Override public AbstractNode visit(Reports reportsItem) { return new Reports.ReportsListNode(); @@ -205,5 +215,10 @@ abstract class AbstractContentChildren extends Keys { NbBundle.getMessage(this.getClass(), "AbstractContentChildren.createAutopsyNodeVisitor.exception.noNodeMsg")); } + + @Override + public AbstractNode visit(FileTypesByMimeType ftByMimeTypeItem) { + return ftByMimeTypeItem.new ByMimeTypeNode(); + } } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractFsContentNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractFsContentNode.java index 5beca1dbc4..a6db3c1df1 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractFsContentNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractFsContentNode.java @@ -84,6 +84,9 @@ public abstract class AbstractFsContentNode extends Abst ss.put(new NodeProperty<>(HIDE_PARENT, HIDE_PARENT, HIDE_PARENT, HIDE_PARENT)); } + // add tags property to the sheet + addTagProperty(ss); + return s; } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/ArtifactStringContent.java b/Core/src/org/sleuthkit/autopsy/datamodel/ArtifactStringContent.java index 0ea877d9a5..5df9f2756f 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/ArtifactStringContent.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/ArtifactStringContent.java @@ -19,7 +19,6 @@ package org.sleuthkit.autopsy.datamodel; import java.text.SimpleDateFormat; -import java.util.Arrays; import java.util.TimeZone; import java.util.logging.Level; @@ -42,7 +41,7 @@ public class ArtifactStringContent implements StringContent { BlackboardArtifact artifact; private String stringContent = ""; static final Logger logger = Logger.getLogger(ArtifactStringContent.class.getName()); - private static SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + private static final SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public ArtifactStringContent(BlackboardArtifact art) { artifact = art; @@ -74,7 +73,7 @@ public class ArtifactStringContent implements StringContent { buffer.append(""); //NON-NLS buffer.append(attr.getAttributeType().getDisplayName()); buffer.append(""); //NON-NLS - + // value column buffer.append(""); //NON-NLS switch (attr.getAttributeType().getValueType()) { diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/AutopsyItemVisitor.java b/Core/src/org/sleuthkit/autopsy/datamodel/AutopsyItemVisitor.java index d673acb71e..2dcc5942db 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/AutopsyItemVisitor.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/AutopsyItemVisitor.java @@ -19,8 +19,6 @@ package org.sleuthkit.autopsy.datamodel; import org.sleuthkit.autopsy.datamodel.accounts.Accounts; -import org.sleuthkit.autopsy.datamodel.accounts.FileTypeExtensionFilters; -import org.sleuthkit.autopsy.datamodel.accounts.RecentFiles; /** * This visitor goes over the AutopsyVisitableItems, which are currently the @@ -33,13 +31,13 @@ public interface AutopsyItemVisitor { T visit(Views v); - T visit(FileTypeExtensionFilters sf); + T visit(FileTypesByExtension sf); - T visit(FileTypeExtensionFilters.RootFilter fsf); + T visit(FileTypesByExtension.RootFilter fsf); - T visit(FileTypeExtensionFilters.DocumentFilter df); + T visit(FileTypesByExtension.DocumentFilter df); - T visit(FileTypeExtensionFilters.ExecutableFilter ef); + T visit(FileTypesByExtension.ExecutableFilter ef); T visit(RecentFiles rf); @@ -71,6 +69,11 @@ public interface AutopsyItemVisitor { T visit(Accounts accountsItem); + T visit(FileTypes fileTypesItem); + + T visit(FileTypesByMimeType aThis); + + static abstract public class Default implements AutopsyItemVisitor { protected abstract T defaultVisit(AutopsyVisitableItem ec); @@ -81,25 +84,30 @@ public interface AutopsyItemVisitor { } @Override - public T visit(FileTypeExtensionFilters sf) { + public T visit(FileTypesByExtension sf) { return defaultVisit(sf); } @Override - public T visit(FileTypeExtensionFilters.RootFilter fsf) { + public T visit(FileTypesByExtension.RootFilter fsf) { return defaultVisit(fsf); } @Override - public T visit(FileTypeExtensionFilters.DocumentFilter df) { + public T visit(FileTypesByExtension.DocumentFilter df) { return defaultVisit(df); } @Override - public T visit(FileTypeExtensionFilters.ExecutableFilter ef) { + public T visit(FileTypesByExtension.ExecutableFilter ef) { return defaultVisit(ef); } - + + @Override + public T visit(FileTypesByMimeType ftByMimeType) { + return defaultVisit(ftByMimeType); + } + @Override public T visit(DeletedContent dc) { return defaultVisit(dc); @@ -170,6 +178,12 @@ public interface AutopsyItemVisitor { return defaultVisit(r); } + + @Override + public T visit(FileTypes ft) { + return defaultVisit(ft); + } + @Override public T visit(Reports reportsItem) { return defaultVisit(reportsItem); diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/AutopsyVisitableItem.java b/Core/src/org/sleuthkit/autopsy/datamodel/AutopsyVisitableItem.java index 90359bf055..6aaa05af94 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/AutopsyVisitableItem.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/AutopsyVisitableItem.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011 Basis Technology Corp. + * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java index 788d6969ec..7d15bb2101 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java @@ -18,6 +18,8 @@ */ package org.sleuthkit.autopsy.datamodel; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; @@ -25,6 +27,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; +import java.util.stream.Collectors; import javax.swing.Action; import org.apache.commons.lang3.StringUtils; import org.openide.nodes.Children; @@ -35,6 +38,10 @@ import org.openide.util.lookup.Lookups; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import org.sleuthkit.autopsy.casemodule.events.BlackBoardArtifactTagAddedEvent; +import org.sleuthkit.autopsy.casemodule.events.BlackBoardArtifactTagDeletedEvent; +import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent; +import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent; import org.sleuthkit.autopsy.timeline.actions.ViewArtifactInTimelineAction; import org.sleuthkit.autopsy.timeline.actions.ViewFileInTimelineAction; import org.sleuthkit.datamodel.AbstractFile; @@ -43,6 +50,7 @@ import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE; import org.sleuthkit.datamodel.BlackboardAttribute; import org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE; import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.Tag; import org.sleuthkit.datamodel.TskCoreException; /** @@ -70,6 +78,39 @@ public class BlackboardArtifactNode extends DisplayableItemNode { private static final Integer[] SHOW_FILE_METADATA = new Integer[]{ BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT.getTypeID(),}; + private final PropertyChangeListener pcl = new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent evt) { + String eventType = evt.getPropertyName(); + if (eventType.equals(Case.Events.BLACKBOARD_ARTIFACT_TAG_ADDED.toString())) { + BlackBoardArtifactTagAddedEvent event = (BlackBoardArtifactTagAddedEvent) evt; + if (event.getAddedTag().getArtifact().equals(artifact)) { + updateSheet(); + } + } else if (eventType.equals(Case.Events.BLACKBOARD_ARTIFACT_TAG_DELETED.toString())) { + BlackBoardArtifactTagDeletedEvent event = (BlackBoardArtifactTagDeletedEvent) evt; + if (event.getDeletedTagInfo().getArtifactID() == artifact.getArtifactID()) { + updateSheet(); + } + } else if (eventType.equals(Case.Events.CONTENT_TAG_ADDED.toString())) { + ContentTagAddedEvent event = (ContentTagAddedEvent) evt; + if (event.getAddedTag().getContent().equals(associated)) { + updateSheet(); + } + } else if (eventType.equals(Case.Events.CONTENT_TAG_DELETED.toString())) { + ContentTagDeletedEvent event = (ContentTagDeletedEvent) evt; + if (event.getDeletedTagInfo().getContentID()== associated.getId()) { + updateSheet(); + } + } else if (eventType.equals(Case.Events.CURRENT_CASE.toString())) { + if (evt.getNewValue() == null) { + // case was closed. Remove listeners so that we don't get called with a stale case handle + removeListeners(); + } + } + } + }; + /** * Construct blackboard artifact node from an artifact and using provided * icon @@ -86,6 +127,7 @@ public class BlackboardArtifactNode extends DisplayableItemNode { this.setName(Long.toString(artifact.getArtifactID())); this.setDisplayName(); this.setIconBaseWithExtension(iconPath); + Case.addPropertyChangeListener(pcl); } /** @@ -103,6 +145,11 @@ public class BlackboardArtifactNode extends DisplayableItemNode { this.setName(Long.toString(artifact.getArtifactID())); this.setDisplayName(); this.setIconBaseWithExtension(ExtractedContent.getIconFilePath(artifact.getArtifactTypeID())); //NON-NLS + Case.addPropertyChangeListener(pcl); + } + + private void removeListeners() { + Case.removePropertyChangeListener(pcl); } @Override @@ -157,7 +204,7 @@ public class BlackboardArtifactNode extends DisplayableItemNode { displayName = associated.getName(); } - // If this is a node for a keyword hit on an artifact, we set the + // If this is a node for a keyword hit on an artifact, we set the // display name to be the artifact type name followed by " Artifact" // e.g. "Messages Artifact". if (artifact != null && artifact.getArtifactTypeID() == ARTIFACT_TYPE.TSK_KEYWORD_HIT.getTypeID()) { @@ -208,6 +255,7 @@ public class BlackboardArtifactNode extends DisplayableItemNode { ss.put(np); } } + final int artifactTypeId = artifact.getArtifactTypeID(); // If mismatch, add props for extension and file type @@ -294,9 +342,24 @@ public class BlackboardArtifactNode extends DisplayableItemNode { } } + // add properties for tags + List tags = new ArrayList<>(); + try { + tags.addAll(Case.getCurrentCase().getServices().getTagsManager().getBlackboardArtifactTagsByArtifact(artifact)); + tags.addAll(Case.getCurrentCase().getServices().getTagsManager().getContentTagsByContent(associated)); + } catch (TskCoreException ex) { + LOGGER.log(Level.SEVERE, "Failed to get tags for artifact " + artifact.getDisplayName(), ex); + } + ss.put(new NodeProperty<>("Tags", NbBundle.getMessage(AbstractAbstractFileNode.class, "BlackboardArtifactNode.createSheet.tags.displayName"), + NO_DESCR, tags.stream().map(t -> t.getName().getDisplayName()).collect(Collectors.joining(", ")))); + return s; } + private void updateSheet() { + this.setSheet(createSheet()); + } + private String getRootParentName() { String parentName = associated.getName(); Content parent = associated; @@ -448,13 +511,8 @@ public class BlackboardArtifactNode extends DisplayableItemNode { return true; } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "BlackboardArtifact"; //NON-NLS -// } + @Override + public String getItemType() { + return getClass().getName(); + } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactTagNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactTagNode.java index fad92a6403..4647298931 100755 --- a/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactTagNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactTagNode.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-2014 Basis Technology Corp. + * Copyright 2013-2016 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -145,13 +145,8 @@ public class BlackboardArtifactTagNode extends DisplayableItemNode { return true; } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "BlackboardArtifactTag"; //NON-NLS -// } + @Override + public String getItemType() { + return getClass().getName(); + } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties b/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties index 3c24e02283..cbe816ffe7 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties @@ -118,16 +118,16 @@ FileSize.createSheet.filterType.displayName=Filter Type FileSize.createSheet.filterType.desc=no description FileSize.exception.notSupported.msg=Not supported for this type of Displayable Item\: {0} FileTypeChildren.exception.notSupported.msg=Not supported for this type of Displayable Item\: {0} -FileTypeNode.createSheet.filterType.name=Filter Type -FileTypeNode.createSheet.filterType.displayName=Filter Type -FileTypeNode.createSheet.filterType.desc=no description -FileTypeNode.createSheet.fileExt.name=File Extensions -FileTypeNode.createSheet.fileExt.displayName=File Extensions -FileTypeNode.createSheet.fileExt.desc=no description -FileTypesNode.fname.text=File Types -FileTypesNode.createSheet.name.name=Name -FileTypesNode.createSheet.name.displayName=Name -FileTypesNode.createSheet.name.desc=no description +FileTypesByExtNode.createSheet.filterType.name=Filter Type +FileTypesByExtNode.createSheet.filterType.displayName=Filter Type +FileTypesByExtNode.createSheet.filterType.desc=no description +FileTypesByExtNode.createSheet.fileExt.name=File Extensions +FileTypesByExtNode.createSheet.fileExt.displayName=File Extensions +FileTypesByExtNode.createSheet.fileExt.desc=no description +FileTypesByExtNode.fname.text=By Extension +FileTypesByExtNode.createSheet.name.name=Name +FileTypesByExtNode.createSheet.name.displayName=Name +FileTypesByExtNode.createSheet.name.desc=no description HashsetHits.createSheet.name.name=Name HashsetHits.createSheet.name.displayName=Name HashsetHits.createSheet.name.desc=no description @@ -222,6 +222,10 @@ ReportNode.reportNameProperty.name=Report Name ReportNode.reportNameProperty.displayName=Report Name ReportNode.reportNameProperty.desc=Name of the report ReportsListNode.displayName=Reports +SlackFileFilterNode.selectionContext.dataSources=Data Sources +SlackFileFilterNode.selectionContext.views=Views +SlackFileNode.getActions.viewInNewWin.text=View in New Window +SlackFileNode.getActions.viewFileInDir.text=View File in Directory TagNameNode.namePlusTags.text={0} Tags TagNameNode.contentTagTypeNodeKey.text=Content Tags TagNameNode.bbArtTagTypeNodeKey.text=Result Tags @@ -266,3 +270,19 @@ DeleteReportAction.actionDisplayName.multipleReports=Delete Reports DeleteReportAction.actionPerformed.showConfirmDialog.title=Confirm Deletion DeleteReportAction.actionPerformed.showConfirmDialog.single.msg=Do you want to delete 1 report from the case? DeleteReportAction.actionPerformed.showConfirmDialog.multiple.msg=Do you want to delete {0} reports from the case? +AbstractAbstractFileNode.addFileProperty.desc=no description +AbstractAbstractFileNode.addFileProperty.tags.name=Tags +AbstractAbstractFileNode.addFileProperty.tags.displayName=Tags +BlackboardArtifactNode.createSheet.tags.name=Tags +BlackboardArtifactNode.createSheet.tags.displayName=Tags +FileTypeExtensionFilters.tskImgFilter.text=Images +FileTypeExtensionFilters.tskVideoFilter.text=Videos +FileTypeExtensionFilters.tskAudioFilter.text=Audio +FileTypeExtensionFilters.tskArchiveFilter.text=Archives +FileTypeExtensionFilters.tskDocumentFilter.text=Documents +FileTypeExtensionFilters.tskExecFilter.text=Executable +FileTypeExtensionFilters.autDocHtmlFilter.text=HTML +FileTypeExtensionFilters.autDocOfficeFilter.text=Office +FileTypeExtensionFilters.autoDocPdfFilter.text=PDF +FileTypeExtensionFilters.autDocTxtFilter.text=Plain Text +FileTypeExtensionFilters.autDocRtfFilter.text=Rich Text \ No newline at end of file diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/Bundle_ja.properties b/Core/src/org/sleuthkit/autopsy/datamodel/Bundle_ja.properties index 036ff29159..4dfb781a45 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/Bundle_ja.properties +++ b/Core/src/org/sleuthkit/autopsy/datamodel/Bundle_ja.properties @@ -112,16 +112,15 @@ FileTypeExtensionFilters.autDocOfficeFilter.text=\u30aa\u30d5\u30a3\u30b9 FileTypeExtensionFilters.autoDocPdfFilter.text=PDF FileTypeExtensionFilters.autDocTxtFilter.text=\u30d7\u30ec\u30fc\u30f3\u30c6\u30ad\u30b9\u30c8 FileTypeExtensionFilters.autDocRtfFilter.text=\u30ea\u30c3\u30c1\u30c6\u30ad\u30b9\u30c8 -FileTypeNode.createSheet.filterType.name=\u30d5\u30a3\u30eb\u30bf\u30fc\u30bf\u30a4\u30d7 -FileTypeNode.createSheet.filterType.displayName=\u30d5\u30a3\u30eb\u30bf\u30fc\u30bf\u30a4\u30d7 -FileTypeNode.createSheet.filterType.desc=\u8aac\u660e\u304c\u3042\u308a\u307e\u305b\u3093 -FileTypeNode.createSheet.fileExt.name=\u30d5\u30a1\u30a4\u30eb\u62e1\u5f35\u5b50 -FileTypeNode.createSheet.fileExt.displayName=\u30d5\u30a1\u30a4\u30eb\u62e1\u5f35\u5b50 -FileTypeNode.createSheet.fileExt.desc=\u8aac\u660e\u304c\u3042\u308a\u307e\u305b\u3093 -FileTypesNode.fname.text=\u30d5\u30a1\u30a4\u30eb\u30bf\u30a4\u30d7 -FileTypesNode.createSheet.name.name=\u540d\u524d -FileTypesNode.createSheet.name.displayName=\u540d\u524d -FileTypesNode.createSheet.name.desc=\u8aac\u660e\u304c\u3042\u308a\u307e\u305b\u3093 +FileTypesByExtNode.createSheet.filterType.name=\u30d5\u30a3\u30eb\u30bf\u30fc\u30bf\u30a4\u30d7 +FileTypesByExtNode.createSheet.filterType.displayName=\u30d5\u30a3\u30eb\u30bf\u30fc\u30bf\u30a4\u30d7 +FileTypesByExtNode.createSheet.filterType.desc=\u8aac\u660e\u304c\u3042\u308a\u307e\u305b\u3093 +FileTypesByExtNode.createSheet.fileExt.name=\u30d5\u30a1\u30a4\u30eb\u62e1\u5f35\u5b50 +FileTypesByExtNode.createSheet.fileExt.displayName=\u30d5\u30a1\u30a4\u30eb\u62e1\u5f35\u5b50 +FileTypesByExtNode.createSheet.fileExt.desc=\u8aac\u660e\u304c\u3042\u308a\u307e\u305b\u3093 +FileTypesByExtNode.createSheet.name.name=\u540d\u524d +FileTypesByExtNode.createSheet.name.displayName=\u540d\u524d +FileTypesByExtNode.createSheet.name.desc=\u8aac\u660e\u304c\u3042\u308a\u307e\u305b\u3093 HashsetHits.createSheet.name.name=\u540d\u524d HashsetHits.createSheet.name.displayName=\u540d\u524d HashsetHits.createSheet.name.desc=\u8aac\u660e\u304c\u3042\u308a\u307e\u305b\u3093 diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/ContentNodeVisitor.java b/Core/src/org/sleuthkit/autopsy/datamodel/ContentNodeVisitor.java index fdda586414..6dbd8002ee 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/ContentNodeVisitor.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/ContentNodeVisitor.java @@ -41,6 +41,8 @@ interface ContentNodeVisitor { T visit(LayoutFileNode lcn); T visit(LocalFileNode dfn); + + T visit(SlackFileNode sfn); /** * Visitor with an implementable default behavior for all types. Override @@ -93,5 +95,10 @@ interface ContentNodeVisitor { public T visit(VirtualDirectoryNode ldn) { return defaultVisit(ldn); } + + @Override + public T visit(SlackFileNode sfn) { + return defaultVisit(sfn); + } } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/ContentTagNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/ContentTagNode.java index b444551a58..74ab98d517 100755 --- a/Core/src/org/sleuthkit/autopsy/datamodel/ContentTagNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/ContentTagNode.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013 Basis Technology Corp. + * Copyright 2013-2016 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -42,6 +42,8 @@ import org.sleuthkit.datamodel.TskCoreException; */ class ContentTagNode extends DisplayableItemNode { + private static final Logger LOGGER = Logger.getLogger(ContentTagNode.class.getName()); + private static final String ICON_PATH = "org/sleuthkit/autopsy/images/blue-tag-icon-16.png"; //NON-NLS private final ContentTag tag; @@ -60,7 +62,7 @@ class ContentTagNode extends DisplayableItemNode { try { contentPath = content.getUniquePath(); } catch (TskCoreException ex) { - Logger.getLogger(ContentTagNode.class.getName()).log(Level.SEVERE, "Failed to get path for content (id = " + content.getId() + ")", ex); //NON-NLS + LOGGER.log(Level.SEVERE, "Failed to get path for content (id = " + content.getId() + ")", ex); //NON-NLS contentPath = NbBundle.getMessage(this.getClass(), "ContentTagNode.createSheet.unavail.path"); } AbstractFile file = content instanceof AbstractFile ? (AbstractFile) content : null; @@ -103,6 +105,7 @@ class ContentTagNode extends DisplayableItemNode { NbBundle.getMessage(this.getClass(), "ContentTagNode.createSheet.fileSize.displayName"), "", content.getSize())); + return propertySheet; } @@ -115,7 +118,7 @@ class ContentTagNode extends DisplayableItemNode { if (file != null) { actions.add(ViewFileInTimelineAction.createViewFileAction(file)); } - actions.add(null); // Adds a menu item separator. + actions.add(null); // Adds a menu item separator. actions.add(DeleteContentTagAction.getInstance()); return actions.toArray(new Action[actions.size()]); } @@ -130,13 +133,8 @@ class ContentTagNode extends DisplayableItemNode { return true; } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "ContentTag"; //NON-NLS -// } + @Override + public String getItemType() { + return getClass().getName(); + } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/ContentUtils.java b/Core/src/org/sleuthkit/autopsy/datamodel/ContentUtils.java index cd127d5276..1b457966fe 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/ContentUtils.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/ContentUtils.java @@ -43,6 +43,7 @@ import org.sleuthkit.datamodel.Image; import org.sleuthkit.datamodel.LayoutFile; import org.sleuthkit.datamodel.LocalFile; import org.sleuthkit.datamodel.ReadContentInputStream; +import org.sleuthkit.datamodel.SlackFile; import org.sleuthkit.datamodel.TskException; import org.sleuthkit.datamodel.VirtualDirectory; @@ -355,6 +356,18 @@ public final class ContentUtils { } return null; } + + @Override + public Void visit(SlackFile f) { + try { + ContentUtils.writeToFile(f, dest, progress, worker, source); + } catch (IOException ex) { + logger.log(Level.SEVERE, + "Trouble extracting slack file to " + dest.getAbsolutePath(), //NON-NLS + ex); + } + return null; + } @Override public Void visit(Directory dir) { diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/DataModelActionsFactory.java b/Core/src/org/sleuthkit/autopsy/datamodel/DataModelActionsFactory.java index 6f47d94093..7ab744d712 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/DataModelActionsFactory.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/DataModelActionsFactory.java @@ -37,6 +37,7 @@ import org.sleuthkit.datamodel.Directory; import org.sleuthkit.datamodel.File; import org.sleuthkit.datamodel.LayoutFile; import org.sleuthkit.datamodel.LocalFile; +import org.sleuthkit.datamodel.SlackFile; import org.sleuthkit.datamodel.VirtualDirectory; /** @@ -80,6 +81,23 @@ public class DataModelActionsFactory { actions.addAll(ContextMenuExtensionPoint.getActions()); return actions; } + + public static List getActions(SlackFile slackFile, boolean isArtifactSource) { + List actions = new ArrayList<>(); + actions.add(new ViewContextAction((isArtifactSource ? VIEW_SOURCE_FILE_IN_DIR : VIEW_FILE_IN_DIR), slackFile)); + final SlackFileNode slackFileNode = new SlackFileNode(slackFile); + actions.add(null); // creates a menu separator + actions.add(new NewWindowViewAction(VIEW_IN_NEW_WINDOW, slackFileNode)); + actions.add(null); // creates a menu separator + actions.add(ExtractAction.getInstance()); + actions.add(null); // creates a menu separator + actions.add(AddContentTagAction.getInstance()); + if (isArtifactSource) { + actions.add(AddBlackboardArtifactTagAction.getInstance()); + } + actions.addAll(ContextMenuExtensionPoint.getActions()); + return actions; + } public static List getActions(LayoutFile file, boolean isArtifactSource) { List actions = new ArrayList<>(); @@ -184,6 +202,8 @@ public class DataModelActionsFactory { return getActions((LocalFile) content, isArtifactSource); } else if (content instanceof DerivedFile) { return getActions((DerivedFile) content, isArtifactSource); + } else if (content instanceof SlackFile) { + return getActions((SlackFile) content, isArtifactSource); } else { return new ArrayList<>(); } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/DataSourcesNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/DataSourcesNode.java index 48bfc74443..7ac9e37398 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/DataSourcesNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/DataSourcesNode.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2014 Basis Technology Corp. + * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,6 +23,7 @@ import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.logging.Level; import org.openide.nodes.Sheet; import org.openide.util.NbBundle; import org.openide.util.lookup.Lookups; @@ -56,15 +57,10 @@ public class DataSourcesNode extends DisplayableItemNode { this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/image.png"); //NON-NLS } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "DataSources"; //NON-NLS -// } + @Override + public String getItemType() { + return getClass().getName(); + } /* * Custom Keys implementation that listens for new data sources being added. @@ -108,7 +104,7 @@ public class DataSourcesNode extends DisplayableItemNode { currentKeys = Case.getCurrentCase().getDataSources(); setKeys(currentKeys); } catch (TskCoreException | IllegalStateException ex) { - logger.severe("Error getting data sources: " + ex.getMessage()); // NON-NLS + logger.log(Level.SEVERE, "Error getting data sources: {0}", ex.getMessage()); // NON-NLS setKeys(Collections.emptySet()); } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/DeletedContent.java b/Core/src/org/sleuthkit/autopsy/datamodel/DeletedContent.java index 3a1e70b164..673486db93 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/DeletedContent.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/DeletedContent.java @@ -147,15 +147,10 @@ public class DeletedContent implements AutopsyVisitableItem { return s; } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "DeletedContent"; //NON-NLS -// } + @Override + public String getItemType() { + return getClass().getName(); + } } public static class DeletedContentsChildren extends ChildFactory { @@ -285,16 +280,6 @@ public class DeletedContent implements AutopsyVisitableItem { updateDisplayName(); } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering - * code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "DeletedContentChildren"; //NON-NLS -// } // update the display name when new events are fired private class DeletedContentNodeObserver implements Observer { @@ -338,8 +323,17 @@ public class DeletedContent implements AutopsyVisitableItem { public boolean isLeafTypeNode() { return true; } - } + @Override + public String getItemType() { + /** + * Return getClass().getName() + filter.getName() if custom + * settings are desired for different filters. + */ + return DisplayableItemNode.FILE_PARENT_NODE_KEY; + } + } + static class DeletedContentChildren extends ChildFactory.Detachable { private final SleuthkitCase skCase; diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/DirectoryNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/DirectoryNode.java index f15eefd22c..59240d204a 100755 --- a/Core/src/org/sleuthkit/autopsy/datamodel/DirectoryNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/DirectoryNode.java @@ -105,13 +105,8 @@ public class DirectoryNode extends AbstractFsContentNode { return false; } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "Directory"; //NON-NLS -// } + @Override + public String getItemType() { + return DisplayableItemNode.FILE_PARENT_NODE_KEY; + } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNode.java index 2bc0a5538a..2e9a042879 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNode.java @@ -33,6 +33,8 @@ import org.sleuthkit.datamodel.TskCoreException; */ public abstract class DisplayableItemNode extends AbstractNode { + final static String FILE_PARENT_NODE_KEY = "orgsleuthkitautopsydatamodel" + "FileTypeParentNode"; + public DisplayableItemNode(Children children) { super(children); } @@ -45,18 +47,22 @@ public abstract class DisplayableItemNode extends AbstractNode { public abstract T accept(DisplayableItemNodeVisitor v); - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code + /** + * Returns type of DisplayableItemNode to allow TableFilterNode to pass + * the information to DataResultViewerTable to allow custom settings for + * column orderings. * - * Added to support this feature. + * @return A String representing the type of node, based on its name and + * whether or not it wraps any special items (filters, artifact + * types). */ -// public abstract String getItemType(); + public abstract String getItemType(); + /** * this code started as a cut and past of * DataResultFilterNode.GetPopupActionsDisplayableItemNodeVisitor.findLinked(BlackboardArtifactNode * ba) * - * * @param artifact * * @return diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNodeVisitor.java b/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNodeVisitor.java index e300ace134..2b9fb9f2b8 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNodeVisitor.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNodeVisitor.java @@ -22,6 +22,7 @@ import org.sleuthkit.autopsy.datamodel.DeletedContent.DeletedContentsChildren.De import org.sleuthkit.autopsy.datamodel.DeletedContent.DeletedContentsNode; import org.sleuthkit.autopsy.datamodel.FileSize.FileSizeRootChildren.FileSizeNode; import org.sleuthkit.autopsy.datamodel.FileSize.FileSizeRootNode; +import org.sleuthkit.autopsy.datamodel.FileTypes.FileTypesNode; import org.sleuthkit.autopsy.datamodel.accounts.Accounts; /** @@ -49,12 +50,15 @@ public interface DisplayableItemNodeVisitor { T visit(VolumeNode vn); + T visit(SlackFileNode sfn); + + /* * Views Area */ T visit(ViewsNode vn); - T visit(FileTypeNode fsfn); + T visit(org.sleuthkit.autopsy.datamodel.FileTypesByExtension.FileExtensionNode fsfn); T visit(DeletedContentNode dcn); @@ -64,7 +68,7 @@ public interface DisplayableItemNodeVisitor { T visit(FileSizeNode fsn); - T visit(FileTypesNode sfn); + T visit(org.sleuthkit.autopsy.datamodel.FileTypesByExtension.FileTypesByExtNode sfn); T visit(RecentFilesNode rfn); @@ -140,6 +144,16 @@ public interface DisplayableItemNodeVisitor { T visit(Accounts.DefaultAccountTypeNode node); + T visit(FileTypes.FileTypesNode fileTypes); + + T visit(FileTypesByMimeType.ByMimeTypeNode ftByMimeTypeNode); + + T visit(FileTypesByMimeType.MediaTypeNode ftByMimeTypeMediaType); + + T visit(FileTypesByMimeType.MediaSubTypeNode ftByMimeTypeMediaSubType); + + T visit(EmptyNode.MessageNode emptyNode); + /** * Visitor with an implementable default behavior for all types. Override * specific visit types to not use the default behavior. @@ -177,6 +191,11 @@ public interface DisplayableItemNodeVisitor { return defaultVisit(vn); } + @Override + public T visit(SlackFileNode sfn) { + return defaultVisit(sfn); + } + @Override public T visit(BlackboardArtifactNode ban) { return defaultVisit(ban); @@ -193,10 +212,30 @@ public interface DisplayableItemNodeVisitor { } @Override - public T visit(FileTypeNode fsfn) { + public T visit(org.sleuthkit.autopsy.datamodel.FileTypesByExtension.FileExtensionNode fsfn) { return defaultVisit(fsfn); } + @Override + public T visit(FileTypesByMimeType.ByMimeTypeNode ftByMimeTypeNode) { + return defaultVisit(ftByMimeTypeNode); + } + + @Override + public T visit(FileTypesByMimeType.MediaTypeNode ftByMimeTypeMediaTypeNode) { + return defaultVisit(ftByMimeTypeMediaTypeNode); + } + + @Override + public T visit(FileTypesByMimeType.MediaSubTypeNode ftByMimeTypeMediaTypeNode) { + return defaultVisit(ftByMimeTypeMediaTypeNode); + } + + @Override + public T visit(EmptyNode.MessageNode ftByMimeTypeEmptyNode) { + return defaultVisit(ftByMimeTypeEmptyNode); + } + @Override public T visit(DeletedContentNode dcn) { return defaultVisit(dcn); @@ -218,7 +257,7 @@ public interface DisplayableItemNodeVisitor { } @Override - public T visit(FileTypesNode sfn) { + public T visit(org.sleuthkit.autopsy.datamodel.FileTypesByExtension.FileTypesByExtNode sfn) { return defaultVisit(sfn); } @@ -257,6 +296,11 @@ public interface DisplayableItemNodeVisitor { return defaultVisit(rn); } + @Override + public T visit(FileTypesNode ft) { + return defaultVisit(ft); + } + @Override public T visit(DataSourcesNode in) { return defaultVisit(in); @@ -381,6 +425,7 @@ public interface DisplayableItemNodeVisitor { public T visit(Accounts.BINNode node) { return defaultVisit(node); } + @Override public T visit(Accounts.DefaultAccountTypeNode node) { return defaultVisit(node); diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/EmailExtracted.java b/Core/src/org/sleuthkit/autopsy/datamodel/EmailExtracted.java index 89e8c5b217..9f8d3d9bd1 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/EmailExtracted.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/EmailExtracted.java @@ -204,15 +204,10 @@ public class EmailExtracted implements AutopsyVisitableItem { return s; } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "EmailExtractedRoot"; //NON-NLS -// } + @Override + public String getItemType() { + return getClass().getName(); + } } /** @@ -364,15 +359,10 @@ public class EmailExtracted implements AutopsyVisitableItem { updateDisplayName(); } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "EmailExtractedAccount"; //NON-NLS -// } + @Override + public String getItemType() { + return getClass().getName(); + } } /** @@ -460,15 +450,10 @@ public class EmailExtracted implements AutopsyVisitableItem { updateDisplayName(); } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "EmailExtractedFolder"; //NON-NLS -// } + @Override + public String getItemType() { + return getClass().getName(); + } } /** diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/EmptyNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/EmptyNode.java new file mode 100644 index 0000000000..c822743c23 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datamodel/EmptyNode.java @@ -0,0 +1,97 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2011-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.datamodel; + +import java.util.List; +import org.openide.nodes.AbstractNode; +import org.openide.nodes.ChildFactory; +import org.openide.nodes.Children; +import org.openide.nodes.Node; + +/** + * Provides a root node for the results views with a single child node that + * displays a message as the sole item in its property sheet, useful for + * displaying explanatory text in the result views when there is a node with no + * children in the tree view. + */ +public final class EmptyNode extends AbstractNode { + + /** + * Provides a root node for the results views with a single child node that + * displays a message as the sole item in its property sheet, useful for + * displaying explanatory text in the result views when there is a node with + * no children in the tree view. + * + * @param displayedMessage The text for the property sheet of the child + * node. + */ + public EmptyNode(String displayedMessage) { + super(Children.create(new EmptyNodeChildren(displayedMessage), true)); + } + + static class EmptyNodeChildren extends ChildFactory { + + String displayedMessage; + + private EmptyNodeChildren(String displayedMessage) { + this.displayedMessage = displayedMessage; + } + + @Override + protected boolean createKeys(List keys) { + keys.add(displayedMessage); + return true; + } + + @Override + protected Node createNodeForKey(String key) { + return new MessageNode(key); + } + + } + + /** + * The single child node of an EmptyNode, responsible for displaying a + * message as the sole item in its property sheet. + */ + static class MessageNode extends DisplayableItemNode { + + MessageNode(String name) { + super(Children.LEAF); + super.setName(name); + setName(name); + setDisplayName(name); + } + + @Override + public boolean isLeafTypeNode() { + return true; + } + + @Override + public T accept(DisplayableItemNodeVisitor v) { + return v.visit(this); + } + + @Override + public String getItemType() { + return getClass().getName(); + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/ExtractedContent.java b/Core/src/org/sleuthkit/autopsy/datamodel/ExtractedContent.java index 4a420f926d..896cec5322 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/ExtractedContent.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/ExtractedContent.java @@ -169,15 +169,10 @@ public class ExtractedContent implements AutopsyVisitableItem { return s; } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "ExtractedContentRoot"; //NON-NLS -// } + @Override + public String getItemType() { + return getClass().getName(); + } } /** @@ -374,15 +369,10 @@ public class ExtractedContent implements AutopsyVisitableItem { return true; } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return type.getDisplayName(); -// } + @Override + public String getItemType() { + return getClass().getName() + type.getDisplayName(); + } } /** diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/FileNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/FileNode.java index b722e33161..7ee5acf08f 100755 --- a/Core/src/org/sleuthkit/autopsy/datamodel/FileNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/FileNode.java @@ -170,13 +170,8 @@ public class FileNode extends AbstractFsContentNode { return true; } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "File"; //NON-NLS -// } + @Override + public String getItemType() { + return getClass().getName(); + } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/FileSize.java b/Core/src/org/sleuthkit/autopsy/datamodel/FileSize.java index c5fee7d696..b31d2e4fd4 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/FileSize.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/FileSize.java @@ -144,15 +144,10 @@ public class FileSize implements AutopsyVisitableItem { return s; } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "FileSizeRoot"; //NON-NLS -// } + @Override + public String getItemType() { + return getClass().getName(); + } } /* @@ -291,16 +286,15 @@ public class FileSize implements AutopsyVisitableItem { updateDisplayName(); } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering - * code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "FileSize"; //NON-NLS -// } + @Override + public String getItemType() { + /** + * Return getClass().getName() + filter.getName() if custom + * settings are desired for different filters. + */ + return DisplayableItemNode.FILE_PARENT_NODE_KEY; + } + // update the display name when new events are fired private class FileSizeNodeObserver implements Observer { diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/FileTypeExtensions.java b/Core/src/org/sleuthkit/autopsy/datamodel/FileTypeExtensions.java index d9cd01b791..4195b33008 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/FileTypeExtensions.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/FileTypeExtensions.java @@ -1,15 +1,15 @@ /* * Autopsy Forensic Browser - * - * Copyright 2011-2013 Basis Technology Corp. + * + * Copyright 2011-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. @@ -74,4 +74,8 @@ public class FileTypeExtensions { public static List getArchiveExtensions() { return ARCHIVE_EXTENSIONS; } + + private FileTypeExtensions() { + } + } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/FileTypeNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/FileTypeNode.java deleted file mode 100644 index 5fcbdd478b..0000000000 --- a/Core/src/org/sleuthkit/autopsy/datamodel/FileTypeNode.java +++ /dev/null @@ -1,277 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2011-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.datamodel; - -import org.sleuthkit.autopsy.datamodel.accounts.FileTypeExtensionFilters; -import java.util.List; -import java.util.Observable; -import java.util.Observer; -import java.util.logging.Level; -import org.openide.nodes.AbstractNode; -import org.openide.nodes.ChildFactory; -import org.openide.nodes.Children; -import org.openide.nodes.Node; -import org.openide.nodes.Sheet; -import org.openide.util.NbBundle; -import org.openide.util.lookup.Lookups; -import org.sleuthkit.autopsy.core.UserPreferences; -import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.datamodel.AbstractFile; -import org.sleuthkit.datamodel.Content; -import org.sleuthkit.datamodel.ContentVisitor; -import org.sleuthkit.datamodel.DerivedFile; -import org.sleuthkit.datamodel.Directory; -import org.sleuthkit.datamodel.File; -import org.sleuthkit.datamodel.LayoutFile; -import org.sleuthkit.datamodel.LocalFile; -import org.sleuthkit.datamodel.SleuthkitCase; -import org.sleuthkit.datamodel.TskCoreException; -import org.sleuthkit.datamodel.TskData; - -/** - * Node for a specific file type / extension. Children of it will be the files - * of that type. - */ -public class FileTypeNode extends DisplayableItemNode { - - FileTypeExtensionFilters.SearchFilterInterface filter; - SleuthkitCase skCase; - - // deprecated in favor of the version that takes an observable to provide refresh updates - @Deprecated - FileTypeNode(FileTypeExtensionFilters.SearchFilterInterface filter, SleuthkitCase skCase) { - super(Children.create(new FileTypeChildFactory(filter, skCase), true), Lookups.singleton(filter.getDisplayName())); - this.filter = filter; - this.skCase = skCase; - init(); - } - - /** - * - * @param filter Extensions that will be shown for this node - * @param skCase - * @param o Observable that sends updates when the child factories - * should refresh - */ - FileTypeNode(FileTypeExtensionFilters.SearchFilterInterface filter, SleuthkitCase skCase, Observable o) { - super(Children.create(new FileTypeChildFactory(filter, skCase, o), true), Lookups.singleton(filter.getDisplayName())); - this.filter = filter; - this.skCase = skCase; - init(); - o.addObserver(new FileTypeNodeObserver()); - } - - private void init() { - super.setName(filter.getName()); - updateDisplayName(); - this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/file-filter-icon.png"); //NON-NLS - } - - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "FileType"; //NON-NLS -// } - - // update the display name when new events are fired - private class FileTypeNodeObserver implements Observer { - - @Override - public void update(Observable o, Object arg) { - updateDisplayName(); - } - } - - private void updateDisplayName() { - final long count = FileTypeChildFactory.calculateItems(skCase, filter); - super.setDisplayName(filter.getDisplayName() + " (" + count + ")"); - } - - @Override - public T accept(DisplayableItemNodeVisitor v) { - return v.visit(this); - } - - @Override - protected Sheet createSheet() { - Sheet s = super.createSheet(); - Sheet.Set ss = s.get(Sheet.PROPERTIES); - if (ss == null) { - ss = Sheet.createPropertiesSet(); - s.put(ss); - } - - ss.put(new NodeProperty<>(NbBundle.getMessage(this.getClass(), "FileTypeNode.createSheet.filterType.name"), - NbBundle.getMessage(this.getClass(), "FileTypeNode.createSheet.filterType.displayName"), - NbBundle.getMessage(this.getClass(), "FileTypeNode.createSheet.filterType.desc"), - filter.getDisplayName())); - String extensions = ""; - for (String ext : filter.getFilter()) { - extensions += "'" + ext + "', "; - } - extensions = extensions.substring(0, extensions.lastIndexOf(',')); - ss.put(new NodeProperty<>(NbBundle.getMessage(this.getClass(), "FileTypeNode.createSheet.fileExt.name"), - NbBundle.getMessage(this.getClass(), "FileTypeNode.createSheet.fileExt.displayName"), - NbBundle.getMessage(this.getClass(), "FileTypeNode.createSheet.fileExt.desc"), - extensions)); - - return s; - } - - @Override - public boolean isLeafTypeNode() { - return true; - } - - /** - * Child node factory for a specific file type - does the database query. - */ - public static class FileTypeChildFactory extends ChildFactory.Detachable { - - private final SleuthkitCase skCase; - private final FileTypeExtensionFilters.SearchFilterInterface filter; - private final static Logger logger = Logger.getLogger(FileTypeChildFactory.class.getName()); - private Observable notifier; - - // use the constructor that gets an observable passed in for updates - @Deprecated - FileTypeChildFactory(FileTypeExtensionFilters.SearchFilterInterface filter, SleuthkitCase skCase) { - super(); - this.filter = filter; - this.skCase = skCase; - notifier = null; - } - - /** - * - * @param filter Extensions to display - * @param skCase - * @param o Observable that will notify when there could be new - * data to display - */ - FileTypeChildFactory(FileTypeExtensionFilters.SearchFilterInterface filter, SleuthkitCase skCase, Observable o) { - super(); - this.filter = filter; - this.skCase = skCase; - notifier = o; - } - - @Override - protected void addNotify() { - if (notifier != null) { - notifier.addObserver(observer); - } - } - - @Override - protected void removeNotify() { - if (notifier != null) { - notifier.deleteObserver(observer); - } - } - private final Observer observer = new FileTypeChildFactoryObserver(); - - // Cause refresh of children if there are changes - private class FileTypeChildFactoryObserver implements Observer { - - @Override - public void update(Observable o, Object arg) { - refresh(true); - } - } - - /** - * Get children count without actually loading all nodes - * - * @return - */ - static long calculateItems(SleuthkitCase sleuthkitCase, FileTypeExtensionFilters.SearchFilterInterface filter) { - try { - return sleuthkitCase.countFilesWhere(createQuery(filter)); - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error getting file search view count", ex); //NON-NLS - return 0; - } - } - - @Override - protected boolean createKeys(List list) { - try { - List files = skCase.findAllFilesWhere(createQuery(filter)); - list.addAll(files); - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Couldn't get search results", ex); //NON-NLS - } - return true; - } - - private static String createQuery(FileTypeExtensionFilters.SearchFilterInterface filter) { - StringBuilder query = new StringBuilder(); - query.append("(dir_type = ").append(TskData.TSK_FS_NAME_TYPE_ENUM.REG.getValue()).append(")"); //NON-NLS - if (UserPreferences.hideKnownFilesInViewsTree()) { - query.append(" AND (known IS NULL OR known != ").append(TskData.FileKnown.KNOWN.getFileKnownValue()).append(")"); //NON-NLS - } - query.append(" AND (NULL"); //NON-NLS - for (String s : filter.getFilter()) { - query.append(" OR LOWER(name) LIKE LOWER('%").append(s).append("')"); //NON-NLS - } - query.append(')'); - return query.toString(); - } - - @Override - protected Node createNodeForKey(Content key) { - return key.accept(new ContentVisitor.Default() { - @Override - public FileNode visit(File f) { - return new FileNode(f, false); - } - - @Override - public DirectoryNode visit(Directory d) { - return new DirectoryNode(d); - } - - @Override - public LayoutFileNode visit(LayoutFile lf) { - return new LayoutFileNode(lf); - } - - @Override - public LocalFileNode visit(DerivedFile df) { - return new LocalFileNode(df); - } - - @Override - public LocalFileNode visit(LocalFile lf) { - return new LocalFileNode(lf); - } - - @Override - protected AbstractNode defaultVisit(Content di) { - throw new UnsupportedOperationException(NbBundle.getMessage(this.getClass(), "FileTypeChildren.exception.notSupported.msg", di.toString())); - } - }); - } - } -} diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/FileTypes.java b/Core/src/org/sleuthkit/autopsy/datamodel/FileTypes.java new file mode 100644 index 0000000000..6998b2ef71 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datamodel/FileTypes.java @@ -0,0 +1,102 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2011-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.datamodel; + +import java.util.Arrays; +import org.openide.nodes.Sheet; +import org.openide.util.NbBundle; +import org.openide.util.lookup.Lookups; +import org.sleuthkit.datamodel.SleuthkitCase; + +/** + * File Types node support + */ +public final class FileTypes implements AutopsyVisitableItem { + + private final SleuthkitCase skCase; + + FileTypes(SleuthkitCase skCase) { + this.skCase = skCase; + } + + @Override + public T accept(AutopsyItemVisitor v) { + return v.visit(this); + } + + SleuthkitCase getSleuthkitCase() { + return skCase; + } + + /** + * Node which will contain By Mime Type and By Extension nodes. + */ + public static final class FileTypesNode extends DisplayableItemNode { + + @NbBundle.Messages("FileTypes.name.text=File Types") + private static final String NAME = Bundle.FileTypes_name_text(); + + FileTypesNode(SleuthkitCase sleuthkitCase) { + super(new RootContentChildren(Arrays.asList( + new FileTypesByExtension(sleuthkitCase), + new FileTypesByMimeType(sleuthkitCase) + )), Lookups.singleton(NAME)); + setName(NAME); + setDisplayName(NAME); + this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/file_types.png"); + } + + @Override + public boolean isLeafTypeNode() { + return false; + } + + @Override + public T accept(DisplayableItemNodeVisitor v) { + return v.visit(this); + } + + @Override + @NbBundle.Messages({ + "FileTypes.createSheet.name.name=Name", + "FileTypes.createSheet.name.displayName=Name", + "FileTypes.createSheet.name.desc=no description"}) + protected Sheet createSheet() { + Sheet s = super.createSheet(); + Sheet.Set ss = s.get(Sheet.PROPERTIES); + if (ss == null) { + ss = Sheet.createPropertiesSet(); + s.put(ss); + } + + ss.put(new NodeProperty<>(Bundle.FileTypes_createSheet_name_name(), + Bundle.FileTypes_createSheet_name_displayName(), + Bundle.FileTypes_createSheet_name_desc(), + NAME + )); + return s; + } + + @Override + public String getItemType() { + return getClass().getName(); + } + + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByExtension.java b/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByExtension.java new file mode 100644 index 0000000000..5cfddc7f7a --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByExtension.java @@ -0,0 +1,639 @@ +/* + * 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.datamodel; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.Arrays; +import java.util.List; +import java.util.Observable; +import java.util.Observer; +import java.util.logging.Level; +import org.openide.nodes.AbstractNode; +import org.openide.nodes.ChildFactory; +import org.openide.nodes.Children; +import org.openide.nodes.Node; +import org.openide.nodes.Sheet; +import org.openide.util.NbBundle; +import org.openide.util.lookup.Lookups; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.core.UserPreferences; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.ingest.IngestManager; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.ContentVisitor; +import org.sleuthkit.datamodel.DerivedFile; +import org.sleuthkit.datamodel.Directory; +import org.sleuthkit.datamodel.File; +import org.sleuthkit.datamodel.LayoutFile; +import org.sleuthkit.datamodel.LocalFile; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.TskData; + +/** + * Filters database results by file extension. + */ + public final class FileTypesByExtension implements AutopsyVisitableItem { + + private final SleuthkitCase skCase; + + public FileTypesByExtension(SleuthkitCase skCase) { + this.skCase = skCase; + } + + public SleuthkitCase getSleuthkitCase() { + return this.skCase; + } + + @Override + public T accept(AutopsyItemVisitor v) { + return v.visit(this); + } + + /** + * Listens for case and ingest invest. Updates observers when events are + * fired. FileType and FileTypes nodes are all listening to this. + */ + private static class FileTypesByExtObservable extends Observable { + + private FileTypesByExtObservable() { + super(); + IngestManager.getInstance().addIngestJobEventListener(pcl); + IngestManager.getInstance().addIngestModuleEventListener(pcl); + Case.addPropertyChangeListener(pcl); + } + + private void removeListeners() { + deleteObservers(); + IngestManager.getInstance().removeIngestJobEventListener(pcl); + IngestManager.getInstance().removeIngestModuleEventListener(pcl); + Case.removePropertyChangeListener(pcl); + } + + private final PropertyChangeListener pcl = (PropertyChangeEvent evt) -> { + String eventType = evt.getPropertyName(); + if (eventType.equals(IngestManager.IngestModuleEvent.CONTENT_CHANGED.toString()) || eventType.equals(IngestManager.IngestJobEvent.COMPLETED.toString()) || eventType.equals(IngestManager.IngestJobEvent.CANCELLED.toString()) || eventType.equals(Case.Events.DATA_SOURCE_ADDED.toString())) { + /** + * Checking for a current case is a stop gap measure until a + * different way of handling the closing of cases is worked out. + * Currently, remote events may be received for a case that is + * already closed. + */ + try { + Case.getCurrentCase(); + update(); + } catch (IllegalStateException notUsed) { + /** + * Case is closed, do nothing. + */ + } + } else if (eventType.equals(Case.Events.CURRENT_CASE.toString())) { + // case was closed. Remove listeners so that we don't get called with a stale case handle + if (evt.getNewValue() == null) { + removeListeners(); + } + } + }; + + private void update() { + setChanged(); + notifyObservers(); + } + } + + /** + * Node for root of file types view. Children are nodes for specific types. + */ + static class FileTypesByExtNode extends DisplayableItemNode { + + private static final String FNAME = NbBundle.getMessage(FileTypesByExtNode.class, "FileTypesByExtNode.fname.text"); + private final FileTypesByExtension.RootFilter filter; + + /** + * + * @param skCase + * @param filter null to display root node of file type tree, pass in + * something to provide a sub-node. + */ + FileTypesByExtNode(SleuthkitCase skCase, FileTypesByExtension.RootFilter filter) { + super(Children.create(new FileTypesByExtNodeChildren(skCase, filter, null), true), Lookups.singleton(filter == null ? FNAME : filter.getName())); + this.filter = filter; + init(); + } + + /** + * + * @param skCase + * @param filter + * @param o Observable that was created by a higher-level node that + * provides updates on events + */ + private FileTypesByExtNode(SleuthkitCase skCase, FileTypesByExtension.RootFilter filter, Observable o) { + super(Children.create(new FileTypesByExtNodeChildren(skCase, filter, o), true), Lookups.singleton(filter == null ? FNAME : filter.getName())); + this.filter = filter; + init(); + } + + private void init() { + // root node of tree + if (filter == null) { + super.setName(FNAME); + super.setDisplayName(FNAME); + } // sub-node in file tree (i.e. documents, exec, etc.) + else { + super.setName(filter.getName()); + super.setDisplayName(filter.getDisplayName()); + } + this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/file_types.png"); //NON-NLS + } + + @Override + public boolean isLeafTypeNode() { + return false; + } + + @Override + public T accept(DisplayableItemNodeVisitor v) { + return v.visit(this); + } + + @Override + protected Sheet createSheet() { + Sheet s = super.createSheet(); + Sheet.Set ss = s.get(Sheet.PROPERTIES); + if (ss == null) { + ss = Sheet.createPropertiesSet(); + s.put(ss); + } + ss.put(new NodeProperty<>(NbBundle.getMessage(this.getClass(), "FileTypesByExtNode.createSheet.name.name"), NbBundle.getMessage(this.getClass(), "FileTypesByExtNode.createSheet.name.displayName"), NbBundle.getMessage(this.getClass(), "FileTypesByExtNode.createSheet.name.desc"), getName())); + return s; + } + + @Override + public String getItemType() { + /** + * Because Documents and Executable are further expandable, their + * column order settings should be stored separately. + */ + if (filter == null) { + return getClass().getName(); + } + if (filter.equals(FileTypesByExtension.RootFilter.TSK_DOCUMENT_FILTER) || filter.equals(FileTypesByExtension.RootFilter.TSK_EXECUTABLE_FILTER)) { + return getClass().getName() + filter.getName(); + } + return getClass().getName(); + } + + private static class FileTypesByExtNodeChildren extends ChildFactory { + + private final SleuthkitCase skCase; + private final FileTypesByExtension.RootFilter filter; + private final Observable notifier; + + /** + * + * @param skCase + * @param filter Is null for root node + * @param o Observable that provides updates based on events + * being fired (or null if one needs to be created) + */ + private FileTypesByExtNodeChildren(SleuthkitCase skCase, FileTypesByExtension.RootFilter filter, Observable o) { + super(); + this.skCase = skCase; + this.filter = filter; + if (o == null) { + this.notifier = new FileTypesByExtObservable(); + } else { + this.notifier = o; + } + } + + @Override + protected boolean createKeys(List list) { + // root node + if (filter == null) { + list.addAll(Arrays.asList(FileTypesByExtension.RootFilter.values())); + } // document and executable has another level of nodes + else if (filter.equals(FileTypesByExtension.RootFilter.TSK_DOCUMENT_FILTER)) { + list.addAll(Arrays.asList(FileTypesByExtension.DocumentFilter.values())); + } else if (filter.equals(FileTypesByExtension.RootFilter.TSK_EXECUTABLE_FILTER)) { + list.addAll(Arrays.asList(FileTypesByExtension.ExecutableFilter.values())); + } + return true; + } + + @Override + protected Node createNodeForKey(FileTypesByExtension.SearchFilterInterface key) { + // make new nodes for the sub-nodes + if (key.getName().equals(FileTypesByExtension.RootFilter.TSK_DOCUMENT_FILTER.getName())) { + return new FileTypesByExtNode(skCase, FileTypesByExtension.RootFilter.TSK_DOCUMENT_FILTER, notifier); + } else if (key.getName().equals(FileTypesByExtension.RootFilter.TSK_EXECUTABLE_FILTER.getName())) { + return new FileTypesByExtNode(skCase, FileTypesByExtension.RootFilter.TSK_EXECUTABLE_FILTER, notifier); + } else { + return new FileExtensionNode(key, skCase, notifier); + } + } + } + + } + + /** + * Node for a specific file type / extension. Children of it will be the + * files of that type. + */ + static class FileExtensionNode extends DisplayableItemNode { + + FileTypesByExtension.SearchFilterInterface filter; + SleuthkitCase skCase; + + /** + * + * @param filter Extensions that will be shown for this node + * @param skCase + * @param o Observable that sends updates when the child factories + * should refresh + */ + FileExtensionNode(FileTypesByExtension.SearchFilterInterface filter, SleuthkitCase skCase, Observable o) { + super(Children.create(new FileExtensionNodeChildren(filter, skCase, o), true), Lookups.singleton(filter.getDisplayName())); + this.filter = filter; + this.skCase = skCase; + init(); + o.addObserver(new ByExtNodeObserver()); + } + + private void init() { + super.setName(filter.getName()); + updateDisplayName(); + this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/file-filter-icon.png"); //NON-NLS + } + + // update the display name when new events are fired + private class ByExtNodeObserver implements Observer { + + @Override + public void update(Observable o, Object arg) { + updateDisplayName(); + } + } + + private void updateDisplayName() { + final long count = FileExtensionNodeChildren.calculateItems(skCase, filter); + super.setDisplayName(filter.getDisplayName() + " (" + count + ")"); + } + + @Override + public T accept(DisplayableItemNodeVisitor v) { + return v.visit(this); + } + + @Override + protected Sheet createSheet() { + Sheet s = super.createSheet(); + Sheet.Set ss = s.get(Sheet.PROPERTIES); + if (ss == null) { + ss = Sheet.createPropertiesSet(); + s.put(ss); + } + ss.put(new NodeProperty<>(NbBundle.getMessage(this.getClass(), "FileTypesByExtNode.createSheet.filterType.name"), NbBundle.getMessage(this.getClass(), "FileTypesByExtNode.createSheet.filterType.displayName"), NbBundle.getMessage(this.getClass(), "FileTypesByExtNode.createSheet.filterType.desc"), filter.getDisplayName())); + String extensions = ""; + for (String ext : filter.getFilter()) { + extensions += "'" + ext + "', "; + } + extensions = extensions.substring(0, extensions.lastIndexOf(',')); + ss.put(new NodeProperty<>(NbBundle.getMessage(this.getClass(), "FileTypesByExtNode.createSheet.fileExt.name"), NbBundle.getMessage(this.getClass(), "FileTypesByExtNode.createSheet.fileExt.displayName"), NbBundle.getMessage(this.getClass(), "FileTypesByExtNode.createSheet.fileExt.desc"), extensions)); + return s; + } + + @Override + public boolean isLeafTypeNode() { + return true; + } + + /** + * Consider allowing different configurations for Images, Videos, etc + * (in which case we'd return getClass().getName() + filter.getName() + * for all filters). + */ + @Override + public String getItemType() { + return DisplayableItemNode.FILE_PARENT_NODE_KEY; + } + + /** + * Child node factory for a specific file type - does the database + * query. + */ + private static class FileExtensionNodeChildren extends ChildFactory.Detachable { + + private final SleuthkitCase skCase; + private final FileTypesByExtension.SearchFilterInterface filter; + private static final Logger LOGGER = Logger.getLogger(FileExtensionNodeChildren.class.getName()); + private final Observable notifier; + + /** + * + * @param filter Extensions to display + * @param skCase + * @param o Observable that will notify when there could be new + * data to display + */ + private FileExtensionNodeChildren(FileTypesByExtension.SearchFilterInterface filter, SleuthkitCase skCase, Observable o) { + super(); + this.filter = filter; + this.skCase = skCase; + notifier = o; + } + + @Override + protected void addNotify() { + if (notifier != null) { + notifier.addObserver(observer); + } + } + + @Override + protected void removeNotify() { + if (notifier != null) { + notifier.deleteObserver(observer); + } + } + private final Observer observer = new FileTypeChildFactoryObserver(); + + // Cause refresh of children if there are changes + private class FileTypeChildFactoryObserver implements Observer { + + @Override + public void update(Observable o, Object arg) { + refresh(true); + } + } + + /** + * Get children count without actually loading all nodes + * + * @return + */ + private static long calculateItems(SleuthkitCase sleuthkitCase, FileTypesByExtension.SearchFilterInterface filter) { + try { + return sleuthkitCase.countFilesWhere(createQuery(filter)); + } catch (TskCoreException ex) { + LOGGER.log(Level.SEVERE, "Error getting file search view count", ex); //NON-NLS + return 0; + } + } + + @Override + protected boolean createKeys(List list) { + try { + List files = skCase.findAllFilesWhere(createQuery(filter)); + list.addAll(files); + } catch (TskCoreException ex) { + LOGGER.log(Level.SEVERE, "Couldn't get search results", ex); //NON-NLS + } + return true; + } + + private static String createQuery(FileTypesByExtension.SearchFilterInterface filter) { + StringBuilder query = new StringBuilder(); + query.append("(dir_type = ").append(TskData.TSK_FS_NAME_TYPE_ENUM.REG.getValue()).append(")"); //NON-NLS + if (UserPreferences.hideKnownFilesInViewsTree()) { + query.append(" AND (known IS NULL OR known != ").append(TskData.FileKnown.KNOWN.getFileKnownValue()).append(")"); //NON-NLS + } + query.append(" AND (NULL"); //NON-NLS + for (String s : filter.getFilter()) { + query.append(" OR LOWER(name) LIKE LOWER('%").append(s).append("')"); //NON-NLS + } + query.append(')'); + return query.toString(); + } + + @Override + protected Node createNodeForKey(Content key) { + return key.accept(new ContentVisitor.Default() { + @Override + public FileNode visit(File f) { + return new FileNode(f, false); + } + + @Override + public DirectoryNode visit(Directory d) { + return new DirectoryNode(d); + } + + @Override + public LayoutFileNode visit(LayoutFile lf) { + return new LayoutFileNode(lf); + } + + @Override + public LocalFileNode visit(DerivedFile df) { + return new LocalFileNode(df); + } + + @Override + public LocalFileNode visit(LocalFile lf) { + return new LocalFileNode(lf); + } + + @Override + protected AbstractNode defaultVisit(Content di) { + throw new UnsupportedOperationException(NbBundle.getMessage(this.getClass(), "FileTypeChildren.exception.notSupported.msg", di.toString())); + } + }); + } + } + } + + // root node filters + public static enum RootFilter implements AutopsyVisitableItem, SearchFilterInterface { + + TSK_IMAGE_FILTER(0, "TSK_IMAGE_FILTER", //NON-NLS + NbBundle.getMessage(FileTypesByExtension.class, "FileTypeExtensionFilters.tskImgFilter.text"), + FileTypeExtensions.getImageExtensions()), + TSK_VIDEO_FILTER(1, "TSK_VIDEO_FILTER", //NON-NLS + NbBundle.getMessage(FileTypesByExtension.class, "FileTypeExtensionFilters.tskVideoFilter.text"), + FileTypeExtensions.getVideoExtensions()), + TSK_AUDIO_FILTER(2, "TSK_AUDIO_FILTER", //NON-NLS + NbBundle.getMessage(FileTypesByExtension.class, "FileTypeExtensionFilters.tskAudioFilter.text"), + FileTypeExtensions.getAudioExtensions()), + TSK_ARCHIVE_FILTER(3, "TSK_ARCHIVE_FILTER", //NON-NLS + NbBundle.getMessage(FileTypesByExtension.class, "FileTypeExtensionFilters.tskArchiveFilter.text"), + FileTypeExtensions.getArchiveExtensions()), + TSK_DOCUMENT_FILTER(3, "TSK_DOCUMENT_FILTER", //NON-NLS + NbBundle.getMessage(FileTypesByExtension.class, "FileTypeExtensionFilters.tskDocumentFilter.text"), + Arrays.asList(".doc", ".docx", ".pdf", ".xls", ".rtf", ".txt")), //NON-NLS + TSK_EXECUTABLE_FILTER(3, "TSK_EXECUTABLE_FILTER", //NON-NLS + NbBundle.getMessage(FileTypesByExtension.class, "FileTypeExtensionFilters.tskExecFilter.text"), + Arrays.asList(".exe", ".dll", ".bat", ".cmd", ".com")); //NON-NLS + + private final int id; + private final String name; + private final String displayName; + private final List filter; + + private RootFilter(int id, String name, String displayName, List filter) { + this.id = id; + this.name = name; + this.displayName = displayName; + this.filter = filter; + } + + @Override + public T accept(AutopsyItemVisitor v) { + return v.visit(this); + } + + @Override + public String getName() { + return this.name; + } + + @Override + public int getId() { + return this.id; + } + + @Override + public String getDisplayName() { + return this.displayName; + } + + @Override + public List getFilter() { + return this.filter; + } + } + + // document sub-node filters + public static enum DocumentFilter implements AutopsyVisitableItem, SearchFilterInterface { + + AUT_DOC_HTML(0, "AUT_DOC_HTML", //NON-NLS + NbBundle.getMessage(FileTypesByExtension.class, "FileTypeExtensionFilters.autDocHtmlFilter.text"), + Arrays.asList(".htm", ".html")), //NON-NLS + AUT_DOC_OFFICE(1, "AUT_DOC_OFFICE", //NON-NLS + NbBundle.getMessage(FileTypesByExtension.class, "FileTypeExtensionFilters.autDocOfficeFilter.text"), + Arrays.asList(".doc", ".docx", ".odt", ".xls", ".xlsx", ".ppt", ".pptx")), //NON-NLS + AUT_DOC_PDF(2, "AUT_DOC_PDF", //NON-NLS + NbBundle.getMessage(FileTypesByExtension.class, "FileTypeExtensionFilters.autoDocPdfFilter.text"), + Arrays.asList(".pdf")), //NON-NLS + AUT_DOC_TXT(3, "AUT_DOC_TXT", //NON-NLS + NbBundle.getMessage(FileTypesByExtension.class, "FileTypeExtensionFilters.autDocTxtFilter.text"), + Arrays.asList(".txt")), //NON-NLS + AUT_DOC_RTF(4, "AUT_DOC_RTF", //NON-NLS + NbBundle.getMessage(FileTypesByExtension.class, "FileTypeExtensionFilters.autDocRtfFilter.text"), + Arrays.asList(".rtf")); //NON-NLS + + private final int id; + private final String name; + private final String displayName; + private final List filter; + + private DocumentFilter(int id, String name, String displayName, List filter) { + this.id = id; + this.name = name; + this.displayName = displayName; + this.filter = filter; + } + + @Override + public T accept(AutopsyItemVisitor v) { + return v.visit(this); + } + + @Override + public String getName() { + return this.name; + } + + @Override + public int getId() { + return this.id; + } + + @Override + public String getDisplayName() { + return this.displayName; + } + + @Override + public List getFilter() { + return this.filter; + } + } + + // executable sub-node filters + public static enum ExecutableFilter implements AutopsyVisitableItem, SearchFilterInterface { + + ExecutableFilter_EXE(0, "ExecutableFilter_EXE", ".exe", Arrays.asList(".exe")), //NON-NLS + ExecutableFilter_DLL(1, "ExecutableFilter_DLL", ".dll", Arrays.asList(".dll")), //NON-NLS + ExecutableFilter_BAT(2, "ExecutableFilter_BAT", ".bat", Arrays.asList(".bat")), //NON-NLS + ExecutableFilter_CMD(3, "ExecutableFilter_CMD", ".cmd", Arrays.asList(".cmd")), //NON-NLS + ExecutableFilter_COM(4, "ExecutableFilter_COM", ".com", Arrays.asList(".com")); //NON-NLS + + private final int id; + private final String name; + private final String displayName; + private final List filter; + + private ExecutableFilter(int id, String name, String displayName, List filter) { + this.id = id; + this.name = name; + this.displayName = displayName; + this.filter = filter; + } + + @Override + public T accept(AutopsyItemVisitor v) { + return v.visit(this); + } + + @Override + public String getName() { + return this.name; + } + + @Override + public int getId() { + return this.id; + } + + @Override + public String getDisplayName() { + return this.displayName; + } + + @Override + public List getFilter() { + return this.filter; + } + } + + interface SearchFilterInterface { + + public String getName(); + + public int getId(); + + public String getDisplayName(); + + public List getFilter(); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByMimeType.java b/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByMimeType.java new file mode 100644 index 0000000000..bbd46ddd12 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesByMimeType.java @@ -0,0 +1,507 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2011-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.datamodel; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Observable; +import java.util.Observer; +import java.util.logging.Level; +import org.openide.nodes.AbstractNode; +import org.openide.nodes.ChildFactory; +import org.openide.nodes.Children; +import org.openide.nodes.Node; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.core.UserPreferences; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.ingest.IngestManager; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.ContentVisitor; +import org.sleuthkit.datamodel.DerivedFile; +import org.sleuthkit.datamodel.Directory; +import org.sleuthkit.datamodel.File; +import org.sleuthkit.datamodel.LayoutFile; +import org.sleuthkit.datamodel.LocalFile; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.TskData; + +/** + * Class which contains the Nodes for the 'By Mime Type' view located in the + * File Types view, shows all files with a mime type. Will initially be empty + * until file type identification has been performed. Contains a Property Change + * Listener which is checking for changes in IngestJobEvent Completed or + * Cancelled and IngestModuleEvent Content Changed. + */ +public final class FileTypesByMimeType extends Observable implements AutopsyVisitableItem { + + private final SleuthkitCase skCase; + /** + * The nodes of this tree will be determined dynamically by the mimetypes + * which exist in the database. This hashmap will store them with the media + * type as the key and a list of media subtypes as the value. + */ + 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. + */ + private final PropertyChangeListener pcl = (PropertyChangeEvent evt) -> { + String eventType = evt.getPropertyName(); + if (eventType.equals(IngestManager.IngestJobEvent.COMPLETED.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. + * Currently, remote events may be received for a case that is + * already closed. + */ + try { + Case.getCurrentCase(); + populateHashMap(); + } catch (IllegalStateException notUsed) { + /** + * Case is closed, do nothing. + */ + } + } else if (eventType.equals(Case.Events.CURRENT_CASE.toString())) { + if (evt.getNewValue() == null) { + removeListeners(); + } + } + }; + + /** + * Retrieve the media types by retrieving the keyset from the hashmap. + * + * @return mediaTypes - a list of strings representing all distinct media + * types of files for this case + */ + private List getMediaTypeList() { + synchronized (existingMimeTypes) { + List mediaTypes = new ArrayList<>(existingMimeTypes.keySet()); + Collections.sort(mediaTypes); + return mediaTypes; + } + } + + /** + * Performs the query on the database to get all distinct MIME types of + * files in it, and populate the hashmap with those results. + */ + private void populateHashMap() { + StringBuilder allDistinctMimeTypesQuery = new StringBuilder(); + allDistinctMimeTypesQuery.append("SELECT DISTINCT mime_type from tsk_files where mime_type IS NOT null"); //NON-NLS + allDistinctMimeTypesQuery.append(" AND dir_type = ").append(TskData.TSK_FS_NAME_TYPE_ENUM.REG.getValue()); //NON-NLS + allDistinctMimeTypesQuery.append(" AND (type IN (").append(TskData.TSK_DB_FILES_TYPE_ENUM.FS.ordinal()).append(","); //NON-NLS + allDistinctMimeTypesQuery.append(TskData.TSK_DB_FILES_TYPE_ENUM.CARVED.ordinal()).append(","); + allDistinctMimeTypesQuery.append(TskData.TSK_DB_FILES_TYPE_ENUM.DERIVED.ordinal()).append(","); + allDistinctMimeTypesQuery.append(TskData.TSK_DB_FILES_TYPE_ENUM.LOCAL.ordinal()).append("))"); + synchronized (existingMimeTypes) { + existingMimeTypes.clear(); + + if (skCase == null) { + + 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()) { + String mimeType[] = mime_type.split("/"); + if (!mimeType[0].isEmpty() && !mimeType[1].isEmpty()) { + if (!existingMimeTypes.containsKey(mimeType[0])) { + existingMimeTypes.put(mimeType[0], new ArrayList<>()); + } + existingMimeTypes.get(mimeType[0]).add(mimeType[1]); + } + } + } + } catch (TskCoreException | SQLException ex) { + LOGGER.log(Level.SEVERE, "Unable to populate File Types by MIME Type tree view from DB: ", ex); //NON-NLS + } + } + + setChanged(); + + notifyObservers(); + } + + FileTypesByMimeType(SleuthkitCase skCase) { + IngestManager.getInstance().addIngestJobEventListener(pcl); + Case.addPropertyChangeListener(pcl); + this.skCase = skCase; + populateHashMap(); + } + + @Override + public T accept(AutopsyItemVisitor v) { + return v.visit(this); + } + + /** + * Method to check if the node in question is a ByMimeTypeNode which is + * empty. + * + * @param node the Node which you wish to check. + * @return True if originNode is an instance of ByMimeTypeNode and is empty, + * false otherwise. + */ + public static boolean isEmptyMimeTypeNode(Node node) { + boolean isEmptyMimeNode = false; + if (node instanceof FileTypesByMimeType.ByMimeTypeNode && ((FileTypesByMimeType.ByMimeTypeNode) node).isEmpty()) { + isEmptyMimeNode = true; + } + return isEmptyMimeNode; + + } + + /** + * Class which represents the root node of the "By MIME Type" tree, will + * have children of each media type present in the database or no children + * when the file detection module has not been run and MIME type is + * currently unknown. + */ + class ByMimeTypeNode extends DisplayableItemNode { + + @NbBundle.Messages("FileTypesByMimeType.name.text=By MIME Type") + final String NAME = Bundle.FileTypesByMimeType_name_text(); + + ByMimeTypeNode() { + super(Children.create(new ByMimeTypeNodeChildren(), true)); + super.setName(NAME); + super.setDisplayName(NAME); + this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/file_types.png"); + } + + @Override + public boolean isLeafTypeNode() { + return false; + } + + @Override + public T accept(DisplayableItemNodeVisitor v) { + return v.visit(this); + } + + @Override + public String getItemType() { + return getClass().getName(); + } + + boolean isEmpty() { + return existingMimeTypes.isEmpty(); + } + + } + + /** + * Creates the children for the "By MIME Type" node these children will each + * represent a distinct media type present in the DB + */ + private class ByMimeTypeNodeChildren extends ChildFactory implements Observer { + + private ByMimeTypeNodeChildren() { + super(); + addObserver(this); + } + + @Override + protected boolean createKeys(List mediaTypeNodes) { + if (!existingMimeTypes.isEmpty()) { + mediaTypeNodes.addAll(getMediaTypeList()); + } + return true; + } + + @Override + protected Node createNodeForKey(String key) { + return new MediaTypeNode(key); + } + + @Override + public void update(Observable o, Object arg) { + refresh(true); + } + + } + + /** + * The Media type node created by the ByMimeTypeNodeChildren and contains + * one of the unique media types present in the database for this case. + */ + class MediaTypeNode extends DisplayableItemNode { + + MediaTypeNode(String name) { + super(Children.create(new MediaTypeNodeChildren(name), true)); + setName(name); + setDisplayName(name); + this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/file_types.png"); + } + + @Override + public boolean isLeafTypeNode() { + return false; + } + + @Override + public T accept(DisplayableItemNodeVisitor v) { + return v.visit(this); + } + + @Override + public String getItemType() { + return getClass().getName(); + } + + } + + /** + * Creates children for media type nodes, children will be MediaSubTypeNodes + * and represent one of the subtypes which are present in the database of + * their media type. + */ + private class MediaTypeNodeChildren extends ChildFactory implements Observer { + + String mediaType; + + MediaTypeNodeChildren(String name) { + addObserver(this); + this.mediaType = name; + } + + @Override + protected boolean createKeys(List mediaTypeNodes) { + mediaTypeNodes.addAll(existingMimeTypes.get(mediaType)); + return true; + } + + @Override + protected Node createNodeForKey(String subtype) { + String mimeType = mediaType + "/" + subtype; + return new MediaSubTypeNode(mimeType); + } + + @Override + public void update(Observable o, Object arg) { + refresh(true); + } + + } + + /** + * Node which represents the media sub type in the By MIME type tree, the + * media subtype is the portion of the MIME type following the /. + */ + class MediaSubTypeNode extends DisplayableItemNode implements Observer { + + private MediaSubTypeNode(String mimeType) { + super(Children.create(new MediaSubTypeNodeChildren(mimeType), true)); + addObserver(this); + init(mimeType); + } + + private void init(String mimeType) { + super.setName(mimeType); + updateDisplayName(mimeType); + this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/file-filter-icon.png"); //NON-NLS + } + + /** + * Updates the display name of the mediaSubTypeNode to include the count + * of files which it represents. + * + * @param mimeType - the complete MimeType, needed for accurate query + * results + */ + private void updateDisplayName(String mimeType) { + + final long count = new MediaSubTypeNodeChildren(mimeType).calculateItems(skCase, mimeType); + + super.setDisplayName(mimeType.split("/")[1] + " (" + count + ")"); + } + + /** + * This returns true because any MediaSubTypeNode that exists is going + * to be a bottom level node in the Tree view on the left of Autopsy. + * + * @return true + */ + @Override + public boolean isLeafTypeNode() { + return true; + } + + @Override + public T accept(DisplayableItemNodeVisitor v) { + return v.visit(this); + } + + @Override + public String getItemType() { + return getClass().getName(); + } + + @Override + public void update(Observable o, Object arg) { + updateDisplayName(getName()); + } + } + + /** + * Factory for populating the contents of the Media Sub Type Node with the + * files that match MimeType which is represented by this position in the + * tree. + */ + private class MediaSubTypeNodeChildren extends ChildFactory.Detachable implements Observer { + + private final String mimeType; + + private MediaSubTypeNodeChildren(String mimeType) { + super(); + addObserver(this); + this.mimeType = mimeType; + } + + /** + * Get children count without actually loading all nodes + * + * @return count(*) - the number of items that will be shown in this + * items Directory Listing + */ + private long calculateItems(SleuthkitCase sleuthkitCase, String mime_type) { + try { + return sleuthkitCase.countFilesWhere(createQuery(mime_type)); + } catch (TskCoreException ex) { + LOGGER.log(Level.SEVERE, "Error getting file search view count", ex); //NON-NLS + return 0; + } + } + + /** + * Uses the createQuery method to complete the query, Select * from + * tsk_files WHERE. The results from the database will contain the files + * which match this mime type and their information. + * + * @param list - will contain all files and their attributes from the + * tsk_files table where mime_type matches the one specified + * @return true + */ + @Override + protected boolean createKeys(List list) { + try { + List files = skCase.findAllFilesWhere(createQuery(mimeType)); + list.addAll(files); + } catch (TskCoreException ex) { + LOGGER.log(Level.SEVERE, "Couldn't get search results", ex); //NON-NLS + } + return true; + } + + /** + * Create the portion of the query following WHERE for a query of the + * database for each file which matches the complete MIME type + * represented by this node. Matches against the mime_type column in + * tsk_files. + * + * @param mimeType - the complete mimetype of the file mediatype/subtype + * @return query.toString - portion of SQL query which will follow a + * WHERE clause. + */ + private String createQuery(String mime_type) { + StringBuilder query = new StringBuilder(); + query.append("(dir_type = ").append(TskData.TSK_FS_NAME_TYPE_ENUM.REG.getValue()).append(")"); //NON-NLS + query.append(" AND (type IN (").append(TskData.TSK_DB_FILES_TYPE_ENUM.FS.ordinal()).append(","); //NON-NLS + query.append(TskData.TSK_DB_FILES_TYPE_ENUM.CARVED.ordinal()).append(","); + query.append(TskData.TSK_DB_FILES_TYPE_ENUM.DERIVED.ordinal()).append(","); + query.append(TskData.TSK_DB_FILES_TYPE_ENUM.LOCAL.ordinal()).append("))"); + if (UserPreferences.hideKnownFilesInViewsTree()) { + query.append(" AND (known IS NULL OR known != ").append(TskData.FileKnown.KNOWN.getFileKnownValue()).append(")"); //NON-NLS + } + query.append(" AND mime_type = '").append(mime_type).append("'"); //NON-NLS + return query.toString(); + } + + @Override + public void update(Observable o, Object arg) { + refresh(true); + } + + /** + * Creates the content to populate the Directory Listing Table view for + * each file + * + * @param key + * @return + */ + @Override + protected Node createNodeForKey(Content key) { + return key.accept(new ContentVisitor.Default() { + @Override + public FileNode visit(File f) { + return new FileNode(f, false); + } + + @Override + public DirectoryNode visit(Directory d) { + return new DirectoryNode(d); + } + + @Override + public LayoutFileNode visit(LayoutFile lf) { + return new LayoutFileNode(lf); + } + + @Override + public LocalFileNode visit(DerivedFile df) { + return new LocalFileNode(df); + } + + @Override + public LocalFileNode visit(LocalFile lf) { + return new LocalFileNode(lf); + } + + @Override + protected AbstractNode defaultVisit(Content di) { + throw new UnsupportedOperationException(NbBundle.getMessage(this.getClass(), "FileTypeChildren.exception.notSupported.msg", di.toString())); + } + }); + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesNode.java deleted file mode 100644 index 1e199334b5..0000000000 --- a/Core/src/org/sleuthkit/autopsy/datamodel/FileTypesNode.java +++ /dev/null @@ -1,233 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2011-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.datamodel; - -import org.sleuthkit.autopsy.datamodel.accounts.FileTypeExtensionFilters; -import java.beans.PropertyChangeEvent; -import java.beans.PropertyChangeListener; -import java.util.Arrays; -import java.util.List; -import java.util.Observable; -import org.openide.nodes.ChildFactory; -import org.openide.nodes.Children; -import org.openide.nodes.Node; -import org.openide.nodes.Sheet; -import org.openide.util.NbBundle; -import org.openide.util.lookup.Lookups; -import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.ingest.IngestManager; -import org.sleuthkit.datamodel.SleuthkitCase; - -/** - * Node for root of file types view. Children are nodes for specific types. - */ -public class FileTypesNode extends DisplayableItemNode { - - private static final String FNAME = NbBundle.getMessage(FileTypesNode.class, "FileTypesNode.fname.text"); - private final FileTypeExtensionFilters.RootFilter filter; - /** - * - * @param skCase - * @param filter null to display root node of file type tree, pass in - * something to provide a sub-node. - */ - FileTypesNode(SleuthkitCase skCase, FileTypeExtensionFilters.RootFilter filter) { - super(Children.create(new FileTypesChildren(skCase, filter, null), true), Lookups.singleton(filter == null ? FNAME : filter.getName())); - this.filter = filter; - init(); - } - - /** - * - * @param skCase - * @param filter - * @param o Observable that was created by a higher-level node that - * provides updates on events - */ - private FileTypesNode(SleuthkitCase skCase, FileTypeExtensionFilters.RootFilter filter, Observable o) { - super(Children.create(new FileTypesChildren(skCase, filter, o), true), Lookups.singleton(filter == null ? FNAME : filter.getName())); - this.filter = filter; - init(); - } - - private void init() { - // root node of tree - if (filter == null) { - super.setName(FNAME); - super.setDisplayName(FNAME); - } // sub-node in file tree (i.e. documents, exec, etc.) - else { - super.setName(filter.getName()); - super.setDisplayName(filter.getDisplayName()); - } - this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/file_types.png"); //NON-NLS - } - - @Override - public boolean isLeafTypeNode() { - return false; - } - - @Override - public T accept(DisplayableItemNodeVisitor v) { - return v.visit(this); - } - - @Override - protected Sheet createSheet() { - Sheet s = super.createSheet(); - Sheet.Set ss = s.get(Sheet.PROPERTIES); - if (ss == null) { - ss = Sheet.createPropertiesSet(); - s.put(ss); - } - - ss.put(new NodeProperty<>(NbBundle.getMessage(this.getClass(), "FileTypesNode.createSheet.name.name"), - NbBundle.getMessage(this.getClass(), "FileTypesNode.createSheet.name.displayName"), - NbBundle.getMessage(this.getClass(), "FileTypesNode.createSheet.name.desc"), - getName())); - return s; - } - - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// if(filter == null) -// return "FileTypes"; //NON-NLS -// if (filter.equals(FileTypeExtensionFilters.RootFilter.TSK_DOCUMENT_FILTER)) -// return "FileTypesDoc"; //NON-NLS -// if (filter.equals(FileTypeExtensionFilters.RootFilter.TSK_EXECUTABLE_FILTER)) -// return "FileTypesExe"; //NON-NLS -// return "FileTypes"; //NON-NLS -// } - - /** - * - */ - static class FileTypesChildren extends ChildFactory { - - private SleuthkitCase skCase; - private FileTypeExtensionFilters.RootFilter filter; - private Observable notifier; - - /** - * - * @param skCase - * @param filter Is null for root node - * @param o Observable that provides updates based on events being - * fired (or null if one needs to be created) - */ - public FileTypesChildren(SleuthkitCase skCase, FileTypeExtensionFilters.RootFilter filter, Observable o) { - super(); - this.skCase = skCase; - this.filter = filter; - if (o == null) { - this.notifier = new FileTypesChildrenObservable(); - } else { - this.notifier = o; - } - } - - /** - * Listens for case and ingest invest. Updates observers when events are - * fired. FileType and FileTypes nodes are all listening to this. - */ - private final class FileTypesChildrenObservable extends Observable { - - FileTypesChildrenObservable() { - IngestManager.getInstance().addIngestJobEventListener(pcl); - IngestManager.getInstance().addIngestModuleEventListener(pcl); - Case.addPropertyChangeListener(pcl); - } - - private void removeListeners() { - deleteObservers(); - IngestManager.getInstance().removeIngestJobEventListener(pcl); - IngestManager.getInstance().removeIngestModuleEventListener(pcl); - Case.removePropertyChangeListener(pcl); - } - - private final PropertyChangeListener pcl = new PropertyChangeListener() { - @Override - public void propertyChange(PropertyChangeEvent evt) { - String eventType = evt.getPropertyName(); - if (eventType.equals(IngestManager.IngestModuleEvent.CONTENT_CHANGED.toString()) - || eventType.equals(IngestManager.IngestJobEvent.COMPLETED.toString()) - || eventType.equals(IngestManager.IngestJobEvent.CANCELLED.toString()) - || eventType.equals(Case.Events.DATA_SOURCE_ADDED.toString())) { - /** - * Checking for a current case is a stop gap measure - * until a different way of handling the closing of - * cases is worked out. Currently, remote events may be - * received for a case that is already closed. - */ - try { - Case.getCurrentCase(); - update(); - } catch (IllegalStateException notUsed) { - /** - * Case is closed, do nothing. - */ - } - } else if (eventType.equals(Case.Events.CURRENT_CASE.toString())) { - // case was closed. Remove listeners so that we don't get called with a stale case handle - if (evt.getNewValue() == null) { - removeListeners(); - } - } - } - }; - - private void update() { - setChanged(); - notifyObservers(); - } - } - - @Override - protected boolean createKeys(List list) { - // root node - if (filter == null) { - list.addAll(Arrays.asList(FileTypeExtensionFilters.RootFilter.values())); - } // document and executable has another level of nodes - else if (filter.equals(FileTypeExtensionFilters.RootFilter.TSK_DOCUMENT_FILTER)) { - list.addAll(Arrays.asList(FileTypeExtensionFilters.DocumentFilter.values())); - } else if (filter.equals(FileTypeExtensionFilters.RootFilter.TSK_EXECUTABLE_FILTER)) { - list.addAll(Arrays.asList(FileTypeExtensionFilters.ExecutableFilter.values())); - } - return true; - } - - @Override - protected Node createNodeForKey(FileTypeExtensionFilters.SearchFilterInterface key) { - // make new nodes for the sub-nodes - if (key.getName().equals(FileTypeExtensionFilters.RootFilter.TSK_DOCUMENT_FILTER.getName())) { - return new FileTypesNode(skCase, FileTypeExtensionFilters.RootFilter.TSK_DOCUMENT_FILTER, notifier); - } else if (key.getName().equals(FileTypeExtensionFilters.RootFilter.TSK_EXECUTABLE_FILTER.getName())) { - return new FileTypesNode(skCase, FileTypeExtensionFilters.RootFilter.TSK_EXECUTABLE_FILTER, notifier); - } else { - return new FileTypeNode(key, skCase, notifier); - } - } - } -} diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/HashsetHits.java b/Core/src/org/sleuthkit/autopsy/datamodel/HashsetHits.java index 2264b95ac6..5305df440b 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/HashsetHits.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/HashsetHits.java @@ -178,15 +178,10 @@ public class HashsetHits implements AutopsyVisitableItem { return s; } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "HashsetRoot"; //NON-NLS -// } + @Override + public String getItemType() { + return getClass().getName(); + } } /** @@ -341,15 +336,14 @@ public class HashsetHits implements AutopsyVisitableItem { updateDisplayName(); } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "HashsetName"; //NON-NLS -// } + @Override + public String getItemType() { + /** + * For custom settings for each hash set, return + * getClass().getName() + hashSetName instead. + */ + return getClass().getName(); + } } /** diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/ImageNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/ImageNode.java index fbdb046ae1..d4f2ce0e8e 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/ImageNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/ImageNode.java @@ -195,13 +195,8 @@ public class ImageNode extends AbstractContentNode { return v.visit(this); } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "Image"; //NON-NLS -// } + @Override + public String getItemType() { + return getClass().getName(); + } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/InterestingHits.java b/Core/src/org/sleuthkit/autopsy/datamodel/InterestingHits.java index b07f5568c0..872b5bcb25 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/InterestingHits.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/InterestingHits.java @@ -172,15 +172,10 @@ public class InterestingHits implements AutopsyVisitableItem { return s; } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "InterestingHitsRoot"; //NON-NLS -// } + @Override + public String getItemType() { + return getClass().getName(); + } } private class SetNameFactory extends ChildFactory.Detachable implements Observer { @@ -328,15 +323,14 @@ public class InterestingHits implements AutopsyVisitableItem { updateDisplayName(); } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "InterestingHitsSetName"; //NON-NLS -// } + @Override + public String getItemType() { + /** + * For custom settings for each rule set, return + * getClass().getName() + setName instead. + */ + return getClass().getName(); + } } private class HitFactory extends ChildFactory implements Observer { diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/KeywordHits.java b/Core/src/org/sleuthkit/autopsy/datamodel/KeywordHits.java index 319d9ecf9f..c7b8983be0 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/KeywordHits.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/KeywordHits.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2015 Basis Technology Corp. + * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -248,15 +248,10 @@ public class KeywordHits implements AutopsyVisitableItem { return s; } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "KeywordRoot"; //NON-NLS -// } + @Override + public String getItemType() { + return getClass().getName(); + } } private class ListFactory extends ChildFactory.Detachable implements Observer { @@ -351,7 +346,7 @@ public class KeywordHits implements AutopsyVisitableItem { public class ListNode extends DisplayableItemNode implements Observer { - private String listName; + private final String listName; public ListNode(String listName) { super(Children.create(new TermFactory(listName), true), Lookups.singleton(listName)); @@ -408,20 +403,15 @@ public class KeywordHits implements AutopsyVisitableItem { updateDisplayName(); } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "KeywordList"; //NON-NLS -// } + @Override + public String getItemType() { + return getClass().getName(); + } } private class TermFactory extends ChildFactory.Detachable implements Observer { - private String setName; + private final String setName; private TermFactory(String setName) { super(); @@ -457,8 +447,8 @@ public class KeywordHits implements AutopsyVisitableItem { public class TermNode extends DisplayableItemNode implements Observer { - private String setName; - private String keyword; + private final String setName; + private final String keyword; public TermNode(String setName, String keyword) { super(Children.create(new HitsFactory(setName, keyword), true), Lookups.singleton(keyword)); @@ -511,21 +501,16 @@ public class KeywordHits implements AutopsyVisitableItem { return s; } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "KeywordTerm"; //NON-NLS -// } + @Override + public String getItemType() { + return getClass().getName(); + } } public class HitsFactory extends ChildFactory.Detachable implements Observer { - private String keyword; - private String setName; + private final String keyword; + private final String setName; public HitsFactory(String setName, String keyword) { super(); diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/KnownFileFilterNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/KnownFileFilterNode.java index 9a06ed470f..8a6dc86f3f 100755 --- a/Core/src/org/sleuthkit/autopsy/datamodel/KnownFileFilterNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/KnownFileFilterNode.java @@ -46,7 +46,7 @@ public class KnownFileFilterNode extends FilterNode { @Override public void preferenceChange(PreferenceChangeEvent evt) { switch (evt.getKey()) { - case UserPreferences.HIDE_KNOWN_FILES_IN_DATA_SOURCES_TREE: + case UserPreferences.HIDE_KNOWN_FILES_IN_DATA_SRCS_TREE: filterFromDataSources = UserPreferences.hideKnownFilesInDataSourcesTree(); break; case UserPreferences.HIDE_KNOWN_FILES_IN_VIEWS_TREE: diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/LayoutFileNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/LayoutFileNode.java index c66d92882c..495737488b 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/LayoutFileNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/LayoutFileNode.java @@ -38,15 +38,6 @@ import org.sleuthkit.datamodel.TskData; */ public class LayoutFileNode extends AbstractAbstractFileNode { - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "LayoutFile"; //NON-NLS -// } public static enum LayoutContentPropertyType { PARTS { @@ -95,6 +86,9 @@ public class LayoutFileNode extends AbstractAbstractFileNode { ss.put(new NodeProperty<>(entry.getKey(), entry.getKey(), NO_DESCR, entry.getValue())); } + // add tags property to the sheet + addTagProperty(ss); + return s; } @@ -135,4 +129,10 @@ public class LayoutFileNode extends AbstractAbstractFileNode { AbstractAbstractFileNode.fillPropertyMap(map, content); map.put(LayoutContentPropertyType.PARTS.toString(), content.getNumParts()); } + + @Override + public String getItemType() { + return getClass().getName(); + } + } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/LocalFileNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/LocalFileNode.java index cef8dd9a56..ecccb26272 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/LocalFileNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/LocalFileNode.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-2014 Basis Technology Corp. + * Copyright 2013-2016 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,6 +19,7 @@ package org.sleuthkit.autopsy.datamodel; import java.util.ArrayList; +import java.util.Arrays; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -74,7 +75,9 @@ public class LocalFileNode extends AbstractAbstractFileNode { for (Map.Entry entry : map.entrySet()) { ss.put(new NodeProperty<>(entry.getKey(), entry.getKey(), NO_DESCR, entry.getValue())); } - // @@@ add more properties here... + + // add tags property to the sheet + addTagProperty(ss); return s; } @@ -82,9 +85,7 @@ public class LocalFileNode extends AbstractAbstractFileNode { @Override public Action[] getActions(boolean context) { List actionsList = new ArrayList<>(); - for (Action a : super.getActions(true)) { - actionsList.add(a); - } + actionsList.addAll(Arrays.asList(super.getActions(true))); actionsList.add(new ViewContextAction(NbBundle.getMessage(this.getClass(), "LocalFileNode.viewFileInDir.text"), this.content)); actionsList.add(null); // creates a menu separator actionsList.add(new NewWindowViewAction( @@ -119,13 +120,8 @@ public class LocalFileNode extends AbstractAbstractFileNode { return true; //!this.hasContentChildren(); } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "LocalFile"; //NON-NLS -// } + @Override + public String getItemType() { + return getClass().getName(); + } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/accounts/RecentFiles.java b/Core/src/org/sleuthkit/autopsy/datamodel/RecentFiles.java similarity index 98% rename from Core/src/org/sleuthkit/autopsy/datamodel/accounts/RecentFiles.java rename to Core/src/org/sleuthkit/autopsy/datamodel/RecentFiles.java index d1ad7cb47b..ab39b69890 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/accounts/RecentFiles.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/RecentFiles.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.datamodel.accounts; +package org.sleuthkit.autopsy.datamodel; import org.sleuthkit.autopsy.datamodel.AutopsyVisitableItem; import org.sleuthkit.autopsy.datamodel.AutopsyItemVisitor; diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/RecentFilesChildren.java b/Core/src/org/sleuthkit/autopsy/datamodel/RecentFilesChildren.java index 6c39d582dd..391a49f757 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/RecentFilesChildren.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/RecentFilesChildren.java @@ -18,7 +18,6 @@ */ package org.sleuthkit.autopsy.datamodel; -import org.sleuthkit.autopsy.datamodel.accounts.RecentFiles; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Arrays; diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/RecentFilesFilterChildren.java b/Core/src/org/sleuthkit/autopsy/datamodel/RecentFilesFilterChildren.java index 2114166074..c6c9c72471 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/RecentFilesFilterChildren.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/RecentFilesFilterChildren.java @@ -28,7 +28,7 @@ import org.sleuthkit.autopsy.coreutils.Logger; import org.openide.nodes.AbstractNode; import org.openide.nodes.ChildFactory; import org.openide.nodes.Node; -import org.sleuthkit.autopsy.datamodel.accounts.RecentFiles.RecentFilesFilter; +import org.sleuthkit.autopsy.datamodel.RecentFiles.RecentFilesFilter; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.ContentVisitor; diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/RecentFilesFilterNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/RecentFilesFilterNode.java index e5eea61c96..66cbc5fb68 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/RecentFilesFilterNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/RecentFilesFilterNode.java @@ -25,7 +25,7 @@ import org.sleuthkit.autopsy.coreutils.Logger; import org.openide.nodes.Children; import org.openide.nodes.Sheet; import org.openide.util.lookup.Lookups; -import org.sleuthkit.autopsy.datamodel.accounts.RecentFiles.RecentFilesFilter; +import org.sleuthkit.autopsy.datamodel.RecentFiles.RecentFilesFilter; import org.sleuthkit.datamodel.SleuthkitCase; /** @@ -83,13 +83,12 @@ public class RecentFilesFilterNode extends DisplayableItemNode { return true; } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "RecentFilesFilter"; //NON-NLS -// } + @Override + public String getItemType() { + if (filter == null) { + return getClass().getName(); + } else { + return getClass().getName() + filter.getName(); + } + } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/RecentFilesNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/RecentFilesNode.java index 21751acdbc..f404b3ac75 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/RecentFilesNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/RecentFilesNode.java @@ -66,13 +66,8 @@ public class RecentFilesNode extends DisplayableItemNode { return s; } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "RecentFiles"; //NON-NLS -// } + @Override + public String getItemType() { + return getClass().getName(); + } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/Reports.java b/Core/src/org/sleuthkit/autopsy/datamodel/Reports.java index 7a4f581966..c606984056 100755 --- a/Core/src/org/sleuthkit/autopsy/datamodel/Reports.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/Reports.java @@ -44,6 +44,7 @@ import org.openide.util.lookup.Lookups; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import org.sleuthkit.autopsy.directorytree.ExternalViewerAction; import org.sleuthkit.datamodel.Report; import org.sleuthkit.datamodel.TskCoreException; @@ -90,15 +91,10 @@ public final class Reports implements AutopsyVisitableItem { return visitor.visit(this); } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "ReportsList"; //NON-NLS -// } + @Override + public String getItemType() { + return getClass().getName(); + } } /** @@ -218,15 +214,11 @@ public final class Reports implements AutopsyVisitableItem { return new OpenReportAction(); } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "Reports"; //NON-NLS -// } + @Override + public String getItemType() { + return getClass().getName(); + } + private static class DeleteReportAction extends AbstractAction { private static final long serialVersionUID = 1L; @@ -292,30 +284,16 @@ public final class Reports implements AutopsyVisitableItem { @Override public void actionPerformed(ActionEvent e) { - File file = new File(ReportNode.this.report.getPath()); - try { - Desktop.getDesktop().open(file); - } catch (IOException ex) { - JOptionPane.showMessageDialog(null, - NbBundle.getMessage(OpenReportAction.class, "OpenReportAction.actionPerformed.NoAssociatedEditorMessage"), - NbBundle.getMessage(OpenReportAction.class, "OpenReportAction.actionPerformed.MessageBoxTitle"), - JOptionPane.ERROR_MESSAGE); - } catch (UnsupportedOperationException ex) { - JOptionPane.showMessageDialog(null, - NbBundle.getMessage(OpenReportAction.class, "OpenReportAction.actionPerformed.NoOpenInEditorSupportMessage"), - NbBundle.getMessage(OpenReportAction.class, "OpenReportAction.actionPerformed.MessageBoxTitle"), - JOptionPane.ERROR_MESSAGE); - } catch (IllegalArgumentException ex) { - JOptionPane.showMessageDialog(null, - NbBundle.getMessage(OpenReportAction.class, "OpenReportAction.actionPerformed.MissingReportFileMessage"), - NbBundle.getMessage(OpenReportAction.class, "OpenReportAction.actionPerformed.MessageBoxTitle"), - JOptionPane.ERROR_MESSAGE); - } catch (SecurityException ex) { - JOptionPane.showMessageDialog(null, - NbBundle.getMessage(OpenReportAction.class, "OpenReportAction.actionPerformed.ReportFileOpenPermissionDeniedMessage"), - NbBundle.getMessage(OpenReportAction.class, "OpenReportAction.actionPerformed.MessageBoxTitle"), - JOptionPane.ERROR_MESSAGE); + String reportPath = ReportNode.this.report.getPath(); + String extension = ""; + int extPosition = reportPath.lastIndexOf('.'); + + if (extPosition != -1) { + extension = reportPath.substring(extPosition, reportPath.length()).toLowerCase(); } + + File file = new File(reportPath); + ExternalViewerAction.openFile("", extension, file); } } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/ResultsNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/ResultsNode.java index ebf71a872d..2363e2cf27 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/ResultsNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/ResultsNode.java @@ -33,6 +33,8 @@ public class ResultsNode extends DisplayableItemNode { @NbBundle.Messages("ResultsNode.name.text=Results") public static final String NAME = Bundle.ResultsNode_name_text(); + + public ResultsNode(SleuthkitCase sleuthkitCase) { super(new RootContentChildren(Arrays.asList( new ExtractedContent(sleuthkitCase), @@ -78,13 +80,8 @@ public class ResultsNode extends DisplayableItemNode { return s; } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "Results"; //NON-NLS -// } + @Override + public String getItemType() { + return getClass().getName(); + } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/SlackFileFilterNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/SlackFileFilterNode.java new file mode 100644 index 0000000000..af43970e8c --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datamodel/SlackFileFilterNode.java @@ -0,0 +1,180 @@ +/* + * 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.datamodel; + +import java.util.prefs.PreferenceChangeEvent; +import java.util.prefs.PreferenceChangeListener; +import org.openide.nodes.FilterNode; +import org.openide.nodes.Node; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.core.UserPreferences; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.TskData; + +/** + * A Filter Node responsible for conditionally filtering out Nodes that + * represent slack files. + * + * Filters known files IF the option to Filter Slack files for the given + * SelectionContext is set. Otherwise, does nothing. + * + */ +public class SlackFileFilterNode extends FilterNode { + + private static boolean filterFromDataSources = UserPreferences.hideSlackFilesInDataSourcesTree(); + private static boolean filterFromViews = UserPreferences.hideSlackFilesInViewsTree(); + + static { + UserPreferences.addChangeListener(new PreferenceChangeListener() { + @Override + public void preferenceChange(PreferenceChangeEvent evt) { + switch (evt.getKey()) { + case UserPreferences.HIDE_SLACK_FILES_IN_DATA_SRCS_TREE: + filterFromDataSources = UserPreferences.hideSlackFilesInDataSourcesTree(); + break; + case UserPreferences.HIDE_SLACK_FILES_IN_VIEWS_TREE: + filterFromViews = UserPreferences.hideSlackFilesInViewsTree(); + break; + } + } + }); + } + + public enum SelectionContext { + + DATA_SOURCES(NbBundle.getMessage(SlackFileFilterNode.class, "SlackFileFilterNode.selectionContext.dataSources")), + VIEWS(NbBundle.getMessage(SlackFileFilterNode.class, "SlackFileFilterNode.selectionContext.views")), + OTHER(""); // Subnode of another node. + + private final String displayName; + + SelectionContext(String displayName) { + this.displayName = displayName; + } + + public static SelectionContext getContextFromName(String name) { + if (name.equals(DATA_SOURCES.getName())) { + return DATA_SOURCES; + } else if (name.equals(VIEWS.getName())) { + return VIEWS; + } else { + return OTHER; + } + } + + private String getName() { + return displayName; + } + } + + /** + * Create a SlackFileFilterNode from the given Node. Note that the Node + * should be from the directory tree. + * + * @param arg + * @param context + */ + public SlackFileFilterNode(Node arg, SelectionContext context) { + super(arg, new SlackFileFilterChildren(arg, context)); + } + + private SlackFileFilterNode(Node arg, boolean filter) { + super(arg, new SlackFileFilterChildren(arg, filter)); + } + + /** + * Get the selection context of a Node in the DirectoryTree. + * + * @param n + * + * @return + */ + public static SelectionContext getSelectionContext(Node n) { + if (n == null || n.getParentNode() == null) { + // Parent of root node or root node. Occurs during case open / close. + return SelectionContext.OTHER; + } else if (n.getParentNode().getParentNode() == null) { + // One level below root node. Should be one of DataSources, Views, or Results + return SelectionContext.getContextFromName(n.getDisplayName()); + } else { + return getSelectionContext(n.getParentNode()); + } + } + + /** + * Complementary class to SlackFileFilterNode. + * + * Filters out children Nodes that represent slack files. Otherwise, returns + * the original node wrapped in another instance of the SlackFileFilterNode. + * + * @author jwallace + */ + private static class SlackFileFilterChildren extends FilterNode.Children { + + /** + * True if this SlackFileFilterChildren should filter out slack files. + */ + private boolean filter; + + /** + * Constructor used when the context has already been determined. + * + * @param arg + * @param filter + */ + private SlackFileFilterChildren(Node arg, boolean filter) { + super(arg); + this.filter = filter; + } + + /** + * Constructor used when the context has not been determined. + * + * @param arg + * @param context + */ + private SlackFileFilterChildren(Node arg, SlackFileFilterNode.SelectionContext context) { + super(arg); + + switch (context) { + case DATA_SOURCES: + filter = filterFromDataSources; + break; + case VIEWS: + filter = filterFromViews; + break; + default: + filter = false; + break; + } + } + + @Override + protected Node[] createNodes(Node arg) { + if (filter) { + // Filter out child nodes that represent slack files + AbstractFile file = arg.getLookup().lookup(AbstractFile.class); + if ((file != null) && file.getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.SLACK)) { + return new Node[]{}; + } + } + return new Node[]{new SlackFileFilterNode(arg, filter)}; + } + } +} \ No newline at end of file diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/SlackFileNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/SlackFileNode.java new file mode 100644 index 0000000000..863de3d9d8 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datamodel/SlackFileNode.java @@ -0,0 +1,120 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2011-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.datamodel; + +import java.util.ArrayList; +import java.util.List; +import javax.swing.Action; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.actions.AddContentTagAction; +import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; +import org.sleuthkit.autopsy.directorytree.ExtractAction; +import org.sleuthkit.autopsy.directorytree.NewWindowViewAction; +import org.sleuthkit.autopsy.directorytree.ViewContextAction; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.TskData.TSK_DB_FILES_TYPE_ENUM; +import org.sleuthkit.datamodel.TskData.TSK_FS_NAME_FLAG_ENUM; + +/** + * This class is the Node for an AbstractFile. It may have derived files + * children. + */ +public class SlackFileNode extends AbstractFsContentNode { + + /** + * Constructor + * + * @param file underlying Content + */ + public SlackFileNode(AbstractFile file) { + this(file, true); + + setIcon(file); + } + + public SlackFileNode(AbstractFile file, boolean directoryBrowseMode) { + super(file, directoryBrowseMode); + + setIcon(file); + } + + private void setIcon(AbstractFile file) { + // set name, display name, and icon + if (file.isDirNameFlagSet(TSK_FS_NAME_FLAG_ENUM.UNALLOC)) { + if (file.getType().equals(TSK_DB_FILES_TYPE_ENUM.CARVED)) { + this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/carved-file-icon-16.png"); //NON-NLS + } else { + this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/file-icon-deleted.png"); //NON-NLS + } + } else { + this.setIconBaseWithExtension(getIconForFileType(file)); + } + } + + @Override + public Action[] getActions(boolean popup) { + List actionsList = new ArrayList<>(); + for (Action a : super.getActions(true)) { + actionsList.add(a); + } + + if (!this.getDirectoryBrowseMode()) { + actionsList.add(new ViewContextAction(NbBundle.getMessage(this.getClass(), "SlackFileNode.viewFileInDir.text"), this.content)); + actionsList.add(null); // creates a menu separator + } + actionsList.add(new NewWindowViewAction( + NbBundle.getMessage(this.getClass(), "SlackFileNode.getActions.viewInNewWin.text"), this)); + actionsList.add(null); // creates a menu separator + actionsList.add(ExtractAction.getInstance()); + actionsList.add(null); // creates a menu separator + actionsList.add(AddContentTagAction.getInstance()); + actionsList.addAll(ContextMenuExtensionPoint.getActions()); + return actionsList.toArray(new Action[actionsList.size()]); + } + + @Override + public T accept(ContentNodeVisitor v) { + return v.visit(this); + } + + @Override + public T accept(DisplayableItemNodeVisitor v) { + return v.visit(this); + } + + // Given a file, returns the correct icon for said + // file based off it's extension + static String getIconForFileType(AbstractFile file) { + + return "org/sleuthkit/autopsy/images/file-icon.png"; //NON-NLS + } + + @Override + public boolean isLeafTypeNode() { + // This seems wrong, but it also seems that it is never called + // because the visitor to figure out if there are children or + // not will check if it has children using the Content API + return true; + } + + @Override + public String getItemType() { + return getClass().getName(); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/Tags.java b/Core/src/org/sleuthkit/autopsy/datamodel/Tags.java index ac85cd8787..b8127fe5a7 100755 --- a/Core/src/org/sleuthkit/autopsy/datamodel/Tags.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/Tags.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2015 Basis Technology Corp. + * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -46,8 +46,8 @@ import org.sleuthkit.datamodel.TskCoreException; * factory built on top of the NetBeans Children.Keys class. */ public class Tags implements AutopsyVisitableItem { - // Creation of a RootNode object corresponding to a Tags object is done - // by a CreateAutopsyNodeVisitor dispatched from the AbstractContentChildren + // Creation of a RootNode object corresponding to a Tags object is done + // by a CreateAutopsyNodeVisitor dispatched from the AbstractContentChildren // override of Children.Keys.createNodes(). private final TagResults tagResults = new TagResults(); @@ -105,19 +105,15 @@ public class Tags implements AutopsyVisitableItem { properties = Sheet.createPropertiesSet(); propertySheet.put(properties); } - properties.put(new NodeProperty<>(NbBundle.getMessage(this.getClass(), "TagsNode.createSheet.name.name"), NbBundle.getMessage(this.getClass(), "TagsNode.createSheet.name.displayName"), "", getName())); + properties.put(new NodeProperty<>(NbBundle.getMessage(this.getClass(), "TagsNode.createSheet.name.name"), + NbBundle.getMessage(this.getClass(), "TagsNode.createSheet.name.displayName"), "", getName())); return propertySheet; } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "TagsRoots"; //NON-NLS -// } + @Override + public String getItemType() { + return getClass().getName(); + } } private class TagNameNodeFactory extends ChildFactory.Detachable implements Observer { @@ -255,7 +251,8 @@ public class Tags implements AutopsyVisitableItem { properties = Sheet.createPropertiesSet(); propertySheet.put(properties); } - properties.put(new NodeProperty<>(NbBundle.getMessage(this.getClass(), "TagNameNode.createSheet.name.name"), NbBundle.getMessage(this.getClass(), "TagNameNode.createSheet.name.displayName"), tagName.getDescription(), getName())); + properties.put(new NodeProperty<>(NbBundle.getMessage(this.getClass(), "TagNameNode.createSheet.name.name"), + NbBundle.getMessage(this.getClass(), "TagNameNode.createSheet.name.displayName"), tagName.getDescription(), getName())); return propertySheet; } @@ -276,15 +273,10 @@ public class Tags implements AutopsyVisitableItem { updateDisplayName(); } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "TagsName"; //NON-NLS -// } + @Override + public String getItemType() { + return getClass().getName(); + } } /** @@ -333,7 +325,7 @@ public class Tags implements AutopsyVisitableItem { public class ContentTagTypeNode extends DisplayableItemNode implements Observer { private final String ICON_PATH = "org/sleuthkit/autopsy/images/tag-folder-blue-icon-16.png"; //NON-NLS - private TagName tagName; + private final TagName tagName; public ContentTagTypeNode(TagName tagName) { super(Children.create(new ContentTagNodeFactory(tagName), true), Lookups.singleton(tagName.getDisplayName() + " " + CONTENT_DISPLAY_NAME)); @@ -362,7 +354,8 @@ public class Tags implements AutopsyVisitableItem { properties = Sheet.createPropertiesSet(); propertySheet.put(properties); } - properties.put(new NodeProperty<>(NbBundle.getMessage(this.getClass(), "ContentTagTypeNode.createSheet.name.name"), NbBundle.getMessage(this.getClass(), "ContentTagTypeNode.createSheet.name.displayName"), "", getName())); + properties.put(new NodeProperty<>(NbBundle.getMessage(this.getClass(), "ContentTagTypeNode.createSheet.name.name"), + NbBundle.getMessage(this.getClass(), "ContentTagTypeNode.createSheet.name.displayName"), "", getName())); return propertySheet; } @@ -381,15 +374,10 @@ public class Tags implements AutopsyVisitableItem { updateDisplayName(); } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "TagsContentType"; //NON-NLS -// } + @Override + public String getItemType() { + return getClass().getName(); + } } private class ContentTagNodeFactory extends ChildFactory implements Observer { @@ -435,7 +423,7 @@ public class Tags implements AutopsyVisitableItem { */ public class BlackboardArtifactTagTypeNode extends DisplayableItemNode implements Observer { - private TagName tagName; + private final TagName tagName; private final String ICON_PATH = "org/sleuthkit/autopsy/images/tag-folder-blue-icon-16.png"; //NON-NLS public BlackboardArtifactTagTypeNode(TagName tagName) { @@ -465,7 +453,8 @@ public class Tags implements AutopsyVisitableItem { properties = Sheet.createPropertiesSet(); propertySheet.put(properties); } - properties.put(new NodeProperty<>(NbBundle.getMessage(this.getClass(), "BlackboardArtifactTagTypeNode.createSheet.name.name"), NbBundle.getMessage(this.getClass(), "BlackboardArtifactTagTypeNode.createSheet.name.displayName"), "", getName())); + properties.put(new NodeProperty<>(NbBundle.getMessage(this.getClass(), "BlackboardArtifactTagTypeNode.createSheet.name.name"), + NbBundle.getMessage(this.getClass(), "BlackboardArtifactTagTypeNode.createSheet.name.displayName"), "", getName())); return propertySheet; } @@ -484,15 +473,10 @@ public class Tags implements AutopsyVisitableItem { updateDisplayName(); } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "TagsBlackboardArtifact"; //NON-NLS -// } + @Override + public String getItemType() { + return getClass().getName(); + } } private class BlackboardArtifactTagNodeFactory extends ChildFactory implements Observer { @@ -521,7 +505,7 @@ public class Tags implements AutopsyVisitableItem { // The blackboard artifact tags to be wrapped are used as the keys. return new BlackboardArtifactTagNode(key); } - + @Override public void update(Observable o, Object arg) { refresh(true); diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/ViewsNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/ViewsNode.java index 89a6ac170f..9d1f0e0d63 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/ViewsNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/ViewsNode.java @@ -18,7 +18,6 @@ */ package org.sleuthkit.autopsy.datamodel; -import org.sleuthkit.autopsy.datamodel.accounts.FileTypeExtensionFilters; import java.util.Arrays; import org.openide.nodes.Sheet; import org.openide.util.NbBundle; @@ -36,7 +35,7 @@ public class ViewsNode extends DisplayableItemNode { public ViewsNode(SleuthkitCase sleuthkitCase) { super(new RootContentChildren(Arrays.asList( - new FileTypeExtensionFilters(sleuthkitCase), + new FileTypes(sleuthkitCase), // June '15: Recent Files was removed because it was not useful w/out filtering // add it back in if we can filter the results to a more managable size. // new RecentFiles(sleuthkitCase), @@ -74,13 +73,8 @@ public class ViewsNode extends DisplayableItemNode { return s; } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "Views"; //NON-NLS -// } + @Override + public String getItemType() { + return getClass().getName(); + } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/VirtualDirectoryNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/VirtualDirectoryNode.java index e6b30a6669..246c6d6e1d 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/VirtualDirectoryNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/VirtualDirectoryNode.java @@ -148,6 +148,7 @@ public class VirtualDirectoryNode extends AbstractAbstractFileNode entry : map.entrySet()) { ss.put(new NodeProperty<>(entry.getKey(), entry.getKey(), NO_DESCR, entry.getValue())); } + addTagProperty(ss); } else { ss.put(new NodeProperty<>(Bundle.VirtualDirectoryNode_createSheet_type_name(), Bundle.VirtualDirectoryNode_createSheet_type_displayName(), @@ -224,13 +225,9 @@ public class VirtualDirectoryNode extends AbstractAbstractFileNode { return v.visit(this); } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "Volume"; //NON-NLS -// } + @Override + public String getItemType() { + return DisplayableItemNode.FILE_PARENT_NODE_KEY; + } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/accounts/Accounts.java b/Core/src/org/sleuthkit/autopsy/datamodel/accounts/Accounts.java index 64c60e542d..7d2bd61100 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/accounts/Accounts.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/accounts/Accounts.java @@ -147,6 +147,14 @@ final public class Accounts implements AutopsyVisitableItem { */ private abstract class ObservingChildren extends Children.Keys { + /** + * Override of default constructor to force lazy creation of nodes, by + * concrete instances of ObservingChildren + */ + ObservingChildren() { + super(true); + } + /** * Create of keys used by this Children object to represent the child * nodes. @@ -335,6 +343,11 @@ final public class Accounts implements AutopsyVisitableItem { public T accept(DisplayableItemNodeVisitor v) { return v.visit(this); } + + @Override + public String getItemType() { + return getClass().getName(); + } } /** @@ -412,6 +425,11 @@ final public class Accounts implements AutopsyVisitableItem { public T accept(DisplayableItemNodeVisitor v) { return v.visit(this); } + + @Override + public String getItemType() { + return getClass().getName(); + } } /** @@ -479,6 +497,11 @@ final public class Accounts implements AutopsyVisitableItem { public T accept(DisplayableItemNodeVisitor v) { return v.visit(this); } + + @Override + public String getItemType() { + return getClass().getName(); + } } /** @@ -612,6 +635,11 @@ final public class Accounts implements AutopsyVisitableItem { return v.visit(this); } + @Override + public String getItemType() { + return getClass().getName(); + } + @Subscribe void handleReviewStatusChange(ReviewStatusChangeEvent event) { updateDisplayName(); @@ -737,6 +765,11 @@ final public class Accounts implements AutopsyVisitableItem { return v.visit(this); } + @Override + public String getItemType() { + return getClass().getName(); + } + @Subscribe void handleReviewStatusChange(ReviewStatusChangeEvent event) { updateDisplayName(); @@ -921,6 +954,11 @@ final public class Accounts implements AutopsyVisitableItem { return v.visit(this); } + @Override + public String getItemType() { + return getClass().getName(); + } + @Override @NbBundle.Messages({ "Accounts.FileWithCCNNode.nameProperty.displayName=File", @@ -1036,8 +1074,8 @@ final public class Accounts implements AutopsyVisitableItem { private BINNode(BinResult bin) { super(Children.LEAF); - setChildren(Children.createLazy(CreditCardNumberFactory::new)); this.bin = bin; + setChildren(Children.createLazy(CreditCardNumberFactory::new)); setName(getBinRangeString()); updateDisplayName(); this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/bank.png"); //NON-NLS @@ -1093,6 +1131,11 @@ final public class Accounts implements AutopsyVisitableItem { return v.visit(this); } + @Override + public String getItemType() { + return getClass().getName(); + } + private Sheet.Set getPropertySet(Sheet s) { Sheet.Set ss = s.get(Sheet.PROPERTIES); if (ss == null) { @@ -1356,18 +1399,27 @@ final public class Accounts implements AutopsyVisitableItem { */ if (newStatus == BlackboardArtifact.ReviewStatus.REJECTED && showRejected == false) { List siblings = Arrays.asList(node.getParentNode().getChildren().getNodes()); - int indexOf = siblings.indexOf(node); - //there is no previous for the first node, so instead we select the next one - Node sibling = indexOf > 0 - ? siblings.get(indexOf - 1) - : siblings.get(indexOf + 1); - createPath = NodeOp.createPath(sibling, null); + if (siblings.size() > 1) { + int indexOf = siblings.indexOf(node); + //there is no previous for the first node, so instead we select the next one + Node sibling = indexOf > 0 + ? siblings.get(indexOf - 1) + : siblings.get(Integer.max(indexOf + 1, siblings.size() - 1)); + createPath = NodeOp.createPath(sibling, null); + } else { + /* if there are no other siblings to select, + * just return null, but note we need to filter + * this out of stream below */ + return null; + } } else { createPath = NodeOp.createPath(node, null); } //for the reselect to work we need to strip off the first part of the path. return Arrays.copyOfRange(createPath, 1, createPath.length); - }).collect(Collectors.toList()); + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); //change status of selected artifacts final Collection artifacts = Utilities.actionsGlobalContext().lookupAll(BlackboardArtifact.class); diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/accounts/Bundle.properties b/Core/src/org/sleuthkit/autopsy/datamodel/accounts/Bundle.properties deleted file mode 100644 index dc778857c5..0000000000 --- a/Core/src/org/sleuthkit/autopsy/datamodel/accounts/Bundle.properties +++ /dev/null @@ -1,12 +0,0 @@ -FileTypeExtensionFilters.tskImgFilter.text=Images -FileTypeExtensionFilters.tskVideoFilter.text=Videos -FileTypeExtensionFilters.tskAudioFilter.text=Audio -FileTypeExtensionFilters.tskArchiveFilter.text=Archives -FileTypeExtensionFilters.tskDocumentFilter.text=Documents -FileTypeExtensionFilters.tskExecFilter.text=Executable -FileTypeExtensionFilters.autDocHtmlFilter.text=HTML -FileTypeExtensionFilters.autDocOfficeFilter.text=Office -FileTypeExtensionFilters.autoDocPdfFilter.text=PDF -FileTypeExtensionFilters.autDocTxtFilter.text=Plain Text -FileTypeExtensionFilters.autDocRtfFilter.text=Rich Text - diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/accounts/FileTypeExtensionFilters.java b/Core/src/org/sleuthkit/autopsy/datamodel/accounts/FileTypeExtensionFilters.java deleted file mode 100644 index 80680e97cc..0000000000 --- a/Core/src/org/sleuthkit/autopsy/datamodel/accounts/FileTypeExtensionFilters.java +++ /dev/null @@ -1,223 +0,0 @@ -/* - * 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.datamodel.accounts; - -import org.sleuthkit.autopsy.datamodel.AutopsyItemVisitor; -import org.sleuthkit.autopsy.datamodel.AutopsyVisitableItem; -import java.util.Arrays; -import java.util.List; -import org.openide.util.NbBundle; -import org.sleuthkit.autopsy.datamodel.FileTypeExtensions; -import org.sleuthkit.datamodel.SleuthkitCase; - -/** - * Filters database results by file extension. - */ -public class FileTypeExtensionFilters implements AutopsyVisitableItem { - - private final SleuthkitCase skCase; - - // root node filters - public enum RootFilter implements AutopsyVisitableItem, SearchFilterInterface { - - TSK_IMAGE_FILTER(0, "TSK_IMAGE_FILTER", //NON-NLS - NbBundle.getMessage(FileTypeExtensionFilters.class, "FileTypeExtensionFilters.tskImgFilter.text"), - FileTypeExtensions.getImageExtensions()), - TSK_VIDEO_FILTER(1, "TSK_VIDEO_FILTER", //NON-NLS - NbBundle.getMessage(FileTypeExtensionFilters.class, "FileTypeExtensionFilters.tskVideoFilter.text"), - FileTypeExtensions.getVideoExtensions()), - TSK_AUDIO_FILTER(2, "TSK_AUDIO_FILTER", //NON-NLS - NbBundle.getMessage(FileTypeExtensionFilters.class, "FileTypeExtensionFilters.tskAudioFilter.text"), - FileTypeExtensions.getAudioExtensions()), - TSK_ARCHIVE_FILTER(3, "TSK_ARCHIVE_FILTER", //NON-NLS - NbBundle.getMessage(FileTypeExtensionFilters.class, "FileTypeExtensionFilters.tskArchiveFilter.text"), - FileTypeExtensions.getArchiveExtensions()), - TSK_DOCUMENT_FILTER(3, "TSK_DOCUMENT_FILTER", //NON-NLS - NbBundle.getMessage(FileTypeExtensionFilters.class, "FileTypeExtensionFilters.tskDocumentFilter.text"), - Arrays.asList(".doc", ".docx", ".pdf", ".xls", ".rtf", ".txt")), //NON-NLS - TSK_EXECUTABLE_FILTER(3, "TSK_EXECUTABLE_FILTER", //NON-NLS - NbBundle.getMessage(FileTypeExtensionFilters.class, "FileTypeExtensionFilters.tskExecFilter.text"), - Arrays.asList(".exe", ".dll", ".bat", ".cmd", ".com")); //NON-NLS - - private final int id; - private final String name; - private final String displayName; - private final List filter; - - private RootFilter(int id, String name, String displayName, List filter) { - this.id = id; - this.name = name; - this.displayName = displayName; - this.filter = filter; - } - - @Override - public T accept(AutopsyItemVisitor v) { - return v.visit(this); - } - - @Override - public String getName() { - return this.name; - } - - @Override - public int getId() { - return this.id; - } - - @Override - public String getDisplayName() { - return this.displayName; - } - - @Override - public List getFilter() { - return this.filter; - } - } - - // document sub-node filters - public enum DocumentFilter implements AutopsyVisitableItem, SearchFilterInterface { - - AUT_DOC_HTML(0, "AUT_DOC_HTML", //NON-NLS - NbBundle.getMessage(FileTypeExtensionFilters.class, "FileTypeExtensionFilters.autDocHtmlFilter.text"), - Arrays.asList(".htm", ".html")), //NON-NLS - AUT_DOC_OFFICE(1, "AUT_DOC_OFFICE", //NON-NLS - NbBundle.getMessage(FileTypeExtensionFilters.class, "FileTypeExtensionFilters.autDocOfficeFilter.text"), - Arrays.asList(".doc", ".docx", ".odt", ".xls", ".xlsx", ".ppt", ".pptx")), //NON-NLS - AUT_DOC_PDF(2, "AUT_DOC_PDF", //NON-NLS - NbBundle.getMessage(FileTypeExtensionFilters.class, "FileTypeExtensionFilters.autoDocPdfFilter.text"), - Arrays.asList(".pdf")), //NON-NLS - AUT_DOC_TXT(3, "AUT_DOC_TXT", //NON-NLS - NbBundle.getMessage(FileTypeExtensionFilters.class, "FileTypeExtensionFilters.autDocTxtFilter.text"), - Arrays.asList(".txt")), //NON-NLS - AUT_DOC_RTF(4, "AUT_DOC_RTF", //NON-NLS - NbBundle.getMessage(FileTypeExtensionFilters.class, "FileTypeExtensionFilters.autDocRtfFilter.text"), - Arrays.asList(".rtf")); //NON-NLS - - private final int id; - private final String name; - private final String displayName; - private final List filter; - - private DocumentFilter(int id, String name, String displayName, List filter) { - this.id = id; - this.name = name; - this.displayName = displayName; - this.filter = filter; - } - - @Override - public T accept(AutopsyItemVisitor v) { - return v.visit(this); - } - - @Override - public String getName() { - return this.name; - } - - @Override - public int getId() { - return this.id; - } - - @Override - public String getDisplayName() { - return this.displayName; - } - - @Override - public List getFilter() { - return this.filter; - } - } - - // executable sub-node filters - public enum ExecutableFilter implements AutopsyVisitableItem, SearchFilterInterface { - - ExecutableFilter_EXE(0, "ExecutableFilter_EXE", ".exe", Arrays.asList(".exe")), //NON-NLS - ExecutableFilter_DLL(1, "ExecutableFilter_DLL", ".dll", Arrays.asList(".dll")), //NON-NLS - ExecutableFilter_BAT(2, "ExecutableFilter_BAT", ".bat", Arrays.asList(".bat")), //NON-NLS - ExecutableFilter_CMD(3, "ExecutableFilter_CMD", ".cmd", Arrays.asList(".cmd")), //NON-NLS - ExecutableFilter_COM(4, "ExecutableFilter_COM", ".com", Arrays.asList(".com")); //NON-NLS - - private final int id; - private final String name; - private final String displayName; - private final List filter; - - private ExecutableFilter(int id, String name, String displayName, List filter) { - this.id = id; - this.name = name; - this.displayName = displayName; - this.filter = filter; - } - - @Override - public T accept(AutopsyItemVisitor v) { - return v.visit(this); - } - - @Override - public String getName() { - return this.name; - } - - @Override - public int getId() { - return this.id; - } - - @Override - public String getDisplayName() { - return this.displayName; - } - - @Override - public List getFilter() { - return this.filter; - } - } - - public FileTypeExtensionFilters(SleuthkitCase skCase) { - this.skCase = skCase; - } - - @Override - public T accept(AutopsyItemVisitor v) { - return v.visit(this); - } - - public SleuthkitCase getSleuthkitCase() { - return this.skCase; - } - - public interface SearchFilterInterface { - - public String getName(); - - public int getId(); - - public String getDisplayName(); - - public List getFilter(); - } -} diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/AddRawImageTask.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/AddRawImageTask.java new file mode 100644 index 0000000000..c85eedaa06 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/AddRawImageTask.java @@ -0,0 +1,183 @@ +package org.sleuthkit.autopsy.datasourceprocessors; + +/* + * Autopsy Forensic Browser + * + * Copyright 2011-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. + */ + + +import java.io.File; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback; +import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgressMonitor; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.Image; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.TskFileRange; +import org.openide.util.NbBundle.Messages; + +/* + * A runnable that adds a raw data source to a case database. + */ +final class AddRawImageTask implements Runnable { + + private static final Logger logger = Logger.getLogger(AddRawImageTask.class.getName()); + private final String deviceId; + private final String imageFilePath; + private final String timeZone; + private final long chunkSize; + private final DataSourceProcessorProgressMonitor progressMonitor; + private final DataSourceProcessorCallback callback; + private boolean criticalErrorOccurred; + private static final long TWO_GB = 2000000000L; + + /** + * Constructs a runnable that adds a raw data source to a case database. + * + * @param deviceId An ASCII-printable identifier for the + * device associated with the data source + * that is intended to be unique across + * multiple cases (e.g., a UUID). + * @param imageFilePath Path to a Raw data source file. + * @param timeZone The time zone to use when processing dates + * and times for the image, obtained from + * java.util.TimeZone.getID. + * @param breakupChunks 2GB or not breakup. + * @param progressMonitor Progress monitor for reporting + * progressMonitor during processing. + * @param callback Callback to call when processing is done. + */ + AddRawImageTask(String deviceId, String imageFilePath, String timeZone, long chunkSize, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) { + this.deviceId = deviceId; + this.imageFilePath = imageFilePath; + this.timeZone = timeZone; + this.chunkSize = chunkSize; + this.callback = callback; + this.progressMonitor = progressMonitor; + } + + /** + * Adds a raw data source to a case database. + */ + @Override + public void run() { + /* + * Process the input image file. + */ + progressMonitor.setIndeterminate(true); + progressMonitor.setProgress(0); + List newDataSources = new ArrayList<>(); + List errorMessages = new ArrayList<>(); + addImageToCase(newDataSources, errorMessages); + + progressMonitor.setProgress(100); + + /** + * Return the results via the callback passed to the constructor. + */ + DataSourceProcessorCallback.DataSourceProcessorResult result; + if (criticalErrorOccurred) { + result = DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS; + } else if (!errorMessages.isEmpty()) { + result = DataSourceProcessorCallback.DataSourceProcessorResult.NONCRITICAL_ERRORS; + } else { + result = DataSourceProcessorCallback.DataSourceProcessorResult.NO_ERRORS; + } + callback.done(result, errorMessages, newDataSources); + criticalErrorOccurred = false; + } + + /** + * Attempts to add the input image to the case. + * + * @param newDataSources If the image is added, a data source is added to + * this list for eventual return to the caller via the + * callback. + * @param errorMessages If there are any error messages, the error messages + * are added to this list for eventual return to the + * caller via the callback. + */ + @Messages({"AddRawImageTask.progress.add.text=Adding raw image: ", + "AddRawImageTask.image.critical.error.adding=Critical error adding ", + "AddRawImageTask.for.device=for device ", + "AddRawImageTask.image.notExisting=is not existing.", + "AddRawImageTask.image.noncritical.error.adding=Non-critical error adding "}) + private void addImageToCase(List dataSources, List errorMessages) { + progressMonitor.setProgressText(Bundle.AddRawImageTask_progress_add_text() + imageFilePath); + List imageFilePaths = new ArrayList<>(); + SleuthkitCase caseDatabase = Case.getCurrentCase().getSleuthkitCase(); + caseDatabase.acquireExclusiveLock(); + + File imageFile = Paths.get(imageFilePath).toFile(); + if (!imageFile.exists()) { + errorMessages.add(Bundle.AddRawImageTask_image_critical_error_adding() + imageFilePath + Bundle.AddRawImageTask_for_device() + + deviceId + Bundle.AddRawImageTask_image_notExisting()); + criticalErrorOccurred = true; + return; + } + + imageFilePaths.add(imageFilePath); + + try { + /* + * Get Image that will be added to case + */ + Image dataSource = caseDatabase.addImageInfo(0, imageFilePaths, timeZone); //TODO: change hard coded deviceId. + dataSources.add(dataSource); + List fileRanges = new ArrayList<>(); + + /* + * Verify the size of the new image. Note that it may not be what is + * expected, but at least part of it was added to the case. + */ + String verificationError = dataSource.verifyImageSize(); + if (!verificationError.isEmpty()) { + errorMessages.add(Bundle.AddRawImageTask_image_noncritical_error_adding() + imageFilePaths + Bundle.AddRawImageTask_for_device() + deviceId + ":" + verificationError); + } + + long imageSize = dataSource.getSize(); + int sequence = 0; + //start byte and end byte + long start = 0; + if (chunkSize > 0 && imageSize >= TWO_GB) { + for (double size = TWO_GB; size < dataSource.getSize(); size += TWO_GB) { + fileRanges.add(new TskFileRange(start, TWO_GB, sequence)); + start += TWO_GB; + sequence++; + } + + } + double leftoverSize = imageSize - sequence * TWO_GB; + fileRanges.add(new TskFileRange(start, (long)leftoverSize, sequence)); + + + caseDatabase.addLayoutFiles(dataSource, fileRanges); + + } catch (TskCoreException ex) { + errorMessages.add(Bundle.AddRawImageTask_image_critical_error_adding() + imageFilePaths + Bundle.AddRawImageTask_for_device() + deviceId + ":" + ex.getLocalizedMessage()); + criticalErrorOccurred = true; + } finally { + caseDatabase.releaseExclusiveLock(); + } + + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/Bundle.properties b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/Bundle.properties new file mode 100755 index 0000000000..4a8de9a48b --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/Bundle.properties @@ -0,0 +1,12 @@ +# To change this license header, choose License Headers in Project Properties. +# To change this template file, choose Tools | Templates +# and open the template in the editor. + +RawDSInputPanel.pathLabel.text=Browse for an unallocated space image file: +RawDSInputPanel.errorLabel.text=Error Label +RawDSInputPanel.browseButton.text=Browse +RawDSInputPanel.pathTextField.text= +RawDSInputPanel.jBreakFileUpLabel.text=Break image up into: +RawDSInputPanel.jNoBreakupRadioButton.text=Do not break up +RawDSInputPanel.j2GBBreakupRadioButton.text=2GB chunks +RawDSInputPanel.timeZoneLabel.text=Please select the input timezone: diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/RawDSInputPanel.form b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/RawDSInputPanel.form new file mode 100755 index 0000000000..ff07eaa18c --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/RawDSInputPanel.form @@ -0,0 +1,170 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/RawDSInputPanel.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/RawDSInputPanel.java new file mode 100755 index 0000000000..8fb6fe5ccf --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/RawDSInputPanel.java @@ -0,0 +1,352 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2011-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.datasourceprocessors; + +import java.io.File; +import java.util.Calendar; +import java.util.SimpleTimeZone; +import java.util.TimeZone; +import javax.swing.JFileChooser; +import javax.swing.JPanel; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; +import org.sleuthkit.autopsy.coreutils.ModuleSettings; +import org.sleuthkit.autopsy.coreutils.PathValidator; + +final class RawDSInputPanel extends JPanel implements DocumentListener { + private static final long TWO_GB = 2000000000L; + private static final long serialVersionUID = 1L; //default + private final String PROP_LASTINPUT_PATH = "LBL_LastInputFile_PATH"; + private final JFileChooser fc = new JFileChooser(); + // Externally supplied name is used to store settings + private final String contextName; + /** + * Creates new form RawDSInputPanel + */ + private RawDSInputPanel(String context) { + initComponents(); + + errorLabel.setVisible(false); + + fc.setDragEnabled(false); + fc.setFileSelectionMode(JFileChooser.FILES_ONLY); + fc.setMultiSelectionEnabled(false); + + this.contextName = context; + } + + /** + * Creates and returns an instance of a RawDSInputPanel. + */ + static synchronized RawDSInputPanel createInstance(String context) { + RawDSInputPanel instance = new RawDSInputPanel(context); + + instance.postInit(); + instance.createTimeZoneList(); + + return instance; + } + + //post-constructor initialization to properly initialize listener support + //without leaking references of uninitialized objects + private void postInit() { + pathTextField.getDocument().addDocumentListener(this); + } + + /** + * Creates the drop down list for the time zones and then makes the local + * machine time zone to be selected. + */ + private void createTimeZoneList() { + // load and add all timezone + String[] ids = SimpleTimeZone.getAvailableIDs(); + for (String id : ids) { + TimeZone zone = TimeZone.getTimeZone(id); + int offset = zone.getRawOffset() / 1000; + int hour = offset / 3600; + int minutes = (offset % 3600) / 60; + String item = String.format("(GMT%+d:%02d) %s", hour, minutes, id); + + timeZoneComboBox.addItem(item); + } + // get the current timezone + TimeZone thisTimeZone = Calendar.getInstance().getTimeZone(); + int thisOffset = thisTimeZone.getRawOffset() / 1000; + int thisHour = thisOffset / 3600; + int thisMinutes = (thisOffset % 3600) / 60; + String formatted = String.format("(GMT%+d:%02d) %s", thisHour, thisMinutes, thisTimeZone.getID()); + + // set the selected timezone + timeZoneComboBox.setSelectedItem(formatted); + } + + /** + * 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() { + + infileTypeButtonGroup = new javax.swing.ButtonGroup(); + pathLabel = new javax.swing.JLabel(); + pathTextField = new javax.swing.JTextField(); + browseButton = new javax.swing.JButton(); + j2GBBreakupRadioButton = new javax.swing.JRadioButton(); + jBreakFileUpLabel = new javax.swing.JLabel(); + jNoBreakupRadioButton = new javax.swing.JRadioButton(); + errorLabel = new javax.swing.JLabel(); + timeZoneLabel = new javax.swing.JLabel(); + timeZoneComboBox = new javax.swing.JComboBox<>(); + + org.openide.awt.Mnemonics.setLocalizedText(pathLabel, org.openide.util.NbBundle.getMessage(RawDSInputPanel.class, "RawDSInputPanel.pathLabel.text")); // NOI18N + + pathTextField.setText(org.openide.util.NbBundle.getMessage(RawDSInputPanel.class, "RawDSInputPanel.pathTextField.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(browseButton, org.openide.util.NbBundle.getMessage(RawDSInputPanel.class, "RawDSInputPanel.browseButton.text")); // NOI18N + browseButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + browseButtonActionPerformed(evt); + } + }); + + infileTypeButtonGroup.add(j2GBBreakupRadioButton); + j2GBBreakupRadioButton.setSelected(true); + org.openide.awt.Mnemonics.setLocalizedText(j2GBBreakupRadioButton, org.openide.util.NbBundle.getMessage(RawDSInputPanel.class, "RawDSInputPanel.j2GBBreakupRadioButton.text")); // NOI18N + j2GBBreakupRadioButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + j2GBBreakupRadioButtonActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(jBreakFileUpLabel, org.openide.util.NbBundle.getMessage(RawDSInputPanel.class, "RawDSInputPanel.jBreakFileUpLabel.text")); // NOI18N + + infileTypeButtonGroup.add(jNoBreakupRadioButton); + org.openide.awt.Mnemonics.setLocalizedText(jNoBreakupRadioButton, org.openide.util.NbBundle.getMessage(RawDSInputPanel.class, "RawDSInputPanel.jNoBreakupRadioButton.text")); // NOI18N + jNoBreakupRadioButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + jNoBreakupRadioButtonActionPerformed(evt); + } + }); + + errorLabel.setForeground(new java.awt.Color(255, 0, 0)); + org.openide.awt.Mnemonics.setLocalizedText(errorLabel, org.openide.util.NbBundle.getMessage(RawDSInputPanel.class, "RawDSInputPanel.errorLabel.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(timeZoneLabel, org.openide.util.NbBundle.getMessage(RawDSInputPanel.class, "RawDSInputPanel.timeZoneLabel.text")); // NOI18N + + timeZoneComboBox.setMaximumRowCount(30); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(pathTextField) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(browseButton, javax.swing.GroupLayout.PREFERRED_SIZE, 77, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(pathLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 218, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGroup(layout.createSequentialGroup() + .addComponent(timeZoneLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 168, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(timeZoneComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, 199, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addGap(0, 19, Short.MAX_VALUE)) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(jBreakFileUpLabel) + .addComponent(errorLabel) + .addGroup(layout.createSequentialGroup() + .addGap(10, 10, 10) + .addComponent(j2GBBreakupRadioButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(jNoBreakupRadioButton))) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(pathLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(pathTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(browseButton)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(timeZoneLabel) + .addComponent(timeZoneComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(errorLabel) + .addGap(5, 5, 5) + .addComponent(jBreakFileUpLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(jNoBreakupRadioButton) + .addComponent(j2GBBreakupRadioButton)) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + }// //GEN-END:initComponents + @SuppressWarnings("deprecation") + private void browseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_browseButtonActionPerformed + String oldText = pathTextField.getText(); + // set the current directory of the FileChooser if the ImagePath Field is valid + File currentDir = new File(oldText); + if (currentDir.exists()) { + fc.setCurrentDirectory(currentDir); + } + + int retval = fc.showOpenDialog(this); + if (retval == JFileChooser.APPROVE_OPTION) { + String path = fc.getSelectedFile().getPath(); + pathTextField.setText(path); + } + }//GEN-LAST:event_browseButtonActionPerformed + + private void j2GBBreakupRadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_j2GBBreakupRadioButtonActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_j2GBBreakupRadioButtonActionPerformed + + private void jNoBreakupRadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jNoBreakupRadioButtonActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_jNoBreakupRadioButtonActionPerformed + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton browseButton; + private javax.swing.JLabel errorLabel; + private javax.swing.ButtonGroup infileTypeButtonGroup; + private javax.swing.JRadioButton j2GBBreakupRadioButton; + private javax.swing.JLabel jBreakFileUpLabel; + private javax.swing.JRadioButton jNoBreakupRadioButton; + private javax.swing.JLabel pathLabel; + private javax.swing.JTextField pathTextField; + private javax.swing.JComboBox timeZoneComboBox; + private javax.swing.JLabel timeZoneLabel; + // End of variables declaration//GEN-END:variables + /** + * Get the path of the user selected image. + * + * @return the image path + */ + String getImageFilePath() { + return pathTextField.getText(); + } + + void reset() { + //reset the UI elements to default + pathTextField.setText(null); + j2GBBreakupRadioButton.setSelected(true); + } + + long getChunkSize() { + if (jNoBreakupRadioButton.isSelected()) { + return -1; + } else { //if have more choices here, the selection of each radiobutton should be checked + return TWO_GB; + } + } + + String getTimeZone() { + String tz = timeZoneComboBox.getSelectedItem().toString(); + return tz.substring(tz.indexOf(")") + 2).trim(); + } + + /** + * Should we enable the next button of the wizard? + * + * @return true if a proper image has been selected, false otherwise + */ + boolean validatePanel() { + errorLabel.setVisible(false); + String path = getImageFilePath(); + if (path == null || path.isEmpty()) { + return false; + } + + // display warning if there is one (but don't disable "next" button) + warnIfPathIsInvalid(path); + + boolean isExist = new File(path).exists(); + + return (isExist); + } + + /** + * Validates path to selected data source and displays warning if it is + * invalid. + * + * @param path Absolute path to the selected data source + */ + @Messages({"RawDSInputPanel.error.text=Path to multi-user data source is on \"C:\" drive"}) + private void warnIfPathIsInvalid(String path) { + if (!PathValidator.isValid(path, Case.getCurrentCase().getCaseType())) { + errorLabel.setVisible(true); + errorLabel.setText(Bundle.RawDSInputPanel_error_text()); + } + } + + void storeSettings() { + String inFilePath = getImageFilePath(); + if (null != inFilePath) { + String imagePath = inFilePath.substring(0, inFilePath.lastIndexOf(File.separator) + 1); + ModuleSettings.setConfigSetting(contextName, PROP_LASTINPUT_PATH, imagePath); + } + } + + void readSettings() { + String inFilePath = ModuleSettings.getConfigSetting(contextName, PROP_LASTINPUT_PATH); + if (null != inFilePath) { + if (!inFilePath.isEmpty()) { + pathTextField.setText(inFilePath); + } + } + } + + /** + * Update functions are called by the pathTextField which has this set as + * it's DocumentEventListener. Each update function fires a property change + * to be caught by the parent panel. + * + * @param e the event, which is ignored + */ + @Override + public void insertUpdate(DocumentEvent e) { + firePropertyChange(DataSourceProcessor.DSP_PANEL_EVENT.UPDATE_UI.toString(), false, true); + } + + @Override + public void removeUpdate(DocumentEvent e) { + firePropertyChange(DataSourceProcessor.DSP_PANEL_EVENT.UPDATE_UI.toString(), false, true); + } + + @Override + public void changedUpdate(DocumentEvent e) { + firePropertyChange(DataSourceProcessor.DSP_PANEL_EVENT.UPDATE_UI.toString(), false, true); + } + + /** + * Set the focus to the pathTextField. + */ + void select() { + pathTextField.requestFocusInWindow(); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/RawDSProcessor.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/RawDSProcessor.java new file mode 100644 index 0000000000..68908ae7a3 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/RawDSProcessor.java @@ -0,0 +1,157 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2011-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.datasourceprocessors; + +import java.util.UUID; +import javax.swing.JPanel; +import org.openide.util.NbBundle.Messages; +import org.openide.util.lookup.ServiceProvider; +import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgressMonitor; +import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback; +import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; + +/** + * A Raw data source processor that implements the DataSourceProcessor service + * provider interface to allow integration with the add data source wizard. + * It also provides a run method overload to allow it to be used independently + * of the wizard. + */ +@ServiceProvider(service = DataSourceProcessor.class) +public class RawDSProcessor implements DataSourceProcessor { + private final RawDSInputPanel configPanel; + private AddRawImageTask addImageTask; + + /* + * Constructs a Raw data source processor that implements the + * DataSourceProcessor service provider interface to allow integration + * with the add data source wizard. It also provides a run method + * overload to allow it to be used independently of the wizard. + */ + public RawDSProcessor() { + configPanel = RawDSInputPanel.createInstance(RawDSProcessor.class.getName()); + } + +/** + * Gets a string that describes the type of data sources this processor is + * able to add to the case database. The string is suitable for display in a + * type selection UI component (e.g., a combo box). + * + * @return A data source type display string for this data source processor. + */ + @Messages({"RawDSProcessor.dataSourceType=Unallocated Space Image File"}) + public static String getType() { + return Bundle.RawDSProcessor_dataSourceType(); + } + + /** + * Gets a string that describes the type of data sources this processor is + * able to add to the case database. The string is suitable for display in a + * type selection UI component (e.g., a combo box). + * + * @return A data source type display string for this data source processor. + */ + @Override + public String getDataSourceType() { + return Bundle.RawDSProcessor_dataSourceType(); + } + + /** + * Gets the panel that allows a user to select a data source and do any + * configuration required by the data source. The panel is less than 544 + * pixels wide and less than 173 pixels high. + * + * @return A selection and configuration panel for this data source + * processor. + */ + @Override + public JPanel getPanel() { + configPanel.readSettings(); + configPanel.select(); + return configPanel; + } + + /** + * Indicates whether the settings in the selection and configuration panel + * are valid and complete. + * + * @return True if the settings are valid and complete and the processor is + * ready to have its run method called, false otherwise. + */ + @Override + public boolean isPanelValid() { + return configPanel.validatePanel(); + } + + /** + * Adds a data source to the case database using a background task in a + * separate thread and the settings provided by the selection and + * configuration panel. Returns as soon as the background task is started. + * The background task uses a callback object to signal task completion and + * return results. + * + * This method should not be called unless isPanelValid returns true. + * + * @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. + */ + @Override + public void run(DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) { + configPanel.storeSettings(); + run(UUID.randomUUID().toString(), configPanel.getImageFilePath(), configPanel.getTimeZone(), configPanel.getChunkSize(), progressMonitor, callback); + } + + /** + * Adds a data source to the case database using a background task in a + * separate thread and the given settings instead of those provided by the + * selection and configuration panel. Returns as soon as the background task + * is started and uses the callback object to signal task completion and + * return results. + * + * @param deviceId An ASCII-printable identifier for the + * device associated with the data source + * that is intended to be unique across + * multiple cases (e.g., a UUID). + * @param rawDSInputFilePath Path to a Raw data source file. + * @param isHandsetFile Indicates whether the XML file is for a + * handset or a SIM. + * @param progressMonitor Progress monitor for reporting progress + * during processing. + */ + private void run(String deviceId, String imageFilePath, String timeZone, long chunkSize, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) { + addImageTask = new AddRawImageTask(deviceId, imageFilePath, timeZone, chunkSize, progressMonitor, callback); + new Thread(addImageTask).start(); + } + + + @Override + public void cancel() { + } + + /** + * Resets the selection and configuration panel for this data source + * processor. + */ + @Override + public void reset() { + configPanel.reset(); + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java b/Core/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java index ec66a5d4c7..de20df20e7 100755 --- a/Core/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/DataResultFilterNode.java @@ -42,9 +42,11 @@ import org.sleuthkit.autopsy.datamodel.DirectoryNode; import org.sleuthkit.autopsy.datamodel.DisplayableItemNode; import org.sleuthkit.autopsy.datamodel.DisplayableItemNodeVisitor; import org.sleuthkit.autopsy.datamodel.FileNode; +import org.sleuthkit.autopsy.datamodel.FileTypes.FileTypesNode; import org.sleuthkit.autopsy.datamodel.LayoutFileNode; import org.sleuthkit.autopsy.datamodel.LocalFileNode; import org.sleuthkit.autopsy.datamodel.Reports; +import org.sleuthkit.autopsy.datamodel.SlackFileNode; import org.sleuthkit.autopsy.datamodel.VirtualDirectoryNode; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; @@ -55,6 +57,7 @@ import org.sleuthkit.datamodel.Directory; import org.sleuthkit.datamodel.File; import org.sleuthkit.datamodel.LayoutFile; import org.sleuthkit.datamodel.LocalFile; +import org.sleuthkit.datamodel.SlackFile; import org.sleuthkit.datamodel.TskException; import org.sleuthkit.datamodel.VirtualDirectory; @@ -200,6 +203,8 @@ public class DataResultFilterNode extends FilterNode { } else if ((c = ban.getLookup().lookup(LocalFile.class)) != null || (c = ban.getLookup().lookup(DerivedFile.class)) != null) { n = new LocalFileNode((AbstractFile) c); + } else if ((c = ban.getLookup().lookup(SlackFile.class)) != null) { + n = new SlackFileNode((SlackFile) c); } if (n != null) { actions.add(null); // creates a menu separator @@ -231,7 +236,13 @@ public class DataResultFilterNode extends FilterNode { // The base class Action is "Collapse All", inappropriate. return null; } - + + @Override + public List visit(FileTypesNode fileTypes) { + return defaultVisit(fileTypes); + } + + @Override protected List defaultVisit(DisplayableItemNode ditem) { //preserve the default node's actions @@ -271,6 +282,7 @@ public class DataResultFilterNode extends FilterNode { } return c; } + } /* @@ -322,7 +334,14 @@ public class DataResultFilterNode extends FilterNode { protected AbstractAction defaultVisit(DisplayableItemNode c) { return openChild(c); } + + @Override + public AbstractAction visit(FileTypesNode fileTypes) { + return openChild(fileTypes); + } + + /** * Tell the originating ExplorerManager to display the given * dataModelNode. diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/DirectoryTreeFilterChildren.java b/Core/src/org/sleuthkit/autopsy/directorytree/DirectoryTreeFilterChildren.java index e6ce27bc0e..2b10d57f09 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/DirectoryTreeFilterChildren.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/DirectoryTreeFilterChildren.java @@ -29,8 +29,10 @@ import org.sleuthkit.autopsy.datamodel.AbstractAbstractFileNode; import org.sleuthkit.autopsy.datamodel.DisplayableItemNode; import org.sleuthkit.autopsy.datamodel.DisplayableItemNodeVisitor; import org.sleuthkit.autopsy.datamodel.FileNode; +import org.sleuthkit.autopsy.datamodel.FileTypes.FileTypesNode; import org.sleuthkit.autopsy.datamodel.LayoutFileNode; import org.sleuthkit.autopsy.datamodel.LocalFileNode; +import org.sleuthkit.autopsy.datamodel.SlackFileNode; import org.sleuthkit.autopsy.datamodel.VirtualDirectoryNode; import org.sleuthkit.autopsy.datamodel.VolumeNode; import org.sleuthkit.datamodel.AbstractFile; @@ -225,6 +227,11 @@ class DirectoryTreeFilterChildren extends FilterNode.Children { public Boolean visit(LayoutFileNode fn) { return visitDeep(fn); } + + @Override + public Boolean visit(SlackFileNode sfn) { + return visitDeep(sfn); + } @Override public Boolean visit(VolumeNode vn) { @@ -236,6 +243,12 @@ class DirectoryTreeFilterChildren extends FilterNode.Children { return visitDeep(vdn); //return ! vdn.hasContentChildren(); } + + @Override + public Boolean visit(FileTypesNode ft) { + return defaultVisit(ft); + } + } private static class ShowItemVisitor extends DisplayableItemNodeVisitor.Default { @@ -267,11 +280,22 @@ class DirectoryTreeFilterChildren extends FilterNode.Children { public Boolean visit(LayoutFileNode ln) { return ln.hasContentChildren(); } + + @Override + public Boolean visit(SlackFileNode sfn) { + return sfn.hasContentChildren(); + } @Override public Boolean visit(VirtualDirectoryNode vdn) { return true; //return vdn.hasContentChildren(); } + + @Override + public Boolean visit(FileTypesNode fileTypes) { + return defaultVisit(fileTypes); + } + } } diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/DirectoryTreeTopComponent.java b/Core/src/org/sleuthkit/autopsy/directorytree/DirectoryTreeTopComponent.java index 739d3fd90c..20ce2db7e8 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/DirectoryTreeTopComponent.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/DirectoryTreeTopComponent.java @@ -18,6 +18,7 @@ */ package org.sleuthkit.autopsy.directorytree; +import org.sleuthkit.autopsy.datamodel.EmptyNode; import java.awt.Cursor; import java.awt.EventQueue; import java.beans.PropertyChangeEvent; @@ -60,13 +61,16 @@ import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.datamodel.BlackboardArtifactNode; import org.sleuthkit.autopsy.datamodel.DataSources; import org.sleuthkit.autopsy.datamodel.DataSourcesNode; +import org.sleuthkit.autopsy.datamodel.DisplayableItemNode; import org.sleuthkit.autopsy.datamodel.ExtractedContent; +import org.sleuthkit.autopsy.datamodel.FileTypesByMimeType; import org.sleuthkit.autopsy.datamodel.KeywordHits; import org.sleuthkit.autopsy.datamodel.KnownFileFilterNode; import org.sleuthkit.autopsy.datamodel.Reports; import org.sleuthkit.autopsy.datamodel.Results; import org.sleuthkit.autopsy.datamodel.ResultsNode; import org.sleuthkit.autopsy.datamodel.RootContentChildren; +import org.sleuthkit.autopsy.datamodel.SlackFileFilterNode; import org.sleuthkit.autopsy.datamodel.Tags; import org.sleuthkit.autopsy.datamodel.Views; import org.sleuthkit.autopsy.datamodel.ViewsNode; @@ -92,7 +96,7 @@ public final class DirectoryTreeTopComponent extends TopComponent implements Dat private final LinkedList backList; private final LinkedList forwardList; private static final String PREFERRED_ID = "DirectoryTreeTopComponent"; //NON-NLS - private static final Logger logger = Logger.getLogger(DirectoryTreeTopComponent.class.getName()); + private static final Logger LOGGER = Logger.getLogger(DirectoryTreeTopComponent.class.getName()); private RootContentChildren contentChildren; /** @@ -126,10 +130,12 @@ public final class DirectoryTreeTopComponent extends TopComponent implements Dat @Override public void preferenceChange(PreferenceChangeEvent evt) { switch (evt.getKey()) { - case UserPreferences.HIDE_KNOWN_FILES_IN_DATA_SOURCES_TREE: + case UserPreferences.HIDE_KNOWN_FILES_IN_DATA_SRCS_TREE: + case UserPreferences.HIDE_SLACK_FILES_IN_DATA_SRCS_TREE: refreshContentTreeSafe(); break; case UserPreferences.HIDE_KNOWN_FILES_IN_VIEWS_TREE: + case UserPreferences.HIDE_SLACK_FILES_IN_VIEWS_TREE: // TODO: Need a way to refresh the Views subtree break; } @@ -307,14 +313,14 @@ public final class DirectoryTreeTopComponent extends TopComponent implements Dat WindowManager winManager = WindowManager.getDefault(); TopComponent win = winManager.findTopComponent(PREFERRED_ID); if (win == null) { - logger.warning( + LOGGER.warning( "Cannot find " + PREFERRED_ID + " component. It will not be located properly in the window system."); //NON-NLS return getDefault(); } if (win instanceof DirectoryTreeTopComponent) { return (DirectoryTreeTopComponent) win; } - logger.warning( + LOGGER.warning( "There seem to be multiple components with the '" + PREFERRED_ID //NON-NLS + "' ID. That is a potential source of errors and unexpected behavior."); //NON-NLS return getDefault(); @@ -360,7 +366,7 @@ public final class DirectoryTreeTopComponent extends TopComponent implements Dat items.add(new Tags()); items.add(new Reports()); contentChildren = new RootContentChildren(items); - + Node root = new AbstractNode(contentChildren) { /** * to override the right click action in the white blank @@ -429,7 +435,7 @@ public final class DirectoryTreeTopComponent extends TopComponent implements Dat try { em.setSelectedNodes(new Node[]{childNodes.getNodeAt(0)}); } catch (Exception ex) { - logger.log(Level.SEVERE, "Error setting default selected node.", ex); //NON-NLS + LOGGER.log(Level.SEVERE, "Error setting default selected node.", ex); //NON-NLS } } @@ -598,6 +604,7 @@ public final class DirectoryTreeTopComponent extends TopComponent implements Dat } } + @NbBundle.Messages("DirectoryTreeTopComponent.emptyMimeNode.text=Data not available. Run file type identification module.") /** * Event handler to run when selection changed * @@ -631,23 +638,28 @@ public final class DirectoryTreeTopComponent extends TopComponent implements Dat if (origin == null) { return; } + Node originNode = origin.getNode(); //set node, wrap in filter node first to filter out children Node drfn = new DataResultFilterNode(originNode, DirectoryTreeTopComponent.this.em); Node kffn = new KnownFileFilterNode(drfn, KnownFileFilterNode.getSelectionContext(originNode)); - /* - * TODO (AUT-1849): Correct or remove peristent column - * reordering code - * - * The following conditional was added to support this - * feature. - */ -// if(originNode instanceof DisplayableItemNode) { -// dataResult.setNode(new TableFilterNode(kffn, true, ((DisplayableItemNode) originNode).getItemType())); -// } else { - dataResult.setNode(new TableFilterNode(kffn, true)); -// } + Node sffn = new SlackFileFilterNode(kffn, SlackFileFilterNode.getSelectionContext(originNode)); + + // Create a TableFilterNode with knowledge of the node's type to allow for column order settings + //Special case for when File Type Identification has not yet been run and + //there are no mime types to populate Files by Mime Type Tree + if (FileTypesByMimeType.isEmptyMimeTypeNode(originNode)) { + EmptyNode emptyNode = new EmptyNode(Bundle.DirectoryTreeTopComponent_emptyMimeNode_text()); + Node emptyDrfn = new DataResultFilterNode(emptyNode, DirectoryTreeTopComponent.this.em); + Node emptyKffn = new KnownFileFilterNode(emptyDrfn, KnownFileFilterNode.getSelectionContext(emptyNode)); + Node emptySffn = new SlackFileFilterNode(emptyKffn, SlackFileFilterNode.getSelectionContext(originNode)); + dataResult.setNode(new TableFilterNode(emptySffn, true, "This Node Is Empty")); //NON-NLS + } else if (originNode instanceof DisplayableItemNode) { + dataResult.setNode(new TableFilterNode(sffn, true, ((DisplayableItemNode) originNode).getItemType())); + } else { + dataResult.setNode(new TableFilterNode(sffn, true)); + } String displayName = ""; Content content = originNode.getLookup().lookup(Content.class); @@ -655,7 +667,7 @@ public final class DirectoryTreeTopComponent extends TopComponent implements Dat try { displayName = content.getUniquePath(); } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Exception while calling Content.getUniquePath() for node: " + originNode); //NON-NLS + LOGGER.log(Level.SEVERE, "Exception while calling Content.getUniquePath() for node: " + originNode); //NON-NLS } } else if (originNode.getLookup().lookup(String.class) != null) { displayName = originNode.getLookup().lookup(String.class); @@ -762,13 +774,13 @@ public final class DirectoryTreeTopComponent extends TopComponent implements Dat Children rootChildren = em.getRootContext().getChildren(); Node dataSourcesFilterNode = rootChildren.findChild(DataSourcesNode.NAME); if (dataSourcesFilterNode == null) { - logger.log(Level.SEVERE, "Cannot find data sources filter node, won't refresh the content tree"); //NON-NLS + LOGGER.log(Level.SEVERE, "Cannot find data sources filter node, won't refresh the content tree"); //NON-NLS return; } DirectoryTreeFilterNode.OriginalNode imagesNodeOrig = dataSourcesFilterNode.getLookup().lookup(DirectoryTreeFilterNode.OriginalNode.class); if (imagesNodeOrig == null) { - logger.log(Level.SEVERE, "Cannot find data sources node, won't refresh the content tree"); //NON-NLS + LOGGER.log(Level.SEVERE, "Cannot find data sources node, won't refresh the content tree"); //NON-NLS return; } @@ -787,8 +799,7 @@ public final class DirectoryTreeTopComponent extends TopComponent implements Dat * Set the selected node using a path to a previously selected node. * * @param previouslySelectedNodePath Path to a previously selected node. - * @param rootNodeName Name of the root node to match, may be - * null. + * @param rootNodeName Name of the root node to match, may be null. */ private void setSelectedNode(final String[] previouslySelectedNodePath, final String rootNodeName) { if (previouslySelectedNodePath == null) { @@ -812,7 +823,7 @@ public final class DirectoryTreeTopComponent extends TopComponent implements Dat for (int i = 0; i < previouslySelectedNodePath.length; ++i) { nodePath.append(previouslySelectedNodePath[i]).append("/"); } - logger.log(Level.WARNING, "Failed to find any nodes to select on path " + nodePath.toString(), ex); //NON-NLS + LOGGER.log(Level.WARNING, "Failed to find any nodes to select on path " + nodePath.toString(), ex); //NON-NLS break; } } @@ -827,7 +838,7 @@ public final class DirectoryTreeTopComponent extends TopComponent implements Dat try { em.setExploredContextAndSelection(selectedNode, new Node[]{selectedNode}); } catch (PropertyVetoException ex) { - logger.log(Level.WARNING, "Property veto from ExplorerManager setting selection to " + selectedNode.getName(), ex); //NON-NLS + LOGGER.log(Level.WARNING, "Property veto from ExplorerManager setting selection to " + selectedNode.getName(), ex); //NON-NLS } } } @@ -867,7 +878,7 @@ public final class DirectoryTreeTopComponent extends TopComponent implements Dat } treeNode = hashsetRootChilds.findChild(setName); } catch (TskException ex) { - logger.log(Level.WARNING, "Error retrieving attributes", ex); //NON-NLS + LOGGER.log(Level.WARNING, "Error retrieving attributes", ex); //NON-NLS } } else if (typeID == BlackboardArtifact.ARTIFACT_TYPE.TSK_KEYWORD_HIT.getTypeID()) { Node keywordRootNode = resultsChilds.findChild(typeName); @@ -894,7 +905,7 @@ public final class DirectoryTreeTopComponent extends TopComponent implements Dat } treeNode = listChildren.findChild(keywordName); } catch (TskException ex) { - logger.log(Level.WARNING, "Error retrieving attributes", ex); //NON-NLS + LOGGER.log(Level.WARNING, "Error retrieving attributes", ex); //NON-NLS } } else if (typeID == BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT.getTypeID() || typeID == BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_ARTIFACT_HIT.getTypeID()) { @@ -911,7 +922,7 @@ public final class DirectoryTreeTopComponent extends TopComponent implements Dat } treeNode = interestingItemsRootChildren.findChild(setName); } catch (TskException ex) { - logger.log(Level.WARNING, "Error retrieving attributes", ex); //NON-NLS + LOGGER.log(Level.WARNING, "Error retrieving attributes", ex); //NON-NLS } } else { Node extractedContent = resultsChilds.findChild(ExtractedContent.NAME); @@ -929,7 +940,7 @@ public final class DirectoryTreeTopComponent extends TopComponent implements Dat try { em.setExploredContextAndSelection(treeNode, new Node[]{treeNode}); } catch (PropertyVetoException ex) { - logger.log(Level.WARNING, "Property Veto: ", ex); //NON-NLS + LOGGER.log(Level.WARNING, "Property Veto: ", ex); //NON-NLS } // Another thread is needed because we have to wait for dataResult to populate @@ -964,7 +975,7 @@ public final class DirectoryTreeTopComponent extends TopComponent implements Dat try { firePropertyChange(BlackboardResultViewer.FINISHED_DISPLAY_EVT, 0, 1); } catch (Exception e) { - logger.log(Level.SEVERE, "DirectoryTreeTopComponent listener threw exception", e); //NON-NLS + LOGGER.log(Level.SEVERE, "DirectoryTreeTopComponent listener threw exception", e); //NON-NLS MessageNotifyUtil.Notify.show(NbBundle.getMessage(this.getClass(), "DirectoryTreeTopComponent.moduleErr"), NbBundle.getMessage(this.getClass(), "DirectoryTreeTopComponent.moduleErr.msg"), diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/ExternalViewerAction.java b/Core/src/org/sleuthkit/autopsy/directorytree/ExternalViewerAction.java index 716ac43005..e79840fd20 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/ExternalViewerAction.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/ExternalViewerAction.java @@ -30,6 +30,7 @@ import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.datamodel.ContentUtils; +import org.sleuthkit.autopsy.datamodel.SlackFileNode; /** * Extracts a File object to a temporary file in the case directory, and then @@ -68,13 +69,12 @@ public class ExternalViewerAction extends AbstractAction { // no point opening a file if it's empty, and java doesn't know how to // find an application for files without an extension // or if file is executable (for security reasons) - if (!(size > 0) || extPos == -1 || isExecutable) { + // Also skip slack files since their extension is the original extension + "-slack" + if (!(size > 0) || extPos == -1 || isExecutable || (fileNode instanceof SlackFileNode)) { this.setEnabled(false); } } - @Messages({"ExternalViewerAction.actionPerformed.failure.message=Could not find a viewer for the given file.", - "ExternalViewerAction.actionPerformed.failure.title=Open Failure"}) @Override public void actionPerformed(ActionEvent e) { // Get the temp folder path of the case @@ -93,32 +93,73 @@ public class ExternalViewerAction extends AbstractAction { logger.log(Level.WARNING, "Can't save to temporary file.", ex); //NON-NLS } + ExternalViewerAction.openFile(fileObject.getMIMEType(), fileObjectExt, tempFile); + + // delete the temporary file on exit + tempFile.deleteOnExit(); + } + + /** + * Opens a file, taking into account user preferences and then the default + * associated application. + * + * @param mimeType MIME type of the file + * @param ext extension of the file + * @param file the file object + */ + @Messages({ + "ExternalViewerAction.actionPerformed.failure.title=Open File Failure", + "ExternalViewerAction.actionPerformed.failure.IO.message=There is no associated editor for files of this type or the associated application failed to launch.", + "ExternalViewerAction.actionPerformed.failure.support.message=This platform (operating system) does not support opening a file in an editor this way.", + "ExternalViewerAction.actionPerformed.failure.missingFile.message=The file no longer exists.", + "ExternalViewerAction.actionPerformed.failure.permission.message=Permission to open the file was denied."}) + public static void openFile(String mimeType, String ext, File file) { /** * Check if the file MIME type or extension exists in the user defined * settings. Otherwise open with the default associated application. */ - String exePath = ExternalViewerRulesManager.getInstance().getExePathForName(fileObject.getMIMEType()); + String exePath = ExternalViewerRulesManager.getInstance().getExePathForName(mimeType); if (exePath.equals("")) { - exePath = ExternalViewerRulesManager.getInstance().getExePathForName(fileObjectExt); + exePath = ExternalViewerRulesManager.getInstance().getExePathForName(ext); } if (!exePath.equals("")) { Runtime runtime = Runtime.getRuntime(); - String[] s = new String[]{exePath, tempFile.getAbsolutePath()}; + String[] s = new String[]{exePath, file.getAbsolutePath()}; try { runtime.exec(s); } catch (IOException ex) { - logger.log(Level.WARNING, "Could not open the specified viewer for the given file: " + tempFile.getName(), ex); //NON-NLS - JOptionPane.showMessageDialog(null, Bundle.ExternalViewerAction_actionPerformed_failure_message(), Bundle.ExternalViewerAction_actionPerformed_failure_title(), JOptionPane.ERROR_MESSAGE); + logger.log(Level.WARNING, "Could not open the specified viewer for the given file: " + file.getName(), ex); //NON-NLS + JOptionPane.showMessageDialog(null, Bundle.ExternalViewerAction_actionPerformed_failure_IO_message(), Bundle.ExternalViewerAction_actionPerformed_failure_title(), JOptionPane.ERROR_MESSAGE); } } else { try { - Desktop.getDesktop().open(tempFile); + Desktop.getDesktop().open(file); } catch (IOException ex) { - logger.log(Level.WARNING, "Could not find a viewer for the given file: " + tempFile.getName(), ex); //NON-NLS - JOptionPane.showMessageDialog(null, Bundle.ExternalViewerAction_actionPerformed_failure_message(), Bundle.ExternalViewerAction_actionPerformed_failure_title(), JOptionPane.ERROR_MESSAGE); + logger.log(Level.WARNING, "Could not find a viewer for the given file: " + file.getName(), ex); //NON-NLS + JOptionPane.showMessageDialog(null, + Bundle.ExternalViewerAction_actionPerformed_failure_IO_message(), + Bundle.ExternalViewerAction_actionPerformed_failure_title(), + JOptionPane.ERROR_MESSAGE); + } catch (UnsupportedOperationException ex) { + logger.log(Level.WARNING, "Platform cannot open " + file.getName() + " in the defined editor.", ex); //NON-NLS + JOptionPane.showMessageDialog(null, + Bundle.ExternalViewerAction_actionPerformed_failure_support_message(), + Bundle.ExternalViewerAction_actionPerformed_failure_title(), + JOptionPane.ERROR_MESSAGE); + } catch (IllegalArgumentException ex) { + logger.log(Level.WARNING, "Could not find the given file: " + file.getName(), ex); //NON-NLS + JOptionPane.showMessageDialog(null, + Bundle.ExternalViewerAction_actionPerformed_failure_missingFile_message(), + Bundle.ExternalViewerAction_actionPerformed_failure_title(), + JOptionPane.ERROR_MESSAGE); + } catch (SecurityException ex) { + logger.log(Level.WARNING, "Could not get permission to open the given file: " + file.getName(), ex); //NON-NLS + JOptionPane.showMessageDialog(null, + Bundle.ExternalViewerAction_actionPerformed_failure_permission_message(), + Bundle.ExternalViewerAction_actionPerformed_failure_title(), + JOptionPane.ERROR_MESSAGE); } } - // delete the file on exit - tempFile.deleteOnExit(); } + } diff --git a/Core/src/org/sleuthkit/autopsy/filesearch/FileSearchPanel.java b/Core/src/org/sleuthkit/autopsy/filesearch/FileSearchPanel.java index 656e22ef9e..23730c99c1 100644 --- a/Core/src/org/sleuthkit/autopsy/filesearch/FileSearchPanel.java +++ b/Core/src/org/sleuthkit/autopsy/filesearch/FileSearchPanel.java @@ -165,8 +165,9 @@ class FileSearchPanel extends javax.swing.JPanel { contentList = Collections.emptyList(); } + SearchNode sn = new SearchNode(contentList); final TopComponent searchResultWin = DataResultTopComponent.createInstance(title, pathText, - new TableFilterNode(new SearchNode(contentList), true), contentList.size()); + new TableFilterNode(sn, true, sn.getName()), contentList.size()); searchResultWin.requestActive(); // make it the active top component diff --git a/Core/src/org/sleuthkit/autopsy/filesearch/MimeTypePanel.java b/Core/src/org/sleuthkit/autopsy/filesearch/MimeTypePanel.java index dfebc0c718..3982cb2779 100755 --- a/Core/src/org/sleuthkit/autopsy/filesearch/MimeTypePanel.java +++ b/Core/src/org/sleuthkit/autopsy/filesearch/MimeTypePanel.java @@ -1,31 +1,35 @@ /* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. + * Autopsy Forensic Browser + * + * Copyright 2011-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.filesearch; -import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.SortedSet; import java.util.logging.Level; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; -import org.apache.tika.mime.MediaType; -import org.apache.tika.mime.MimeTypes; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector; -/** - * - * @author oliver - */ public class MimeTypePanel extends javax.swing.JPanel { - private static final SortedSet mediaTypes = MimeTypes.getDefaultMimeTypes().getMediaTypeRegistry().getTypes(); private static final Logger logger = Logger.getLogger(MimeTypePanel.class.getName()); private static final long serialVersionUID = 1L; @@ -45,8 +49,8 @@ public class MimeTypePanel extends javax.swing.JPanel { private String[] getMimeTypeArray() { Set fileTypesCollated = new HashSet<>(); - for (MediaType mediaType : mediaTypes) { - fileTypesCollated.add(mediaType.toString()); + for (String mediaType : FileTypeDetector.getStandardDetectedTypes()) { + fileTypesCollated.add(mediaType); } FileTypeDetector fileTypeDetector; @@ -78,7 +82,7 @@ public class MimeTypePanel extends javax.swing.JPanel { boolean isSelected() { return this.mimeTypeCheckBox.isSelected(); } - + void setComponentsEnabled() { boolean enabled = this.isSelected(); this.mimeTypeList.setEnabled(enabled); diff --git a/Core/src/org/sleuthkit/autopsy/ingest/GetRootDirectoryVisitor.java b/Core/src/org/sleuthkit/autopsy/ingest/GetRootDirectoryVisitor.java index 14a1f446ae..28085cee96 100755 --- a/Core/src/org/sleuthkit/autopsy/ingest/GetRootDirectoryVisitor.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/GetRootDirectoryVisitor.java @@ -27,6 +27,7 @@ import org.sleuthkit.datamodel.File; import org.sleuthkit.datamodel.FileSystem; import org.sleuthkit.datamodel.LayoutFile; import org.sleuthkit.datamodel.LocalFile; +import org.sleuthkit.datamodel.SlackFile; import org.sleuthkit.datamodel.VirtualDirectory; /** @@ -85,4 +86,11 @@ final class GetRootDirectoryVisitor extends GetFilesContentVisitor { return getAllFromChildren(localFile); } + @Override + public Collection visit(SlackFile slackFile) { + //can have slack files + //TODO test this and overall scheduler with local files + return getAllFromChildren(slackFile); + } + } diff --git a/Core/src/org/sleuthkit/autopsy/ingest/IngestMessageTopComponent.java b/Core/src/org/sleuthkit/autopsy/ingest/IngestMessageTopComponent.java index dfd5adf689..04052b1ce6 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/IngestMessageTopComponent.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/IngestMessageTopComponent.java @@ -46,9 +46,9 @@ final class IngestMessageTopComponent extends TopComponent { private static final Logger logger = Logger.getLogger(IngestMessageTopComponent.class.getName()); private IngestMessageMainPanel messagePanel; private IngestManager manager; - private static String PREFERRED_ID = "IngestMessageTopComponent"; //NON-NLS - private ActionListener showIngestInboxAction; - private static final Pattern tagRemove = Pattern.compile("<.+?>"); + private static final String PREFERRED_ID = "IngestMessageTopComponent"; //NON-NLS + private final ActionListener showIngestInboxAction; + private static final Pattern TAG_REMOVE = Pattern.compile("<.+?>"); public IngestMessageTopComponent() { initComponents(); @@ -292,6 +292,7 @@ final class IngestMessageTopComponent extends TopComponent { */ } + @Override public Action[] getActions() { //disable TC toolbar actions return new Action[0]; @@ -302,7 +303,7 @@ final class IngestMessageTopComponent extends TopComponent { return string; } - Matcher m = tagRemove.matcher(string); + Matcher m = TAG_REMOVE.matcher(string); return m.replaceAll(""); } } diff --git a/Core/src/org/sleuthkit/autopsy/ingest/IngestMessagesToolbar.java b/Core/src/org/sleuthkit/autopsy/ingest/IngestMessagesToolbar.java index 5c44c49cbf..fac3307653 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/IngestMessagesToolbar.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/IngestMessagesToolbar.java @@ -1,15 +1,15 @@ /* * Autopsy Forensic Browser - * + * * Copyright 2011-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. @@ -145,8 +145,9 @@ class IngestMessagesToolbar extends javax.swing.JPanel { if (mode != null) { //TopComponent[] tcs = mode.getTopComponents(); mode.dockInto(tc); + String something = mode.getName(); tc.open(); - //tc.requestActive(); + //tc.requestActive(); } } @@ -158,7 +159,7 @@ class IngestMessagesToolbar extends javax.swing.JPanel { private static class IngestMessagesButton extends JButton { - private static final int fontSize = 9; + private static final int FONT_SIZE = 9; private int messages = 0; @Override @@ -169,7 +170,7 @@ class IngestMessagesToolbar extends javax.swing.JPanel { return; } //paint text - Font messagesFont = g.getFont().deriveFont(Font.PLAIN, fontSize); + Font messagesFont = g.getFont().deriveFont(Font.PLAIN, FONT_SIZE); String messageStr = Integer.toString(messages); final int len = messageStr.length(); g.setFont(messagesFont); @@ -181,9 +182,9 @@ class IngestMessagesToolbar extends javax.swing.JPanel { } g.setColor(Color.GRAY); //g.fillRect(x, 1, dx, fontSize); - g.fillRoundRect(x, 1, dx, fontSize, 2, 2); + g.fillRoundRect(x, 1, dx, FONT_SIZE, 2, 2); g.setColor(Color.WHITE); - g.drawString(messageStr, x + 2, fontSize); + g.drawString(messageStr, x + 2, FONT_SIZE); } void setMessages(int messages) { 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/embeddedfileextractor/EmbeddedFileExtractorIngestModule.java b/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/EmbeddedFileExtractorIngestModule.java index b1fa9d478e..582c5c04c4 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/EmbeddedFileExtractorIngestModule.java +++ b/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/EmbeddedFileExtractorIngestModule.java @@ -108,7 +108,8 @@ public final class EmbeddedFileExtractorIngestModule implements FileIngestModule @Override public ProcessResult process(AbstractFile abstractFile) { // skip the unallocated blocks - if (abstractFile.getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS)) { + if ((abstractFile.getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS)) || + (abstractFile.getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.SLACK))) { return ProcessResult.OK; } diff --git a/Core/src/org/sleuthkit/autopsy/modules/exif/ExifParserFileIngestModule.java b/Core/src/org/sleuthkit/autopsy/modules/exif/ExifParserFileIngestModule.java index 1fd1bd9116..a1d0809ae0 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/exif/ExifParserFileIngestModule.java +++ b/Core/src/org/sleuthkit/autopsy/modules/exif/ExifParserFileIngestModule.java @@ -104,7 +104,8 @@ public final class ExifParserFileIngestModule implements FileIngestModule { blackboard = Case.getCurrentCase().getServices().getBlackboard(); //skip unalloc - if (content.getType().equals(TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS)) { + if ((content.getType().equals(TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS) || + (content.getType().equals(TSK_DB_FILES_TYPE_ENUM.SLACK)))) { return ProcessResult.OK; } diff --git a/Core/src/org/sleuthkit/autopsy/modules/fileextmismatch/Bundle.properties b/Core/src/org/sleuthkit/autopsy/modules/fileextmismatch/Bundle.properties index 6faf2a71eb..79d8ab3dff 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/fileextmismatch/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/modules/fileextmismatch/Bundle.properties @@ -19,7 +19,6 @@ FileExtMismatchIngestModule.complete.totalFiles=Total Files Processed FileExtMismatchIngestModule.complete.svcMsg.text=File Extension Mismatch Results FileExtMismatchOptionsPanelController.moduleErr=Module Error FileExtMismatchOptionsPanelController.moduleErr.msg=A module caused an error listening to FileExtMismatchOptionsPanelController updates. See log to determine which module. Some data could be incomplete. -FileExtMismatchModuleSettingsPanel.skipTextPlain.text=Skip text files FileExtMismatchModuleSettingsPanel.skipNoExtCheckBox.text=Skip files without extensions FileExtMismatchSettingsPanel.extHeaderLabel.text=Allowed Extensions: FileExtMismatchSettingsPanel.removeExtButton.text=Delete Extension @@ -55,3 +54,6 @@ FileExtMismatchSettingsPanel.removeExtButton.noneSelected.title=No extension sel FileExtMismatchSettingsPanel.removeExtButton.noMimeTypeSelected.message=No MIME type selected\! FileExtMismatchSettingsPanel.removeExtButton.noMimeTypeSelected.title=No MIME type selected FileExtMismatchSettingsPanel.removeTypeButton.toolTipText= +FileExtMismatchModuleSettingsPanel.checkAllRadioButton.text=Check all file types +FileExtMismatchModuleSettingsPanel.checkNoTextRadioButton.text=Check all file types except text files +FileExtMismatchModuleSettingsPanel.checkMediaExeRadioButton.text=Check only multimedia and executable files diff --git a/Core/src/org/sleuthkit/autopsy/modules/fileextmismatch/Bundle_ja.properties b/Core/src/org/sleuthkit/autopsy/modules/fileextmismatch/Bundle_ja.properties index e4e4186689..fc35bab029 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/fileextmismatch/Bundle_ja.properties +++ b/Core/src/org/sleuthkit/autopsy/modules/fileextmismatch/Bundle_ja.properties @@ -33,7 +33,6 @@ FileExtMismatchIngestModule.complete.svcMsg.text=\u30d5\u30a1\u30a4\u30eb\u62e1\ FileExtMismatchOptionsPanelController.moduleErr=\u30e2\u30b8\u30e5\u30fc\u30eb\u30a8\u30e9\u30fc FileExtMismatchOptionsPanelController.moduleErr.msg=FileExtMismatchOptionsPanelController\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u306e\u78ba\u8a8d\u4e2d\u306b\u30e2\u30b8\u30e5\u30fc\u30eb\u304c\u30a8\u30e9\u30fc\u3092\u8d77\u3053\u3057\u307e\u3057\u305f\u3002\u3069\u306e\u30e2\u30b8\u30e5\u30fc\u30eb\u304b\u30ed\u30b0\u3092\u78ba\u8a8d\u3057\u3066\u4e0b\u3055\u3044\u3002\u4e00\u90e8\u306e\u30c7\u30fc\u30bf\u304c\u4e0d\u5b8c\u5168\u304b\u3082\u3057\u308c\u307e\u305b\u3093\u3002 AddFileExtensionAction.extHeaderLbl.text=\u4e0b\u8a18\u7528\u306b\u8a31\u53ef\u3059\u308b\u62e1\u5f35\u5b50 -FileExtMismatchModuleSettingsPanel.skipTextPlain.text=\u30c6\u30ad\u30b9\u30c8\u30d5\u30a1\u30a4\u30eb\u3092\u30b9\u30ad\u30c3\u30d7 FileExtMismatchModuleSettingsPanel.skipNoExtCheckBox.text=\u62e1\u5f35\u5b50\u306e\u7121\u3044\u30d5\u30a1\u30a4\u30eb\u3092\u30b9\u30ad\u30c3\u30d7 FileExtMismatchSettingsPanel.extHeaderLabel.text=\u8a31\u53ef\u3059\u308b\u62e1\u5f35\u5b50\uff1a FileExtMismatchSettingsPanel.removeExtButton.text=\u9078\u629e\u3057\u305f\u62e1\u5f35\u5b50\u3092\u524a\u9664 diff --git a/Core/src/org/sleuthkit/autopsy/modules/fileextmismatch/FileExtMismatchDetectorModuleSettings.java b/Core/src/org/sleuthkit/autopsy/modules/fileextmismatch/FileExtMismatchDetectorModuleSettings.java index 0efbd24db8..2e11bccc41 100755 --- a/Core/src/org/sleuthkit/autopsy/modules/fileextmismatch/FileExtMismatchDetectorModuleSettings.java +++ b/Core/src/org/sleuthkit/autopsy/modules/fileextmismatch/FileExtMismatchDetectorModuleSettings.java @@ -20,71 +20,161 @@ package org.sleuthkit.autopsy.modules.fileextmismatch; import java.io.IOException; import java.io.ObjectInputStream; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.sleuthkit.autopsy.ingest.IngestModuleIngestJobSettings; /** - * Ingest options for the file extension mismatch detector ingest module. + * Ingest options for the file extension mismatch detection ingest module. */ final class FileExtMismatchDetectorModuleSettings implements IngestModuleIngestJobSettings { private static final long serialVersionUID = 1L; private long versionNumber; private boolean skipFilesWithNoExtension; - private boolean skipFilesWithTextPlainMimeType; + @Deprecated + private boolean skipFilesWithTextPlainMimeType; // No longer used, retained to maintain serialization compatibility. private boolean skipKnownFiles; + private CHECK_TYPE checkType; + /* + * Extension mismatches can be checked for all files, for all files except + * text files, or for media and executable files only. + */ + enum CHECK_TYPE { + ALL, NO_TEXT_FILES, ONLY_MEDIA_AND_EXE + } + + /* + * The set of the MIME types that will be checked for extension mismatches + * when checkType is ONLY_MEDIA_AND_EXE. + */ + static final Set MEDIA_AND_EXE_MIME_TYPES = Stream.of( + "image/bmp", + "image/gif", + "image/jpeg", + "image/png", + "image/tiff", + "image/x-ms-bmp", + "application/dos-exe", + "application/exe", + "application/x-dosexec", + "application/x-exe", + "application/x-msdownload").collect(Collectors.toSet()); + + /** + * Constructs an object with the ingest options for the file extension + * mismatch detection ingest module. + */ FileExtMismatchDetectorModuleSettings() { + this.versionNumber = 2; this.skipFilesWithNoExtension = true; - this.skipFilesWithTextPlainMimeType = true; this.skipKnownFiles = true; + this.checkType = CHECK_TYPE.ONLY_MEDIA_AND_EXE; } - FileExtMismatchDetectorModuleSettings(boolean skipKnownFiles, boolean skipFilesWithNoExtension, boolean skipFilesWithTextPlainMimeType) { - this.skipFilesWithNoExtension = skipFilesWithNoExtension; - this.skipFilesWithTextPlainMimeType = skipFilesWithTextPlainMimeType; - this.skipKnownFiles = skipKnownFiles; - } - + /** + * Gets the serialization version number. + * + * @return A serialization version number. + */ @Override public long getVersionNumber() { return serialVersionUID; } + /** + * Sets the flag indicating whether or not files without extensions should + * be skipped during file extension mismatch checking. + * + * @param skipFilesWithNoExtension The desired value of the flag. + */ void setSkipFilesWithNoExtension(boolean skipFilesWithNoExtension) { this.skipFilesWithNoExtension = skipFilesWithNoExtension; } + /** + * Gets the flag indicating whether or not files without extensions should + * be skipped during file extension mismatch checking. + * + * @return The flag value. + */ boolean skipFilesWithNoExtension() { return skipFilesWithNoExtension; } - void setSkipFilesWithTextPlainMimeType(boolean skipFilesWithTextPlainMimeType) { - this.skipFilesWithTextPlainMimeType = skipFilesWithTextPlainMimeType; - } - - boolean skipFilesWithTextPlainMimeType() { - return skipFilesWithTextPlainMimeType; - } - - boolean skipKnownFiles() { - return skipKnownFiles; - } - + /** + * Sets the flag indicating whether or not known files should be skipped + * during file extension mismatch checking. + * + * @param skipKnownFiles The desired value of the flag. + */ void setSkipKnownFiles(boolean skipKnownFiles) { this.skipKnownFiles = skipKnownFiles; } + /** + * Gets the flag indicating whether or not known files should be skipped + * during file extension mismatch checking. + * + * @return The flag value. + */ + boolean skipKnownFiles() { + return skipKnownFiles; + } + + /** + * Sets whether extension mismatches should be checked for all files, for + * all files except text files, or for media and executable files only. + * + * @param checkType The check type. + */ + void setCheckType(CHECK_TYPE checkType) { + this.checkType = checkType; + } + + /** + * Gets whether extension mismatches should be checked for all files, for + * all files except text files, or for media and executable files only. + * + * @return checkType The check type. + */ + CHECK_TYPE getCheckType() { + return checkType; + } + + /** + * Called by convention by the serialization infrastructure when + * deserializing a FileExtMismatchDetectorModuleSettings object. + * + * @param in The object input stream provided by the serialization + * infrastructure. + * + * @throws IOException If there is a problem reading the + * serialized data. + * @throws ClassNotFoundException If the class definition for the serialized + * data cannot be found. + */ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); if (0L == versionNumber) { /* * If the version number is set to the Java field default value of - * zero, then skipKnownFiles is a new field. Change this to the - * desired default value of true. + * zero, then versionNumber and skipKnownFiles are new fields. + * Change this to the desired default value of true. */ skipKnownFiles = true; + versionNumber = 1; + } + if (1 == versionNumber) { + /* + * Set the default value of the new checkType field, it is currently + * null. + */ + checkType = CHECK_TYPE.ONLY_MEDIA_AND_EXE; + versionNumber = 2; } - versionNumber = 1; } - + } diff --git a/Core/src/org/sleuthkit/autopsy/modules/fileextmismatch/FileExtMismatchIngestModule.java b/Core/src/org/sleuthkit/autopsy/modules/fileextmismatch/FileExtMismatchIngestModule.java index 80fd6280db..76ce1eb28c 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/fileextmismatch/FileExtMismatchIngestModule.java +++ b/Core/src/org/sleuthkit/autopsy/modules/fileextmismatch/FileExtMismatchIngestModule.java @@ -34,6 +34,7 @@ import org.sleuthkit.autopsy.ingest.IngestMessage; import org.sleuthkit.autopsy.ingest.IngestModuleReferenceCounter; import org.sleuthkit.autopsy.ingest.IngestServices; import org.sleuthkit.autopsy.ingest.ModuleDataEvent; +import org.sleuthkit.autopsy.modules.fileextmismatch.FileExtMismatchDetectorModuleSettings.CHECK_TYPE; import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; @@ -116,6 +117,7 @@ public class FileExtMismatchIngestModule implements FileIngestModule { // skip non-files if ((abstractFile.getType() == TskData.TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS) || (abstractFile.getType() == TskData.TSK_DB_FILES_TYPE_ENUM.UNUSED_BLOCKS) + || (abstractFile.getType() == TskData.TSK_DB_FILES_TYPE_ENUM.SLACK) || (abstractFile.isFile() == false)) { return ProcessResult.OK; } @@ -173,9 +175,16 @@ public class FileExtMismatchIngestModule implements FileIngestModule { if (currActualSigType == null) { return false; } - if (settings.skipFilesWithTextPlainMimeType()) { - if (!currActualExt.isEmpty() && currActualSigType.equals("text/plain")) { //NON-NLS - return false; + if (settings.getCheckType() != CHECK_TYPE.ALL) { + if (settings.getCheckType() == CHECK_TYPE.NO_TEXT_FILES) { + if (!currActualExt.isEmpty() && currActualSigType.equals("text/plain")) { //NON-NLS + return false; + } + } + if (settings.getCheckType() == CHECK_TYPE.ONLY_MEDIA_AND_EXE) { + if (!FileExtMismatchDetectorModuleSettings.MEDIA_AND_EXE_MIME_TYPES.contains(currActualSigType)) { + return false; + } } } diff --git a/Core/src/org/sleuthkit/autopsy/modules/fileextmismatch/FileExtMismatchModuleSettingsPanel.form b/Core/src/org/sleuthkit/autopsy/modules/fileextmismatch/FileExtMismatchModuleSettingsPanel.form index 878f2ca87b..aee1d6cedf 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/fileextmismatch/FileExtMismatchModuleSettingsPanel.form +++ b/Core/src/org/sleuthkit/autopsy/modules/fileextmismatch/FileExtMismatchModuleSettingsPanel.form @@ -1,6 +1,10 @@
+ + + + @@ -19,23 +23,30 @@ - + + + - + + + + + + + + - - - + @@ -52,19 +63,6 @@ - - - - - - - - - - - - - @@ -76,5 +74,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/modules/fileextmismatch/FileExtMismatchModuleSettingsPanel.java b/Core/src/org/sleuthkit/autopsy/modules/fileextmismatch/FileExtMismatchModuleSettingsPanel.java index a59f445387..7c141f8733 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/fileextmismatch/FileExtMismatchModuleSettingsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/modules/fileextmismatch/FileExtMismatchModuleSettingsPanel.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2014 Basis Technology Corp. + * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,11 +22,12 @@ import org.sleuthkit.autopsy.ingest.IngestModuleIngestJobSettings; import org.sleuthkit.autopsy.ingest.IngestModuleIngestJobSettingsPanel; /** - * UI component used to set ingest job options for file extension mismatch - * detector ingest modules. + * UI component used to set ingest job options for the file extension mismatch + * detection ingest module. */ final class FileExtMismatchModuleSettingsPanel extends IngestModuleIngestJobSettingsPanel { + private static final long serialVersionUID = 1L; private final FileExtMismatchDetectorModuleSettings settings; FileExtMismatchModuleSettingsPanel(FileExtMismatchDetectorModuleSettings settings) { @@ -36,8 +37,21 @@ final class FileExtMismatchModuleSettingsPanel extends IngestModuleIngestJobSett } private void customizeComponents() { + switch (settings.getCheckType()) { + case ALL: + checkAllRadioButton.setSelected(true); + break; + case NO_TEXT_FILES: + checkNoTextRadioButton.setSelected(true); + break; + case ONLY_MEDIA_AND_EXE: + checkMediaExeRadioButton.setSelected(true); + break; + default: + checkMediaExeRadioButton.setSelected(true); + break; + } skipNoExtCheckBox.setSelected(settings.skipFilesWithNoExtension()); - skipTextPlain.setSelected(settings.skipFilesWithTextPlainMimeType()); skipKnownFiles.setSelected(settings.skipKnownFiles()); } @@ -55,9 +69,12 @@ final class FileExtMismatchModuleSettingsPanel extends IngestModuleIngestJobSett // //GEN-BEGIN:initComponents private void initComponents() { + checkTypeButtonGroup = new javax.swing.ButtonGroup(); skipNoExtCheckBox = new javax.swing.JCheckBox(); - skipTextPlain = new javax.swing.JCheckBox(); skipKnownFiles = new javax.swing.JCheckBox(); + checkAllRadioButton = new javax.swing.JRadioButton(); + checkNoTextRadioButton = new javax.swing.JRadioButton(); + checkMediaExeRadioButton = new javax.swing.JRadioButton(); skipNoExtCheckBox.setSelected(true); skipNoExtCheckBox.setText(org.openide.util.NbBundle.getMessage(FileExtMismatchModuleSettingsPanel.class, "FileExtMismatchModuleSettingsPanel.skipNoExtCheckBox.text")); // NOI18N @@ -67,14 +84,6 @@ final class FileExtMismatchModuleSettingsPanel extends IngestModuleIngestJobSett } }); - skipTextPlain.setText(org.openide.util.NbBundle.getMessage(FileExtMismatchModuleSettingsPanel.class, "FileExtMismatchModuleSettingsPanel.skipTextPlain.text")); // NOI18N - skipTextPlain.setSelected(true); - skipTextPlain.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - skipTextPlainActionPerformed(evt); - } - }); - skipKnownFiles.setSelected(true); skipKnownFiles.setText(org.openide.util.NbBundle.getMessage(FileExtMismatchModuleSettingsPanel.class, "FileExtMismatchModuleSettingsPanel.skipKnownFiles.text")); // NOI18N skipKnownFiles.addActionListener(new java.awt.event.ActionListener() { @@ -83,6 +92,30 @@ final class FileExtMismatchModuleSettingsPanel extends IngestModuleIngestJobSett } }); + checkTypeButtonGroup.add(checkAllRadioButton); + checkAllRadioButton.setText(org.openide.util.NbBundle.getMessage(FileExtMismatchModuleSettingsPanel.class, "FileExtMismatchModuleSettingsPanel.checkAllRadioButton.text")); // NOI18N + checkAllRadioButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + checkAllRadioButtonActionPerformed(evt); + } + }); + + checkTypeButtonGroup.add(checkNoTextRadioButton); + checkNoTextRadioButton.setText(org.openide.util.NbBundle.getMessage(FileExtMismatchModuleSettingsPanel.class, "FileExtMismatchModuleSettingsPanel.checkNoTextRadioButton.text")); // NOI18N + checkNoTextRadioButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + checkNoTextRadioButtonActionPerformed(evt); + } + }); + + checkTypeButtonGroup.add(checkMediaExeRadioButton); + checkMediaExeRadioButton.setText(org.openide.util.NbBundle.getMessage(FileExtMismatchModuleSettingsPanel.class, "FileExtMismatchModuleSettingsPanel.checkMediaExeRadioButton.text")); // NOI18N + checkMediaExeRadioButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + checkMediaExeRadioButtonActionPerformed(evt); + } + }); + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( @@ -90,20 +123,27 @@ final class FileExtMismatchModuleSettingsPanel extends IngestModuleIngestJobSett .addGroup(layout.createSequentialGroup() .addContainerGap() .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(skipTextPlain) + .addComponent(checkAllRadioButton) + .addComponent(checkNoTextRadioButton) + .addComponent(checkMediaExeRadioButton) .addComponent(skipNoExtCheckBox) .addComponent(skipKnownFiles)) - .addGap(0, 10, Short.MAX_VALUE)) + .addGap(0, 0, Short.MAX_VALUE)) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(checkAllRadioButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(checkNoTextRadioButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(checkMediaExeRadioButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) .addComponent(skipNoExtCheckBox) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(skipTextPlain) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(skipKnownFiles) - .addContainerGap(28, Short.MAX_VALUE)) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); }// //GEN-END:initComponents @@ -111,17 +151,28 @@ final class FileExtMismatchModuleSettingsPanel extends IngestModuleIngestJobSett settings.setSkipFilesWithNoExtension(skipNoExtCheckBox.isSelected()); }//GEN-LAST:event_skipNoExtCheckBoxActionPerformed - private void skipTextPlainActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_skipTextPlainActionPerformed - settings.setSkipFilesWithTextPlainMimeType(skipTextPlain.isSelected()); - }//GEN-LAST:event_skipTextPlainActionPerformed - private void skipKnownFilesActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_skipKnownFilesActionPerformed settings.setSkipKnownFiles(skipKnownFiles.isSelected()); }//GEN-LAST:event_skipKnownFilesActionPerformed + private void checkAllRadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_checkAllRadioButtonActionPerformed + settings.setCheckType(FileExtMismatchDetectorModuleSettings.CHECK_TYPE.ALL); + }//GEN-LAST:event_checkAllRadioButtonActionPerformed + + private void checkNoTextRadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_checkNoTextRadioButtonActionPerformed + settings.setCheckType(FileExtMismatchDetectorModuleSettings.CHECK_TYPE.NO_TEXT_FILES); + }//GEN-LAST:event_checkNoTextRadioButtonActionPerformed + + private void checkMediaExeRadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_checkMediaExeRadioButtonActionPerformed + settings.setCheckType(FileExtMismatchDetectorModuleSettings.CHECK_TYPE.ONLY_MEDIA_AND_EXE); + }//GEN-LAST:event_checkMediaExeRadioButtonActionPerformed + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JRadioButton checkAllRadioButton; + private javax.swing.JRadioButton checkMediaExeRadioButton; + private javax.swing.JRadioButton checkNoTextRadioButton; + private javax.swing.ButtonGroup checkTypeButtonGroup; private javax.swing.JCheckBox skipKnownFiles; private javax.swing.JCheckBox skipNoExtCheckBox; - private javax.swing.JCheckBox skipTextPlain; // End of variables declaration//GEN-END:variables } diff --git a/Core/src/org/sleuthkit/autopsy/modules/fileextmismatch/FileExtMismatchSettingsPanel.form b/Core/src/org/sleuthkit/autopsy/modules/fileextmismatch/FileExtMismatchSettingsPanel.form index 8c34097b98..162aef8994 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/fileextmismatch/FileExtMismatchSettingsPanel.form +++ b/Core/src/org/sleuthkit/autopsy/modules/fileextmismatch/FileExtMismatchSettingsPanel.form @@ -47,7 +47,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeDetector.java b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeDetector.java index 378c758f16..af796f0f6b 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeDetector.java +++ b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeDetector.java @@ -19,11 +19,13 @@ package org.sleuthkit.autopsy.modules.filetypeid; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.SortedSet; +import java.util.TreeSet; import java.util.logging.Level; +import java.util.stream.Collectors; import org.apache.tika.Tika; -import org.apache.tika.mime.MediaType; import org.apache.tika.mime.MimeTypes; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; @@ -49,6 +51,7 @@ public class FileTypeDetector { private final byte buffer[] = new byte[BUFFER_SIZE]; private final List userDefinedFileTypes; private final List autopsyDefinedFileTypes; + private static SortedSet detectedTypes; //no optional parameters /** * Constructs an object that detects the MIME type of a file by an @@ -100,6 +103,21 @@ public class FileTypeDetector { || isDetectableByTika(mimeType); } + /** + * Returns an unmodifiable list of standard MIME types that does not contain + * types with optional parameters. The list has no duplicate types and is in + * alphabetical order. + * + * @return an unmodifiable view of a set of MIME types + */ + public static synchronized SortedSet getStandardDetectedTypes() { + if (detectedTypes == null) { + detectedTypes = org.apache.tika.mime.MimeTypes.getDefaultMimeTypes().getMediaTypeRegistry().getTypes() + .stream().filter(t -> !t.hasParameters()).map(s -> s.toString()).collect(Collectors.toCollection(TreeSet::new)); + } + return Collections.unmodifiableSortedSet(detectedTypes); + } + /** * Determines whether or not a given MIME type is detectable as a * user-defined MIME type by this detector. @@ -126,15 +144,7 @@ public class FileTypeDetector { * @return True or false. */ private boolean isDetectableByTika(String mimeType) { - String[] split = mimeType.split("/"); - if (split.length == 2) { - String type = split[0]; - String subtype = split[1]; - MediaType mediaType = new MediaType(type, subtype); - SortedSet m = MimeTypes.getDefaultMimeTypes().getMediaTypeRegistry().getTypes(); - return m.contains(mediaType); - } - return false; + return FileTypeDetector.getStandardDetectedTypes().contains(removeOptionalParameter(mimeType)); } /** @@ -196,7 +206,10 @@ public class FileTypeDetector { */ String mimeType = file.getMIMEType(); if (null != mimeType) { - return mimeType; + // We remove the optional parameter to allow this method to work + // with legacy databases that may contain MIME types with the + // optional parameter attached. + return removeOptionalParameter(mimeType); } /* @@ -207,7 +220,8 @@ public class FileTypeDetector { if (!file.isFile() || file.getSize() <= 0 || (file.getType() == TskData.TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS) || (file.getType() == TskData.TSK_DB_FILES_TYPE_ENUM.UNUSED_BLOCKS) - || (file.getType() == TskData.TSK_DB_FILES_TYPE_ENUM.VIRTUAL_DIR)) { + || (file.getType() == TskData.TSK_DB_FILES_TYPE_ENUM.VIRTUAL_DIR) + || (file.getType() == TskData.TSK_DB_FILES_TYPE_ENUM.SLACK)) { mimeType = MimeTypes.OCTET_STREAM; } @@ -247,6 +261,10 @@ public class FileTypeDetector { * Remove the Tika suffix from the MIME type name. */ mimeType = tikaType.replace("tika-", ""); //NON-NLS + /* + * Remove the optional parameter from the MIME type. + */ + mimeType = removeOptionalParameter(mimeType); } catch (Exception ignored) { /* @@ -282,22 +300,25 @@ public class FileTypeDetector { * Add the MIME type to the files table in the case database. */ Case.getCurrentCase().getSleuthkitCase().setFileMIMEType(file, mimeType); - - /* - * Post to the blackboard, adding the file type attribute to the - * general info artifact. A property change is not fired for this - * posting because general info artifacts are different from other - * artifacts, e.g., they are not displayed in the results tree. - */ - BlackboardArtifact getInfoArt = file.getGenInfoArtifact(); - @SuppressWarnings("deprecation") - BlackboardAttribute batt = new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_FILE_TYPE_SIG, FileTypeIdModuleFactory.getModuleName(), mimeType); - getInfoArt.addAttribute(batt); } return mimeType; } + /** + * Removes the optional parameter from a MIME type string + * @param mimeType + * @return MIME type without the optional parameter + */ + private String removeOptionalParameter(String mimeType) { + int indexOfSemicolon = mimeType.indexOf(";"); + if (indexOfSemicolon != -1 ) { + return mimeType.substring(0, indexOfSemicolon).trim(); + } else { + return mimeType; + } + } + /** * Determines whether or not the a file matches a user-defined custom file * type. diff --git a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeIdIngestModule.java b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeIdIngestModule.java index 5c02d69424..ef4e0add71 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeIdIngestModule.java +++ b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/FileTypeIdIngestModule.java @@ -84,7 +84,6 @@ public class FileTypeIdIngestModule implements FileIngestModule { @Override public ProcessResult process(AbstractFile file) { - /** * Attempt to detect the file type. Do it within an exception firewall, * so that any issues with reading file content or complaints from tika diff --git a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/TikaFileTypeDetector.java b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/TikaFileTypeDetector.java index fd5b100e54..c237fe6222 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/filetypeid/TikaFileTypeDetector.java +++ b/Core/src/org/sleuthkit/autopsy/modules/filetypeid/TikaFileTypeDetector.java @@ -24,9 +24,6 @@ import org.apache.tika.mime.MediaType; import org.apache.tika.mime.MimeTypes; import org.sleuthkit.datamodel.AbstractFile; -import org.sleuthkit.datamodel.BlackboardArtifact; -import org.sleuthkit.datamodel.BlackboardAttribute; -import org.sleuthkit.datamodel.TskCoreException; /** * @deprecated Use org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector @@ -39,30 +36,6 @@ public class TikaFileTypeDetector { private final int BUFFER_SIZE = 64 * 1024; //how many bytes to pass in private final byte buffer[] = new byte[BUFFER_SIZE]; - /** - * Detect the mime type of the passed in file and save it to the blackboard - * - * @param abstractFile - * - * @return mime type or null - * - * @throws TskCoreException - * @deprecated Use FileTypeDetector.detectAndPostToBlackboard(AbstractFile - * file) instead. - */ - @Deprecated - public synchronized String detectAndSave(AbstractFile abstractFile) throws TskCoreException { - String mimeType = detect(abstractFile); - if (mimeType != null) { - // add artifact - BlackboardArtifact getInfoArt = abstractFile.getGenInfoArtifact(); - BlackboardAttribute batt = new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_FILE_TYPE_SIG.getTypeID(), FileTypeIdModuleFactory.getModuleName(), mimeType); - getInfoArt.addAttribute(batt); - - // we don't fire the event because we just updated TSK_GEN_INFO, which isn't displayed in the tree and is vague. - } - return mimeType; - } /** * Detect the mime type of the passed in file diff --git a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbIngestModule.java b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbIngestModule.java index 98baebadd5..7326debb84 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbIngestModule.java +++ b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbIngestModule.java @@ -146,7 +146,8 @@ public class HashDbIngestModule implements FileIngestModule { blackboard = Case.getCurrentCase().getServices().getBlackboard(); // Skip unallocated space files. - if (file.getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS)) { + if ((file.getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS) || + file.getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.SLACK))) { return ProcessResult.OK; } 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/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesIdentifierIngestModule.java b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesIdentifierIngestModule.java index 95f52b06e1..173029e3fe 100755 --- a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesIdentifierIngestModule.java +++ b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesIdentifierIngestModule.java @@ -39,6 +39,7 @@ import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardAttribute; import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.TskData; /** * A file ingest module that generates interesting files set hit artifacts for @@ -102,6 +103,11 @@ final class FilesIdentifierIngestModule implements FileIngestModule { @Messages({"FilesIdentifierIngestModule.indexError.message=Failed to index interesting file hit artifact for keyword search."}) public ProcessResult process(AbstractFile file) { blackboard = Case.getCurrentCase().getServices().getBlackboard(); + + // Skip slack space files. + if (file.getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.SLACK)) { + return ProcessResult.OK; + } // See if the file belongs to any defined interesting files set. List filesSets = FilesIdentifierIngestModule.interestingFileSetsByJob.get(this.context.getJobId()); diff --git a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetRulePanel.java b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetRulePanel.java index 9ee7f1a172..29d56e6f8a 100755 --- a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetRulePanel.java +++ b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetRulePanel.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2014 Basis Technology Corp. + * Copyright 2014-2016 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,15 +24,12 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.SortedSet; import java.util.logging.Level; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JOptionPane; -import org.apache.tika.mime.MediaType; -import org.apache.tika.mime.MimeTypes; import org.openide.DialogDisplayer; import org.openide.NotifyDescriptor; import org.openide.util.NbBundle; @@ -58,7 +55,6 @@ final class FilesSetRulePanel extends javax.swing.JPanel { "FilesSetRulePanel.ZeroFileSizeError=File size condition value must not be 0 (Unless = is selected)." }) - private static final SortedSet mediaTypes = MimeTypes.getDefaultMimeTypes().getMediaTypeRegistry().getTypes(); private static final Logger logger = Logger.getLogger(FilesSetRulePanel.class.getName()); private static final String SLEUTHKIT_PATH_SEPARATOR = "/"; // NON-NLS private static final List ILLEGAL_FILE_NAME_CHARS = InterestingItemDefsManager.getIllegalFileNameChars(); @@ -106,8 +102,8 @@ final class FilesSetRulePanel extends javax.swing.JPanel { private void populateMimeTypesComboBox() { Set fileTypesCollated = new HashSet<>(); - for (MediaType mediaType : mediaTypes) { - fileTypesCollated.add(mediaType.toString()); + for (String mediaType : FileTypeDetector.getStandardDetectedTypes()) { + fileTypesCollated.add(mediaType); } FileTypeDetector fileTypeDetector; @@ -321,7 +317,7 @@ final class FilesSetRulePanel extends javax.swing.JPanel { } } - // The path condition, if specified, must either be a regular expression + // The path condition, if specified, must either be a regular expression // that compiles or a string without illegal file path chars. if (this.pathCheck.isSelected()) { if (this.pathTextField.getText().isEmpty()) { @@ -412,7 +408,7 @@ final class FilesSetRulePanel extends javax.swing.JPanel { } } else { logger.log(Level.SEVERE, "Attempt to get name condition with illegal chars"); // NON-NLS - throw new IllegalStateException("The files set rule panel name condition is not in a valid state"); // NON-NLS + throw new IllegalStateException("The files set rule panel name condition is not in a valid state"); // NON-NLS } } return condition; @@ -498,7 +494,7 @@ final class FilesSetRulePanel extends javax.swing.JPanel { condition = new FilesSet.Rule.ParentPathCondition(path); } else { logger.log(Level.SEVERE, "Attempt to get path condition with illegal chars"); // NON-NLS - throw new IllegalStateException("The files set rule panel path condition is not in a valid state"); // NON-NLS + throw new IllegalStateException("The files set rule panel path condition is not in a valid state"); // NON-NLS } } } diff --git a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/InterestingItemDefsPanel.java b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/InterestingItemDefsPanel.java index ea1b2f2fdb..94af650828 100755 --- a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/InterestingItemDefsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/InterestingItemDefsPanel.java @@ -25,7 +25,6 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.SortedSet; import java.util.TreeMap; import java.util.logging.Level; import javax.swing.DefaultListModel; @@ -33,8 +32,6 @@ import javax.swing.JButton; import javax.swing.JOptionPane; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; -import org.apache.tika.mime.MediaType; -import org.apache.tika.mime.MimeTypes; import org.netbeans.spi.options.OptionsPanelController; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.corecomponents.OptionsPanel; @@ -57,16 +54,15 @@ final class InterestingItemDefsPanel extends IngestModuleGlobalSettingsPanel imp "InterestingItemsDefsPanel.saveError=Error saving interesting files sets to file." }) - private static final SortedSet mediaTypes = MimeTypes.getDefaultMimeTypes().getMediaTypeRegistry().getTypes(); private final DefaultListModel setsListModel = new DefaultListModel<>(); private final DefaultListModel rulesListModel = new DefaultListModel<>(); private final Logger logger = Logger.getLogger(InterestingItemDefsPanel.class.getName()); - private JButton okButton = new JButton("OK"); - private JButton cancelButton = new JButton("Cancel"); + private final JButton okButton = new JButton("OK"); + private final JButton cancelButton = new JButton("Cancel"); - // The following is a map of interesting files set names to interesting - // files set definitions. It is a snapshot of the files set definitions - // obtained from the interesting item definitions manager at the time the + // The following is a map of interesting files set names to interesting + // files set definitions. It is a snapshot of the files set definitions + // obtained from the interesting item definitions manager at the time the // the panel is loaded. When the panel saves or stores its settings, these // definitions, possibly changed, are submitted back to the interesting item // definitions manager. Note that it is a tree map to aid in displaying @@ -88,10 +84,10 @@ final class InterestingItemDefsPanel extends IngestModuleGlobalSettingsPanel imp @NbBundle.Messages({"InterestingItemDefsPanel.Title=Global Interesting Items Settings"}) private void customInit() { setName(Bundle.InterestingItemDefsPanel_Title()); - + Set fileTypesCollated = new HashSet<>(); - for (MediaType mediaType : mediaTypes) { - fileTypesCollated.add(mediaType.toString()); + for (String mediaType : FileTypeDetector.getStandardDetectedTypes()) { + fileTypesCollated.add(mediaType); } FileTypeDetector fileTypeDetector; @@ -119,7 +115,7 @@ final class InterestingItemDefsPanel extends IngestModuleGlobalSettingsPanel imp this.fileSizeUnitComboBox.setSelectedIndex(1); this.equalitySignComboBox.setSelectedIndex(2); } - + /** * @inheritDoc */ @@ -156,14 +152,14 @@ final class InterestingItemDefsPanel extends IngestModuleGlobalSettingsPanel imp this.filesSets = new TreeMap<>(); } - // Populate the list model for the interesting files sets list + // Populate the list model for the interesting files sets list // component. for (FilesSet set : this.filesSets.values()) { this.setsListModel.addElement(set); } if (!this.filesSets.isEmpty()) { - // Select the first files set by default. The list selections + // Select the first files set by default. The list selections // listeners will then populate the other components. EventQueue.invokeLater(() -> { InterestingItemDefsPanel.this.setsList.setSelectedIndex(0); @@ -222,7 +218,7 @@ final class InterestingItemDefsPanel extends IngestModuleGlobalSettingsPanel imp // components. FilesSet selectedSet = InterestingItemDefsPanel.this.setsList.getSelectedValue(); if (selectedSet != null) { - // Populate the components that display the properties of the + // Populate the components that display the properties of the // selected files set. InterestingItemDefsPanel.this.setDescriptionTextArea.setText(selectedSet.getDescription()); InterestingItemDefsPanel.this.ignoreKnownFilesCheckbox.setSelected(selectedSet.ignoresKnownFiles()); @@ -269,7 +265,7 @@ final class InterestingItemDefsPanel extends IngestModuleGlobalSettingsPanel imp FilesSet.Rule.MimeTypeCondition mimeTypeCondition = rule.getMimeTypeCondition(); FilesSet.Rule.FileSizeCondition fileSizeCondition = rule.getFileSizeCondition(); - // Populate the components that display the properties of the + // Populate the components that display the properties of the // selected rule. if (nameCondition != null) { InterestingItemDefsPanel.this.fileNameTextField.setText(nameCondition.getTextToMatch()); @@ -345,7 +341,7 @@ final class InterestingItemDefsPanel extends IngestModuleGlobalSettingsPanel imp panel = new FilesSetPanel(); } - // Do a dialog box with the files set panel until the user either enters + // Do a dialog box with the files set panel until the user either enters // a valid definition or cancels. Note that the panel gives the user // feedback when isValidDefinition() is called. int option = JOptionPane.OK_OPTION; @@ -366,7 +362,7 @@ final class InterestingItemDefsPanel extends IngestModuleGlobalSettingsPanel imp Map rules = new HashMap<>(); if (selectedSet != null) { // Interesting file sets are immutable for thread safety, - // so editing a files set definition is a replacement operation. + // so editing a files set definition is a replacement operation. // Preserve the existing rules from the set being edited. rules.putAll(selectedSet.getRules()); } @@ -391,7 +387,7 @@ final class InterestingItemDefsPanel extends IngestModuleGlobalSettingsPanel imp // Creating a new rule definition. panel = new FilesSetRulePanel(okButton, cancelButton); } - // Do a dialog box with the files set panel until the user either enters + // Do a dialog box with the files set panel until the user either enters // a valid definition or cancels. Note that the panel gives the user // feedback when isValidDefinition() is called. int option = JOptionPane.OK_OPTION; @@ -401,12 +397,12 @@ final class InterestingItemDefsPanel extends IngestModuleGlobalSettingsPanel imp if (option == JOptionPane.OK_OPTION) { // Interesting file sets are immutable for thread safety, - // so editing a files set rule definition is a replacement + // so editing a files set rule definition is a replacement // operation. Preserve the existing rules from the set being edited. FilesSet selectedSet = this.setsList.getSelectedValue(); Map rules = new HashMap<>(selectedSet.getRules()); - // Remove the "old" rule definition and add the new/edited + // Remove the "old" rule definition and add the new/edited // definition. if (selectedRule != null) { rules.remove(selectedRule.getUuid()); @@ -414,12 +410,12 @@ final class InterestingItemDefsPanel extends IngestModuleGlobalSettingsPanel imp FilesSet.Rule newRule = new FilesSet.Rule(panel.getRuleName(), panel.getFileNameCondition(), panel.getMetaTypeCondition(), panel.getPathCondition(), panel.getMimeTypeCondition(), panel.getFileSizeCondition()); rules.put(newRule.getUuid(), newRule); - // Add the new/edited files set definition, replacing any previous + // Add the new/edited files set definition, replacing any previous // definition with the same name and refreshing the display. this.replaceFilesSet(selectedSet, selectedSet.getName(), selectedSet.getDescription(), selectedSet.ignoresKnownFiles(), rules); - // Select the new/edited rule. Queue it up so it happens after the - // selection listeners react to the selection of the "new" files + // Select the new/edited rule. Queue it up so it happens after the + // selection listeners react to the selection of the "new" files // set. EventQueue.invokeLater(() -> { this.rulesList.setSelectedValue(newRule, true); @@ -459,8 +455,8 @@ final class InterestingItemDefsPanel extends IngestModuleGlobalSettingsPanel imp this.setsListModel.addElement(set); } - // Select the new/edited files set definition in the set definitions - // list. This will cause the selection listeners to repopulate the + // Select the new/edited files set definition in the set definitions + // list. This will cause the selection listeners to repopulate the // subordinate components. this.setsList.setSelectedValue(newSet, true); } diff --git a/Core/src/org/sleuthkit/autopsy/modules/vmextractor/Bundle.properties b/Core/src/org/sleuthkit/autopsy/modules/vmextractor/Bundle.properties index 76c16fd56f..b454208635 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/vmextractor/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/modules/vmextractor/Bundle.properties @@ -1,6 +1,5 @@ VMExtractorIngestModuleFactory.moduleDisplayName=Virtual Machine Extractor VMExtractorIngestModuleFactory.moduleDescription=Extracts virtual machine files and adds them to a case as data sources. -VMExtractorIngestModuleFactory.version=1.0 VMExtractorIngestModule.addedVirtualMachineImage.message=Added virtual machine image {0} VMExtractorIngestModule.searchingImage.message=Searching image for virtual machine files VMExtractorIngestModule.exportingToDisk.message=Exporting virtual machine files to disk diff --git a/Core/src/org/sleuthkit/autopsy/modules/vmextractor/VMExtractorIngestModuleFactory.java b/Core/src/org/sleuthkit/autopsy/modules/vmextractor/VMExtractorIngestModuleFactory.java index e26bb81e51..c525e87dad 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/vmextractor/VMExtractorIngestModuleFactory.java +++ b/Core/src/org/sleuthkit/autopsy/modules/vmextractor/VMExtractorIngestModuleFactory.java @@ -20,6 +20,7 @@ package org.sleuthkit.autopsy.modules.vmextractor; import org.openide.util.NbBundle; import org.openide.util.lookup.ServiceProvider; +import org.sleuthkit.autopsy.coreutils.Version; import org.sleuthkit.autopsy.ingest.DataSourceIngestModule; import org.sleuthkit.autopsy.ingest.IngestModuleFactory; import org.sleuthkit.autopsy.ingest.IngestModuleFactoryAdapter; @@ -53,7 +54,7 @@ public final class VMExtractorIngestModuleFactory extends IngestModuleFactoryAda @Override public String getModuleVersionNumber() { - return NbBundle.getMessage(this.getClass(), "VMExtractorIngestModuleFactory.version"); + return Version.getVersion(); } @Override diff --git a/Core/src/org/sleuthkit/autopsy/python/JythonModuleLoader.java b/Core/src/org/sleuthkit/autopsy/python/JythonModuleLoader.java index c9175f3754..b50742f197 100755 --- a/Core/src/org/sleuthkit/autopsy/python/JythonModuleLoader.java +++ b/Core/src/org/sleuthkit/autopsy/python/JythonModuleLoader.java @@ -74,7 +74,7 @@ public final class JythonModuleLoader { // add python modules from 'autospy/build/cluster/InternalPythonModules' folder // which are copied from 'autopsy/*/release/InternalPythonModules' folders. - for (File f : InstalledFileLocator.getDefault().locateAll("InternalPythonModules", JythonModuleLoader.class.getPackage().getName(), false)) { //NON-NLS + for (File f : InstalledFileLocator.getDefault().locateAll("InternalPythonModules", "org.sleuthkit.autopsy.core", false)) { //NON-NLS Collections.addAll(pythonModuleDirs, f.listFiles()); } // add python modules from 'testuserdir/python_modules' folder diff --git a/Core/src/org/sleuthkit/autopsy/report/ReportGenerator.java b/Core/src/org/sleuthkit/autopsy/report/ReportGenerator.java index 6ed903c7b2..6f32fd2f65 100644 --- a/Core/src/org/sleuthkit/autopsy/report/ReportGenerator.java +++ b/Core/src/org/sleuthkit/autopsy/report/ReportGenerator.java @@ -170,6 +170,8 @@ class ReportGenerator { TableReportGenerator generator = new TableReportGenerator(artifactTypeSelections, tagNameSelections, progressPanel, tableReport); generator.execute(); tableReport.endReport(); + // finish progress, wrap up + progressPanel.complete(ReportProgressPanel.ReportStatus.COMPLETE); errorList = generator.getErrorList(); }); worker.execute(); diff --git a/Core/src/org/sleuthkit/autopsy/report/ReportWizardAction.java b/Core/src/org/sleuthkit/autopsy/report/ReportWizardAction.java index 574459fe11..5e32ce5016 100644 --- a/Core/src/org/sleuthkit/autopsy/report/ReportWizardAction.java +++ b/Core/src/org/sleuthkit/autopsy/report/ReportWizardAction.java @@ -65,7 +65,6 @@ public final class ReportWizardAction extends CallableSystemAction implements Pr wiz.setTitleFormat(new MessageFormat("{0} {1}")); wiz.setTitle(NbBundle.getMessage(ReportWizardAction.class, "ReportWizardAction.reportWiz.title")); if (DialogDisplayer.getDefault().notify(wiz) == WizardDescriptor.FINISH_OPTION) { - @SuppressWarnings("unchecked") ReportGenerator generator = new ReportGenerator(); //NON-NLS TableReportModule tableReport = (TableReportModule) wiz.getProperty("tableModule"); GeneralReportModule generalReport = (GeneralReportModule) wiz.getProperty("generalModule"); diff --git a/Core/src/org/sleuthkit/autopsy/report/TableReportGenerator.java b/Core/src/org/sleuthkit/autopsy/report/TableReportGenerator.java index f4afd1bd1e..d3a782948c 100755 --- a/Core/src/org/sleuthkit/autopsy/report/TableReportGenerator.java +++ b/Core/src/org/sleuthkit/autopsy/report/TableReportGenerator.java @@ -114,9 +114,6 @@ class TableReportGenerator { // report on the tagged images makeThumbnailTable(); } - - // finish progress, wrap up - progressPanel.complete(ReportProgressPanel.ReportStatus.COMPLETE); } /** diff --git a/Core/src/org/sleuthkit/autopsy/timeline/actions/ViewFileInTimelineAction.java b/Core/src/org/sleuthkit/autopsy/timeline/actions/ViewFileInTimelineAction.java index f90837e1f6..75085293a2 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/actions/ViewFileInTimelineAction.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/actions/ViewFileInTimelineAction.java @@ -24,6 +24,7 @@ import org.openide.util.NbBundle; import org.openide.util.actions.SystemAction; import org.sleuthkit.autopsy.timeline.OpenTimelineAction; import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.TskData; /** * An action to prompt the user to pick an timestamp/event associated with the @@ -38,6 +39,11 @@ public final class ViewFileInTimelineAction extends AbstractAction { private ViewFileInTimelineAction(AbstractFile file, String displayName) { super(displayName); this.file = file; + + if(file.getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.SLACK) + || file.getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS)){ + this.setEnabled(false); + } } @NbBundle.Messages({"ViewFileInTimelineAction.viewFile.displayName=View File in Timeline... "}) diff --git a/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java b/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java index 90757b0a69..75c64fc5ef 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java @@ -517,7 +517,8 @@ public class EventsRepository { //reset database //TODO: can we do more incremental updates? -jm eventDB.reInitializeDB(); //grab ids of all files - List fileIDs = skCase.findAllFileIdsWhere("name != '.' AND name != '..'"); //NON-NLS + List fileIDs = skCase.findAllFileIdsWhere("name != '.' AND name != '..'" + + " AND type != " + TskData.TSK_DB_FILES_TYPE_ENUM.SLACK.ordinal()); //NON-NLS final int numFiles = fileIDs.size(); trans = eventDB.beginTransaction(); diff --git a/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventNode.java b/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventNode.java index eb2abdde37..cf0524a893 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventNode.java @@ -160,15 +160,11 @@ public class EventNode extends DisplayableItemNode { throw new UnsupportedOperationException("Not supported yet."); // NON-NLS } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "Event"; -// } + @Override + public String getItemType() { + return getClass().getName(); + } + /** * We use TimeProperty instead of a normal NodeProperty to correctly display * the date/time when the user changes the timezone setting. diff --git a/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventRootNode.java b/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventRootNode.java index 3d1d44132e..4103d845bc 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventRootNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventRootNode.java @@ -61,16 +61,11 @@ public class EventRootNode extends DisplayableItemNode { return null; } + @Override + public String getItemType() { + return getClass().getName(); + } - /* - * TODO (AUT-1849): Correct or remove peristent column reordering code - * - * Added to support this feature. - */ -// @Override -// public String getItemType() { -// return "EventRoot"; -// } /** * ChildFactory for EventNodes. */ diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/listvew/ListTimeline.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/listvew/ListTimeline.java index 19f929faa3..f7bfba4501 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/listvew/ListTimeline.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/listvew/ListTimeline.java @@ -37,6 +37,7 @@ import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.concurrent.ConcurrentSkipListSet; +import java.util.function.Consumer; import java.util.function.Function; import java.util.logging.Level; import java.util.stream.Collectors; @@ -47,6 +48,7 @@ import javafx.beans.binding.StringBinding; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; import javafx.collections.ListChangeListener; +import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.geometry.Pos; import javafx.scene.Node; @@ -687,7 +689,12 @@ class ListTimeline extends BorderPane { private class ScrollToFirst extends org.controlsfx.control.action.Action { ScrollToFirst() { - super("", actionEvent -> scrollToAndFocus(0)); + super("", new Consumer() { //do not make this a lambda function see issue 2147 + @Override + public void accept(ActionEvent actionEvent) { + scrollToAndFocus(0); + } + }); setGraphic(new ImageView(FIRST)); disabledProperty().bind(table.getFocusModel().focusedIndexProperty().lessThan(1)); } @@ -696,7 +703,12 @@ class ListTimeline extends BorderPane { private class ScrollToLast extends org.controlsfx.control.action.Action { ScrollToLast() { - super("", actionEvent -> scrollToAndFocus(table.getItems().size() - 1)); + super("", new Consumer() { //do not make this a lambda function see issue 2147 + @Override + public void accept(ActionEvent actionEvent) { + scrollToAndFocus(table.getItems().size() - 1); + } + }); setGraphic(new ImageView(LAST)); IntegerBinding size = Bindings.size(table.getItems()); disabledProperty().bind(size.isEqualTo(0).or( @@ -707,36 +719,38 @@ class ListTimeline extends BorderPane { private class ScrollToNext extends org.controlsfx.control.action.Action { ScrollToNext() { - super("", actionEvent -> { - - ChronoField selectedChronoField = scrollInrementComboBox.getSelectionModel().getSelectedItem(); - ZoneId timeZoneID = TimeLineController.getTimeZoneID(); - TemporalUnit selectedUnit = selectedChronoField.getBaseUnit(); - - int focusedIndex = table.getFocusModel().getFocusedIndex(); - CombinedEvent focusedItem = table.getFocusModel().getFocusedItem(); - if (-1 == focusedIndex || null == focusedItem) { - focusedItem = visibleEvents.first(); - focusedIndex = table.getItems().indexOf(focusedItem); - } - - ZonedDateTime focusedDateTime = Instant.ofEpochMilli(focusedItem.getStartMillis()).atZone(timeZoneID); - ZonedDateTime nextDateTime = focusedDateTime.plus(1, selectedUnit);// - for (ChronoField field : SCROLL_BY_UNITS) { - if (field.getBaseUnit().getDuration().compareTo(selectedUnit.getDuration()) < 0) { - nextDateTime = nextDateTime.with(field, field.rangeRefinedBy(nextDateTime).getMinimum());// + super("", new Consumer() { //do not make this a lambda function see issue 2147 + @Override + public void accept(ActionEvent actionEvent) { + ChronoField selectedChronoField = scrollInrementComboBox.getSelectionModel().getSelectedItem(); + ZoneId timeZoneID = TimeLineController.getTimeZoneID(); + TemporalUnit selectedUnit = selectedChronoField.getBaseUnit(); + + int focusedIndex = table.getFocusModel().getFocusedIndex(); + CombinedEvent focusedItem = table.getFocusModel().getFocusedItem(); + if (-1 == focusedIndex || null == focusedItem) { + focusedItem = visibleEvents.first(); + focusedIndex = table.getItems().indexOf(focusedItem); } - } - long nextMillis = nextDateTime.toInstant().toEpochMilli(); - - int nextIndex = table.getItems().size() - 1; - for (int i = focusedIndex; i < table.getItems().size(); i++) { - if (table.getItems().get(i).getStartMillis() >= nextMillis) { - nextIndex = i; - break; + + ZonedDateTime focusedDateTime = Instant.ofEpochMilli(focusedItem.getStartMillis()).atZone(timeZoneID); + ZonedDateTime nextDateTime = focusedDateTime.plus(1, selectedUnit);// + for (ChronoField field : SCROLL_BY_UNITS) { + if (field.getBaseUnit().getDuration().compareTo(selectedUnit.getDuration()) < 0) { + nextDateTime = nextDateTime.with(field, field.rangeRefinedBy(nextDateTime).getMinimum());// + } } + long nextMillis = nextDateTime.toInstant().toEpochMilli(); + + int nextIndex = table.getItems().size() - 1; + for (int i = focusedIndex; i < table.getItems().size(); i++) { + if (table.getItems().get(i).getStartMillis() >= nextMillis) { + nextIndex = i; + break; + } + } + scrollToAndFocus(nextIndex); } - scrollToAndFocus(nextIndex); }); setGraphic(new ImageView(NEXT)); IntegerBinding size = Bindings.size(table.getItems()); @@ -749,37 +763,40 @@ class ListTimeline extends BorderPane { private class ScrollToPrevious extends org.controlsfx.control.action.Action { ScrollToPrevious() { - super("", actionEvent -> { - ZoneId timeZoneID = TimeLineController.getTimeZoneID(); - ChronoField selectedChronoField = scrollInrementComboBox.getSelectionModel().getSelectedItem(); - TemporalUnit selectedUnit = selectedChronoField.getBaseUnit(); - - int focusedIndex = table.getFocusModel().getFocusedIndex(); - CombinedEvent focusedItem = table.getFocusModel().getFocusedItem(); - if (-1 == focusedIndex || null == focusedItem) { - focusedItem = visibleEvents.last(); - focusedIndex = table.getItems().indexOf(focusedItem); - } - - ZonedDateTime focusedDateTime = Instant.ofEpochMilli(focusedItem.getStartMillis()).atZone(timeZoneID); - ZonedDateTime previousDateTime = focusedDateTime.minus(1, selectedUnit);// - - for (ChronoField field : SCROLL_BY_UNITS) { - if (field.getBaseUnit().getDuration().compareTo(selectedUnit.getDuration()) < 0) { - previousDateTime = previousDateTime.with(field, field.rangeRefinedBy(previousDateTime).getMaximum());// + super("", new Consumer() { //do not make this a lambda function see issue 2147 + @Override + public void accept(ActionEvent actionEvent) { + ZoneId timeZoneID = TimeLineController.getTimeZoneID(); + ChronoField selectedChronoField = scrollInrementComboBox.getSelectionModel().getSelectedItem(); + TemporalUnit selectedUnit = selectedChronoField.getBaseUnit(); + + int focusedIndex = table.getFocusModel().getFocusedIndex(); + CombinedEvent focusedItem = table.getFocusModel().getFocusedItem(); + if (-1 == focusedIndex || null == focusedItem) { + focusedItem = visibleEvents.last(); + focusedIndex = table.getItems().indexOf(focusedItem); } - } - long previousMillis = previousDateTime.toInstant().toEpochMilli(); - - int previousIndex = 0; - for (int i = focusedIndex; i > 0; i--) { - if (table.getItems().get(i).getStartMillis() <= previousMillis) { - previousIndex = i; - break; + + ZonedDateTime focusedDateTime = Instant.ofEpochMilli(focusedItem.getStartMillis()).atZone(timeZoneID); + ZonedDateTime previousDateTime = focusedDateTime.minus(1, selectedUnit);// + + for (ChronoField field : SCROLL_BY_UNITS) { + if (field.getBaseUnit().getDuration().compareTo(selectedUnit.getDuration()) < 0) { + previousDateTime = previousDateTime.with(field, field.rangeRefinedBy(previousDateTime).getMaximum());// + } } + long previousMillis = previousDateTime.toInstant().toEpochMilli(); + + int previousIndex = 0; + for (int i = focusedIndex; i > 0; i--) { + if (table.getItems().get(i).getStartMillis() <= previousMillis) { + previousIndex = i; + break; + } + } + + scrollToAndFocus(previousIndex); } - - scrollToAndFocus(previousIndex); }); setGraphic(new ImageView(PREVIOUS)); disabledProperty().bind(table.getFocusModel().focusedIndexProperty().lessThan(1)); 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..8ab88c13b5 --- /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.7 + + + + 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..e213633e07 --- /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.AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException; +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; +import org.sleuthkit.autopsy.corecomponentinterfaces.AutoIngestDataSourceProcessor; + +/** + * 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 AutoIngestDataSourceProcessorException) { + 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, AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException { + 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, AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException { + 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, AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException { + 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, AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException { + 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, AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException { + 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, AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException { + 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(AutoIngestDataSourceProcessor.class); + + Map validDataSourceProcessorsMap = new HashMap<>(); + for (AutoIngestDataSourceProcessor processor : processorCandidates) { + try { + int confidence = processor.canProcess(dataSource.getPath()); + if(confidence > 0){ + validDataSourceProcessorsMap.put(processor, confidence); + } + } catch (AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException 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(AutoIngestDataSourceProcessor 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 (AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException 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 AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException("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..f8494d8fa0 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/AutoIngestSettingsPanel.form @@ -0,0 +1,657 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..0377a704b8 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/AutoIngestSettingsPanel.java @@ -0,0 +1,1451 @@ +/* + * 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() { + boolean needsRestart = false; + AutoIngestUserPreferences.SelectedMode storedMode = AutoIngestUserPreferences.getMode(); + + if (AutoIngestUserPreferences.getJoinAutoModeCluster() != cbJoinAutoIngestCluster.isSelected()) { + needsRestart = true; + } + + AutoIngestUserPreferences.setJoinAutoModeCluster(cbJoinAutoIngestCluster.isSelected()); + if (!cbJoinAutoIngestCluster.isSelected()) { + AutoIngestUserPreferences.setMode(AutoIngestUserPreferences.SelectedMode.STANDALONE); + //before return popup the message + if (needsRestart) { + SwingUtilities.invokeLater(() -> { + JOptionPane.showMessageDialog(null, + NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.MustRestart"), + NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.restartRequiredLabel.text"), + JOptionPane.WARNING_MESSAGE); + }); + } + return; + } + + if (jRadioButtonAutomated.isSelected()) { + if (storedMode != AutoIngestUserPreferences.SelectedMode.AUTOMATED) { + needsRestart = true; + } + String thePath = AutoIngestUserPreferences.getAutoModeImageFolder(); + if (thePath != null && 0 != inputPathTextField.getText().compareTo(thePath)) { + needsRestart = true; + } + thePath = AutoIngestUserPreferences.getAutoModeResultsFolder(); + if (thePath != null && 0 != outputPathTextField.getText().compareTo(thePath)) { + needsRestart = true; + } + + 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()) { + if (storedMode != AutoIngestUserPreferences.SelectedMode.REVIEW) { + needsRestart = true; + } + String thePath = AutoIngestUserPreferences.getAutoModeResultsFolder(); + if (thePath != null && 0 != outputPathTextField.getText().compareTo(thePath)) { + needsRestart = true; + } + + AutoIngestUserPreferences.setMode(AutoIngestUserPreferences.SelectedMode.REVIEW); + String resultsFolderPath = getNormalizedFolderPath(outputPathTextField.getText().trim()); + AutoIngestUserPreferences.setAutoModeResultsFolder(resultsFolderPath); + } + if (needsRestart) { + SwingUtilities.invokeLater(() -> { + JOptionPane.showMessageDialog(null, + NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.MustRestart"), + NbBundle.getMessage(AutoIngestSettingsPanel.class, "AutoIngestSettingsPanel.restartRequiredLabel.text"), + JOptionPane.WARNING_MESSAGE); + }); + } + + } + + 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; + } + + 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.setVisible(false); + 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.setVisible(false); + 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.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..f24f1e6f55 --- /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 cases 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=Shared cases folder for automated processing, i.e., the location where case folder will be created by automated processing mode for presentation to the user in examiner mode. +AutoIngestSettingsPanel.outputPathTextField.text= +AutoIngestSettingsPanel.jLabelSelectOutputFolder.text=Select shared cases 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..6487b32acc 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/ImageGallery/nbproject/project.xml b/ImageGallery/nbproject/project.xml index 82215f7680..b9e8b60120 100644 --- a/ImageGallery/nbproject/project.xml +++ b/ImageGallery/nbproject/project.xml @@ -127,7 +127,7 @@ 10 - 10.6 + 10.7 diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java index 98d8591b21..55fb260bbd 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java @@ -717,7 +717,7 @@ public final class ImageGalleryController implements Executor { } } - @NbBundle.Messages({"BulkTask.committingDb.status=commiting image/video database", + @NbBundle.Messages({"BulkTask.committingDb.status=committing image/video database", "BulkTask.stopCopy.status=Stopping copy to drawable db task.", "BulkTask.errPopulating.errMsg=There was an error populating Image Gallery database."}) abstract static private class BulkTransferTask extends BackgroundTask { @@ -821,7 +821,7 @@ public final class ImageGalleryController implements Executor { * adds them to the Drawable DB. Uses the presence of a mimetype as an * approximation to 'analyzed'. */ - @NbBundle.Messages({"CopyAnalyzedFiles.committingDb.status=commiting image/video database", + @NbBundle.Messages({"CopyAnalyzedFiles.committingDb.status=committing image/video database", "CopyAnalyzedFiles.stopCopy.status=Stopping copy to drawable db task.", "CopyAnalyzedFiles.errPopulating.errMsg=There was an error populating Image Gallery database."}) static private class CopyAnalyzedFiles extends BulkTransferTask { @@ -875,7 +875,7 @@ public final class ImageGalleryController implements Executor { * TODO: create methods to simplify progress value/text updates to both * netbeans and ImageGallery progress/status */ - @NbBundle.Messages({"PrePopulateDataSourceFiles.committingDb.status=commiting image/video database"}) + @NbBundle.Messages({"PrePopulateDataSourceFiles.committingDb.status=committing image/video database"}) static private class PrePopulateDataSourceFiles extends BulkTransferTask { private static final Logger LOGGER = Logger.getLogger(PrePopulateDataSourceFiles.class.getName()); diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/AddTagAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/AddTagAction.java index 5e365aadbb..e116ff171c 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/AddTagAction.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/AddTagAction.java @@ -111,6 +111,7 @@ public class AddTagAction extends Action { Platform.runLater(() -> new Alert(Alert.AlertType.ERROR, Bundle.AddDrawableTagAction_addTagsToFiles_alert(fileID)).show() ); + break; } } return null; diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeAction.java index b91b857e60..e0b1cdbc15 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeAction.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/CategorizeAction.java @@ -173,6 +173,7 @@ public class CategorizeAction extends Action { Bundle.CategorizeTask_errorUnable_msg(fileID), Bundle.CategorizeTask_errorUnable_title(), JOptionPane.ERROR_MESSAGE); + break; } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenHelpAction.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenHelpAction.java index 360ee70bb1..8aab0ba937 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenHelpAction.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/actions/OpenHelpAction.java @@ -44,7 +44,7 @@ public final class OpenHelpAction implements ActionListener { @Override public void actionPerformed(ActionEvent e) { try { - Desktop.getDesktop().browse(URI.create("http://sleuthkit.org/autopsy/docs/user-docs/4.1/image_gallery_page.html")); //NON-NLS + Desktop.getDesktop().browse(URI.create("http://sleuthkit.org/autopsy/docs/user-docs/4.3/image_gallery_page.html")); //NON-NLS } catch (IOException ex) { Logger.getLogger(OpenHelpAction.class.getName()).log(Level.SEVERE, "failed to open help page", ex); //NON-NLS } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/CategoryManager.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/CategoryManager.java index 3da1a4e482..f18e96795b 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/CategoryManager.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/CategoryManager.java @@ -261,6 +261,7 @@ public class CategoryManager { tagsManager.deleteContentTag(ct); } catch (TskCoreException tskException) { LOGGER.log(Level.SEVERE, "Failed to delete content tag. Unable to maintain categories in a consistent state.", tskException); //NON-NLS + break; } } } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java index 3e9a139202..ae9e17bb4a 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/grouping/GroupManager.java @@ -188,7 +188,7 @@ public class GroupManager { * the groups the given file is a part of * * @return a a set of {@link GroupKey}s representing the group(s) the given - * file is a part of + * file is a part of */ synchronized public Set> getGroupKeysForFileID(Long fileID) { try { @@ -208,7 +208,7 @@ public class GroupManager { * @param groupKey * * @return return the DrawableGroup (if it exists) for the given GroupKey, - * or null if no group exists for that key. + * or null if no group exists for that key. */ @Nullable public DrawableGroup getGroupForKey(@Nonnull GroupKey groupKey) { @@ -266,15 +266,16 @@ public class GroupManager { */ @ThreadConfined(type = ThreadType.JFX) public void markGroupSeen(DrawableGroup group, boolean seen) { - - db.markGroupSeen(group.getGroupKey(), seen); - group.setSeen(seen); - if (seen) { - unSeenGroups.removeAll(group); - } else if (unSeenGroups.contains(group) == false) { - unSeenGroups.add(group); + if (nonNull(db)) { + db.markGroupSeen(group.getGroupKey(), seen); + group.setSeen(seen); + if (seen) { + unSeenGroups.removeAll(group); + } else if (unSeenGroups.contains(group) == false) { + unSeenGroups.add(group); + } + FXCollections.sort(unSeenGroups, applySortOrder(sortOrder, sortBy)); } - FXCollections.sort(unSeenGroups, applySortOrder(sortOrder, sortBy)); } /** @@ -283,7 +284,7 @@ public class GroupManager { * no-op * * @param groupKey the value of groupKey - * @param fileID the value of file + * @param fileID the value of file */ public synchronized DrawableGroup removeFromGroup(GroupKey groupKey, final Long fileID) { //get grouping this file would be in @@ -327,7 +328,7 @@ public class GroupManager { */ @SuppressWarnings({"unchecked"}) public > List findValuesForAttribute(DrawableAttribute groupBy) { - List values; + List values = Collections.emptyList(); try { switch (groupBy.attrName) { //these cases get special treatment @@ -343,30 +344,36 @@ public class GroupManager { values = (List) Arrays.asList(false, true); break; case HASHSET: - TreeSet names = new TreeSet<>((Collection) db.getHashSetNames()); - values = new ArrayList<>(names); + if (nonNull(db)) { + TreeSet names = new TreeSet<>((Collection) db.getHashSetNames()); + values = new ArrayList<>(names); + } break; case MIME_TYPE: - HashSet types = new HashSet<>(); - try (SleuthkitCase.CaseDbQuery executeQuery = controller.getSleuthKitCase().executeQuery("select group_concat(obj_id), mime_type from tsk_files group by mime_type "); //NON-NLS - ResultSet resultSet = executeQuery.getResultSet();) { - while (resultSet.next()) { - final String mimeType = resultSet.getString("mime_type"); //NON-NLS - String objIds = resultSet.getString("group_concat(obj_id)"); //NON-NLS + if (nonNull(db)) { + HashSet types = new HashSet<>(); + try (SleuthkitCase.CaseDbQuery executeQuery = controller.getSleuthKitCase().executeQuery("select group_concat(obj_id), mime_type from tsk_files group by mime_type "); //NON-NLS + ResultSet resultSet = executeQuery.getResultSet();) { + while (resultSet.next()) { + final String mimeType = resultSet.getString("mime_type"); //NON-NLS + String objIds = resultSet.getString("group_concat(obj_id)"); //NON-NLS - Pattern.compile(",").splitAsStream(objIds) - .map(Long::valueOf) - .filter(db::isInDB) - .findAny().ifPresent(obj_id -> types.add(mimeType)); + Pattern.compile(",").splitAsStream(objIds) + .map(Long::valueOf) + .filter(db::isInDB) + .findAny().ifPresent(obj_id -> types.add(mimeType)); + } + } catch (SQLException | TskCoreException ex) { + Exceptions.printStackTrace(ex); } - } catch (SQLException | TskCoreException ex) { - Exceptions.printStackTrace(ex); + values = new ArrayList<>((Collection) types); } - values = new ArrayList((Collection) types); break; default: //otherwise do straight db query - return db.findValuesForAttribute(groupBy, sortBy, sortOrder); + if (nonNull(db)) { + values = db.findValuesForAttribute(groupBy, sortBy, sortOrder); + } } return values; @@ -378,60 +385,69 @@ public class GroupManager { } public Set getFileIDsInGroup(GroupKey groupKey) throws TskCoreException { + Set fileIDsToReturn = Collections.emptySet(); switch (groupKey.getAttribute().attrName) { //these cases get special treatment case CATEGORY: - return getFileIDsWithCategory((Category) groupKey.getValue()); + fileIDsToReturn = getFileIDsWithCategory((Category) groupKey.getValue()); + break; case TAGS: - return getFileIDsWithTag((TagName) groupKey.getValue()); + fileIDsToReturn = getFileIDsWithTag((TagName) groupKey.getValue()); + break; case MIME_TYPE: - return getFileIDsWithMimeType((String) groupKey.getValue()); + fileIDsToReturn = getFileIDsWithMimeType((String) groupKey.getValue()); + break; // case HASHSET: //comment out this case to use db functionality for hashsets // return getFileIDsWithHashSetName((String) groupKey.getValue()); default: //straight db query - return db.getFileIDsInGroup(groupKey); + if (nonNull(db)) { + fileIDsToReturn = db.getFileIDsInGroup(groupKey); + } } + return fileIDsToReturn; } // @@@ This was kind of slow in the profiler. Maybe we should cache it. // Unless the list of file IDs is necessary, use countFilesWithCategory() to get the counts. public Set getFileIDsWithCategory(Category category) throws TskCoreException { + Set fileIDsToReturn = Collections.emptySet(); + if (nonNull(db)) { + try { + final DrawableTagsManager tagsManager = controller.getTagsManager(); + if (category == Category.ZERO) { + List< TagName> tns = Stream.of(Category.ONE, Category.TWO, Category.THREE, Category.FOUR, Category.FIVE) + .map(tagsManager::getTagName) + .collect(Collectors.toList()); - try { - final DrawableTagsManager tagsManager = controller.getTagsManager(); - if (category == Category.ZERO) { - List< TagName> tns = Stream.of(Category.ONE, Category.TWO, Category.THREE, Category.FOUR, Category.FIVE) - .map(tagsManager::getTagName) - .collect(Collectors.toList()); - - Set files = new HashSet<>(); - for (TagName tn : tns) { - if (tn != null) { - List contentTags = tagsManager.getContentTagsByTagName(tn); - files.addAll(contentTags.stream() - .filter(ct -> ct.getContent() instanceof AbstractFile) - .filter(ct -> db.isInDB(ct.getContent().getId())) - .map(ct -> ct.getContent().getId()) - .collect(Collectors.toSet())); + Set files = new HashSet<>(); + for (TagName tn : tns) { + if (tn != null) { + List contentTags = tagsManager.getContentTagsByTagName(tn); + files.addAll(contentTags.stream() + .filter(ct -> ct.getContent() instanceof AbstractFile) + .filter(ct -> db.isInDB(ct.getContent().getId())) + .map(ct -> ct.getContent().getId()) + .collect(Collectors.toSet())); + } } + + fileIDsToReturn = db.findAllFileIdsWhere("obj_id NOT IN (" + StringUtils.join(files, ',') + ")"); //NON-NLS + } else { + + List contentTags = tagsManager.getContentTagsByTagName(tagsManager.getTagName(category)); + fileIDsToReturn = contentTags.stream() + .filter(ct -> ct.getContent() instanceof AbstractFile) + .filter(ct -> db.isInDB(ct.getContent().getId())) + .map(ct -> ct.getContent().getId()) + .collect(Collectors.toSet()); } - - return db.findAllFileIdsWhere("obj_id NOT IN (" + StringUtils.join(files, ',') + ")"); //NON-NLS - } else { - - List contentTags = tagsManager.getContentTagsByTagName(tagsManager.getTagName(category)); - return contentTags.stream() - .filter(ct -> ct.getContent() instanceof AbstractFile) - .filter(ct -> db.isInDB(ct.getContent().getId())) - .map(ct -> ct.getContent().getId()) - .collect(Collectors.toSet()); - + } catch (TskCoreException ex) { + LOGGER.log(Level.WARNING, "TSK error getting files in Category:" + category.getDisplayName(), ex); //NON-NLS + throw ex; } - } catch (TskCoreException ex) { - LOGGER.log(Level.WARNING, "TSK error getting files in Category:" + category.getDisplayName(), ex); //NON-NLS - throw ex; } + return fileIDsToReturn; } public Set getFileIDsWithTag(TagName tagName) throws TskCoreException { @@ -439,7 +455,7 @@ public class GroupManager { Set files = new HashSet<>(); List contentTags = controller.getTagsManager().getContentTagsByTagName(tagName); for (ContentTag ct : contentTags) { - if (ct.getContent() instanceof AbstractFile && db.isInDB(ct.getContent().getId())) { + if (ct.getContent() instanceof AbstractFile && nonNull(db) && db.isInDB(ct.getContent().getId())) { files.add(ct.getContent().getId()); } } @@ -496,7 +512,7 @@ public class GroupManager { * @param groupBy * @param sortBy * @param sortOrder - * @param force true to force a full db query regroup + * @param force true to force a full db query regroup */ public synchronized > void regroup(final DrawableAttribute groupBy, final GroupSortBy sortBy, final SortOrder sortOrder, Boolean force) { @@ -513,7 +529,7 @@ public class GroupManager { groupByTask.cancel(true); } - groupByTask = new ReGroupTask(groupBy, sortBy, sortOrder); + groupByTask = new ReGroupTask<>(groupBy, sortBy, sortOrder); Platform.runLater(() -> { regroupProgress.bind(groupByTask.progressProperty()); }); @@ -543,14 +559,14 @@ public class GroupManager { GroupKey newGroupKey = null; final long fileID = evt.getAddedTag().getContent().getId(); if (groupBy == DrawableAttribute.CATEGORY && CategoryManager.isCategoryTagName(evt.getAddedTag().getName())) { - newGroupKey = new GroupKey(DrawableAttribute.CATEGORY, CategoryManager.categoryFromTagName(evt.getAddedTag().getName())); + newGroupKey = new GroupKey<>(DrawableAttribute.CATEGORY, CategoryManager.categoryFromTagName(evt.getAddedTag().getName())); for (GroupKey oldGroupKey : groupMap.keySet()) { if (oldGroupKey.equals(newGroupKey) == false) { removeFromGroup(oldGroupKey, fileID); } } } else if (groupBy == DrawableAttribute.TAGS && CategoryManager.isNotCategoryTagName(evt.getAddedTag().getName())) { - newGroupKey = new GroupKey(DrawableAttribute.TAGS, evt.getAddedTag().getName()); + newGroupKey = new GroupKey<>(DrawableAttribute.TAGS, evt.getAddedTag().getName()); } if (newGroupKey != null) { DrawableGroup g = getGroupForKey(newGroupKey); @@ -580,9 +596,9 @@ public class GroupManager { final ContentTagDeletedEvent.DeletedContentTagInfo deletedTagInfo = evt.getDeletedTagInfo(); final TagName tagName = deletedTagInfo.getName(); if (groupBy == DrawableAttribute.CATEGORY && CategoryManager.isCategoryTagName(tagName)) { - groupKey = new GroupKey(DrawableAttribute.CATEGORY, CategoryManager.categoryFromTagName(tagName)); + groupKey = new GroupKey<>(DrawableAttribute.CATEGORY, CategoryManager.categoryFromTagName(tagName)); } else if (groupBy == DrawableAttribute.TAGS && CategoryManager.isNotCategoryTagName(tagName)) { - groupKey = new GroupKey(DrawableAttribute.TAGS, tagName); + groupKey = new GroupKey<>(DrawableAttribute.TAGS, tagName); } if (groupKey != null) { final long fileID = deletedTagInfo.getContentID(); @@ -644,8 +660,9 @@ public class GroupManager { * task was still running) */ - } else { // no task or un-cancelled task - if ((groupKey.getAttribute() != DrawableAttribute.PATH) || db.isGroupAnalyzed(groupKey)) { + } else // no task or un-cancelled task + { + if (nonNull(db) && ((groupKey.getAttribute() != DrawableAttribute.PATH) || db.isGroupAnalyzed(groupKey))) { /* * for attributes other than path we can't be sure a group is * fully analyzed because we don't know all the files that will @@ -681,6 +698,7 @@ public class GroupManager { markGroupSeen(group, groupSeen); }); return group; + } } catch (TskCoreException ex) { LOGGER.log(Level.SEVERE, "failed to get files for group: " + groupKey.getAttribute().attrName.toString() + " = " + groupKey.getValue(), ex); //NON-NLS @@ -701,7 +719,7 @@ public class GroupManager { ResultSet resultSet = executeQuery.getResultSet();) { while (resultSet.next()) { final long fileID = resultSet.getLong("obj_id"); //NON-NLS - if (db.isInDB(fileID)) { + if (nonNull(db) && db.isInDB(fileID)) { hashSet.add(fileID); } } @@ -776,7 +794,7 @@ public class GroupManager { updateMessage(Bundle.ReGroupTask_progressUpdate(groupBy.attrName.toString(), val)); updateProgress(p, vals.size()); groupProgress.progress(Bundle.ReGroupTask_progressUpdate(groupBy.attrName.toString(), val), p); - popuplateIfAnalyzed(new GroupKey(groupBy, val), this); + popuplateIfAnalyzed(new GroupKey<>(groupBy, val), this); } Platform.runLater(() -> FXCollections.sort(analyzedGroups, applySortOrder(sortOrder, sortBy))); diff --git a/KeywordSearch/nbproject/project.xml b/KeywordSearch/nbproject/project.xml index 28fcbd05e5..32bd79c8a2 100644 --- a/KeywordSearch/nbproject/project.xml +++ b/KeywordSearch/nbproject/project.xml @@ -119,7 +119,7 @@ 10 - 10.6 + 10.7 diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AbstractFileChunk.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AbstractFileChunk.java index 83dac4645b..5253e5e240 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AbstractFileChunk.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AbstractFileChunk.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2012 Basis Technology Corp. + * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,47 +19,73 @@ package org.sleuthkit.autopsy.keywordsearch; import java.nio.charset.Charset; -import org.openide.util.NbBundle; import org.sleuthkit.autopsy.keywordsearch.Ingester.IngesterException; /** - * Represents each string chunk to be indexed, a derivative of TextExtractor - * file + * A representation of a chunk of text from a file that can be used, when + * supplied with an Ingester, to index the chunk for search. */ -class AbstractFileChunk { +final class AbstractFileChunk { - private int chunkID; - private TextExtractor parent; + private final int chunkNumber; + private final TextExtractor textExtractor; - AbstractFileChunk(TextExtractor parent, int chunkID) { - this.parent = parent; - this.chunkID = chunkID; - } - - public TextExtractor getParent() { - return parent; - } - - public int getChunkId() { - return chunkID; + /** + * Constructs a representation of a chunk of text from a file that can be + * used, when supplied with an Ingester, to index the chunk for search. + * + * @param textExtractor A TextExtractor for the file. + * @param chunkNumber A sequence number for the chunk. + */ + AbstractFileChunk(TextExtractor textExtractor, int chunkNumber) { + this.textExtractor = textExtractor; + this.chunkNumber = chunkNumber; } /** - * return String representation of the absolute id (parent and child) + * Gets the TextExtractor for the source file of the text chunk. * - * @return + * @return A reference to the TextExtractor. */ - String getIdString() { - return Server.getChunkIdString(this.parent.getSourceFile().getId(), this.chunkID); + TextExtractor getTextExtractor() { + return textExtractor; } - void index(Ingester ingester, byte[] content, long contentSize, Charset indexCharset) throws IngesterException { - ByteContentStream bcs = new ByteContentStream(content, contentSize, parent.getSourceFile(), indexCharset); + /** + * Gets the sequence number of the text chunk. + * + * @return The chunk number. + */ + int getChunkNumber() { + return chunkNumber; + } + + /** + * Gets the id of the text chunk. + * + * @return An id of the form [source file object id]_[chunk number] + */ + String getChunkId() { + return Server.getChunkIdString(this.textExtractor.getSourceFile().getId(), this.chunkNumber); + } + + /** + * Indexes the text chunk. + * + * @param ingester An Ingester to do the indexing. + * @param chunkBytes The raw bytes of the text chunk. + * @param chunkSize The size of the text chunk in bytes. + * @param charSet The char set to use during indexing. + * + * @throws org.sleuthkit.autopsy.keywordsearch.Ingester.IngesterException + */ + void index(Ingester ingester, byte[] chunkBytes, long chunkSize, Charset charSet) throws IngesterException { + ByteContentStream bcs = new ByteContentStream(chunkBytes, chunkSize, textExtractor.getSourceFile(), charSet); try { - ingester.ingest(this, bcs, content.length); - } catch (Exception ingEx) { - throw new IngesterException(NbBundle.getMessage(this.getClass(), "AbstractFileChunk.index.exception.msg", - parent.getSourceFile().getId(), chunkID), ingEx); + ingester.ingest(this, bcs, chunkBytes.length); + } catch (Exception ex) { + throw new IngesterException(String.format("Error ingesting (indexing) file chunk: %s", getChunkId()), ex); } } + } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AbstractFileStringContentStream.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AbstractFileStringContentStream.java index b62cc46bba..e8a7efdde0 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AbstractFileStringContentStream.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AbstractFileStringContentStream.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011 Basis Technology Corp. + * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,7 +25,6 @@ import java.io.Reader; import java.nio.charset.Charset; import org.openide.util.NbBundle; -import org.sleuthkit.autopsy.coreutils.Logger; import org.apache.solr.common.util.ContentStream; import org.sleuthkit.datamodel.AbstractContent; import org.sleuthkit.datamodel.AbstractFile; @@ -36,11 +35,10 @@ import org.sleuthkit.datamodel.AbstractFile; class AbstractFileStringContentStream implements ContentStream { //input - private AbstractFile content; - private Charset charset; + private final AbstractFile content; + private final Charset charset; //converted - private InputStream stream; - private static Logger logger = Logger.getLogger(AbstractFileStringContentStream.class.getName()); + private final InputStream stream; public AbstractFileStringContentStream(AbstractFile content, Charset charset, InputStream inputStream) { this.content = content; diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AddKeywordsDialog.form b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AddKeywordsDialog.form new file mode 100644 index 0000000000..65cdd9f88d --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AddKeywordsDialog.form @@ -0,0 +1,185 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AddKeywordsDialog.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AddKeywordsDialog.java new file mode 100644 index 0000000000..0f5d42ca03 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/AddKeywordsDialog.java @@ -0,0 +1,314 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2011-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.keywordsearch; + +import java.awt.Dimension; +import java.awt.Toolkit; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.List; +import java.util.ArrayList; +import java.util.Arrays; +import javax.swing.JFrame; +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; +import javax.swing.SwingUtilities; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import org.openide.util.NbBundle; +import org.openide.windows.WindowManager; + +/** + * Dialog to add one or more keywords to a list + */ +class AddKeywordsDialog extends javax.swing.JDialog { + + List newKeywords = new ArrayList<>(); + + /** + * Creates new form AddKeywordsDialog. + * Note that this does not display the dialog - call display() after creation. + * @param initialKeywords Keywords to populate the list with + * @param type Starting keyword type + */ + AddKeywordsDialog(){ + super((JFrame) WindowManager.getDefault().getMainWindow(), + NbBundle.getMessage(AddKeywordsDialog.class, "AddKeywordsDialog.addKeywordsTitle.text"), + true); + initComponents(); + + // Set the add button to only be active when there is text in the text area + addButton.setEnabled(false); + keywordTextArea.getDocument().addDocumentListener(new DocumentListener() { + @Override + public void changedUpdate(DocumentEvent e) { + fire(); + } + @Override + public void removeUpdate(DocumentEvent e) { + fire(); + } + @Override + public void insertUpdate(DocumentEvent e) { + fire(); + } + private void fire() { + enableButtons(); + } + }); + } + + /** + * Display the dialog + */ + void display() { + newKeywords.clear(); + Dimension screenDimension = Toolkit.getDefaultToolkit().getScreenSize(); + setLocation((screenDimension.width - getSize().width) / 2, (screenDimension.height - getSize().height) / 2); + setVisible(true); + } + + /** + * Set the initial contents of the text box. + * Intended to be used to redisplay any keywords that contained errors + * @param initialKeywords + */ + void setInitialKeywordList(String initialKeywords){ + keywordTextArea.setText(initialKeywords); + } + + + private void enableButtons(){ + addButton.setEnabled(! keywordTextArea.getText().isEmpty()); + } + + /** + * Get the list of keywords from the text area + * @return list of keywords + */ + List getKeywords(){ + return newKeywords; + } + + /** + * Get whether the regex option is selected + * @return true if the regex radio button is selected + */ + boolean isKeywordRegex(){ + return regexRadioButton.isSelected(); + } + + /** + * Get whether the exact match option is selected + * @return true if the exact match radio button is selected + */ + boolean isKeywordExact(){ + return exactRadioButton.isSelected(); + } + + /** + * 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() { + + keywordTypeButtonGroup = new javax.swing.ButtonGroup(); + exactRadioButton = new javax.swing.JRadioButton(); + substringRadioButton = new javax.swing.JRadioButton(); + regexRadioButton = new javax.swing.JRadioButton(); + jScrollPane1 = new javax.swing.JScrollPane(); + keywordTextArea = new javax.swing.JTextArea(); + enterKeywordsLabel = new javax.swing.JLabel(); + keywordTypeLabel = new javax.swing.JLabel(); + addButton = new javax.swing.JButton(); + cancelButton = new javax.swing.JButton(); + pasteButton = new javax.swing.JButton(); + + setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE); + + keywordTypeButtonGroup.add(exactRadioButton); + exactRadioButton.setSelected(true); + org.openide.awt.Mnemonics.setLocalizedText(exactRadioButton, org.openide.util.NbBundle.getMessage(AddKeywordsDialog.class, "AddKeywordsDialog.exactRadioButton.text")); // NOI18N + + keywordTypeButtonGroup.add(substringRadioButton); + org.openide.awt.Mnemonics.setLocalizedText(substringRadioButton, org.openide.util.NbBundle.getMessage(AddKeywordsDialog.class, "AddKeywordsDialog.substringRadioButton.text")); // NOI18N + + keywordTypeButtonGroup.add(regexRadioButton); + org.openide.awt.Mnemonics.setLocalizedText(regexRadioButton, org.openide.util.NbBundle.getMessage(AddKeywordsDialog.class, "AddKeywordsDialog.regexRadioButton.text")); // NOI18N + + keywordTextArea.setColumns(20); + keywordTextArea.setRows(5); + keywordTextArea.addMouseListener(new java.awt.event.MouseAdapter() { + public void mouseClicked(java.awt.event.MouseEvent evt) { + keywordTextAreaMouseClicked(evt); + } + }); + jScrollPane1.setViewportView(keywordTextArea); + + org.openide.awt.Mnemonics.setLocalizedText(enterKeywordsLabel, org.openide.util.NbBundle.getMessage(AddKeywordsDialog.class, "AddKeywordsDialog.enterKeywordsLabel.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(keywordTypeLabel, org.openide.util.NbBundle.getMessage(AddKeywordsDialog.class, "AddKeywordsDialog.keywordTypeLabel.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(addButton, org.openide.util.NbBundle.getMessage(AddKeywordsDialog.class, "AddKeywordsDialog.addButton.text")); // NOI18N + addButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + addButtonActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(cancelButton, org.openide.util.NbBundle.getMessage(AddKeywordsDialog.class, "AddKeywordsDialog.cancelButton.text")); // NOI18N + cancelButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + cancelButtonActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(pasteButton, org.openide.util.NbBundle.getMessage(AddKeywordsDialog.class, "AddKeywordsDialog.pasteButton.text")); // NOI18N + pasteButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + pasteButtonActionPerformed(evt); + } + }); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); + getContentPane().setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(enterKeywordsLabel) + .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 249, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(pasteButton)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(addButton, javax.swing.GroupLayout.PREFERRED_SIZE, 84, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(cancelButton, javax.swing.GroupLayout.PREFERRED_SIZE, 84, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(keywordTypeLabel) + .addGroup(layout.createSequentialGroup() + .addGap(10, 10, 10) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(substringRadioButton) + .addComponent(exactRadioButton) + .addComponent(regexRadioButton)))) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addContainerGap() + .addComponent(enterKeywordsLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(keywordTypeLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(exactRadioButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(substringRadioButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(regexRadioButton) + .addGap(194, 194, 194)) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addGap(0, 0, Short.MAX_VALUE) + .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 278, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(5, 5, 5))) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(addButton) + .addComponent(cancelButton)) + .addComponent(pasteButton)) + .addContainerGap()) + ); + + pack(); + }// //GEN-END:initComponents + + private void pasteButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_pasteButtonActionPerformed + keywordTextArea.paste(); + }//GEN-LAST:event_pasteButtonActionPerformed + + private void addButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addButtonActionPerformed + // Save the values from the list + newKeywords.addAll(Arrays.asList(keywordTextArea.getText().split("\\r?\\n"))); + + setVisible(false); + dispose(); + }//GEN-LAST:event_addButtonActionPerformed + + private void cancelButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cancelButtonActionPerformed + setVisible(false); + dispose(); + }//GEN-LAST:event_cancelButtonActionPerformed + + private void keywordTextAreaMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_keywordTextAreaMouseClicked + if (SwingUtilities.isRightMouseButton(evt)) { + JPopupMenu popup = new JPopupMenu(); + + JMenuItem cutMenu = new JMenuItem("Cut"); // NON-NLS + cutMenu.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + keywordTextArea.cut(); + } + }); + + JMenuItem copyMenu = new JMenuItem("Copy"); // NON-NLS + copyMenu.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + keywordTextArea.copy(); + } + }); + + JMenuItem pasteMenu = new JMenuItem("Paste"); // NON-NLS + pasteMenu.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + keywordTextArea.paste(); + } + }); + + popup.add(cutMenu); + popup.add(copyMenu); + popup.add(pasteMenu); + popup.show(keywordTextArea, evt.getX(), evt.getY()); + } + }//GEN-LAST:event_keywordTextAreaMouseClicked + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton addButton; + private javax.swing.JButton cancelButton; + private javax.swing.JLabel enterKeywordsLabel; + private javax.swing.JRadioButton exactRadioButton; + private javax.swing.JScrollPane jScrollPane1; + private javax.swing.JTextArea keywordTextArea; + private javax.swing.ButtonGroup keywordTypeButtonGroup; + private javax.swing.JLabel keywordTypeLabel; + private javax.swing.JButton pasteButton; + private javax.swing.JRadioButton regexRadioButton; + private javax.swing.JRadioButton substringRadioButton; + // End of variables declaration//GEN-END:variables +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle.properties b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle.properties index 66a007a67c..43ba62c3c6 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle.properties +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle.properties @@ -22,13 +22,15 @@ KeywordSearchEditListPanel.saveListButton.text=Copy List KeywordSearchEditListPanel.addWordField.text= KeywordSearchEditListPanel.addWordButton.text=New keyword KeywordSearchEditListPanel.chRegex.text=Regular Expression -KeywordSearchEditListPanel.deleteWordButton.text=Delete keyword +KeywordSearchEditListPanel.deleteWordButton.text=Delete keywords KeywordSearchEditListPanel.cutMenuItem.text=Cut KeywordSearchEditListPanel.selectAllMenuItem.text=Select All KeywordSearchEditListPanel.pasteMenuItem.text=Paste KeywordSearchEditListPanel.copyMenuItem.text=Copy KeywordSearchEditListPanel.exportButton.text=Export List KeywordSearchEditListPanel.deleteListButton.text=Delete List +KeywordSearchEditListPanel.emptyKeyword.text=Empty keyword +KeywordSearchEditListPanel.errorAddingKeywords.text=Error adding keyword(s) KeywordSearchListsManagementPanel.newListButton.text=New List KeywordSearchListsManagementPanel.importButton.text=Import List KeywordSearchListsViewerPanel.searchAddButton.text=Search @@ -87,7 +89,7 @@ KeywordSearchConfigurationPanel1.customizeComponents.noOwDefaultMsg=Cannot overw KeywordSearchConfigurationPanel1.customizeComponents.kwListExistMsg=Keyword List <{0}> already exists, do you want to replace it? KeywordSearchConfigurationPanel1.customizeComponents.kwListSavedMsg=Keyword List <{0}> saved KeywordSearchEditListPanel.customizeComponents.kwReToolTip=Keyword is a regular expression -KeywordSearchEditListPanel.customizeComponents.addWordToolTip=Add a new word to the keyword search list +KeywordSearchEditListPanel.customizeComponents.addWordToolTip=Add new words to the keyword search list KeywordSearchEditListPanel.customizeComponents.enterNewWordToolTip=Enter a new word or regex to search KeywordSearchEditListPanel.customizeComponents.exportToFile=Export the current keyword list to a file KeywordSearchEditListPanel.customizeComponents.saveCurrentWIthNewNameToolTip=Save the current keyword list with a new name @@ -101,7 +103,6 @@ KeywordSearchEditListPanel.exportButtonActionPerformed.fileFilterLabel=Keyword L KeywordSearchEditListPanel.exportButtonActionPerformed.fileExistPrompt=File {0} exists, overwrite? KeywordSearchEditListPanel.exportButtonActionPerformed.kwListExportedMsg=Keyword lists exported KeywordSearchEditListPanel.kwColName=Keyword -KeywordSearchEditListPanel.exportButtonActionPerformed.regExColName=RegEx KeywordSearchEditListPanel.addKeyword.message=Add a new word to the keyword search list\: KeywordSearchEditListPanel.addKeyword.title=New keyword KeywordSearchFilterNode.getFileActions.openExternViewActLbl=Open in External Viewer @@ -132,7 +133,7 @@ KeywordSearchListsViewerPanel.initIngest.ongoingIngestMsg=Files Indexed\: {0} (i KeywordSearchListsViewerPanel.initIngest.fileIndexCtMsg=Files Indexed\: {0} KeywordSearch.selectedColLbl=Selected KeywordSearch.nameColLbl=Name -KeywordSearch.regExColLbl=RegEx +KeywordSearch.typeColLbl=Keyword Type KeywordSearchQueryManager.execute.exeWinTitle=Keyword search {0} - {1} KeywordSearch.newKeywordListMsg=New Keyword List KeywordSearch.importListFileDialogMsg=Error importing keyword list from file {0} @@ -157,7 +158,6 @@ DropdownSearchPanel.cutMenuItem.text=Cut DropdownSearchPanel.selectAllMenuItem.text=Select All DropdownSearchPanel.pasteMenuItem.text=Paste DropdownSearchPanel.copyMenuItem.text=Copy -AbstractFileChunk.index.exception.msg=Problem ingesting file string chunk\: {0}, chunk\: {1} AbstractFileStringContentStream.getSize.exception.msg=Cannot tell how many chars in converted string, until entire string is converted AbstractFileStringContentStream.getSrcInfo.text=File\:{0} ByteContentStream.getSrcInfo.text=File\:{0} @@ -187,7 +187,6 @@ Ingester.FscContentStream.getSrcInfo=File\:{0} Ingester.FscContentStream.getReader=Not supported yet. Ingester.NullContentStream.getSrcInfo.text=File\:{0} Ingester.NullContentStream.getReader=Not supported yet. -Keyword.toString.text=Keyword'{'query\={0}, isLiteral\={1}, keywordType\={2}'}' KeywordSearch.moduleErr=Module Error KeywordSearch.fireNumIdxFileChg.moduleErr.msg=A module caused an error listening to KeywordSearch updates. See log to determine which module. Some data could be incomplete. KeywordSearchListsEncase.save.exception.msg=Not supported yet. @@ -292,3 +291,25 @@ RawText.getText.error.msg=Error getting text GlobalListsManagementPanel.newListButton.text=New List GlobalListsManagementPanel.importButton.text=Import List GlobalListsManagementPanel.keywordListsLabel.text=Keyword Lists: +NewKeywordPanel.regexButton.text=Regular Expression +NewKeywordPanel.exactButton.text=Exact Match +NewKeywordPanel.substringButton.text=Substring Match +NewKeywordPanel.keywordTextField.text= +NewKeywordPanel.newKeywordLabel.text=Enter a new keyword: +AddKeywordsDialog.exactRadioButton.text=Exact Match +AddKeywordsDialog.substringRadioButton.text=Substring Match +AddKeywordsDialog.regexRadioButton.text=Regular Expression +AddKeywordsDialog.keywordTypeLabel.text=Select type for keywords: +AddKeywordsDialog.enterKeywordsLabel.text=Enter keywords (one per line) below: +AddKeywordsDialog.pasteButton.text=Paste From Clipboard +AddKeywordsDialog.addButton.text=OK +AddKeywordsDialog.cancelButton.text=Cancel +AddKeywordsDialog.addKeywordsTitle.text=New keywords +GlobalEditListPanel.newKeywordsButton.text=New keywords +GlobalEditListPanel.addKeywordResults.text=Add keyword results +GlobalEditListPanel.keywordsAdded.text={0} keyword was successfully added. +GlobalEditListPanel.keywordsAddedPlural.text={0} keywords were successfully added. +GlobalEditListPanel.keywordDupesSkipped.text={0} keyword was already in the list. +GlobalEditListPanel.keywordDupesSkippedPlural.text={0} keywords were already in the list. +GlobalEditListPanel.keywordErrors.text={0} keyword could not be parsed. Please review and try again. +GlobalEditListPanel.keywordErrorsPlural.text={0} keywords could not be parsed. Please review and try again. diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle_ja.properties b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle_ja.properties index 827842afd2..b96be19429 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle_ja.properties +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle_ja.properties @@ -133,7 +133,6 @@ OptionsCategory_Keywords_KeywordSearchOptions=\u30ad\u30fc\u30ef\u30fc\u30c9\u69 ExtractedContentPanel.pageOfLabel.text=of ExtractedContentPanel.pageCurLabel.text=- ExtractedContentPanel.pageTotalLabel.text=- -AbstractFileChunk.index.exception.msg=\u30d5\u30a1\u30a4\u30eb\u30b9\u30c8\u30ea\u30f3\u30b0\u30c1\u30e3\u30f3\u30af\u306e\u30a4\u30f3\u30b8\u30a7\u30b9\u30c8\u4e2d\u306b\u554f\u984c\u304c\u767a\u751f\u3057\u307e\u3057\u305f\uff1a {0}, \u30c1\u30e3\u30f3\u30af\: {1} AbstractFileStringContentStream.getSize.exception.msg=\u30b9\u30c8\u30ea\u30f3\u30b0\u5168\u4f53\u304c\u5909\u63db\u3055\u308c\u306a\u3051\u308c\u3070\u3001\u5909\u63db\u3055\u308c\u305f\u30b9\u30c8\u30ea\u30f3\u30b0\u5185\u306e\u30ad\u30e3\u30e9\u30af\u30bf\u30fc\u6570\u306f\u4e0d\u660e\u3067\u3059\u3002 AbstractFileStringContentStream.getSrcInfo.text=\u30d5\u30a1\u30a4\u30eb\uff1a{0} ByteContentStream.getSrcInfo.text=\u30d5\u30a1\u30a4\u30eb\uff1a{0} @@ -208,7 +207,6 @@ KeywordSearchIngestModule.doInBackGround.pendingMsg=\uff08\u30da\u30f3\u30c7\u30 SearchRunner.doInBackGround.cancelMsg=\uff08\u30ad\u30e3\u30f3\u30bb\u30eb\u4e2d\u2026\uff09 Server.addDoc.exception.msg2=\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u30cf\u30f3\u30c9\u30e9\u30fc\u3092\u4f7f\u7528\u3057\u307e\u3057\u305f\u304c\u3001\u30a4\u30f3\u30c7\u30c3\u30af\u30b9\u306b\u6b21\u306e\u30c9\u30ad\u30e5\u30e1\u30f3\u30c8\u3092\u8ffd\u52a0\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\uff1a{0} ExtractedContentViewer.getSolrContent.txtBodyItal={0} -Keyword.toString.text=Keyword'{'query\={0}, isLiteral\={1}, keywordType\={2}'}' KeywordSearchJobSettingsPanel.keywordSearchEncodings.text=- KeywordSearchJobSettingsPanel.languagesValLabel.text=- KeywordSearchJobSettingsPanel.encodingsLabel.text=\u30a8\u30f3\u30b3\u30fc\u30c7\u30a3\u30f3\u30b0\uff1a diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/DropdownListSearchPanel.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/DropdownListSearchPanel.java index a58328c113..40cc83cd92 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/DropdownListSearchPanel.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/DropdownListSearchPanel.java @@ -1,15 +1,15 @@ /* * Autopsy Forensic Browser - * + * * Copyright 2011-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. @@ -39,9 +39,6 @@ import org.openide.util.NbBundle; import org.openide.util.actions.SystemAction; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.ingest.IngestManager; -import javax.swing.ImageIcon; -import static javax.swing.SwingConstants.CENTER; -import javax.swing.table.DefaultTableCellRenderer; /** * Viewer panel widget for keyword lists that is used in the ingest config and @@ -93,10 +90,9 @@ class DropdownListSearchPanel extends KeywordSearchPanel { for (int i = 0; i < keywordsTable.getColumnCount(); i++) { column = keywordsTable.getColumnModel().getColumn(i); if (i == 0) { - column.setPreferredWidth(((int) (rightWidth * 0.78))); + column.setPreferredWidth(((int) (rightWidth * 0.60))); } else { - column.setPreferredWidth(((int) (rightWidth * 0.20))); - column.setCellRenderer(new CheckBoxRenderer()); + column.setPreferredWidth(((int) (rightWidth * 0.38))); } } @@ -123,12 +119,9 @@ class DropdownListSearchPanel extends KeywordSearchPanel { public void propertyChange(PropertyChangeEvent evt) { Object source = evt.getSource(); if (source instanceof String && ((String) source).equals("LOCAL")) { //NON-NLS - EventQueue.invokeLater(new Runnable() { - @Override - public void run() { - ingestRunning = IngestManager.getInstance().isIngestRunning(); - updateComponents(); - } + EventQueue.invokeLater(() -> { + ingestRunning = IngestManager.getInstance().isIngestRunning(); + updateComponents(); }); } } @@ -320,8 +313,8 @@ class DropdownListSearchPanel extends KeywordSearchPanel { private class KeywordListsTableModel extends AbstractTableModel { //data - private XmlKeywordSearchList listsHandle = XmlKeywordSearchList.getCurrent(); - private List listData = new ArrayList<>(); + private final XmlKeywordSearchList listsHandle = XmlKeywordSearchList.getCurrent(); + private final List listData = new ArrayList<>(); @Override public int getColumnCount() { @@ -496,7 +489,7 @@ class DropdownListSearchPanel extends KeywordSearchPanel { ret = NbBundle.getMessage(this.getClass(), "KeywordSearch.nameColLbl"); break; case 1: - ret = NbBundle.getMessage(this.getClass(), "KeywordSearch.regExColLbl"); + ret = NbBundle.getMessage(this.getClass(), "KeywordSearch.typeColLbl"); break; default: break; @@ -519,7 +512,7 @@ class DropdownListSearchPanel extends KeywordSearchPanel { ret = (Object) entry.name; break; case 1: - ret = (Object) entry.regex; + ret = (Object) entry.keywordType; break; default: break; @@ -559,11 +552,11 @@ class DropdownListSearchPanel extends KeywordSearchPanel { private class KeywordTableEntry implements Comparable { String name; - Boolean regex; + String keywordType; KeywordTableEntry(Keyword keyword) { - this.name = keyword.getQuery(); - this.regex = !keyword.isLiteral(); + this.name = keyword.getSearchTerm(); + this.keywordType = keyword.getSearchTermType(); } @Override @@ -598,33 +591,4 @@ class DropdownListSearchPanel extends KeywordSearchPanel { return this; } } - - /** - * A cell renderer for boolean cells that shows a center-aligned green check - * mark if true, nothing if false. - */ - private class CheckBoxRenderer extends DefaultTableCellRenderer { - - private static final long serialVersionUID = 1L; - final ImageIcon theCheck = new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/keywordsearch/checkmark.png")); // NON-NLS - - CheckBoxRenderer() { - setHorizontalAlignment(CENTER); - } - - @Override - public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { - - if ((value instanceof Boolean)) { - if ((Boolean) value) { - setIcon(theCheck); - setToolTipText(Bundle.IsRegularExpression()); - } else { - setIcon(null); - setToolTipText(null); - } - } - return this; - } - } } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/DropdownSingleTermSearchPanel.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/DropdownSingleTermSearchPanel.java index 4f5ffe545b..a5024fd7b0 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/DropdownSingleTermSearchPanel.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/DropdownSingleTermSearchPanel.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2014 Basis Technology Corp. + * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +18,6 @@ */ package org.sleuthkit.autopsy.keywordsearch; -import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.FocusEvent; @@ -27,11 +26,15 @@ import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import javax.swing.JMenuItem; - import org.sleuthkit.autopsy.coreutils.Logger; /** - * A simple UI for finding text after ingest + * A dropdown panel that provides GUI components that allow a user to do three + * types of ad hoc single keyword searches. The first option is a standard + * Lucene query for one or more terms, with or without wildcards and explicit + * Boolean operators, or a phrase. The second option is a Lucene query for a + * substring of a single term. The third option is a regex query using first the + * terms component, followed by standard Lucene queries for any terms found. * * The toolbar uses a different font from the rest of the application, * Monospaced 14, due to the necessity to find a font that displays both Arabic @@ -41,46 +44,61 @@ import org.sleuthkit.autopsy.coreutils.Logger; */ public class DropdownSingleTermSearchPanel extends KeywordSearchPanel { - private static final Logger logger = Logger.getLogger(DropdownSingleTermSearchPanel.class.getName()); - private static DropdownSingleTermSearchPanel instance = null; + private static final long serialVersionUID = 1L; + private static final Logger LOGGER = Logger.getLogger(DropdownSingleTermSearchPanel.class.getName()); + private static DropdownSingleTermSearchPanel defaultInstance = null; /** - * Creates new form DropdownSingleTermSearchPanel + * Gets the default instance of a dropdown panel that provides GUI + * components that allow a user to do three types of ad hoc single keyword + * searches. + * @return the default instance of DropdownSingleKeywordSearchPanel + */ + public static synchronized DropdownSingleTermSearchPanel getDefault() { + if (null == defaultInstance) { + defaultInstance = new DropdownSingleTermSearchPanel(); + } + return defaultInstance; + } + + /** + * Constructs a dropdown panel that provides GUI components that allow a + * user to do three types of ad hoc single keyword searches. */ public DropdownSingleTermSearchPanel() { initComponents(); customizeComponents(); } + /** + * Does additional initialization of the GUI components created by the + * initComponents method. + */ private void customizeComponents() { keywordTextField.addFocusListener(new FocusListener() { @Override public void focusGained(FocusEvent e) { - //do nothing } @Override public void focusLost(FocusEvent e) { if (keywordTextField.getText().equals("")) { - resetSearchBox(); + clearSearchBox(); } } }); keywordTextField.setComponentPopupMenu(rightClickMenu); - ActionListener actList = new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - JMenuItem jmi = (JMenuItem) e.getSource(); - if (jmi.equals(cutMenuItem)) { - keywordTextField.cut(); - } else if (jmi.equals(copyMenuItem)) { - keywordTextField.copy(); - } else if (jmi.equals(pasteMenuItem)) { - keywordTextField.paste(); - } else if (jmi.equals(selectAllMenuItem)) { - keywordTextField.selectAll(); - } + ActionListener actList = (ActionEvent e) -> { + JMenuItem jmi = (JMenuItem) e.getSource(); + if (jmi.equals(cutMenuItem)) { + keywordTextField.cut(); + } else if (jmi.equals(copyMenuItem)) { + keywordTextField.copy(); + } else if (jmi.equals(pasteMenuItem)) { + keywordTextField.paste(); + } else if (jmi.equals(selectAllMenuItem)) { + keywordTextField.selectAll(); } }; cutMenuItem.addActionListener(actList); @@ -89,36 +107,43 @@ public class DropdownSingleTermSearchPanel extends KeywordSearchPanel { selectAllMenuItem.addActionListener(actList); } - public static synchronized DropdownSingleTermSearchPanel getDefault() { - if (instance == null) { - instance = new DropdownSingleTermSearchPanel(); - } - return instance; - } - + /** + * Add an action listener to the Search buttom component of the panel. + * + * @param actionListener The actin listener. + */ void addSearchButtonActionListener(ActionListener actionListener) { searchButton.addActionListener(actionListener); } - void resetSearchBox() { + /** + * Clears the text in the query text field, i.e., sets it to the emtpy + * string. + */ + void clearSearchBox() { keywordTextField.setText(""); } + /** + * Gets a single keyword list consisting of a single keyword encapsulating + * the input term(s)/phrase/substring/regex. + * + * @return The keyword list. + */ @Override List getKeywordLists() { List keywords = new ArrayList<>(); - keywords.add(new Keyword(keywordTextField.getText(), - !regexRadioButton.isSelected(), exactRadioButton.isSelected())); - + keywords.add(new Keyword(keywordTextField.getText(), !regexRadioButton.isSelected(), exactRadioButton.isSelected())); List keywordLists = new ArrayList<>(); keywordLists.add(new KeywordList(keywords)); - return keywordLists; } + /** + * Not implemented. + */ @Override protected void postFilesIndexedChange() { - //nothing to update } /** @@ -154,7 +179,7 @@ public class DropdownSingleTermSearchPanel extends KeywordSearchPanel { org.openide.awt.Mnemonics.setLocalizedText(selectAllMenuItem, org.openide.util.NbBundle.getMessage(DropdownSingleTermSearchPanel.class, "DropdownSearchPanel.selectAllMenuItem.text")); // NOI18N rightClickMenu.add(selectAllMenuItem); - keywordTextField.setFont(new java.awt.Font("Monospaced", 0, 14)); // NOI18N NON-NLS + keywordTextField.setFont(new java.awt.Font("Monospaced", 0, 14)); // NOI18N keywordTextField.setText(org.openide.util.NbBundle.getMessage(DropdownSingleTermSearchPanel.class, "DropdownSearchPanel.keywordTextField.text")); // NOI18N keywordTextField.setBorder(new javax.swing.border.LineBorder(new java.awt.Color(192, 192, 192), 1, true)); keywordTextField.setMinimumSize(new java.awt.Dimension(2, 25)); @@ -170,7 +195,7 @@ public class DropdownSingleTermSearchPanel extends KeywordSearchPanel { } }); - searchButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/keywordsearch/search-icon.png"))); // NOI18N NON-NLS + searchButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/keywordsearch/search-icon.png"))); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(searchButton, org.openide.util.NbBundle.getMessage(DropdownSingleTermSearchPanel.class, "DropdownSearchPanel.searchButton.text")); // NOI18N searchButton.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { @@ -224,18 +249,33 @@ public class DropdownSingleTermSearchPanel extends KeywordSearchPanel { ); }// //GEN-END:initComponents + /** + * Action performed by the action listener for the search button. + * + * @param evt The action event. + */ private void searchButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_searchButtonActionPerformed keywordTextFieldActionPerformed(evt); }//GEN-LAST:event_searchButtonActionPerformed + /** + * Action performed by the action listener for the keyword text field. + * + * @param evt The action event. + */ private void keywordTextFieldActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_keywordTextFieldActionPerformed try { search(); } catch (Exception e) { - logger.log(Level.SEVERE, "search() threw exception", e); //NON-NLS + LOGGER.log(Level.SEVERE, "Error performing ad hoc single keyword search", e); //NON-NLS } }//GEN-LAST:event_keywordTextFieldActionPerformed + /** + * Mouse event handler for the keyword text field. + * + * @param evt The mouse event. + */ private void keywordTextFieldMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_keywordTextFieldMouseClicked if (evt.isPopupTrigger()) { rightClickMenu.show(evt.getComponent(), evt.getX(), evt.getY()); diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/DropdownToolbar.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/DropdownToolbar.java index 7c24b99337..68c482edd4 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/DropdownToolbar.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/DropdownToolbar.java @@ -1,26 +1,21 @@ /* * Autopsy Forensic Browser - * - * Copyright 2011 Basis Technology Corp. + * + * Copyright 2003-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. */ - -/* - * DropdownToolbar - * - */ package org.sleuthkit.autopsy.keywordsearch; import java.awt.event.ActionEvent; @@ -37,22 +32,26 @@ import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.core.RuntimeProperties; /** - * Keyword search tool bar (in upper right, by default) with drop down panels - * for ad hoc searches by list or expression. + * A panel that provides a toolbar button for the dropdown keyword list search + * panel and dropdown single keyword search panel. Displayed in the upper right + * hand corner of the application by default. */ class DropdownToolbar extends javax.swing.JPanel { - private static final Logger logger = Logger.getLogger(DropdownToolbar.class.getName()); - private KeywordPropertyChangeListener listener; - private boolean active = false; + private static final long serialVersionUID = 1L; + private static final Logger LOGGER = Logger.getLogger(DropdownToolbar.class.getName()); private static DropdownToolbar instance; + private SearchSettingsChangeListener searchSettingsChangeListener; + private boolean active = false; private DropdownSingleTermSearchPanel dropPanel = null; - private DropdownToolbar() { - initComponents(); - customizeComponents(); - } - + /** + * Gets the singleton panel that provides a toolbar button for the dropdown + * keyword list search panel and dropdown single keyword search panel. + * Displayed in the upper right hand corner of the application by default. + * + * @return The panel. + */ public synchronized static DropdownToolbar getDefault() { if (instance == null) { instance = new DropdownToolbar(); @@ -60,18 +59,30 @@ class DropdownToolbar extends javax.swing.JPanel { return instance; } + /** + * Constructs a panel that provides a toolbar button for the dropdown + * keyword list search panel and dropdown single keyword search panel. + * Displayed in the upper right hand corner of the application by default. + */ + private DropdownToolbar() { + initComponents(); + customizeComponents(); + } + + /** + * Does additional initialization of the GUI components created by the + * initComponents method. + */ private void customizeComponents() { - listener = new KeywordPropertyChangeListener(); - KeywordSearch.getServer().addServerActionListener(listener); - Case.addPropertyChangeListener(listener); + searchSettingsChangeListener = new SearchSettingsChangeListener(); + KeywordSearch.getServer().addServerActionListener(searchSettingsChangeListener); + Case.addPropertyChangeListener(searchSettingsChangeListener); DropdownListSearchPanel listsPanel = DropdownListSearchPanel.getDefault(); - listsPanel.addSearchButtonActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - listsMenu.setVisible(false); - } + listsPanel.addSearchButtonActionListener((ActionEvent e) -> { + listsMenu.setVisible(false); }); + // Adding border of six to account for menu border listsMenu.setSize(listsPanel.getPreferredSize().width + 6, listsPanel.getPreferredSize().height + 6); listsMenu.add(listsPanel); @@ -120,6 +131,27 @@ class DropdownToolbar extends javax.swing.JPanel { } + private void maybeShowListsPopup(MouseEvent evt) { + if (!active) { + return; + } + if (evt != null && !SwingUtilities.isLeftMouseButton(evt)) { + return; + } + listsMenu.show(listsButton, listsButton.getWidth() - listsMenu.getWidth(), listsButton.getHeight() - 1); + } + + private void maybeShowSearchPopup(MouseEvent evt) { + if (!active) { + return; + } + if (evt != null && !SwingUtilities.isLeftMouseButton(evt)) { + return; + } + searchMenu.show(searchDropButton, searchDropButton.getWidth() - searchMenu.getWidth(), searchDropButton.getHeight() - 1); + } + + /** * 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 @@ -216,13 +248,13 @@ class DropdownToolbar extends javax.swing.JPanel { private javax.swing.JPopupMenu searchMenu; // End of variables declaration//GEN-END:variables - private class KeywordPropertyChangeListener implements PropertyChangeListener { + private class SearchSettingsChangeListener implements PropertyChangeListener { @Override public void propertyChange(PropertyChangeEvent evt) { String changed = evt.getPropertyName(); if (changed.equals(Case.Events.CURRENT_CASE.toString())) { - dropPanel.resetSearchBox(); + dropPanel.clearSearchBox(); setFields(null != evt.getNewValue() && RuntimeProperties.coreComponentsAreActive()); } else if (changed.equals(Server.CORE_EVT)) { final Server.CORE_EVT_STATES state = (Server.CORE_EVT_STATES) evt.getNewValue(); @@ -232,9 +264,9 @@ class DropdownToolbar extends javax.swing.JPanel { final int numIndexedFiles = KeywordSearch.getServer().queryNumIndexedFiles(); KeywordSearch.fireNumIndexedFilesChange(null, numIndexedFiles); } catch (NoOpenCoreException ex) { - logger.log(Level.SEVERE, "Error executing Solr query, {0}", ex); //NON-NLS + LOGGER.log(Level.SEVERE, "Error executing Solr query, {0}", ex); //NON-NLS } catch (KeywordSearchModuleException se) { - logger.log(Level.SEVERE, "Error executing Solr query, {0}", se.getMessage()); //NON-NLS + LOGGER.log(Level.SEVERE, "Error executing Solr query, {0}", se.getMessage()); //NON-NLS } break; case STOPPED: @@ -251,23 +283,4 @@ class DropdownToolbar extends javax.swing.JPanel { } } - private void maybeShowListsPopup(MouseEvent evt) { - if (!active) { - return; - } - if (evt != null && !SwingUtilities.isLeftMouseButton(evt)) { - return; - } - listsMenu.show(listsButton, listsButton.getWidth() - listsMenu.getWidth(), listsButton.getHeight() - 1); - } - - private void maybeShowSearchPopup(MouseEvent evt) { - if (!active) { - return; - } - if (evt != null && !SwingUtilities.isLeftMouseButton(evt)) { - return; - } - searchMenu.show(searchDropButton, searchDropButton.getWidth() - searchMenu.getWidth(), searchDropButton.getHeight() - 1); - } } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalEditListPanel.form b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalEditListPanel.form index c2e3d6430c..c43b029272 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalEditListPanel.form +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalEditListPanel.form @@ -168,7 +168,7 @@ - + @@ -180,8 +180,8 @@ - + @@ -189,19 +189,6 @@
- - - - - - - - - - - - - @@ -215,6 +202,19 @@ + + + + + + + + + + + + + diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalEditListPanel.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalEditListPanel.java index 4935861899..8c8262ecdf 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalEditListPanel.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalEditListPanel.java @@ -1,15 +1,15 @@ /* * Autopsy Forensic Browser - * + * * Copyright 2011-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. @@ -42,18 +42,6 @@ import org.openide.util.NbBundle; import org.sleuthkit.autopsy.corecomponents.OptionsPanel; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.ingest.IngestManager; -import java.awt.Component; -import java.awt.Dimension; -import java.awt.FlowLayout; -import javax.swing.ImageIcon; -import javax.swing.JCheckBox; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JTextField; -import static javax.swing.SwingConstants.CENTER; -import javax.swing.table.DefaultTableCellRenderer; -import org.openide.util.NbBundle.Messages; /** * GlobalEditListPanel widget to manage keywords in lists @@ -75,7 +63,7 @@ class GlobalEditListPanel extends javax.swing.JPanel implements ListSelectionLis } private void customizeComponents() { - newWordButton.setToolTipText((NbBundle.getMessage(this.getClass(), "KeywordSearchEditListPanel.customizeComponents.addWordToolTip"))); + newKeywordsButton.setToolTipText((NbBundle.getMessage(this.getClass(), "KeywordSearchEditListPanel.customizeComponents.addWordToolTip"))); exportButton.setToolTipText(NbBundle.getMessage(this.getClass(), "KeywordSearchEditListPanel.customizeComponents.exportToFile")); saveListButton.setToolTipText(NbBundle.getMessage(this.getClass(), "KeywordSearchEditListPanel.customizeComponents.saveCurrentWIthNewNameToolTip")); deleteWordButton.setToolTipText(NbBundle.getMessage(this.getClass(), "KeywordSearchEditListPanel.customizeComponents.removeSelectedMsg")); @@ -90,7 +78,6 @@ class GlobalEditListPanel extends javax.swing.JPanel implements ListSelectionLis column.setPreferredWidth(((int) (width * 0.90))); } else { column.setPreferredWidth(((int) (width * 0.10))); - column.setCellRenderer(new CheckBoxRenderer()); } } keywordTable.setCellSelectionEnabled(false); @@ -100,7 +87,7 @@ class GlobalEditListPanel extends javax.swing.JPanel implements ListSelectionLis lsm.addListSelectionListener(new ListSelectionListener() { @Override public void valueChanged(ListSelectionEvent e) { - if (lsm.isSelectionEmpty() || currentKeywordList.isLocked() || IngestManager.getInstance().isIngestRunning()) { + if (lsm.isSelectionEmpty() || currentKeywordList.isEditable() || IngestManager.getInstance().isIngestRunning()) { deleteWordButton.setEnabled(false); } else { deleteWordButton.setEnabled(true); @@ -110,18 +97,13 @@ class GlobalEditListPanel extends javax.swing.JPanel implements ListSelectionLis setButtonStates(); - setButtonStates(); - IngestManager.getInstance().addIngestJobEventListener(new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { Object source = evt.getSource(); if (source instanceof String && ((String) source).equals("LOCAL")) { //NON-NLS - EventQueue.invokeLater(new Runnable() { - @Override - public void run() { - setButtonStates(); - } + EventQueue.invokeLater(() -> { + setButtonStates(); }); } } @@ -140,9 +122,9 @@ class GlobalEditListPanel extends javax.swing.JPanel implements ListSelectionLis listOptionsSeparator.setEnabled(canEditList); // items that need an unlocked list w/out ingest running - boolean isListLocked = ((isListSelected == false) || (currentKeywordList.isLocked())); + boolean isListLocked = ((isListSelected == false) || (currentKeywordList.isEditable())); boolean canAddWord = isListSelected && !isIngestRunning && !isListLocked; - newWordButton.setEnabled(canAddWord); + newKeywordsButton.setEnabled(canAddWord); keywordOptionsLabel.setEnabled(canAddWord); keywordOptionsSeparator.setEnabled(canAddWord); deleteListButton.setEnabled(canAddWord); @@ -172,8 +154,8 @@ class GlobalEditListPanel extends javax.swing.JPanel implements ListSelectionLis jScrollPane1 = new javax.swing.JScrollPane(); keywordTable = new javax.swing.JTable(); addKeywordPanel = new javax.swing.JPanel(); - newWordButton = new javax.swing.JButton(); deleteWordButton = new javax.swing.JButton(); + newKeywordsButton = new javax.swing.JButton(); ingestMessagesCheckbox = new javax.swing.JCheckBox(); keywordsLabel = new javax.swing.JLabel(); keywordOptionsLabel = new javax.swing.JLabel(); @@ -197,14 +179,6 @@ class GlobalEditListPanel extends javax.swing.JPanel implements ListSelectionLis keywordTable.getTableHeader().setReorderingAllowed(false); jScrollPane1.setViewportView(keywordTable); - newWordButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/keywordsearch/new16.png"))); // NOI18N - newWordButton.setText(org.openide.util.NbBundle.getMessage(GlobalEditListPanel.class, "KeywordSearchEditListPanel.addWordButton.text")); // NOI18N - newWordButton.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - newWordButtonActionPerformed(evt); - } - }); - deleteWordButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/keywordsearch/delete16.png"))); // NOI18N deleteWordButton.setText(org.openide.util.NbBundle.getMessage(GlobalEditListPanel.class, "KeywordSearchEditListPanel.deleteWordButton.text")); // NOI18N deleteWordButton.addActionListener(new java.awt.event.ActionListener() { @@ -213,12 +187,20 @@ class GlobalEditListPanel extends javax.swing.JPanel implements ListSelectionLis } }); + newKeywordsButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/keywordsearch/new16.png"))); // NOI18N + newKeywordsButton.setText(org.openide.util.NbBundle.getMessage(GlobalEditListPanel.class, "GlobalEditListPanel.newKeywordsButton.text")); // NOI18N + newKeywordsButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + newKeywordsButtonActionPerformed(evt); + } + }); + javax.swing.GroupLayout addKeywordPanelLayout = new javax.swing.GroupLayout(addKeywordPanel); addKeywordPanel.setLayout(addKeywordPanelLayout); addKeywordPanelLayout.setHorizontalGroup( addKeywordPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(addKeywordPanelLayout.createSequentialGroup() - .addComponent(newWordButton) + .addComponent(newKeywordsButton) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(deleteWordButton) .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) @@ -228,8 +210,8 @@ class GlobalEditListPanel extends javax.swing.JPanel implements ListSelectionLis .addGroup(addKeywordPanelLayout.createSequentialGroup() .addGap(0, 0, 0) .addGroup(addKeywordPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(newWordButton) - .addComponent(deleteWordButton)) + .addComponent(deleteWordButton) + .addComponent(newKeywordsButton)) .addGap(72, 72, 72)) ); @@ -350,62 +332,6 @@ class GlobalEditListPanel extends javax.swing.JPanel implements ListSelectionLis ); }// //GEN-END:initComponents - private void newWordButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_newWordButtonActionPerformed - JCheckBox chRegex = new JCheckBox(NbBundle.getMessage(this.getClass(), "KeywordSearchEditListPanel.chRegex.text")); - chRegex.setToolTipText(NbBundle.getMessage(this.getClass(), "KeywordSearchEditListPanel.customizeComponents.kwReToolTip")); - JTextField addWordField = new JTextField(25); - addWordField.setToolTipText(NbBundle.getMessage(this.getClass(), "KeywordSearchEditListPanel.customizeComponents.enterNewWordToolTip")); - - JPanel addKeywordPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - addKeywordPanel.add(new JLabel(NbBundle.getMessage(this.getClass(), "KeywordSearchEditListPanel.addKeyword.message"))); - addKeywordPanel.add(addWordField); - addKeywordPanel.add(chRegex); - - addKeywordPanel.setPreferredSize(new Dimension(250, 80)); - - int result = JOptionPane.showConfirmDialog(null, addKeywordPanel, - NbBundle.getMessage(this.getClass(), "KeywordSearchEditListPanel.addKeyword.title"), - JOptionPane.OK_CANCEL_OPTION); - - if (result == JOptionPane.OK_OPTION) { - String newWord = addWordField.getText().trim(); - boolean isLiteral = !chRegex.isSelected(); - final Keyword keyword = new Keyword(newWord, isLiteral); - - if (newWord.equals("")) { - return; - } else if (currentKeywordList.hasKeyword(keyword)) { - KeywordSearchUtil.displayDialog(NbBundle.getMessage(this.getClass(), "KeywordSearchEditListPanel.newKwTitle"), - NbBundle.getMessage(this.getClass(), "KeywordSearchEditListPanel.addWordButtonAction.kwAlreadyExistsMsg"), KeywordSearchUtil.DIALOG_MESSAGE_TYPE.INFO); - return; - } - - //check if valid - boolean valid = true; - try { - Pattern.compile(newWord); - } catch (PatternSyntaxException ex1) { - valid = false; - } catch (IllegalArgumentException ex2) { - valid = false; - } - if (!valid) { - KeywordSearchUtil.displayDialog(NbBundle.getMessage(this.getClass(), "KeywordSearchEditListPanel.newKwTitle"), - NbBundle.getMessage(this.getClass(), "KeywordSearchEditListPanel.invalidKwMsg"), KeywordSearchUtil.DIALOG_MESSAGE_TYPE.ERROR); - return; - } - - //add & reset checkbox - tableModel.addKeyword(keyword); - XmlKeywordSearchList.getCurrent().addList(currentKeywordList); - firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null); - setFocusOnKeywordTextBox(); - setButtonStates(); - } else { - return; - } - }//GEN-LAST:event_newWordButtonActionPerformed - private void deleteWordButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_deleteWordButtonActionPerformed if (KeywordSearchUtil.displayConfirmDialog(NbBundle.getMessage(this.getClass(), "KeywordSearchEditListPanel.removeKwMsg"), NbBundle.getMessage(this.getClass(), "KeywordSearchEditListPanel.deleteWordButtonActionPerformed.delConfirmMsg"), KeywordSearchUtil.DIALOG_MESSAGE_TYPE.WARN)) { @@ -478,6 +404,100 @@ class GlobalEditListPanel extends javax.swing.JPanel implements ListSelectionLis firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null); }//GEN-LAST:event_deleteListButtonActionPerformed + private void newKeywordsButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_newKeywordsButtonActionPerformed + String keywordsToRedisplay = ""; + AddKeywordsDialog dialog = new AddKeywordsDialog(); + + int goodCount; + int dupeCount; + int badCount = 1; // Default to 1 so we enter the loop the first time + + while(badCount > 0){ + dialog.setInitialKeywordList(keywordsToRedisplay); + dialog.display(); + + goodCount = 0; + dupeCount = 0; + badCount = 0; + keywordsToRedisplay = ""; + + if(!dialog.getKeywords().isEmpty()){ + + + for(String newWord:dialog.getKeywords()){ + if (newWord.isEmpty()) { + continue; + } + + final Keyword keyword = new Keyword(newWord, !dialog.isKeywordRegex(), dialog.isKeywordExact()); + if (currentKeywordList.hasKeyword(keyword)) { + dupeCount++; + continue; + } + + //check if valid + boolean valid = true; + try { + Pattern.compile(newWord); + } catch (PatternSyntaxException ex1) { + valid = false; + } catch (IllegalArgumentException ex2) { + valid = false; + } + if (!valid) { + + // Invalid keywords will reappear in the UI + keywordsToRedisplay += newWord + "\n"; + badCount++; + continue; + } + + // Add the new keyword + tableModel.addKeyword(keyword); + goodCount++; + } + XmlKeywordSearchList.getCurrent().addList(currentKeywordList); + firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null); + + if((badCount > 0) || (dupeCount > 0)){ + // Display the error counts to the user + // The add keywords dialog will pop up again if any were invalid with any + // invalid entries (valid entries and dupes will disappear) + + String summary = ""; + KeywordSearchUtil.DIALOG_MESSAGE_TYPE level = KeywordSearchUtil.DIALOG_MESSAGE_TYPE.INFO; + if(goodCount > 0){ + if(goodCount > 1){ + summary += NbBundle.getMessage(GlobalEditListPanel.class, "GlobalEditListPanel.keywordsAddedPlural.text", goodCount) + "\n"; + } else { + summary += NbBundle.getMessage(GlobalEditListPanel.class, "GlobalEditListPanel.keywordsAdded.text", goodCount) + "\n"; + } + } + if(dupeCount > 0){ + if(dupeCount > 1){ + summary += NbBundle.getMessage(GlobalEditListPanel.class, "GlobalEditListPanel.keywordDupesSkippedPlural.text", dupeCount) + "\n"; + } else { + summary += NbBundle.getMessage(GlobalEditListPanel.class, "GlobalEditListPanel.keywordDupesSkipped.text", dupeCount) + "\n"; + } + level = KeywordSearchUtil.DIALOG_MESSAGE_TYPE.WARN; + } + if(badCount > 0){ + if(badCount > 1){ + summary += NbBundle.getMessage(GlobalEditListPanel.class, "GlobalEditListPanel.keywordErrorsPlural.text", badCount) + "\n"; + } else { + summary += NbBundle.getMessage(GlobalEditListPanel.class, "GlobalEditListPanel.keywordErrors.text", badCount) + "\n"; + } + level = KeywordSearchUtil.DIALOG_MESSAGE_TYPE.ERROR; + } + KeywordSearchUtil.displayDialog(NbBundle.getMessage(this.getClass(), "GlobalEditListPanel.addKeywordResults.text"), + summary, level); + } + } + } + setFocusOnKeywordTextBox(); + setButtonStates(); + }//GEN-LAST:event_newKeywordsButtonActionPerformed + // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JPanel addKeywordPanel; private javax.swing.JButton deleteListButton; @@ -492,7 +512,7 @@ class GlobalEditListPanel extends javax.swing.JPanel implements ListSelectionLis private javax.swing.JPanel listEditorPanel; private javax.swing.JLabel listOptionsLabel; private javax.swing.JSeparator listOptionsSeparator; - private javax.swing.JButton newWordButton; + private javax.swing.JButton newKeywordsButton; private javax.swing.JButton saveListButton; // End of variables declaration//GEN-END:variables @@ -563,11 +583,10 @@ class GlobalEditListPanel extends javax.swing.JPanel implements ListSelectionLis colName = NbBundle.getMessage(this.getClass(), "KeywordSearchEditListPanel.kwColName"); break; case 1: - colName = NbBundle.getMessage(this.getClass(), "KeywordSearchEditListPanel.exportButtonActionPerformed.regExColName"); + colName = NbBundle.getMessage(this.getClass(), "KeywordSearch.typeColLbl"); break; default: ; - } return colName; } @@ -581,10 +600,10 @@ class GlobalEditListPanel extends javax.swing.JPanel implements ListSelectionLis Keyword word = currentKeywordList.getKeywords().get(rowIndex); switch (columnIndex) { case 0: - ret = (Object) word.getQuery(); + ret = word.getSearchTerm(); break; case 1: - ret = (Object) !word.isLiteral(); + ret = word.getSearchTermType(); break; default: logger.log(Level.SEVERE, "Invalid table column index: {0}", columnIndex); //NON-NLS @@ -629,40 +648,10 @@ class GlobalEditListPanel extends javax.swing.JPanel implements ListSelectionLis } } - /** - * A cell renderer for boolean cells that shows a center-aligned green check - * mark if true, nothing if false. - */ - private class CheckBoxRenderer extends DefaultTableCellRenderer { - - private static final long serialVersionUID = 1L; - final ImageIcon theCheck = new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/keywordsearch/checkmark.png")); // NON-NLS - - CheckBoxRenderer() { - setHorizontalAlignment(CENTER); - } - - @Override - @Messages("IsRegularExpression=Keyword is a regular expression") - public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { - - if ((value instanceof Boolean)) { - if ((Boolean) value) { - setIcon(theCheck); - setToolTipText(Bundle.IsRegularExpression()); - } else { - setIcon(null); - setToolTipText(null); - } - } - return this; - } - } - /** * Set the keyboard focus to new keyword textbox. */ void setFocusOnKeywordTextBox() { - newWordButton.requestFocus(); + newKeywordsButton.requestFocus(); } } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalListSettingsPanel.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalListSettingsPanel.java index 48893e213c..bac57fc76d 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalListSettingsPanel.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalListSettingsPanel.java @@ -1,15 +1,15 @@ /* * Autopsy Forensic Browser - * - * Copyright 2011-2014 Basis Technology Corp. + * + * Copyright 2011-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. @@ -76,13 +76,13 @@ final class GlobalListSettingsPanel extends javax.swing.JPanel implements Option JOptionPane.PLAIN_MESSAGE, null, null, - currentKeywordList != null ? currentKeywordList.getName() : ""); + currentKeywordList.getName()); if (listName == null || listName.trim().equals("")) { return; } XmlKeywordSearchList writer = XmlKeywordSearchList.getCurrent(); - if (writer.listExists(listName) && writer.getList(listName).isLocked()) { + if (writer.listExists(listName) && writer.getList(listName).isEditable()) { KeywordSearchUtil.displayDialog(FEATURE_NAME, NbBundle.getMessage(this.getClass(), "KeywordSearchConfigurationPanel1.customizeComponents.noOwDefaultMsg"), KeywordSearchUtil.DIALOG_MESSAGE_TYPE.WARN); return; } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalListsManagementPanel.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalListsManagementPanel.java index 8d04e2a8f6..abc9a0bebd 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalListsManagementPanel.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalListsManagementPanel.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2014 Basis Technology Corp. + * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -40,8 +40,8 @@ class GlobalListsManagementPanel extends javax.swing.JPanel implements OptionsPa private static final long serialVersionUID = 1L; - private Logger logger = Logger.getLogger(GlobalListsManagementPanel.class.getName()); - private KeywordListTableModel tableModel; + private final Logger logger = Logger.getLogger(GlobalListsManagementPanel.class.getName()); + private final KeywordListTableModel tableModel; private final org.sleuthkit.autopsy.keywordsearch.GlobalListSettingsPanel globalListSettingsPanel; GlobalListsManagementPanel(org.sleuthkit.autopsy.keywordsearch.GlobalListSettingsPanel gsp) { @@ -176,7 +176,7 @@ class GlobalListsManagementPanel extends javax.swing.JPanel implements OptionsPa } boolean shouldAdd = false; if (writer.listExists(listName)) { - if (writer.getList(listName).isLocked()) { + if (writer.getList(listName).isEditable()) { boolean replace = KeywordSearchUtil.displayConfirmDialog( NbBundle.getMessage(this.getClass(), "KeywordSearch.newKeywordListMsg"), NbBundle.getMessage(this.getClass(), "KeywordSearchListsManagementPanel.newKeywordListDescription", listName), @@ -345,7 +345,7 @@ class GlobalListsManagementPanel extends javax.swing.JPanel implements OptionsPa private class KeywordListTableModel extends AbstractTableModel { - private XmlKeywordSearchList listsHandle = XmlKeywordSearchList.getCurrent(); + private final XmlKeywordSearchList listsHandle = XmlKeywordSearchList.getCurrent(); @Override public int getColumnCount() { diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/HighlightedText.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/HighlightedText.java index 4e4b55d5f7..02b3391b5f 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/HighlightedText.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/HighlightedText.java @@ -31,6 +31,8 @@ import org.sleuthkit.autopsy.coreutils.Logger; import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.SolrRequest.METHOD; import org.apache.solr.client.solrj.response.QueryResponse; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.coreutils.Version; import org.sleuthkit.autopsy.datamodel.TextMarkupLookup; import org.sleuthkit.autopsy.keywordsearch.KeywordQueryFilter.FilterType; @@ -82,7 +84,7 @@ class HighlightedText implements IndexedText, TextMarkupLookup { //when the results are not known and need to requery to get hits HighlightedText(long objectId, String solrQuery, boolean isRegex, String originalQuery) { - this(objectId, solrQuery, isRegex); + this(objectId, KeywordSearchUtil.quoteQuery(solrQuery), isRegex); this.originalQuery = originalQuery; } @@ -100,6 +102,7 @@ class HighlightedText implements IndexedText, TextMarkupLookup { * The main goal of this method is to figure out which pages / chunks have * hits. */ + @Messages({"HighlightedText.query.exception.msg=Could not perform the query to get chunk info and get highlights:"}) private void loadPageInfo() { if (isPageInfoLoaded) { return; @@ -143,8 +146,9 @@ class HighlightedText implements IndexedText, TextMarkupLookup { chunksQuery.addFilter(new KeywordQueryFilter(FilterType.CHUNK, this.objectId)); try { hits = chunksQuery.performQuery(); - } catch (NoOpenCoreException ex) { - logger.log(Level.INFO, "Could not get chunk info and get highlights", ex); //NON-NLS + } catch (KeywordSearchModuleException | NoOpenCoreException ex) { + logger.log(Level.SEVERE, "Could not perform the query to get chunk info and get highlights:" + keywordQuery.getSearchTerm(), ex); //NON-NLS + MessageNotifyUtil.Notify.error(Bundle.HighlightedText_query_exception_msg() + keywordQuery.getSearchTerm(), ex.getCause().getMessage()); return; } } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Ingester.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Ingester.java index e7325f776c..256d4508f2 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Ingester.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Ingester.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2015 Basis Technology Corp. + * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -43,6 +43,7 @@ import org.sleuthkit.datamodel.File; import org.sleuthkit.datamodel.LayoutFile; import org.sleuthkit.datamodel.LocalFile; import org.sleuthkit.datamodel.ReadContentInputStream; +import org.sleuthkit.datamodel.SlackFile; import org.sleuthkit.datamodel.TskCoreException; /** @@ -59,7 +60,7 @@ class Ingester { //for ingesting chunk as SolrInputDocument (non-content-streaming, by-pass tika) //TODO use a streaming way to add content to /update handler private static final int MAX_DOC_CHUNK_SIZE = 1024 * 1024; - private static final String docContentEncoding = "UTF-8"; //NON-NLS + private static final String ENCODING = "UTF-8"; //NON-NLS private Ingester() { } @@ -134,7 +135,7 @@ class Ingester { //overwrite id with the chunk id params.put(Server.Schema.ID.toString(), - Server.getChunkIdString(sourceContent.getId(), fec.getChunkId())); + Server.getChunkIdString(sourceContent.getId(), fec.getChunkNumber())); ingest(bcs, params, size); } @@ -215,6 +216,13 @@ class Ingester { return params; } + @Override + public Map visit(SlackFile f) { + Map params = getCommonFields(f); + getCommonFileContentFields(params, f); + return params; + } + private Map getCommonFileContentFields(Map params, AbstractFile file) { params.put(Server.Schema.CTIME.toString(), ContentUtils.getStringTimeISO8601(file.getCtime(), file)); params.put(Server.Schema.ATIME.toString(), ContentUtils.getStringTimeISO8601(file.getAtime(), file)); @@ -256,7 +264,6 @@ class Ingester { * @throws org.sleuthkit.autopsy.keywordsearch.Ingester.IngesterException */ void ingest(ContentStream cs, Map fields, final long size) throws IngesterException { - if (fields.get(Server.Schema.IMAGE_ID.toString()) == null) { //skip the file, image id unknown String msg = NbBundle.getMessage(this.getClass(), @@ -298,7 +305,7 @@ class Ingester { if (read != 0) { String s = ""; try { - s = new String(docChunkContentBuf, 0, read, docContentEncoding); + s = new String(docChunkContentBuf, 0, read, ENCODING); // Sanitize by replacing non-UTF-8 characters with caret '^' before adding to index char[] chars = null; for (int i = 0; i < s.length(); i++) { diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Keyword.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Keyword.java index 293244c6f1..0c84da9c73 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Keyword.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Keyword.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2014 Basis Technology Corp. + * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,77 +22,145 @@ import org.openide.util.NbBundle; import org.sleuthkit.datamodel.BlackboardAttribute; /** - * Representation of single keyword to search for + * A representation of a keyword for which to search. The search term for the + * keyword may be either a literal term, to be treated as either a whole word or + * a substring, or a regex. + * + * It is currently possible to optionally associate an artifact attribute type + * with a keyword. This feature was added to support an initial implementation + * of account number search and may be removed in the future. */ class Keyword { - private String keywordString; // keyword to search for - private boolean isLiteral; // false if reg exp - private boolean isWholeword; // false if match a substring - private BlackboardAttribute.ATTRIBUTE_TYPE keywordType = null; + private String searchTerm; + private boolean isLiteral; + private boolean isWholeWord; + private BlackboardAttribute.ATTRIBUTE_TYPE artifactAtrributeType; /** + * Constructs a representation of a keyword for which to search. The search + * term for the keyword may be either a literal term that will be treated as + * a whole word, or a regex. * - * @param query Keyword to search for - * @param isLiteral false if reg exp + * @param searchTerm The search term for the keyword. + * @param isLiteral Whether or not the search term is a literal term that + * will be treated as a whole word, instead of a regex. */ - Keyword(String query, boolean isLiteral) { - this.keywordString = query; + Keyword(String searchTerm, boolean isLiteral) { + this.searchTerm = searchTerm; this.isLiteral = isLiteral; - this.isWholeword = true; + this.isWholeWord = true; } /** + * Constructs a representation of a keyword for which to search. The search + * term may be either a literal term, to be treated as either a whole word + * or as a substring, or a regex. * - * @param query Keyword to search for - * @param isLiteral false if reg exp - * @param isWholeword false to match substring (undefined behavior if regexp - * is true) + * @param searchTerm The search term. + * @param isLiteral Whether or not the search term is a literal term, + * instead of a regex. + * @param isWholeWord Whether or not the search term, if it is a literal + * search term, should be treated as a whole word rather + * than a substring. */ - Keyword(String query, boolean isLiteral, boolean isWholeword) { - this.keywordString = query; + Keyword(String searchTerm, boolean isLiteral, boolean isWholeWord) { + this.searchTerm = searchTerm; this.isLiteral = isLiteral; - this.isWholeword = isWholeword; + this.isWholeWord = isWholeWord; } /** + * Constructs a representation of a keyword for which to search, for the + * purpose of finding a specific artifact attribute. The search term may be + * either a literal term, to be treated as a whole word, or a regex. * - * @param query Keyword to search for - * @param isLiteral false if reg exp - * @param keywordType + * The association of an artifact attribute type with a keyword was added to + * support an initial implementation of account number search and may be + * removed in the future. + * + * @param searchTerm The search term. + * @param isLiteral Whether or not the search term is a literal term, to + * be treated as a whole word, instead of a regex. + * @param keywordType The artifact attribute type. */ - Keyword(String query, boolean isLiteral, BlackboardAttribute.ATTRIBUTE_TYPE keywordType) { - this(query, isLiteral); - this.keywordType = keywordType; - } - - void setType(BlackboardAttribute.ATTRIBUTE_TYPE keywordType) { - this.keywordType = keywordType; - } - - BlackboardAttribute.ATTRIBUTE_TYPE getType() { - return this.keywordType; + Keyword(String searchTerm, boolean isLiteral, BlackboardAttribute.ATTRIBUTE_TYPE artifactAtrributeType) { + this(searchTerm, isLiteral); + this.artifactAtrributeType = artifactAtrributeType; } /** + * Gets the search term for the keyword, which may be either a literal term + * or a regex. * - * @return Keyword to search for + * @return The search term. */ - String getQuery() { - return keywordString; + String getSearchTerm() { + return searchTerm; } - boolean isLiteral() { + /** + * Indicates whether the search term for the keyword is a literal term or a + * regex. + * + * @return True or false. + */ + boolean searchTermIsLiteral() { return isLiteral; } - boolean isWholeword() { - return isWholeword; + /** + * Indicates whether or not the search term for the keyword, if it is a + * literal term and not a regex, will be treated as a whole word or as a + * substring. + * + * @return True or false. + */ + boolean searchTermIsWholeWord() { + return isWholeWord; + } + + String getSearchTermType() { + if (isLiteral) { + if (isWholeWord) { + return NbBundle.getMessage(NewKeywordPanel.class, "NewKeywordPanel.exactButton.text"); + } else { + return NbBundle.getMessage(NewKeywordPanel.class, "NewKeywordPanel.substringButton.text"); + } + } else { + return NbBundle.getMessage(NewKeywordPanel.class, "NewKeywordPanel.regexButton.text"); + } + } + + /** + * Sets the artifact attribute type associated with the keyword, if any. + * + * The association of an artifact attribute type with the keyword was added + * to support an initial implementation of account number search and may be + * removed in the future. + * + * @param artifactAtrributeType + */ + void setArtifactAttributeType(BlackboardAttribute.ATTRIBUTE_TYPE artifactAtrributeType) { + this.artifactAtrributeType = artifactAtrributeType; + } + + /** + * Gets the artifact attribute type associated with the keyword, if any. + * + * The association of an artifact attribute type with the keyword was added + * to support an initial implementation of account number search and may be + * removed in the future. + * + * @return A attribute type object or null. + */ + BlackboardAttribute.ATTRIBUTE_TYPE getArtifactAttributeType() { + return this.artifactAtrributeType; } @Override public String toString() { - return NbBundle.getMessage(this.getClass(), "Keyword.toString.text", keywordString, isLiteral, keywordType); + return String.format("Keyword{searchTerm='%s', isLiteral=%s, isWholeWord=%s}", searchTerm, isLiteral, isWholeWord); } @Override @@ -103,21 +171,19 @@ class Keyword { if (getClass() != obj.getClass()) { return false; } - final Keyword other = (Keyword) obj; - if ((this.keywordString == null) ? (other.keywordString != null) : !this.keywordString.equals(other.keywordString)) { - return false; - } - if (this.isLiteral != other.isLiteral) { - return false; - } - return true; + Keyword other = (Keyword) obj; + return (this.searchTerm.equals(other.getSearchTerm()) + && this.isLiteral == other.searchTermIsLiteral() + && this.isWholeWord == other.searchTermIsWholeWord()); } @Override public int hashCode() { int hash = 7; - hash = 17 * hash + (this.keywordString != null ? this.keywordString.hashCode() : 0); + hash = 17 * hash + this.searchTerm.hashCode(); hash = 17 * hash + (this.isLiteral ? 1 : 0); + hash = 17 * hash + (this.isWholeWord ? 1 : 0); return hash; } + } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordList.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordList.java index 2dd2933fa9..13f094a89d 100755 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordList.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordList.java @@ -1,15 +1,15 @@ /* * Autopsy Forensic Browser - * - * Copyright 2011-2014 Basis Technology Corp. + * + * Copyright 2011-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. @@ -21,32 +21,75 @@ package org.sleuthkit.autopsy.keywordsearch; import java.util.Date; import java.util.List; +/** + * A list of keywords for which to search. Includes list creation and + * modification metadata and settings that determine how the list is to be used + * when ingesting a data source. Standard lists provided by Autopsy may be + * marked as not editable. + */ public class KeywordList { private String name; private Date created; private Date modified; private Boolean useForIngest; - private Boolean ingestMessages; + private Boolean postIngestMessages; private List keywords; - private Boolean locked; + private Boolean isEditable; - KeywordList(String name, Date created, Date modified, Boolean useForIngest, Boolean ingestMessages, List keywords, boolean locked) { + /** + * Constructs a list of keywords for which to search. Includes list creation + * and modification metadata and settings that determine how the list is to + * be used when ingesting a data source. Standard lists provided by Autopsy + * may be marked as not editable. + * + * @param name The name to asociate with the list. + * @param created When the list was created. + * @param modified When the list was last modified. + * @param useForIngest Whether or not the list is to be used when + * ingesting a data source. + * @param postIngestMessages Whether or not to post ingest inbox messages + * when a keyword within the list is found while + * ingesting a data source. + * @param keywords The keywords that make up the list. + * @param isEditable Whether or not the list may be edited by a + * user; standard lists provided by Autopsy should + * not be edited. + */ + KeywordList(String name, Date created, Date modified, Boolean useForIngest, Boolean postIngestMessages, List keywords, boolean isEditable) { this.name = name; this.created = created; this.modified = modified; this.useForIngest = useForIngest; - this.ingestMessages = ingestMessages; + this.postIngestMessages = postIngestMessages; this.keywords = keywords; - this.locked = locked; + this.isEditable = isEditable; } + /** + * Constructs a list of keywords for which to search. Includes list creation + * and modification metadata and settings that determine how the list is to + * be used when ingesting a data source. The list will be marked as a + * standard lists provided by Autopsy that should not be treated as + * editable. + * + * @param name The name to asociate with the list. + * @param created When the list was created. + * @param modified When the list was last modified. + * @param useForIngest Whether or not the list is to be used when + * ingesting a data source. + * @param postIngestMessages Whether or not to post ingest inbox messages + * when a keyword within the list is found while + * ingesting a data source. + * @param keywords The keywords that make up the list. + */ KeywordList(String name, Date created, Date modified, Boolean useForIngest, Boolean ingestMessages, List keywords) { this(name, created, modified, useForIngest, ingestMessages, keywords, false); } /** - * Create an unnamed list. Usually used for ad-hoc searches + * Constructs a temporary list of keywords to be used for ad hoc keyword + * search and then discarded. * * @param keywords */ @@ -54,74 +97,116 @@ public class KeywordList { this("", new Date(0), new Date(0), false, false, keywords, false); } - @Override - public boolean equals(Object obj) { - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - final KeywordList other = (KeywordList) obj; - if ((this.name == null) ? (other.name != null) : !this.name.equals(other.name)) { - return false; - } - return true; - } - - @Override - public int hashCode() { - int hash = 5; - return hash; - } - + /** + * Get the name assigned to the keyword list. + * + * @return The list name. + */ String getName() { return name; } + /** + * Gets the date the keyword list was created. + * + * @return The date. + */ Date getDateCreated() { return created; } + /** + * Gets the date the keyword list was last modified. + * + * @return The date. + */ Date getDateModified() { return modified; } + /** + * Gets whether or not the list should be used when ingesting a data source. + * + * @return True or false. + */ Boolean getUseForIngest() { return useForIngest; } - void setUseForIngest(boolean use) { - this.useForIngest = use; + /** + * Sets whether or not the list should be used when ingesting a data source. + * + * @param useForIngest True or false. + */ + void setUseForIngest(boolean useForIngest) { + this.useForIngest = useForIngest; } + /** + * Gets whether or not to post ingest inbox messages when a keyword within + * the list is found while ingesting a data source. + * + * @return true or false + */ Boolean getIngestMessages() { - return ingestMessages; + return postIngestMessages; } - void setIngestMessages(boolean ingestMessages) { - this.ingestMessages = ingestMessages; + /** + * Sets whether or not to post ingest inbox messages when a keyword within + * the list is found while ingesting a data source. + * + * @param postIngestMessages True or false. + */ + void setIngestMessages(boolean postIngestMessages) { + this.postIngestMessages = postIngestMessages; } + /** + * Gets the keywords included in the list + * + * @return A colleciton of Keyword objects. + */ List getKeywords() { return keywords; } + /** + * Indicates whether or not a given keyword is included in the list. + * + * @param keyword The keyword of interest. + * + * @return + */ boolean hasKeyword(Keyword keyword) { return keywords.contains(keyword); } - boolean hasKeyword(String keyword) { - //note, this ignores isLiteral - for (Keyword k : keywords) { - if (k.getQuery().equals(keyword)) { + /** + * Indicates whether or not a given search term is included in the list. + * + * @param searchTerm The search term. + * + * @return True or false. + */ + boolean hasSearchTerm(String searchTerm) { + for (Keyword word : keywords) { + if (word.getSearchTerm().equals(searchTerm)) { return true; } } return false; } - Boolean isLocked() { - return locked; + /** + * Indicates Whether or not the list should be editable by a user; standard + * lists provided by Autopsy should be marked as not editable when they are + * contructed. + * + * @return True or false. + */ + Boolean isEditable() { + return isEditable; } + } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearch.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearch.java index 4513aca040..f491d1696e 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearch.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearch.java @@ -22,7 +22,6 @@ import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.IOException; -import java.nio.file.Paths; import java.util.logging.FileHandler; import java.util.logging.Level; import java.util.logging.Logger; @@ -136,8 +135,7 @@ public class KeywordSearch { Thread.sleep(2000); server.closeCore(); } catch (Exception ex) { - String caseId = Paths.get(closedCase.getCaseDirectory(), closedCase.getName()).toString(); - logger.log(Level.SEVERE, String.format("Failed to close core for %s", caseId), ex); //NON-NLS + logger.log(Level.SEVERE, String.format("Failed to close core for %s", closedCase.getCaseDirectory()), ex); //NON-NLS if (RuntimeProperties.coreComponentsAreActive()) { MessageNotifyUtil.Notify.error(NbBundle.getMessage(KeywordSearch.class, "KeywordSearch.closeCore.notification.msg"), ex.getMessage()); } @@ -152,8 +150,7 @@ public class KeywordSearch { try { server.openCoreForCase(openedCase); } catch (Exception ex) { - String caseId = Paths.get(openedCase.getCaseDirectory(), openedCase.getName()).toString(); - logger.log(Level.SEVERE, String.format("Failed to open or create core for %s", caseId), ex); //NON-NLS + logger.log(Level.SEVERE, String.format("Failed to open or create core for %s", openedCase.getCaseDirectory()), ex); //NON-NLS if (RuntimeProperties.coreComponentsAreActive()) { MessageNotifyUtil.Notify.error(NbBundle.getMessage(KeywordSearch.class, "KeywordSearch.openCore.notification.msg"), ex.getMessage()); } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchGlobalSettingsPanel.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchGlobalSettingsPanel.java index a88b11582b..1756bfc1ec 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchGlobalSettingsPanel.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchGlobalSettingsPanel.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2014 Basis Technology Corp. + * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchList.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchList.java index e8fb196310..01f0e146f0 100755 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchList.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchList.java @@ -46,7 +46,7 @@ abstract class KeywordSearchList { private static final String CCN_REGEX = ".*[3456]([ -]?\\d){11,18}.*"; //12-19 digits, with possible single spaces or dashes in between. first digit is 3,4,5, or 6 //NON-NLS protected String filePath; - Map theLists; //the keyword data + Map theLists; //the keyword data PropertyChangeSupport changeSupport; protected List lockedLists; @@ -141,7 +141,7 @@ abstract class KeywordSearchList { //we want to preserve state of locked lists List toClear = new ArrayList<>(); for (String list : theLists.keySet()) { - if (theLists.get(list).isLocked() == false) { + if (theLists.get(list).isEditable() == false) { toClear.add(list); } } @@ -173,7 +173,7 @@ abstract class KeywordSearchList { public List getListsL(boolean locked) { List ret = new ArrayList<>(); for (KeywordList list : theLists.values()) { - if (list.isLocked().equals(locked)) { + if (list.isEditable().equals(locked)) { ret.add(list); } } @@ -200,7 +200,7 @@ abstract class KeywordSearchList { ArrayList lists = new ArrayList<>(); for (String listName : theLists.keySet()) { KeywordList list = theLists.get(listName); - if (locked == list.isLocked()) { + if (locked == list.isEditable()) { lists.add(listName); } } @@ -218,7 +218,7 @@ abstract class KeywordSearchList { public KeywordList getListWithKeyword(String keyword) { KeywordList found = null; for (KeywordList list : theLists.values()) { - if (list.hasKeyword(keyword)) { + if (list.hasSearchTerm(keyword)) { found = list; break; } @@ -246,7 +246,7 @@ abstract class KeywordSearchList { int numLists = 0; for (String listName : theLists.keySet()) { KeywordList list = theLists.get(listName); - if (locked == list.isLocked()) { + if (locked == list.isEditable()) { ++numLists; } } @@ -320,7 +320,7 @@ abstract class KeywordSearchList { } boolean addList(String name, List newList, boolean useForIngest, boolean ingestMessages) { - //make sure that the list is readded as a locked/built in list + //make sure that the list is readded as a locked/built in list boolean isLocked = this.lockedLists.contains(name); return addList(name, newList, useForIngest, ingestMessages, isLocked); } @@ -330,7 +330,7 @@ abstract class KeywordSearchList { } boolean addList(KeywordList list) { - return addList(list.getName(), list.getKeywords(), list.getUseForIngest(), list.getIngestMessages(), list.isLocked()); + return addList(list.getName(), list.getKeywords(), list.getUseForIngest(), list.getIngestMessages(), list.isEditable()); } /** @@ -437,7 +437,7 @@ abstract class KeywordSearchList { */ boolean deleteList(String name) { KeywordList delList = getList(name); - if (delList != null && !delList.isLocked()) { + if (delList != null && !delList.isEditable()) { theLists.remove(name); } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchQuery.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchQuery.java index 2073a25df3..34e530956a 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchQuery.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchQuery.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011 Basis Technology Corp. + * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,11 +18,8 @@ */ package org.sleuthkit.autopsy.keywordsearch; -import org.sleuthkit.datamodel.AbstractFile; - /** - * Interface for a search query. Implemented by various engines or methods of - * using the same engine. One of these is created for each query. + * Interface for kewyord search queries. */ interface KeywordSearchQuery { @@ -33,17 +30,18 @@ interface KeywordSearchQuery { * * @return true if the query passed validation */ - public boolean validate(); + boolean validate(); /** * execute query and return results without publishing them return results * for all matching terms * + * @throws KeywordSearchModuleException error while executing Solr term query * @throws NoOpenCoreException if query failed due to server error, this * could be a notification to stop processing * @return */ - public QueryResults performQuery() throws NoOpenCoreException; + QueryResults performQuery() throws KeywordSearchModuleException, NoOpenCoreException; /** * Set an optional filter to narrow down the search Adding multiple filters @@ -51,14 +49,14 @@ interface KeywordSearchQuery { * * @param filter filter to set on the query */ - public void addFilter(KeywordQueryFilter filter); + void addFilter(KeywordQueryFilter filter); /** * Set an optional SOLR field to narrow down the search * * @param field field to set on the query */ - public void setField(String field); + void setField(String field); /** * Modify the query string to be searched as a substring instead of a whole @@ -66,39 +64,39 @@ interface KeywordSearchQuery { * * @param isSubstring */ - public void setSubstringQuery(); + void setSubstringQuery(); /** * escape the query string and use the escaped string in the query */ - public void escape(); + void escape(); /** * * @return true if query was escaped */ - public boolean isEscaped(); + boolean isEscaped(); /** * * @return true if query is a literal query (non regex) */ - public boolean isLiteral(); + boolean isLiteral(); /** * return original keyword/query string * * @return the query String supplied originally */ - public String getQueryString(); + String getQueryString(); /** * return escaped keyword/query string if escaping was done * * @return the escaped query string, or original string if no escaping done */ - public String getEscapedQueryString(); + String getEscapedQueryString(); - public KeywordCachedArtifact writeSingleFileHitsToBlackBoard(String termHit, KeywordHit hit, String snippet, String listName); + KeywordCachedArtifact writeSingleFileHitsToBlackBoard(String termHit, KeywordHit hit, String snippet, String listName); } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchQueryDelegator.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchQueryDelegator.java index 8d73afad65..a6713d3145 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchQueryDelegator.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchQueryDelegator.java @@ -29,6 +29,7 @@ import org.openide.nodes.Children; import org.openide.nodes.Node; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.corecomponents.DataResultTopComponent; +import org.sleuthkit.autopsy.corecomponents.TableFilterNode; import org.sleuthkit.autopsy.coreutils.Logger; /** @@ -55,20 +56,20 @@ class KeywordSearchQueryDelegator { for (KeywordList keywordList : keywordLists) { for (Keyword keyword : keywordList.getKeywords()) { KeywordSearchQuery query; - if (keyword.isLiteral()) { + if (keyword.searchTermIsLiteral()) { // literal, exact match - if (keyword.isWholeword()) { + if (keyword.searchTermIsWholeWord()) { query = new LuceneQuery(keywordList, keyword); query.escape(); } // literal, substring match else { - query = new TermComponentQuery(keywordList, keyword); + query = new TermsComponentQuery(keywordList, keyword); query.escape(); query.setSubstringQuery(); } } // regexp else { - query = new TermComponentQuery(keywordList, keyword); + query = new TermsComponentQuery(keywordList, keyword); } queryDelegates.add(query); } @@ -108,7 +109,8 @@ class KeywordSearchQueryDelegator { final String pathText = NbBundle.getMessage(this.getClass(), "KeywordSearchQueryManager.pathText.text"); - DataResultTopComponent.initInstance(pathText, rootNode, queryRequests.size(), searchResultWin); + DataResultTopComponent.initInstance(pathText, new TableFilterNode(rootNode, true, KeywordSearch.class.getName()), + queryRequests.size(), searchResultWin); searchResultWin.requestActive(); } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchResultFactory.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchResultFactory.java index 835fe539ee..d2c5592219 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchResultFactory.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchResultFactory.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2015 Basis Technology Corp. + * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,19 +25,19 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; -import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.logging.Level; import javax.swing.SwingWorker; import org.netbeans.api.progress.ProgressHandle; import org.openide.nodes.ChildFactory; import org.openide.nodes.Children; import org.openide.nodes.Node; -import org.openide.util.Cancellable; import org.openide.util.NbBundle; import org.openide.util.lookup.Lookups; import org.sleuthkit.autopsy.corecomponents.DataResultTopComponent; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.datamodel.AbstractAbstractFileNode; import org.sleuthkit.autopsy.datamodel.AbstractFsContentNode; import org.sleuthkit.autopsy.datamodel.KeyValue; @@ -80,13 +80,11 @@ class KeywordSearchResultFactory extends ChildFactory { } }, } - private Collection queryRequests; - private final DataResultTopComponent viewer; //viewer driving this child node factory + private final Collection queryRequests; private static final Logger logger = Logger.getLogger(KeywordSearchResultFactory.class.getName()); KeywordSearchResultFactory(Collection queryRequests, DataResultTopComponent viewer) { this.queryRequests = queryRequests; - this.viewer = viewer; } /** @@ -142,6 +140,7 @@ class KeywordSearchResultFactory extends ChildFactory { * * @return */ + @NbBundle.Messages({"KeywordSearchResultFactory.query.exception.msg=Could not perform the query "}) private boolean createFlatKeys(QueryRequest queryRequest, List toPopulate) { /** * Check the validity of the requested query. @@ -158,10 +157,11 @@ class KeywordSearchResultFactory extends ChildFactory { QueryResults queryResults; try { queryResults = keywordSearchQuery.performQuery(); - } catch (NoOpenCoreException ex) { + } catch (KeywordSearchModuleException | NoOpenCoreException ex) { logger.log(Level.SEVERE, "Could not perform the query " + keywordSearchQuery.getQueryString(), ex); //NON-NLS + MessageNotifyUtil.Notify.error(Bundle.KeywordSearchResultFactory_query_exception_msg() + keywordSearchQuery.getQueryString(), ex.getCause().getMessage()); return false; - } + } int id = 0; List tempList = new ArrayList<>(); @@ -194,7 +194,8 @@ class KeywordSearchResultFactory extends ChildFactory { if (hit.isArtifactHit()) { name = hit.getArtifact().getDisplayName() + " Artifact"; // NON-NLS } - tempList.add(new KeyValueQueryContent(name, properties, ++id, hit.getSolrObjectId(), content, highlightQueryEscaped, keywordSearchQuery, queryResults)); + ++id; + tempList.add(new KeyValueQueryContent(name, properties, id, hit.getSolrObjectId(), content, highlightQueryEscaped, keywordSearchQuery, queryResults)); } // Add all the nodes to toPopulate at once. Minimizes node creation @@ -220,7 +221,7 @@ class KeywordSearchResultFactory extends ChildFactory { * @return A consistent collection of keyword hits */ Collection getOneHitPerObject(QueryResults queryResults) { - HashMap hits = new HashMap(); + HashMap hits = new HashMap<>(); for (Keyword keyWord : queryResults.getKeywords()) { for (KeywordHit hit : queryResults.getResults(keyWord)) { // add hit with lowest SolrObjectID-Chunk-ID combination. @@ -255,8 +256,8 @@ class KeywordSearchResultFactory extends ChildFactory { //the query is executed later on demand if (queryResults.getKeywords().size() == 1) { //simple case, no need to process subqueries and do special escaping - Keyword term = queryResults.getKeywords().iterator().next(); - return constructEscapedSolrQuery(term.getQuery(), literal_query); + Keyword keyword = queryResults.getKeywords().iterator().next(); + return constructEscapedSolrQuery(keyword.getSearchTerm(), literal_query); } else { //find terms for this content hit List hitTerms = new ArrayList<>(); @@ -274,7 +275,7 @@ class KeywordSearchResultFactory extends ChildFactory { int curTerm = 0; for (Keyword term : hitTerms) { //escape subqueries, MAKE SURE they are not escaped again later - highlightQuery.append(constructEscapedSolrQuery(term.getQuery(), literal_query)); + highlightQuery.append(constructEscapedSolrQuery(term.getSearchTerm(), literal_query)); if (lastTerm != curTerm) { highlightQuery.append(" "); //acts as OR || } @@ -308,7 +309,7 @@ class KeywordSearchResultFactory extends ChildFactory { @Override protected Node createNodeForKey(KeyValueQueryContent key) { final Content content = key.getContent(); - final String queryStr = key.getQueryStr();; + final String queryStr = key.getQueryStr(); QueryResults hits = key.getHits(); Node kvNode = new KeyValueNode(key, Children.LEAF, Lookups.singleton(content)); @@ -326,10 +327,10 @@ class KeywordSearchResultFactory extends ChildFactory { class KeyValueQueryContent extends KeyValue { private long solrObjectId; - private Content content; - private String queryStr; - private QueryResults hits; - private KeywordSearchQuery query; + private final Content content; + private final String queryStr; + private final QueryResults hits; + private final KeywordSearchQuery query; /** * NOTE Parameters are defined based on how they are currently used in @@ -378,32 +379,21 @@ class KeywordSearchResultFactory extends ChildFactory { */ static class BlackboardResultWriter extends SwingWorker { - private static List writers = new ArrayList<>(); - //lock utilized to enqueue writers and limit execution to 1 at a time - private static final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(true); //use fairness policy - //private static final Lock writerLock = rwLock.writeLock(); + private static final List writers = new ArrayList<>(); private ProgressHandle progress; - private KeywordSearchQuery query; - private String listName; - private QueryResults hits; + private final KeywordSearchQuery query; + private final QueryResults hits; private Collection newArtifacts = new ArrayList<>(); private static final int QUERY_DISPLAY_LEN = 40; BlackboardResultWriter(QueryResults hits, String listName) { this.hits = hits; this.query = hits.getQuery(); - this.listName = listName; } protected void finalizeWorker() { deregisterWriter(this); - - EventQueue.invokeLater(new Runnable() { - @Override - public void run() { - progress.finish(); - } - }); + EventQueue.invokeLater(progress::finish); } @Override @@ -411,35 +401,24 @@ class KeywordSearchResultFactory extends ChildFactory { registerWriter(this); //register (synchronized on class) outside of writerLock to prevent deadlock final String queryStr = query.getQueryString(); final String queryDisp = queryStr.length() > QUERY_DISPLAY_LEN ? queryStr.substring(0, QUERY_DISPLAY_LEN - 1) + " ..." : queryStr; - //block until previous writer is done - //writerLock.lock(); - try { - progress = ProgressHandle.createHandle( - NbBundle.getMessage(this.getClass(), "KeywordSearchResultFactory.progress.saving", queryDisp), new Cancellable() { - @Override - public boolean cancel() { - return BlackboardResultWriter.this.cancel(true); - } - }); - - // Create blackboard artifacts + progress = ProgressHandle.createHandle(NbBundle.getMessage(this.getClass(), "KeywordSearchResultFactory.progress.saving", queryDisp), () -> BlackboardResultWriter.this.cancel(true)); newArtifacts = hits.writeAllHitsToBlackBoard(progress, null, this, false); } finally { finalizeWorker(); } - return null; } @Override protected void done() { try { - // test if any exceptions were thrown get(); - } catch (InterruptedException | ExecutionException ex) { - logger.log(Level.SEVERE, "Error querying ", ex); //NON-NLS - } + } catch (InterruptedException | CancellationException ex) { + logger.log(Level.WARNING, "User cancelled writing of ad hoc search query results for '{0}' to the blackboard", query.getQueryString()); //NON-NLS + } catch (ExecutionException ex) { + logger.log(Level.SEVERE, "Error writing of ad hoc search query results for " + query.getQueryString() + " to the blackboard", ex); //NON-NLS + } } private static synchronized void registerWriter(BlackboardResultWriter writer) { diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/LuceneQuery.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/LuceneQuery.java index 0242d57027..752bfaec64 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/LuceneQuery.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/LuceneQuery.java @@ -32,11 +32,9 @@ import org.apache.solr.client.solrj.SolrRequest.METHOD; import org.apache.solr.client.solrj.response.QueryResponse; import org.apache.solr.common.SolrDocument; import org.apache.solr.common.SolrDocumentList; -import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.coreutils.EscapeUtil; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.coreutils.Version; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE; @@ -55,7 +53,7 @@ class LuceneQuery implements KeywordSearchQuery { private final String keywordString; //original unescaped query private String keywordStringEscaped; private boolean isEscaped; - private Keyword keywordQuery = null; + private Keyword keyword = null; private KeywordList keywordList = null; private final List filters = new ArrayList<>(); private String field = null; @@ -72,15 +70,15 @@ class LuceneQuery implements KeywordSearchQuery { /** * Constructor with query to process. * - * @param keywordQuery + * @param keyword */ - public LuceneQuery(KeywordList keywordList, Keyword keywordQuery) { + public LuceneQuery(KeywordList keywordList, Keyword keyword) { this.keywordList = keywordList; - this.keywordQuery = keywordQuery; + this.keyword = keyword; // @@@ BC: Long-term, we should try to get rid of this string and use only the // keyword object. Refactoring did not make its way through this yet. - this.keywordString = keywordQuery.getQuery(); + this.keywordString = keyword.getSearchTerm(); this.keywordStringEscaped = this.keywordString; } @@ -128,7 +126,7 @@ class LuceneQuery implements KeywordSearchQuery { } @Override - public QueryResults performQuery() throws NoOpenCoreException { + public QueryResults performQuery() throws KeywordSearchModuleException, NoOpenCoreException { QueryResults results = new QueryResults(this, keywordList); //in case of single term literal query there is only 1 term boolean showSnippets = KeywordSearchSettings.getShowSnippets(); @@ -168,8 +166,8 @@ class LuceneQuery implements KeywordSearchQuery { //bogus - workaround the dir tree table issue //attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_KEYWORD_REGEXP.getTypeID(), MODULE_NAME, "", "")); //selector - if (keywordQuery != null) { - BlackboardAttribute.ATTRIBUTE_TYPE selType = keywordQuery.getType(); + if (keyword != null) { + BlackboardAttribute.ATTRIBUTE_TYPE selType = keyword.getArtifactAttributeType(); if (selType != null) { attributes.add(new BlackboardAttribute(selType, MODULE_NAME, termHit)); } @@ -199,7 +197,7 @@ class LuceneQuery implements KeywordSearchQuery { * * @throws NoOpenCoreException */ - private List performLuceneQuery(boolean snippets) throws NoOpenCoreException { + private List performLuceneQuery(boolean snippets) throws KeywordSearchModuleException, NoOpenCoreException { List matches = new ArrayList<>(); boolean allMatchesFetched = false; final Server solrServer = KeywordSearch.getServer(); @@ -208,23 +206,13 @@ class LuceneQuery implements KeywordSearchQuery { QueryResponse response; SolrDocumentList resultList; Map>> highlightResponse; - Set uniqueSolrDocumentsWithHits; - try { - response = solrServer.query(q, METHOD.POST); + response = solrServer.query(q, METHOD.POST); - resultList = response.getResults(); + resultList = response.getResults(); - // objectId_chunk -> "text" -> List of previews - highlightResponse = response.getHighlighting(); - - // get the unique set of files with hits - uniqueSolrDocumentsWithHits = filterOneHitPerDocument(resultList); - } catch (KeywordSearchModuleException ex) { - logger.log(Level.SEVERE, "Error executing Lucene Solr Query: " + keywordString, ex); //NON-NLS - MessageNotifyUtil.Notify.error(NbBundle.getMessage(Server.class, "Server.query.exception.msg", keywordString), ex.getCause().getMessage()); - return matches; - } + // objectId_chunk -> "text" -> List of previews + highlightResponse = response.getHighlighting(); // cycle through results in sets of MAX_RESULTS for (int start = 0; !allMatchesFetched; start = start + MAX_RESULTS) { @@ -239,8 +227,7 @@ class LuceneQuery implements KeywordSearchQuery { //no case open, must be just closed return matches; } - - for (SolrDocument resultDoc : uniqueSolrDocumentsWithHits) { + for (SolrDocument resultDoc : resultList) { KeywordHit contentHit; try { contentHit = createKeywordtHit(resultDoc, highlightResponse, sleuthkitCase); @@ -276,7 +263,7 @@ class LuceneQuery implements KeywordSearchQuery { q.setRows(MAX_RESULTS); q.setFields(Server.Schema.ID.toString()); - + q.addSort(Server.Schema.ID.toString(), SolrQuery.ORDER.asc); for (KeywordQueryFilter filter : filters) { q.addFilterQuery(filter.toString()); } @@ -305,36 +292,6 @@ class LuceneQuery implements KeywordSearchQuery { return q; } - /** - * Create the minimum set of documents. Ignores chunk IDs. Only one hit per - * file in results. - * - * @param resultList - * - * @return - */ - private Set filterOneHitPerDocument(SolrDocumentList resultList) { - // sort the list so that we consistently pick the same chunk each time. - // note this sort is doing a string comparison and not an integer comparison, so - // chunk 10 will be smaller than chunk 9. - Collections.sort(resultList, new Comparator() { - @Override - public int compare(SolrDocument left, SolrDocument right) { - // ID is in the form of ObjectId_Chunk - String leftID = left.getFieldValue(Server.Schema.ID.toString()).toString(); - String rightID = right.getFieldValue(Server.Schema.ID.toString()).toString(); - return leftID.compareTo(rightID); - } - }); - - // NOTE: We could probably just iterate through the list and compare each ID with the - // previous ID to get the unique documents faster than using this set now that the list - // is sorted. - Set solrDocumentsWithMatches = new TreeSet<>(new SolrDocumentComparatorIgnoresChunkId()); - solrDocumentsWithMatches.addAll(resultList); - return solrDocumentsWithMatches; - } - private KeywordHit createKeywordtHit(SolrDocument solrDoc, Map>> highlightResponse, SleuthkitCase caseDb) throws TskException { /** * Get the first snippet from the document if keyword search is diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/NewKeywordPanel.form b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/NewKeywordPanel.form new file mode 100755 index 0000000000..2c72c5055c --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/NewKeywordPanel.form @@ -0,0 +1,108 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/NewKeywordPanel.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/NewKeywordPanel.java new file mode 100755 index 0000000000..f96738be67 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/NewKeywordPanel.java @@ -0,0 +1,142 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2011-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.keywordsearch; + +/** + * A panel that is contained in NewKeywordDialog used when the user wants to add + * a new keyword to a list via the options panel. This panel allows the user to + * indicate whether they want the keyword to be an exact match, a substring, or + * a regular expression. + */ +class NewKeywordPanel extends javax.swing.JPanel { + + /** + * Creates new form NewKeywordPanel + */ + public NewKeywordPanel() { + initComponents(); + } + + /** + * @return the text for the new keyword + */ + String getKeywordText() { + return keywordTextField.getText(); + } + + /** + * @return true if the regular expression radio button is selected + */ + boolean isKeywordRegex() { + return regexButton.isSelected(); + } + + /** + * @return true if the exact match radio button is selected + */ + boolean isKeywordExact() { + return exactButton.isSelected(); + } + + /** + * 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() { + + keywordTypeButtonGroup = new javax.swing.ButtonGroup(); + keywordTextField = new javax.swing.JTextField(); + exactButton = new javax.swing.JRadioButton(); + substringButton = new javax.swing.JRadioButton(); + regexButton = new javax.swing.JRadioButton(); + newKeywordLabel = new javax.swing.JLabel(); + + keywordTextField.setText(org.openide.util.NbBundle.getMessage(NewKeywordPanel.class, "NewKeywordPanel.keywordTextField.text")); // NOI18N + keywordTextField.addAncestorListener(new javax.swing.event.AncestorListener() { + public void ancestorMoved(javax.swing.event.AncestorEvent evt) { + } + public void ancestorAdded(javax.swing.event.AncestorEvent evt) { + keywordTextFieldAncestorAdded(evt); + } + public void ancestorRemoved(javax.swing.event.AncestorEvent evt) { + } + }); + + keywordTypeButtonGroup.add(exactButton); + exactButton.setSelected(true); + org.openide.awt.Mnemonics.setLocalizedText(exactButton, org.openide.util.NbBundle.getMessage(NewKeywordPanel.class, "NewKeywordPanel.exactButton.text")); // NOI18N + + keywordTypeButtonGroup.add(substringButton); + org.openide.awt.Mnemonics.setLocalizedText(substringButton, org.openide.util.NbBundle.getMessage(NewKeywordPanel.class, "NewKeywordPanel.substringButton.text")); // NOI18N + + keywordTypeButtonGroup.add(regexButton); + org.openide.awt.Mnemonics.setLocalizedText(regexButton, org.openide.util.NbBundle.getMessage(NewKeywordPanel.class, "NewKeywordPanel.regexButton.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(newKeywordLabel, org.openide.util.NbBundle.getMessage(NewKeywordPanel.class, "NewKeywordPanel.newKeywordLabel.text")); // NOI18N + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(exactButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(substringButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(regexButton)) + .addComponent(newKeywordLabel) + .addComponent(keywordTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 305, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(newKeywordLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(keywordTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(exactButton) + .addComponent(substringButton) + .addComponent(regexButton)) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + }// //GEN-END:initComponents + + private void keywordTextFieldAncestorAdded(javax.swing.event.AncestorEvent evt) {//GEN-FIRST:event_keywordTextFieldAncestorAdded + evt.getComponent().requestFocusInWindow(); + }//GEN-LAST:event_keywordTextFieldAncestorAdded + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JRadioButton exactButton; + private javax.swing.JTextField keywordTextField; + private javax.swing.ButtonGroup keywordTypeButtonGroup; + private javax.swing.JLabel newKeywordLabel; + private javax.swing.JRadioButton regexButton; + private javax.swing.JRadioButton substringButton; + // End of variables declaration//GEN-END:variables +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/QueryResults.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/QueryResults.java index d3e9bfb912..316e4f3717 100755 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/QueryResults.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/QueryResults.java @@ -115,7 +115,7 @@ class QueryResults { for (final Keyword keyword : getKeywords()) { if (worker.isCancelled()) { - logger.log(Level.INFO, "Cancel detected, bailing before new keyword processed: {0}", keyword.getQuery()); //NON-NLS + logger.log(Level.INFO, "Cancel detected, bailing before new keyword processed: {0}", keyword.getSearchTerm()); //NON-NLS break; } @@ -124,7 +124,7 @@ class QueryResults { progress.progress(keyword.toString(), unitProgress); } if (subProgress != null) { - String hitDisplayStr = keyword.getQuery(); + String hitDisplayStr = keyword.getSearchTerm(); if (hitDisplayStr.length() > 50) { hitDisplayStr = hitDisplayStr.substring(0, 49) + "..."; } @@ -132,7 +132,7 @@ class QueryResults { } for (KeywordHit hit : getOneHitPerObject(keyword)) { - String termString = keyword.getQuery(); + String termString = keyword.getSearchTerm(); final String snippetQuery = KeywordSearchUtil.escapeLuceneQuery(termString); String snippet; try { diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/SearchRunner.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/SearchRunner.java index 3c234c4fa3..e41a03554a 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/SearchRunner.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/SearchRunner.java @@ -37,7 +37,9 @@ import org.netbeans.api.progress.aggregate.AggregateProgressHandle; import org.netbeans.api.progress.aggregate.ProgressContributor; import org.openide.util.Cancellable; import org.openide.util.NbBundle; +import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.coreutils.StopWatch; import org.sleuthkit.autopsy.ingest.IngestMessage; import org.sleuthkit.autopsy.ingest.IngestServices; @@ -383,6 +385,7 @@ public final class SearchRunner { } @Override + @Messages("SearchRunner.query.exception.msg=Error performing query:") protected Object doInBackground() throws Exception { final String displayName = NbBundle.getMessage(this.getClass(), "KeywordSearchIngestModule.doInBackGround.displayName") + (finalRun ? (" - " + NbBundle.getMessage(this.getClass(), "KeywordSearchIngestModule.doInBackGround.finalizeMsg")) : ""); @@ -403,7 +406,7 @@ public final class SearchRunner { ProgressContributor[] subProgresses = new ProgressContributor[keywords.size()]; int i = 0; for (Keyword keywordQuery : keywords) { - subProgresses[i] = AggregateProgressFactory.createProgressContributor(keywordQuery.getQuery()); + subProgresses[i] = AggregateProgressFactory.createProgressContributor(keywordQuery.getSearchTerm()); progressGroup.addContributor(subProgresses[i]); i++; } @@ -419,11 +422,11 @@ public final class SearchRunner { for (Keyword keywordQuery : keywords) { if (this.isCancelled()) { - logger.log(Level.INFO, "Cancel detected, bailing before new keyword processed: {0}", keywordQuery.getQuery()); //NON-NLS + logger.log(Level.INFO, "Cancel detected, bailing before new keyword processed: {0}", keywordQuery.getSearchTerm()); //NON-NLS return null; } - final String queryStr = keywordQuery.getQuery(); + final String queryStr = keywordQuery.getSearchTerm(); final KeywordList list = keywordToList.get(queryStr); //new subProgress will be active after the initial query @@ -434,9 +437,9 @@ public final class SearchRunner { KeywordSearchQuery keywordSearchQuery = null; - boolean isRegex = !keywordQuery.isLiteral(); + boolean isRegex = !keywordQuery.searchTermIsLiteral(); if (isRegex) { - keywordSearchQuery = new TermComponentQuery(list, keywordQuery); + keywordSearchQuery = new TermsComponentQuery(list, keywordQuery); } else { keywordSearchQuery = new LuceneQuery(list, keywordQuery); keywordSearchQuery.escape(); @@ -453,18 +456,16 @@ public final class SearchRunner { // Do the actual search try { queryResults = keywordSearchQuery.performQuery(); - } catch (NoOpenCoreException ex) { - logger.log(Level.WARNING, "Error performing query: " + keywordQuery.getQuery(), ex); //NON-NLS + } catch (KeywordSearchModuleException | NoOpenCoreException ex) { + logger.log(Level.SEVERE, "Error performing query: " + keywordQuery.getSearchTerm(), ex); //NON-NLS + MessageNotifyUtil.Notify.error(Bundle.SearchRunner_query_exception_msg() + keywordQuery.getSearchTerm(), ex.getCause().getMessage()); //no reason to continue with next query if recovery failed //or wait for recovery to kick in and run again later //likely case has closed and threads are being interrupted return null; } catch (CancellationException e) { - logger.log(Level.INFO, "Cancel detected, bailing during keyword query: {0}", keywordQuery.getQuery()); //NON-NLS + logger.log(Level.INFO, "Cancel detected, bailing during keyword query: {0}", keywordQuery.getSearchTerm()); //NON-NLS return null; - } catch (Exception e) { - logger.log(Level.WARNING, "Error performing query: " + keywordQuery.getQuery(), e); //NON-NLS - continue; } // calculate new results by substracting results already obtained in this ingest @@ -481,7 +482,7 @@ public final class SearchRunner { int totalUnits = newResults.getKeywords().size(); subProgresses[keywordsSearched].start(totalUnits); int unitProgress = 0; - String queryDisplayStr = keywordQuery.getQuery(); + String queryDisplayStr = keywordQuery.getSearchTerm(); if (queryDisplayStr.length() > 50) { queryDisplayStr = queryDisplayStr.substring(0, 49) + "..."; } @@ -547,7 +548,7 @@ public final class SearchRunner { keywordLists.add(list); for (Keyword k : list.getKeywords()) { keywords.add(k); - keywordToList.put(k.getQuery(), list); + keywordToList.put(k.getSearchTerm(), list); } } } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/TermComponentQuery.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/TermComponentQuery.java deleted file mode 100644 index a3e74d6514..0000000000 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/TermComponentQuery.java +++ /dev/null @@ -1,436 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2011-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.keywordsearch; - -import com.google.common.base.CharMatcher; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.logging.Level; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import org.apache.commons.lang.StringUtils; -import org.apache.commons.validator.routines.checkdigit.LuhnCheckDigit; -import org.apache.solr.client.solrj.SolrQuery; -import org.apache.solr.client.solrj.response.TermsResponse.Term; -import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.coreutils.Version; -import org.sleuthkit.autopsy.datamodel.CreditCards; -import org.sleuthkit.datamodel.AbstractFile; -import org.sleuthkit.datamodel.Account; -import org.sleuthkit.datamodel.BlackboardArtifact; -import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE; -import org.sleuthkit.datamodel.BlackboardAttribute; -import org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE; -import org.sleuthkit.datamodel.TskCoreException; -import org.sleuthkit.datamodel.TskData; - -/** - * Performs a regular expression query to the SOLR/Lucene instance. - */ -final class TermComponentQuery implements KeywordSearchQuery { - - private static final Logger LOGGER = Logger.getLogger(TermComponentQuery.class.getName()); - private static final boolean DEBUG = Version.Type.DEVELOPMENT.equals(Version.getBuildType()); - - private static final String MODULE_NAME = KeywordSearchModuleFactory.getModuleName(); - private static final BlackboardAttribute.Type KEYWORD_SEARCH_DOCUMENT_ID = new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_KEYWORD_SEARCH_DOCUMENT_ID); - - //TODO: move these regex and the luhn check to a new class, something like: CreditCardNumberValidator - /* - * Track 2 is numeric plus six punctuation symbolls :;<=>? - * - * This regex matches 12-19 digit ccns embeded in a track 2 formated string. - * This regex matches (and extracts groups) even if the entire track is not - * present as long as the part that is conforms to the track format. - * - */ - private static final Pattern TRACK2_PATTERN = Pattern.compile( - "[:;<=>?]?" //(optional)start sentinel //NON-NLS - + "(?[3456]([ -]?\\d){11,18})" //12-19 digits, with possible single spaces or dashes in between. first digit is 3,4,5, or 6 //NON-NLS - + "(?:[:;<=>?]" //separator //NON-NLS - + "(?:(?\\d{4})" //4 digit expiration date YYMM //NON-NLS - + "(?:(?\\d{3})" //3 digit service code //NON-NLS - + "(?:(?[^:;<=>?]*)" //discretionary data, not containing punctuation marks //NON-NLS - + "(?:[:;<=>?]" //end sentinel //NON-NLS - + "(?.)" //longitudinal redundancy check //NON-NLS - + "?)?)?)?)?)?"); //close nested optional groups //NON-NLS - - /* - * Track 1 is alphanumeric. - * - * This regex matches 12-19 digit ccns embeded in a track 1 formated string. - * This regex matches (and extracts groups) even if the entire track is not - * present as long as the part that is conforms to the track format. - */ - private static final Pattern TRACK1_PATTERN = Pattern.compile( - "(?:" //begin nested optinal group //NON-NLS - + "%?" //optional start sentinal: % //NON-NLS - + "B)?" //format code //NON-NLS - + "(?[3456]([ -]?\\d){11,18})" //12-19 digits, with possible single spaces or dashes in between. first digit is 3,4,5, or 6 //NON-NLS - + "\\^" //separator //NON-NLS - + "(?[^^]{2,26})" //2-26 charachter name, not containing ^ //NON-NLS - + "(?:\\^" //separator //NON-NLS - + "(?:(?:\\^|(?\\d{4}))" //separator or 4 digit expiration YYMM //NON-NLS - + "(?:(?:\\^|(?\\d{3}))"//separator or 3 digit service code //NON-NLS - + "(?:(?[^?]*)" // discretionary data not containing separator //NON-NLS - + "(?:\\?" // end sentinal: ? //NON-NLS - + "(?.)" //longitudinal redundancy check //NON-NLS - + "?)?)?)?)?)?");//close nested optional groups //NON-NLS - private static final Pattern CCN_PATTERN = Pattern.compile("(?[3456]([ -]?\\d){11,18})"); //12-19 digits, with possible single spaces or dashes in between. first digit is 3,4,5, or 6 //NON-NLS - private static final LuhnCheckDigit LUHN_CHECK = new LuhnCheckDigit(); - - //corresponds to field in Solr schema, analyzed with white-space tokenizer only - private static final String TERMS_SEARCH_FIELD = Server.Schema.CONTENT_WS.toString(); - private static final String TERMS_HANDLER = "/terms"; //NON-NLS - private static final int TERMS_TIMEOUT = 90 * 1000; //in ms - private static final String CASE_INSENSITIVE = "case_insensitive"; //NON-NLS - private static final int MAX_TERMS_RESULTS = 20000; - - private String escapedQuery; - private final KeywordList keywordList; - private final Keyword keyword; - private boolean isEscaped; - private final List filters = new ArrayList<>(); - - TermComponentQuery(KeywordList keywordList, Keyword keyword) { - this.keyword = keyword; - - this.keywordList = keywordList; - this.escapedQuery = keyword.getQuery(); - } - - @Override - public void addFilter(KeywordQueryFilter filter) { - this.filters.add(filter); - } - - /** - * @param field - * - * @deprecated This method is unused and no-op - */ - @Override - @Deprecated - public void setField(String field) { - } - - @Override - public void setSubstringQuery() { - escapedQuery = ".*" + escapedQuery + ".*"; - } - - @Override - public void escape() { - escapedQuery = Pattern.quote(keyword.getQuery()); - isEscaped = true; - } - - @Override - public boolean validate() { - if (escapedQuery.isEmpty()) { - return false; - } - - try { - Pattern.compile(escapedQuery); - return true; - } catch (IllegalArgumentException ex) { - return false; - } - } - - @Override - public boolean isEscaped() { - return isEscaped; - } - - @Override - public boolean isLiteral() { - return false; - } - - @Override - public String getEscapedQueryString() { - return this.escapedQuery; - } - - @Override - public String getQueryString() { - return keyword.getQuery(); - } - - @Override - public KeywordCachedArtifact writeSingleFileHitsToBlackBoard(String termHit, KeywordHit hit, String snippet, String listName) { - BlackboardArtifact newArtifact; - - Collection attributes = new ArrayList<>(); - if (keyword.getType() == ATTRIBUTE_TYPE.TSK_CARD_NUMBER) { - attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_ACCOUNT_TYPE, MODULE_NAME, Account.Type.CREDIT_CARD.name())); - - Map parsedTrackAttributeMap = new HashMap<>(); - - //try to match it against the track 1 regex - Matcher matcher = TRACK1_PATTERN.matcher(hit.getSnippet()); - if (matcher.find()) { - parseTrack1Data(parsedTrackAttributeMap, matcher); - } - - //then try to match it against the track 2 regex - matcher = TRACK2_PATTERN.matcher(hit.getSnippet()); - if (matcher.find()) { - parseTrack2Data(parsedTrackAttributeMap, matcher); - } - - //if we couldn't parse the CCN abort this artifact - final BlackboardAttribute ccnAttribute = parsedTrackAttributeMap.get(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_CARD_NUMBER)); - if (ccnAttribute == null || StringUtils.isBlank(ccnAttribute.getValueString())) { - if (hit.isArtifactHit()) { - LOGGER.log(Level.SEVERE, String.format("Failed to parse credit card account number for artifact keyword hit: term = %s, snippet = '%s', artifact id = %d", termHit, hit.getSnippet(), hit.getArtifact().getArtifactID())); - } else { - LOGGER.log(Level.SEVERE, String.format("Failed to parse credit card account number for content keyword hit: term = %s, snippet = '%s', object id = %d", termHit, hit.getSnippet(), hit.getContent().getId())); - } - return null; - } - - attributes.addAll(parsedTrackAttributeMap.values()); - - //look up the bank name, schem, etc from the BIN - final int bin = Integer.parseInt(ccnAttribute.getValueString().substring(0, 8)); - CreditCards.BankIdentificationNumber binInfo = CreditCards.getBINInfo(bin); - if (binInfo != null) { - binInfo.getScheme().ifPresent(scheme - -> attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_CARD_SCHEME, MODULE_NAME, scheme))); - binInfo.getCardType().ifPresent(cardType - -> attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_CARD_TYPE, MODULE_NAME, cardType))); - binInfo.getBrand().ifPresent(brand - -> attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_BRAND_NAME, MODULE_NAME, brand))); - binInfo.getBankName().ifPresent(bankName - -> attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_BANK_NAME, MODULE_NAME, bankName))); - binInfo.getBankPhoneNumber().ifPresent(phoneNumber - -> attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_PHONE_NUMBER, MODULE_NAME, phoneNumber))); - binInfo.getBankURL().ifPresent(url - -> attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_URL, MODULE_NAME, url))); - binInfo.getCountry().ifPresent(country - -> attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_COUNTRY, MODULE_NAME, country))); - binInfo.getBankCity().ifPresent(city - -> attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_CITY, MODULE_NAME, city))); - } - - /* if the hit is from unused or unalocated blocks, record the - * KEYWORD_SEARCH_DOCUMENT_ID, so we can show just that chunk in the - * UI - */ - if (hit.getContent() instanceof AbstractFile) { - AbstractFile file = (AbstractFile) hit.getContent(); - if (file.getType() == TskData.TSK_DB_FILES_TYPE_ENUM.UNUSED_BLOCKS - || file.getType() == TskData.TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS) { - attributes.add(new BlackboardAttribute(KEYWORD_SEARCH_DOCUMENT_ID, MODULE_NAME, hit.getSolrDocumentId())); - } - } - - // make account artifact - try { - newArtifact = hit.getContent().newArtifact(ARTIFACT_TYPE.TSK_ACCOUNT); - } catch (TskCoreException tskCoreException) { - LOGGER.log(Level.SEVERE, "Error adding bb artifact for account", tskCoreException); //NON-NLS - return null; - } - } else { - - //regex match - attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_KEYWORD, MODULE_NAME, termHit)); - //regex keyword - attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_KEYWORD_REGEXP, MODULE_NAME, keyword.getQuery())); - - //make keyword hit artifact - try { - newArtifact = hit.getContent().newArtifact(ARTIFACT_TYPE.TSK_KEYWORD_HIT); - - } catch (TskCoreException tskCoreException) { - LOGGER.log(Level.SEVERE, "Error adding bb artifact for keyword hit", tskCoreException); //NON-NLS - return null; - } - } - if (StringUtils.isNotBlank(listName)) { - attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_SET_NAME, MODULE_NAME, listName)); - } - //preview - if (snippet != null) { - attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_KEYWORD_PREVIEW, MODULE_NAME, snippet)); - } - - if (hit.isArtifactHit()) { - attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT, MODULE_NAME, hit.getArtifact().getArtifactID())); - } - - try { - //TODO: do we still/really need this KeywordCachedArtifact class? - newArtifact.addAttributes(attributes); - KeywordCachedArtifact writeResult = new KeywordCachedArtifact(newArtifact); - writeResult.add(attributes); - return writeResult; - } catch (TskCoreException e) { - LOGGER.log(Level.SEVERE, "Error adding bb attributes for terms search artifact", e); //NON-NLS - return null; - } - } - - @Override - public QueryResults performQuery() throws NoOpenCoreException { - /* - * Execute the regex query to get a list of terms that match the regex. - * Note that the field that is being searched is tokenized based on - * whitespace. - */ - //create the query - final SolrQuery q = new SolrQuery(); - q.setRequestHandler(TERMS_HANDLER); - q.setTerms(true); - q.setTermsRegexFlag(CASE_INSENSITIVE); - q.setTermsRegex(escapedQuery); - q.addTermsField(TERMS_SEARCH_FIELD); - q.setTimeAllowed(TERMS_TIMEOUT); - q.setShowDebugInfo(DEBUG); - q.setTermsLimit(MAX_TERMS_RESULTS); - LOGGER.log(Level.INFO, "Query: {0}", q.toString()); //NON-NLS - - //execute the query - List terms = null; - try { - terms = KeywordSearch.getServer().queryTerms(q).getTerms(TERMS_SEARCH_FIELD); - } catch (KeywordSearchModuleException ex) { - LOGGER.log(Level.SEVERE, "Error executing the regex terms query: " + keyword.getQuery(), ex); //NON-NLS - //TODO: this is almost certainly wrong and guaranteed to throw a NPE at some point!!!! - } - - /* - * For each term that matched the regex, query for full set of document - * hits for that term. - */ - QueryResults results = new QueryResults(this, keywordList); - int resultSize = 0; - - for (Term term : terms) { - final String termStr = KeywordSearchUtil.escapeLuceneQuery(term.getTerm()); - - if (keyword.getType() == ATTRIBUTE_TYPE.TSK_CARD_NUMBER) { - //If the keyword is a credit card number, pass it through luhn validator - Matcher matcher = CCN_PATTERN.matcher(term.getTerm()); - matcher.find(); - final String ccn = CharMatcher.anyOf(" -").removeFrom(matcher.group("ccn")); - if (false == LUHN_CHECK.isValid(ccn)) { - continue; //if the hit does not pass the luhn check, skip it. - } - } - - /* - * Note: we can't set filter query on terms query but setting filter - * query on fileResults query will yield the same result - */ - LuceneQuery filesQuery = new LuceneQuery(keywordList, new Keyword(termStr, true)); - filters.forEach(filesQuery::addFilter); - - try { - QueryResults fileQueryResults = filesQuery.performQuery(); - Set filesResults = new HashSet<>(); - for (Keyword key : fileQueryResults.getKeywords()) { //flatten results into a single list - List keyRes = fileQueryResults.getResults(key); - resultSize += keyRes.size(); - filesResults.addAll(keyRes); - } - results.addResult(new Keyword(term.getTerm(), false), new ArrayList<>(filesResults)); - } catch (NoOpenCoreException | RuntimeException e) { - LOGGER.log(Level.WARNING, "Error executing Solr query,", e); //NON-NLS - throw e; - } - } - - //TODO limit how many results we store, not to hit memory limits - LOGGER.log(Level.INFO, "Regex # results: {0}", resultSize); //NON-NLS - - return results; - } - - @Override - public KeywordList getKeywordList() { - return keywordList; - } - - /** - * Add an attribute of the the given type to the given artifact with the - * value taken from the matcher. If an attribute of the given type already - * exists on the artifact or if the value is null, no attribute is added. - * - * @param attributeMap - * @param attrType - * @param groupName - * @param matcher * - */ - static private void addAttributeIfNotAlreadyCaptured(Map attributeMap, ATTRIBUTE_TYPE attrType, String groupName, Matcher matcher) { - BlackboardAttribute.Type type = new BlackboardAttribute.Type(attrType); - - attributeMap.computeIfAbsent(type, (BlackboardAttribute.Type t) -> { - String value = matcher.group(groupName); - if (attrType.equals(ATTRIBUTE_TYPE.TSK_CARD_NUMBER)) { - value = CharMatcher.anyOf(" -").removeFrom(value); - } - if (StringUtils.isNotBlank(value)) { - return new BlackboardAttribute(attrType, MODULE_NAME, value); - } - return null; - }); - } - - /** - * Parse the track 2 data from a KeywordHit and add it to the given - * artifact. - * - * @param attributeMAp - * @param matcher - */ - static private void parseTrack2Data(Map attributeMAp, Matcher matcher) { - //try to add all the attrributes common to track 1 and 2 - addAttributeIfNotAlreadyCaptured(attributeMAp, ATTRIBUTE_TYPE.TSK_CARD_NUMBER, "accountNumber", matcher); - addAttributeIfNotAlreadyCaptured(attributeMAp, ATTRIBUTE_TYPE.TSK_CARD_EXPIRATION, "expiration", matcher); - addAttributeIfNotAlreadyCaptured(attributeMAp, ATTRIBUTE_TYPE.TSK_CARD_SERVICE_CODE, "serviceCode", matcher); - addAttributeIfNotAlreadyCaptured(attributeMAp, ATTRIBUTE_TYPE.TSK_CARD_DISCRETIONARY, "discretionary", matcher); - addAttributeIfNotAlreadyCaptured(attributeMAp, ATTRIBUTE_TYPE.TSK_CARD_LRC, "LRC", matcher); - - } - - /** - * Parse the track 1 data from a KeywordHit and add it to the given - * artifact. - * - * @param attributeMap - * @param matcher - */ - static private void parseTrack1Data(Map attributeMap, Matcher matcher) { - // track 1 has all the fields present in track 2 - parseTrack2Data(attributeMap, matcher); - //plus it also has the account holders name - addAttributeIfNotAlreadyCaptured(attributeMap, ATTRIBUTE_TYPE.TSK_NAME_PERSON, "name", matcher); - } -} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/TermsComponentQuery.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/TermsComponentQuery.java new file mode 100644 index 0000000000..e356c0b8c6 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/TermsComponentQuery.java @@ -0,0 +1,508 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2011-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.keywordsearch; + +import com.google.common.base.CharMatcher; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.validator.routines.checkdigit.LuhnCheckDigit; +import org.apache.solr.client.solrj.SolrQuery; +import org.apache.solr.client.solrj.response.TermsResponse.Term; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.Version; +import org.sleuthkit.autopsy.datamodel.CreditCards; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.Account; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE; +import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.TskData; + +/** + * Implements a regex query that will be performed as a two step operation. In + * the first step, the Solr terms component is used to find any terms in the + * index that match the regex. In the second step, term queries are executed for + * each matched term to produce the set of keyword hits for the regex. + */ +final class TermsComponentQuery implements KeywordSearchQuery { + + private static final Logger LOGGER = Logger.getLogger(TermsComponentQuery.class.getName()); + private static final String MODULE_NAME = KeywordSearchModuleFactory.getModuleName(); + private static final String SEARCH_HANDLER = "/terms"; //NON-NLS + private static final String SEARCH_FIELD = Server.Schema.CONTENT_WS.toString(); + private static final int TERMS_SEARCH_TIMEOUT = 90 * 1000; // Milliseconds + private static final String CASE_INSENSITIVE = "case_insensitive"; //NON-NLS + private static final boolean DEBUG_FLAG = Version.Type.DEVELOPMENT.equals(Version.getBuildType()); + private static final int MAX_TERMS_QUERY_RESULTS = 20000; + private final KeywordList keywordList; + private final Keyword keyword; + private String searchTerm; + private boolean searchTermIsEscaped; + private final List filters = new ArrayList<>(); // THIS APPEARS TO BE UNUSED + + /* + * The following fields are part of the initial implementation of credit + * card account search and should be factored into another class when time + * permits. + */ + private static final Pattern CREDIT_CARD_NUM_PATTERN = Pattern.compile("(?[3456]([ -]?\\d){11,18})"); //12-19 digits, with possible single spaces or dashes in between. First digit is 3,4,5, or 6 //NON-NLS + private static final LuhnCheckDigit CREDIT_CARD_NUM_LUHN_CHECK = new LuhnCheckDigit(); + private static final Pattern CREDIT_CARD_TRACK1_PATTERN = Pattern.compile( + /* + * Track 1 is alphanumeric. + * + * This regex matches 12-19 digit ccns embeded in a track 1 formated + * string. This regex matches (and extracts groups) even if the + * entire track is not present as long as the part that is conforms + * to the track format. + */ + "(?:" //begin nested optinal group //NON-NLS + + "%?" //optional start sentinal: % //NON-NLS + + "B)?" //format code //NON-NLS + + "(?[3456]([ -]?\\d){11,18})" //12-19 digits, with possible single spaces or dashes in between. first digit is 3,4,5, or 6 //NON-NLS + + "\\^" //separator //NON-NLS + + "(?[^^]{2,26})" //2-26 charachter name, not containing ^ //NON-NLS + + "(?:\\^" //separator //NON-NLS + + "(?:(?:\\^|(?\\d{4}))" //separator or 4 digit expiration YYMM //NON-NLS + + "(?:(?:\\^|(?\\d{3}))"//separator or 3 digit service code //NON-NLS + + "(?:(?[^?]*)" // discretionary data not containing separator //NON-NLS + + "(?:\\?" // end sentinal: ? //NON-NLS + + "(?.)" //longitudinal redundancy check //NON-NLS + + "?)?)?)?)?)?");//close nested optional groups //NON-NLS + private static final Pattern CREDIT_CARD_TRACK2_PATTERN = Pattern.compile( + /* + * Track 2 is numeric plus six punctuation symbolls :;<=>? + * + * This regex matches 12-19 digit ccns embeded in a track 2 formated + * string. This regex matches (and extracts groups) even if the + * entire track is not present as long as the part that is conforms + * to the track format. + * + */ + "[:;<=>?]?" //(optional)start sentinel //NON-NLS + + "(?[3456]([ -]?\\d){11,18})" //12-19 digits, with possible single spaces or dashes in between. first digit is 3,4,5, or 6 //NON-NLS + + "(?:[:;<=>?]" //separator //NON-NLS + + "(?:(?\\d{4})" //4 digit expiration date YYMM //NON-NLS + + "(?:(?\\d{3})" //3 digit service code //NON-NLS + + "(?:(?[^:;<=>?]*)" //discretionary data, not containing punctuation marks //NON-NLS + + "(?:[:;<=>?]" //end sentinel //NON-NLS + + "(?.)" //longitudinal redundancy check //NON-NLS + + "?)?)?)?)?)?"); //close nested optional groups //NON-NLS + private static final BlackboardAttribute.Type KEYWORD_SEARCH_DOCUMENT_ID = new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_KEYWORD_SEARCH_DOCUMENT_ID); + + /** + * Constructs an object that implements a regex query that will be performed + * as a two step operation. In the first step, the Solr terms component is + * used to find any terms in the index that match the regex. In the second + * step, term queries are executed for each matched term to produce the set + * of keyword hits for the regex. + * + * @param keywordList A keyword list that contains the keyword that provides + * the regex search term for the query. + * @param keyword The keyword that provides the regex search term for + * the query. + */ + // TODO: Why is both the list and the keyword added to the state of this + // object? + // TODO: Why is the search term not escaped and given substring wildcards, + // if needed, here in the constructor? + TermsComponentQuery(KeywordList keywordList, Keyword keyword) { + this.keywordList = keywordList; + this.keyword = keyword; + this.searchTerm = keyword.getSearchTerm(); + } + + /** + * Gets the keyword list that contains the keyword that provides the regex + * search term for the query. + * + * @return The keyword list. + */ + @Override + public KeywordList getKeywordList() { + return keywordList; + } + + /** + * Gets the original search term for the query, without any escaping or, if + * it is a literal term, the addition of wildcards for a substring search. + * + * @return The original search term. + */ + @Override + public String getQueryString() { + return keyword.getSearchTerm(); + } + + /** + * Indicates whether or not the search term for the query is a literal term + * that needs have wildcards added to it to make the query a substring + * search. + * + * @return True or false. + */ + @Override + public boolean isLiteral() { + return false; + } + + /** + * Adds wild cards to the search term for the query, which makes the query a + * substring search, if it is a literal search term. + */ + @Override + public void setSubstringQuery() { + searchTerm = ".*" + searchTerm + ".*"; + } + + /** + * Escapes the search term for the query. + */ + @Override + public void escape() { + searchTerm = Pattern.quote(keyword.getSearchTerm()); + searchTermIsEscaped = true; + } + + /** + * Indicates whether or not the search term has been escaped yet. + * + * @return True or false. + */ + @Override + public boolean isEscaped() { + return searchTermIsEscaped; + } + + /** + * Gets the escaped search term for the query, assuming it has been escaped + * by a call to TermsComponentQuery.escape. + * + * @return The search term, possibly escaped. + */ + @Override + public String getEscapedQueryString() { + return this.searchTerm; + } + + /** + * Indicates whether or not the search term is a valid regex. + * + * @return True or false. + */ + @Override + public boolean validate() { + if (searchTerm.isEmpty()) { + return false; + } + try { + Pattern.compile(searchTerm); + return true; + } catch (IllegalArgumentException ex) { + return false; + } + } + + /** + * Does nothing, not applicable to a regex query, which always searches a + * field created specifically for regex sesarches. + * + * @param field The name of a Solr document field to search. + */ + @Override + public void setField(String field) { + } + + /** + * Adds a filter to the query. + * + * @param filter The filter. + */ + // TODO: Document this better. + @Override + public void addFilter(KeywordQueryFilter filter) { + this.filters.add(filter); + } + + /** + * Executes the regex query as a two step operation. In the first step, the + * Solr terms component is used to find any terms in the index that match + * the regex. In the second step, term queries are executed for each matched + * term to produce the set of keyword hits for the regex. + * + * @return A QueryResult object or null. + * + * @throws NoOpenCoreException + */ + @Override + public QueryResults performQuery() throws KeywordSearchModuleException, NoOpenCoreException { + /* + * Do a query using the Solr terms component to find any terms in the + * index that match the regex. + */ + final SolrQuery termsQuery = new SolrQuery(); + termsQuery.setRequestHandler(SEARCH_HANDLER); + termsQuery.setTerms(true); + termsQuery.setTermsRegexFlag(CASE_INSENSITIVE); + termsQuery.setTermsRegex(searchTerm); + termsQuery.addTermsField(SEARCH_FIELD); + termsQuery.setTimeAllowed(TERMS_SEARCH_TIMEOUT); + termsQuery.setShowDebugInfo(DEBUG_FLAG); + termsQuery.setTermsLimit(MAX_TERMS_QUERY_RESULTS); + List terms = KeywordSearch.getServer().queryTerms(termsQuery).getTerms(SEARCH_FIELD); + /* + * Do a term query for each term that matched the regex. + */ + QueryResults results = new QueryResults(this, keywordList); + for (Term term : terms) { + /* + * If searching for credit card account numbers, do a Luhn check on + * the term and discard it if it does not pass. + */ + if (keyword.getArtifactAttributeType() == ATTRIBUTE_TYPE.TSK_CARD_NUMBER) { + Matcher matcher = CREDIT_CARD_NUM_PATTERN.matcher(term.getTerm()); + matcher.find(); + final String ccn = CharMatcher.anyOf(" -").removeFrom(matcher.group("ccn")); + if (false == CREDIT_CARD_NUM_LUHN_CHECK.isValid(ccn)) { + continue; + } + } + + /* + * Do an ordinary query with the escaped term and convert the query + * results into a single list of keyword hits without duplicates. + * + * Note that the filters field appears to be unused. There is an old + * comment here, what does it mean? "Note: we can't set filter query + * on terms query but setting filter query on fileResults query will + * yield the same result." The filter is NOT being added to the term + * query. + */ + String escapedTerm = KeywordSearchUtil.escapeLuceneQuery(term.getTerm()); + LuceneQuery termQuery = new LuceneQuery(keywordList, new Keyword(escapedTerm, true)); + filters.forEach(termQuery::addFilter); // This appears to be unused + QueryResults termQueryResult = termQuery.performQuery(); + Set termHits = new HashSet<>(); + for (Keyword word : termQueryResult.getKeywords()) { + termHits.addAll(termQueryResult.getResults(word)); + } + results.addResult(new Keyword(term.getTerm(), false), new ArrayList<>(termHits)); + } + return results; + } + + /** + * Converts the keyword hits for a given search term into artifacts. + * + * @param searchTerm The search term. + * @param hit The keyword hit. + * @param snippet The document snippet that contains the hit + * @param listName The name of the keyword list that contained the keyword + * for which the hit was found. + * + * + * + * @return An object that wraps an artifact and a mapping by id of its + * attributes. + */ + // TODO: Are we actually making meaningful use of the KeywordCachedArtifact + // class? + @Override + public KeywordCachedArtifact writeSingleFileHitsToBlackBoard(String searchTerm, KeywordHit hit, String snippet, String listName) { + /* + * Create either a "plain vanilla" keyword hit artifact with keyword and + * regex attributes, or a credit card account artifact with attributes + * parsed from from the snippet for the hit and looked up based on the + * parsed bank identifcation number. + */ + BlackboardArtifact newArtifact; + Collection attributes = new ArrayList<>(); + if (keyword.getArtifactAttributeType() != ATTRIBUTE_TYPE.TSK_CARD_NUMBER) { + attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_KEYWORD, MODULE_NAME, searchTerm)); + attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_KEYWORD_REGEXP, MODULE_NAME, keyword.getSearchTerm())); + try { + newArtifact = hit.getContent().newArtifact(ARTIFACT_TYPE.TSK_KEYWORD_HIT); + + } catch (TskCoreException ex) { + LOGGER.log(Level.SEVERE, "Error adding artifact for keyword hit to blackboard", ex); //NON-NLS + return null; + } + } else { + /* + * Parse the credit card account attributes from the snippet for the + * hit. + */ + attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_ACCOUNT_TYPE, MODULE_NAME, Account.Type.CREDIT_CARD.name())); + Map parsedTrackAttributeMap = new HashMap<>(); + Matcher matcher = CREDIT_CARD_TRACK1_PATTERN.matcher(hit.getSnippet()); + if (matcher.find()) { + parseTrack1Data(parsedTrackAttributeMap, matcher); + } + matcher = CREDIT_CARD_TRACK2_PATTERN.matcher(hit.getSnippet()); + if (matcher.find()) { + parseTrack2Data(parsedTrackAttributeMap, matcher); + } + final BlackboardAttribute ccnAttribute = parsedTrackAttributeMap.get(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_CARD_NUMBER)); + if (ccnAttribute == null || StringUtils.isBlank(ccnAttribute.getValueString())) { + if (hit.isArtifactHit()) { + LOGGER.log(Level.SEVERE, String.format("Failed to parse credit card account number for artifact keyword hit: term = %s, snippet = '%s', artifact id = %d", searchTerm, hit.getSnippet(), hit.getArtifact().getArtifactID())); //NON-NLS + } else { + LOGGER.log(Level.SEVERE, String.format("Failed to parse credit card account number for content keyword hit: term = %s, snippet = '%s', object id = %d", searchTerm, hit.getSnippet(), hit.getContent().getId())); //NON-NLS + } + return null; + } + attributes.addAll(parsedTrackAttributeMap.values()); + + /* + * Look up the bank name, scheme, etc. attributes for the bank + * indentification number (BIN). + */ + final int bin = Integer.parseInt(ccnAttribute.getValueString().substring(0, 8)); + CreditCards.BankIdentificationNumber binInfo = CreditCards.getBINInfo(bin); + if (binInfo != null) { + binInfo.getScheme().ifPresent(scheme + -> attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_CARD_SCHEME, MODULE_NAME, scheme))); + binInfo.getCardType().ifPresent(cardType + -> attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_CARD_TYPE, MODULE_NAME, cardType))); + binInfo.getBrand().ifPresent(brand + -> attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_BRAND_NAME, MODULE_NAME, brand))); + binInfo.getBankName().ifPresent(bankName + -> attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_BANK_NAME, MODULE_NAME, bankName))); + binInfo.getBankPhoneNumber().ifPresent(phoneNumber + -> attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_PHONE_NUMBER, MODULE_NAME, phoneNumber))); + binInfo.getBankURL().ifPresent(url + -> attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_URL, MODULE_NAME, url))); + binInfo.getCountry().ifPresent(country + -> attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_COUNTRY, MODULE_NAME, country))); + binInfo.getBankCity().ifPresent(city + -> attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_CITY, MODULE_NAME, city))); + } + + /* + * If the hit is from unused or unallocated space, record the Solr + * document id to support showing just the chunk that contained the + * hit. + */ + if (hit.getContent() instanceof AbstractFile) { + AbstractFile file = (AbstractFile) hit.getContent(); + if (file.getType() == TskData.TSK_DB_FILES_TYPE_ENUM.UNUSED_BLOCKS + || file.getType() == TskData.TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS) { + attributes.add(new BlackboardAttribute(KEYWORD_SEARCH_DOCUMENT_ID, MODULE_NAME, hit.getSolrDocumentId())); + } + } + + /* + * Create an account artifact. + */ + try { + newArtifact = hit.getContent().newArtifact(ARTIFACT_TYPE.TSK_ACCOUNT); + } catch (TskCoreException ex) { + LOGGER.log(Level.SEVERE, "Error adding artifact for account to blackboard", ex); //NON-NLS + return null; + } + } + + if (StringUtils.isNotBlank(listName)) { + attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_SET_NAME, MODULE_NAME, listName)); + } + if (snippet != null) { + attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_KEYWORD_PREVIEW, MODULE_NAME, snippet)); + } + if (hit.isArtifactHit()) { + attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT, MODULE_NAME, hit.getArtifact().getArtifactID())); + } + + try { + newArtifact.addAttributes(attributes); + KeywordCachedArtifact writeResult = new KeywordCachedArtifact(newArtifact); + writeResult.add(attributes); + return writeResult; + } catch (TskCoreException e) { + LOGGER.log(Level.SEVERE, "Error adding bb attributes for terms search artifact", e); //NON-NLS + return null; + } + } + + /** + * Parses the track 2 data from the snippet for a credit card account number + * hit and turns them into artifact attributes. + * + * @param attributesMap A map of artifact attribute objects, used to avoid + * creating duplicate attributes. + * @param matcher A matcher for the snippet. + */ + static private void parseTrack2Data(Map attributesMap, Matcher matcher) { + addAttributeIfNotAlreadyCaptured(attributesMap, ATTRIBUTE_TYPE.TSK_CARD_NUMBER, "accountNumber", matcher); + addAttributeIfNotAlreadyCaptured(attributesMap, ATTRIBUTE_TYPE.TSK_CARD_EXPIRATION, "expiration", matcher); + addAttributeIfNotAlreadyCaptured(attributesMap, ATTRIBUTE_TYPE.TSK_CARD_SERVICE_CODE, "serviceCode", matcher); + addAttributeIfNotAlreadyCaptured(attributesMap, ATTRIBUTE_TYPE.TSK_CARD_DISCRETIONARY, "discretionary", matcher); + addAttributeIfNotAlreadyCaptured(attributesMap, ATTRIBUTE_TYPE.TSK_CARD_LRC, "LRC", matcher); + } + + /** + * Parses the track 1 data from the snippet for a credit card account number + * hit and turns them into artifact attributes. The track 1 data has the + * same fields as the track two data, plus the account holder's name. + * + * @param attributesMap A map of artifact attribute objects, used to avoid + * creating duplicate attributes. + * @param matcher A matcher for the snippet. + */ + static private void parseTrack1Data(Map attributeMap, Matcher matcher) { + parseTrack2Data(attributeMap, matcher); + addAttributeIfNotAlreadyCaptured(attributeMap, ATTRIBUTE_TYPE.TSK_NAME_PERSON, "name", matcher); + } + + /** + * Creates an attribute of the the given type to the given artifact with a + * value parsed from the snippet for a credit account number hit. + * + * @param attributesMap A map of artifact attribute objects, used to avoid + * creating duplicate attributes. + * @param attrType The type of attribute to create. + * @param groupName The group name of the regular expression that was + * used to parse the attribute data. + * @param matcher A matcher for the snippet. + */ + static private void addAttributeIfNotAlreadyCaptured(Map attributeMap, ATTRIBUTE_TYPE attrType, String groupName, Matcher matcher) { + BlackboardAttribute.Type type = new BlackboardAttribute.Type(attrType); + attributeMap.computeIfAbsent(type, (BlackboardAttribute.Type t) -> { + String value = matcher.group(groupName); + if (attrType.equals(ATTRIBUTE_TYPE.TSK_CARD_NUMBER)) { + value = CharMatcher.anyOf(" -").removeFrom(value); + } + if (StringUtils.isNotBlank(value)) { + return new BlackboardAttribute(attrType, MODULE_NAME, value); + } + return null; + }); + } + +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/XmlKeywordSearchList.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/XmlKeywordSearchList.java index 2bef2852e9..a5c25d1dc7 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/XmlKeywordSearchList.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/XmlKeywordSearchList.java @@ -55,6 +55,7 @@ final class XmlKeywordSearchList extends KeywordSearchList { private static final String LIST_INGEST_MSGS = "ingest_messages"; //NON-NLS private static final String KEYWORD_EL = "keyword"; //NON-NLS private static final String KEYWORD_LITERAL_ATTR = "literal"; //NON-NLS + private static final String KEYWORD_WHOLE_ATTR = "whole"; //NON-NLS private static final String KEYWORD_SELECTOR_ATTR = "selector"; //NON-NLS private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss"; //NON-NLS private static final String ENCODING = "UTF-8"; //NON-NLS @@ -99,7 +100,7 @@ final class XmlKeywordSearchList extends KeywordSearchList { doc.appendChild(rootEl); for (String listName : theLists.keySet()) { - if (theLists.get(listName).isLocked() == true) { + if (theLists.get(listName).isEditable() == true) { continue; } KeywordList list = theLists.get(listName); @@ -123,13 +124,15 @@ final class XmlKeywordSearchList extends KeywordSearchList { for (Keyword keyword : keywords) { Element keywordEl = doc.createElement(KEYWORD_EL); - String literal = keyword.isLiteral() ? "true" : "false"; //NON-NLS + String literal = keyword.searchTermIsLiteral() ? "true" : "false"; //NON-NLS keywordEl.setAttribute(KEYWORD_LITERAL_ATTR, literal); - BlackboardAttribute.ATTRIBUTE_TYPE selectorType = keyword.getType(); + String whole = keyword.searchTermIsWholeWord() ? "true" : "false"; //NON-NLS + keywordEl.setAttribute(KEYWORD_WHOLE_ATTR, whole); + BlackboardAttribute.ATTRIBUTE_TYPE selectorType = keyword.getArtifactAttributeType(); if (selectorType != null) { keywordEl.setAttribute(KEYWORD_SELECTOR_ATTR, selectorType.getLabel()); } - keywordEl.setTextContent(keyword.getQuery()); + keywordEl.setTextContent(keyword.getSearchTerm()); listEl.appendChild(keywordEl); } rootEl.appendChild(listEl); @@ -195,14 +198,20 @@ final class XmlKeywordSearchList extends KeywordSearchList { Element wordEl = (Element) wordsNList.item(j); String literal = wordEl.getAttribute(KEYWORD_LITERAL_ATTR); boolean isLiteral = literal.equals("true"); //NON-NLS - Keyword keyword = new Keyword(wordEl.getTextContent(), isLiteral); + Keyword keyword; + String whole = wordEl.getAttribute(KEYWORD_WHOLE_ATTR); + if (whole.equals("")) { + keyword = new Keyword(wordEl.getTextContent(), isLiteral); + } else { + boolean isWhole = whole.equals("true"); + keyword = new Keyword(wordEl.getTextContent(), isLiteral, isWhole); + } String selector = wordEl.getAttribute(KEYWORD_SELECTOR_ATTR); if (!selector.equals("")) { BlackboardAttribute.ATTRIBUTE_TYPE selectorType = BlackboardAttribute.ATTRIBUTE_TYPE.fromLabel(selector); - keyword.setType(selectorType); + keyword.setArtifactAttributeType(selectorType); } words.add(keyword); - } theLists.put(name, list); } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/notes.txt b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/notes.txt deleted file mode 100755 index 1c771a55cd..0000000000 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/notes.txt +++ /dev/null @@ -1,5 +0,0 @@ -- Tika has a bug in the true type font parser included in fontbox. It should - be fixed in the next release of Tika (1.5, or a 1.4 point release). Until then - we bypass Tika when it detects a type of "application/x-font-ttf". See - AbstractFileTikaTextExtract::isSupported. This should be removed when we - update Tika. \ No newline at end of file diff --git a/NEWS.txt b/NEWS.txt index 598ef1d707..7a0c8cb200 100644 --- a/NEWS.txt +++ b/NEWS.txt @@ -1,14 +1,36 @@ +---------------- VERSION 4.3.0 -------------- +Improvements: +- Creation and analysis (e.g., keyword search) of virtual files for slack +space. +- A preloader in an Android device image does not prevent adding the image as +a data source (reading of secondary GPT tables supported). +- User can add data sources with no file systems or unsupported file systems +as "unallocated space image files" for carving, keyword search, etc. +- File extension mismatch analysis can be configured to check all file types, +all file types except text files, or only multimedia and executable files. +- Column order changes in table views are "sticky" for each type of tree view +item. +- Tree view has new file types by MIME type sub tree. +- User can bulk add list of keywords to a keyword list. +- Tagged items are highlighted in table views. +- Toolbar button for Image/Video Gallery +- New "Experimental" module (activate via Tools, Plugins) with auto ingest +feature. +- Assorted bug fixes and minor enhancements. + ---------------- VERSION 4.2.0 -------------- Improvements: - Credit card account search. - Encoding/decoding of extracted files to avoid anti-virus alerts/quarantine. -- Ingest history (start time, end time, status, which versions of which ingest modules were run). +- Ingest history (start time, end time, status, which versions of which ingest +modules were run). - Ingest history used to warn before doing redundant analysis. - Options panel for managing custom tag names. - Options panel for setting external viewer associations. - Keyboard shortcut for applying Bookmark tags. - Improved PhotoRec carver ingest module cancellation responsiveness. -- Results content viewer formats dates. +- Results content viewer formats dates instead of showing raw seconds since +epoch. - Update to PostgreSQL 9.5. - Assorted bug fixes and minor enhancements. diff --git a/RecentActivity/nbproject/project.xml b/RecentActivity/nbproject/project.xml index a470d5ea0d..e3559b4155 100644 --- a/RecentActivity/nbproject/project.xml +++ b/RecentActivity/nbproject/project.xml @@ -60,7 +60,7 @@ 10 - 10.6 + 10.7 diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractRegistry.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractRegistry.java index 8a55b855e8..d924a9cabe 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractRegistry.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractRegistry.java @@ -1,19 +1,19 @@ /* * * Autopsy Forensic Browser - * + * * Copyright 2012-2014 Basis Technology Corp. - * + * * Copyright 2012 42six Solutions. * Contact: aebadirad 42six com * Project Contact/Architect: 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. @@ -68,7 +68,7 @@ class ExtractRegistry extends Extract { private String RR_PATH; private String RR_FULL_PATH; private Path rrHome; // Path to the Autopsy version of RegRipper - private Path rrFullHome; // Path to the full version of RegRipper + private Path rrFullHome; // Path to the full version of RegRipper private Content dataSource; private IngestJobContext context; final private static UsbDeviceIdMapper USB_MAPPER = new UsbDeviceIdMapper(); @@ -198,7 +198,7 @@ class ExtractRegistry extends Extract { } // create a report for the full output - if (regOutputFiles.fullPlugins.isEmpty() == false) { + if (!regOutputFiles.fullPlugins.isEmpty()) { try { currentCase.addReport(regOutputFiles.fullPlugins, NbBundle.getMessage(this.getClass(), "ExtractRegistry.parentModuleName.noSpace"), "RegRipper " + regFile.getUniquePath()); //NON-NLS } catch (TskCoreException e) { @@ -363,321 +363,327 @@ class ExtractRegistry extends Extract { String winver = ""; // If all artifact nodes should really go under one Blackboard artifact, need to process it differently - if (dataType.equals("WinVersion")) { //NON-NLS + switch (dataType) { + case "WinVersion": //NON-NLS + String version = ""; + String systemRoot = ""; + String productId = ""; + String regOwner = ""; + String regOrg = ""; + Long installtime = null; + for (int j = 0; j < myartlist.getLength(); j++) { + Node artchild = myartlist.item(j); + // If it has attributes, then it is an Element (based off API) + if (artchild.hasAttributes()) { + Element artnode = (Element) artchild; - String version = ""; - String systemRoot = ""; - String productId = ""; - String regOwner = ""; - String regOrg = ""; - Long installtime = null; - - for (int j = 0; j < myartlist.getLength(); j++) { - Node artchild = myartlist.item(j); - // If it has attributes, then it is an Element (based off API) - if (artchild.hasAttributes()) { - Element artnode = (Element) artchild; - - String value = artnode.getTextContent().trim(); - String name = artnode.getAttribute("name"); //NON-NLS - - if (name.equals("ProductName")) { // NON-NLS - version = value; - } else if (name.equals("CSDVersion")) { // NON-NLS - // This is dependant on the fact that ProductName shows up first in the module output - version = version + " " + value; - } else if (name.equals("SystemRoot")) { //NON-NLS - systemRoot = value; - } else if (name.equals("ProductId")) { //NON-NLS - productId = value; - } else if (name.equals("RegisteredOwner")) { //NON-NLS - regOwner = value; - } else if (name.equals("RegisteredOrganization")) { //NON-NLS - regOrg = value; - } else if (name.equals("InstallDate")) { //NON-NLS - try { - Long epochtime = new SimpleDateFormat("EEE MMM d HH:mm:ss yyyy").parse(value).getTime(); - installtime = epochtime; - String Tempdate = installtime.toString(); - installtime = Long.valueOf(Tempdate) / 1000; - } catch (ParseException e) { - logger.log(Level.SEVERE, "RegRipper::Conversion on DateTime -> ", e); //NON-NLS + String value = artnode.getTextContent().trim(); + String name = artnode.getAttribute("name"); //NON-NLS + switch (name) { + case "ProductName": // NON-NLS + version = value; + break; + case "CSDVersion": // NON-NLS + // This is dependant on the fact that ProductName shows up first in the module output + version = version + " " + value; + break; + case "SystemRoot": //NON-NLS + systemRoot = value; + break; + case "ProductId": //NON-NLS + productId = value; + break; + case "RegisteredOwner": //NON-NLS + regOwner = value; + break; + case "RegisteredOrganization": //NON-NLS + regOrg = value; + break; + case "InstallDate": //NON-NLS + try { + Long epochtime = new SimpleDateFormat("EEE MMM d HH:mm:ss yyyy").parse(value).getTime(); + installtime = epochtime; + String Tempdate = installtime.toString(); + installtime = Long.valueOf(Tempdate) / 1000; + } catch (ParseException e) { + logger.log(Level.SEVERE, "RegRipper::Conversion on DateTime -> ", e); //NON-NLS + } break; + default: + break; } } - } - } - - try { - Collection bbattributes = new ArrayList<>(); - bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_PROG_NAME, parentModuleName, version)); - if (installtime != null) { - bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_DATETIME, parentModuleName, installtime)); - } - bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_PATH, parentModuleName, systemRoot)); - bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_PRODUCT_ID, parentModuleName, productId)); - bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_OWNER, parentModuleName, regOwner)); - bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_ORGANIZATION, parentModuleName, regOrg)); - - // Check if there is already an OS_INFO artifact for this file, and add to that if possible. - ArrayList results = tempDb.getBlackboardArtifacts(ARTIFACT_TYPE.TSK_OS_INFO, regFile.getId()); - if (results.isEmpty()) { - BlackboardArtifact bbart = regFile.newArtifact(ARTIFACT_TYPE.TSK_OS_INFO); - bbart.addAttributes(bbattributes); - - // index the artifact for keyword search - this.indexArtifact(bbart); - } else { - results.get(0).addAttributes(bbattributes); - } - - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error adding installed program artifact to blackboard."); //NON-NLS - } - } else if (dataType.equals("Profiler")) { // NON-NLS - - String os = ""; - String procArch = ""; - String procId = ""; - String tempDir = ""; - - for (int j = 0; j < myartlist.getLength(); j++) { - Node artchild = myartlist.item(j); - // If it has attributes, then it is an Element (based off API) - if (artchild.hasAttributes()) { - Element artnode = (Element) artchild; - - String value = artnode.getTextContent().trim(); - String name = artnode.getAttribute("name"); //NON-NLS - - if (name.equals("OS")) { // NON-NLS - os = value; - } else if (name.equals("PROCESSOR_ARCHITECTURE")) { // NON-NLS - procArch = value; - } else if (name.equals("PROCESSOR_IDENTIFIER")) { //NON-NLS - procId = value; - } else if (name.equals("TEMP")) { //NON-NLS - tempDir = value; - } - } - } - - try { - Collection bbattributes = new ArrayList<>(); - bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_VERSION, parentModuleName, os)); - bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_PROCESSOR_ARCHITECTURE, parentModuleName, procArch)); - bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_TEMP_DIR, parentModuleName, tempDir)); - - // Check if there is already an OS_INFO artifact for this file and add to that if possible - ArrayList results = tempDb.getBlackboardArtifacts(ARTIFACT_TYPE.TSK_OS_INFO, regFile.getId()); - if (results.isEmpty()) { - BlackboardArtifact bbart = regFile.newArtifact(ARTIFACT_TYPE.TSK_OS_INFO); - bbart.addAttributes(bbattributes); - - // index the artifact for keyword search - this.indexArtifact(bbart); - } else { - results.get(0).addAttributes(bbattributes); - } - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error adding os info artifact to blackboard."); //NON-NLS - } - } else if (dataType.equals("CompName")) { // NON-NLS - - String compName = ""; - String domain = ""; - - for (int j = 0; j < myartlist.getLength(); j++) { - Node artchild = myartlist.item(j); - // If it has attributes, then it is an Element (based off API) - if (artchild.hasAttributes()) { - Element artnode = (Element) artchild; - - String value = artnode.getTextContent().trim(); - String name = artnode.getAttribute("name"); //NON-NLS - - if (name.equals("ComputerName")) { // NON-NLS - compName = value; - } else if (name.equals("Domain")) { // NON-NLS - domain = value; - } - } - } - - try { - Collection bbattributes = new ArrayList<>(); - bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_NAME, parentModuleName, compName)); - bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_DOMAIN, parentModuleName, domain)); - - // Check if there is already an OS_INFO artifact for this file and add to that if possible - ArrayList results = tempDb.getBlackboardArtifacts(ARTIFACT_TYPE.TSK_OS_INFO, regFile.getId()); - if (results.isEmpty()) { - BlackboardArtifact bbart = regFile.newArtifact(ARTIFACT_TYPE.TSK_OS_INFO); - bbart.addAttributes(bbattributes); - - // index the artifact for keyword search - this.indexArtifact(bbart); - } else { - results.get(0).addAttributes(bbattributes); - } - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error adding os info artifact to blackboard."); //NON-NLS - } - } else { - for (int j = 0; j < myartlist.getLength(); j++) { - Node artchild = myartlist.item(j); - // If it has attributes, then it is an Element (based off API) - if (artchild.hasAttributes()) { - Element artnode = (Element) artchild; - - String value = artnode.getTextContent().trim(); + } try { Collection bbattributes = new ArrayList<>(); - - switch (dataType) { - case "recentdocs": //NON-NLS - // BlackboardArtifact bbart = tempDb.getContentById(orgId).newArtifact(ARTIFACT_TYPE.TSK_RECENT_OBJECT); - // bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_LAST_ACCESSED.getTypeID(), "RecentActivity", dataType, mtime)); - // bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_NAME.getTypeID(), "RecentActivity", dataType, mtimeItem)); - // bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_VALUE.getTypeID(), "RecentActivity", dataType, value)); - // bbart.addAttributes(bbattributes); - // @@@ BC: Why are we ignoring this... - break; - case "usb": //NON-NLS - try { - Long usbMtime = Long.parseLong(artnode.getAttribute("mtime")); //NON-NLS - usbMtime = Long.valueOf(usbMtime.toString()); - - BlackboardArtifact bbart = regFile.newArtifact(ARTIFACT_TYPE.TSK_DEVICE_ATTACHED); - bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_DATETIME, parentModuleName, usbMtime)); - String dev = artnode.getAttribute("dev"); //NON-NLS - String make = ""; - String model = dev; - if (dev.toLowerCase().contains("vid")) { //NON-NLS - USBInfo info = USB_MAPPER.parseAndLookup(dev); - if (info.getVendor() != null) { - make = info.getVendor(); - } - if (info.getProduct() != null) { - model = info.getProduct(); - } - } - bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_DEVICE_MAKE, parentModuleName, make)); - bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_DEVICE_MODEL, parentModuleName, model)); - bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_DEVICE_ID, parentModuleName, value)); - bbart.addAttributes(bbattributes); - - // index the artifact for keyword search - this.indexArtifact(bbart); - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error adding device attached artifact to blackboard."); //NON-NLS - } - break; - case "uninstall": //NON-NLS - Long itemMtime = null; - try { - Long epochtime = new SimpleDateFormat("EEE MMM d HH:mm:ss yyyy").parse(artnode.getAttribute("mtime")).getTime(); //NON-NLS - itemMtime = epochtime; - itemMtime = itemMtime / 1000; - } catch (ParseException e) { - logger.log(Level.WARNING, "Failed to parse epoch time for installed program artifact."); //NON-NLS - } - - try { - bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_PROG_NAME, parentModuleName, value)); - bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_DATETIME, parentModuleName, itemMtime)); - BlackboardArtifact bbart = regFile.newArtifact(ARTIFACT_TYPE.TSK_INSTALLED_PROG); - bbart.addAttributes(bbattributes); - - // index the artifact for keyword search - this.indexArtifact(bbart); - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error adding installed program artifact to blackboard."); //NON-NLS - } - break; - case "office": //NON-NLS - String officeName = artnode.getAttribute("name"); //NON-NLS - - try { - BlackboardArtifact bbart = regFile.newArtifact(ARTIFACT_TYPE.TSK_RECENT_OBJECT); - // @@@ BC: Consider removing this after some more testing. It looks like an Mtime associated with the root key and not the individual item - if (mtime != null) { - bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_DATETIME_ACCESSED, parentModuleName, mtime)); - } - bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_NAME, parentModuleName, officeName)); - bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_VALUE, parentModuleName, value)); - bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_PROG_NAME, parentModuleName, artnode.getNodeName())); - bbart.addAttributes(bbattributes); - - // index the artifact for keyword search - this.indexArtifact(bbart); - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error adding recent object artifact to blackboard."); //NON-NLS - } - break; - - case "ProcessorArchitecture": //NON-NLS - // Architecture is now included under Profiler - //try { - // String processorArchitecture = value; - // if (processorArchitecture.equals("AMD64")) - // processorArchitecture = "x86-64"; - - // BlackboardArtifact bbart = regFile.newArtifact(ARTIFACT_TYPE.TSK_OS_INFO); - // bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_PROCESSOR_ARCHITECTURE.getTypeID(), parentModuleName, processorArchitecture)); - // bbart.addAttributes(bbattributes); - //} catch (TskCoreException ex) { - // logger.log(Level.SEVERE, "Error adding os info artifact to blackboard."); //NON-NLS - //} - break; - - case "ProfileList": //NON-NLS - try { - - String homeDir = value; - String sid = artnode.getAttribute("sid"); //NON-NLS - String username = artnode.getAttribute("username"); //NON-NLS - - BlackboardArtifact bbart = regFile.newArtifact(ARTIFACT_TYPE.TSK_OS_ACCOUNT); - bbart.addAttribute(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_USER_NAME, - parentModuleName, username)); - bbart.addAttribute(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_USER_ID, - parentModuleName, sid)); - bbart.addAttribute(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_PATH, - parentModuleName, homeDir)); - // index the artifact for keyword search - this.indexArtifact(bbart); - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error adding account artifact to blackboard."); //NON-NLS - } - break; - - case "NtuserNetwork": // NON-NLS - try { - String localPath = artnode.getAttribute("localPath"); //NON-NLS - String remoteName = value; - BlackboardArtifact bbart = regFile.newArtifact(ARTIFACT_TYPE.TSK_REMOTE_DRIVE); - bbart.addAttribute(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_LOCAL_PATH, - parentModuleName, localPath)); - bbart.addAttribute(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_REMOTE_PATH, - parentModuleName, remoteName)); - // index the artifact for keyword search - this.indexArtifact(bbart); - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error adding network artifact to blackboard."); //NON-NLS - } - break; - - case "shellfolders": // NON-NLS - // The User Shell Folders subkey stores the paths to Windows Explorer folders for the current user of the computer - // (https://technet.microsoft.com/en-us/library/Cc962613.aspx). - // No useful information. Skip. - break; - - default: - logger.log(Level.WARNING, "Unrecognized node name: {0}", dataType); //NON-NLS - break; + bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_PROG_NAME, parentModuleName, version)); + if (installtime != null) { + bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_DATETIME, parentModuleName, installtime)); } + bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_PATH, parentModuleName, systemRoot)); + bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_PRODUCT_ID, parentModuleName, productId)); + bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_OWNER, parentModuleName, regOwner)); + bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_ORGANIZATION, parentModuleName, regOrg)); + + // Check if there is already an OS_INFO artifact for this file, and add to that if possible. + ArrayList results = tempDb.getBlackboardArtifacts(ARTIFACT_TYPE.TSK_OS_INFO, regFile.getId()); + if (results.isEmpty()) { + BlackboardArtifact bbart = regFile.newArtifact(ARTIFACT_TYPE.TSK_OS_INFO); + bbart.addAttributes(bbattributes); + + // index the artifact for keyword search + this.indexArtifact(bbart); + } else { + results.get(0).addAttributes(bbattributes); + } + + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Error adding installed program artifact to blackboard."); //NON-NLS } - } + break; + case "Profiler": // NON-NLS + String os = ""; + String procArch = ""; + String procId = ""; + String tempDir = ""; + for (int j = 0; j < myartlist.getLength(); j++) { + Node artchild = myartlist.item(j); + // If it has attributes, then it is an Element (based off API) + if (artchild.hasAttributes()) { + Element artnode = (Element) artchild; + + String value = artnode.getTextContent().trim(); + String name = artnode.getAttribute("name"); //NON-NLS + switch (name) { + case "OS": // NON-NLS + os = value; + break; + case "PROCESSOR_ARCHITECTURE": // NON-NLS + procArch = value; + break; + case "PROCESSOR_IDENTIFIER": //NON-NLS + procId = value; + break; + case "TEMP": //NON-NLS + tempDir = value; + break; + default: + break; + } + } + } try { + Collection bbattributes = new ArrayList<>(); + bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_VERSION, parentModuleName, os)); + bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_PROCESSOR_ARCHITECTURE, parentModuleName, procArch)); + bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_TEMP_DIR, parentModuleName, tempDir)); + + // Check if there is already an OS_INFO artifact for this file and add to that if possible + ArrayList results = tempDb.getBlackboardArtifacts(ARTIFACT_TYPE.TSK_OS_INFO, regFile.getId()); + if (results.isEmpty()) { + BlackboardArtifact bbart = regFile.newArtifact(ARTIFACT_TYPE.TSK_OS_INFO); + bbart.addAttributes(bbattributes); + + // index the artifact for keyword search + this.indexArtifact(bbart); + } else { + results.get(0).addAttributes(bbattributes); + } + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Error adding os info artifact to blackboard."); //NON-NLS + } + break; + case "CompName": // NON-NLS + String compName = ""; + String domain = ""; + for (int j = 0; j < myartlist.getLength(); j++) { + Node artchild = myartlist.item(j); + // If it has attributes, then it is an Element (based off API) + if (artchild.hasAttributes()) { + Element artnode = (Element) artchild; + + String value = artnode.getTextContent().trim(); + String name = artnode.getAttribute("name"); //NON-NLS + + if (name.equals("ComputerName")) { // NON-NLS + compName = value; + } else if (name.equals("Domain")) { // NON-NLS + domain = value; + } + } + } try { + Collection bbattributes = new ArrayList<>(); + bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_NAME, parentModuleName, compName)); + bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_DOMAIN, parentModuleName, domain)); + + // Check if there is already an OS_INFO artifact for this file and add to that if possible + ArrayList results = tempDb.getBlackboardArtifacts(ARTIFACT_TYPE.TSK_OS_INFO, regFile.getId()); + if (results.isEmpty()) { + BlackboardArtifact bbart = regFile.newArtifact(ARTIFACT_TYPE.TSK_OS_INFO); + bbart.addAttributes(bbattributes); + + // index the artifact for keyword search + this.indexArtifact(bbart); + } else { + results.get(0).addAttributes(bbattributes); + } + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Error adding os info artifact to blackboard."); //NON-NLS + } + break; + default: + for (int j = 0; j < myartlist.getLength(); j++) { + Node artchild = myartlist.item(j); + // If it has attributes, then it is an Element (based off API) + if (artchild.hasAttributes()) { + Element artnode = (Element) artchild; + + String value = artnode.getTextContent().trim(); + Collection bbattributes = new ArrayList<>(); + + switch (dataType) { + case "recentdocs": //NON-NLS + // BlackboardArtifact bbart = tempDb.getContentById(orgId).newArtifact(ARTIFACT_TYPE.TSK_RECENT_OBJECT); + // bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_LAST_ACCESSED.getTypeID(), "RecentActivity", dataType, mtime)); + // bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_NAME.getTypeID(), "RecentActivity", dataType, mtimeItem)); + // bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_VALUE.getTypeID(), "RecentActivity", dataType, value)); + // bbart.addAttributes(bbattributes); + // @@@ BC: Why are we ignoring this... + break; + case "usb": //NON-NLS + try { + Long usbMtime = Long.parseLong(artnode.getAttribute("mtime")); //NON-NLS + usbMtime = Long.valueOf(usbMtime.toString()); + + BlackboardArtifact bbart = regFile.newArtifact(ARTIFACT_TYPE.TSK_DEVICE_ATTACHED); + bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_DATETIME, parentModuleName, usbMtime)); + String dev = artnode.getAttribute("dev"); //NON-NLS + String make = ""; + String model = dev; + if (dev.toLowerCase().contains("vid")) { //NON-NLS + USBInfo info = USB_MAPPER.parseAndLookup(dev); + if (info.getVendor() != null) { + make = info.getVendor(); + } + if (info.getProduct() != null) { + model = info.getProduct(); + } + } + bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_DEVICE_MAKE, parentModuleName, make)); + bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_DEVICE_MODEL, parentModuleName, model)); + bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_DEVICE_ID, parentModuleName, value)); + bbart.addAttributes(bbattributes); + + // index the artifact for keyword search + this.indexArtifact(bbart); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Error adding device attached artifact to blackboard."); //NON-NLS + } + break; + case "uninstall": //NON-NLS + Long itemMtime = null; + try { + Long epochtime = new SimpleDateFormat("EEE MMM d HH:mm:ss yyyy").parse(artnode.getAttribute("mtime")).getTime(); //NON-NLS + itemMtime = epochtime; + itemMtime = itemMtime / 1000; + } catch (ParseException e) { + logger.log(Level.WARNING, "Failed to parse epoch time for installed program artifact."); //NON-NLS + } + + try { + bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_PROG_NAME, parentModuleName, value)); + bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_DATETIME, parentModuleName, itemMtime)); + BlackboardArtifact bbart = regFile.newArtifact(ARTIFACT_TYPE.TSK_INSTALLED_PROG); + bbart.addAttributes(bbattributes); + + // index the artifact for keyword search + this.indexArtifact(bbart); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Error adding installed program artifact to blackboard."); //NON-NLS + } + break; + case "office": //NON-NLS + String officeName = artnode.getAttribute("name"); //NON-NLS + + try { + BlackboardArtifact bbart = regFile.newArtifact(ARTIFACT_TYPE.TSK_RECENT_OBJECT); + // @@@ BC: Consider removing this after some more testing. It looks like an Mtime associated with the root key and not the individual item + if (mtime != null) { + bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_DATETIME_ACCESSED, parentModuleName, mtime)); + } + bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_NAME, parentModuleName, officeName)); + bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_VALUE, parentModuleName, value)); + bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_PROG_NAME, parentModuleName, artnode.getNodeName())); + bbart.addAttributes(bbattributes); + + // index the artifact for keyword search + this.indexArtifact(bbart); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Error adding recent object artifact to blackboard."); //NON-NLS + } + break; + + case "ProcessorArchitecture": //NON-NLS + // Architecture is now included under Profiler + //try { + // String processorArchitecture = value; + // if (processorArchitecture.equals("AMD64")) + // processorArchitecture = "x86-64"; + + // BlackboardArtifact bbart = regFile.newArtifact(ARTIFACT_TYPE.TSK_OS_INFO); + // bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_PROCESSOR_ARCHITECTURE.getTypeID(), parentModuleName, processorArchitecture)); + // bbart.addAttributes(bbattributes); + //} catch (TskCoreException ex) { + // logger.log(Level.SEVERE, "Error adding os info artifact to blackboard."); //NON-NLS + //} + break; + + case "ProfileList": //NON-NLS + try { + + String homeDir = value; + String sid = artnode.getAttribute("sid"); //NON-NLS + String username = artnode.getAttribute("username"); //NON-NLS + + BlackboardArtifact bbart = regFile.newArtifact(ARTIFACT_TYPE.TSK_OS_ACCOUNT); + bbart.addAttribute(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_USER_NAME, + parentModuleName, username)); + bbart.addAttribute(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_USER_ID, + parentModuleName, sid)); + bbart.addAttribute(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_PATH, + parentModuleName, homeDir)); + // index the artifact for keyword search + this.indexArtifact(bbart); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Error adding account artifact to blackboard."); //NON-NLS + } + break; + + case "NtuserNetwork": // NON-NLS + try { + String localPath = artnode.getAttribute("localPath"); //NON-NLS + String remoteName = value; + BlackboardArtifact bbart = regFile.newArtifact(ARTIFACT_TYPE.TSK_REMOTE_DRIVE); + bbart.addAttribute(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_LOCAL_PATH, + parentModuleName, localPath)); + bbart.addAttribute(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_REMOTE_PATH, + parentModuleName, remoteName)); + // index the artifact for keyword search + this.indexArtifact(bbart); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Error adding network artifact to blackboard."); //NON-NLS + } + break; + + case "shellfolders": // NON-NLS + // The User Shell Folders subkey stores the paths to Windows Explorer folders for the current user of the computer + // (https://technet.microsoft.com/en-us/library/Cc962613.aspx). + // No useful information. Skip. + break; + + default: + logger.log(Level.WARNING, "Unrecognized node name: {0}", dataType); //NON-NLS + break; + } + } + } break; } } return true; @@ -706,5 +712,4 @@ class ExtractRegistry extends Extract { this.context = context; analyzeRegistryFiles(); } - } diff --git a/Testing/nbproject/project.xml b/Testing/nbproject/project.xml index 81fc629964..bf4478337b 100644 --- a/Testing/nbproject/project.xml +++ b/Testing/nbproject/project.xml @@ -12,7 +12,7 @@ 10 - 10.6 + 10.7 diff --git a/Testing/test/qa-functional/src/org/sleuthkit/autopsy/testing/RegressionTest.java b/Testing/test/qa-functional/src/org/sleuthkit/autopsy/testing/RegressionTest.java index 26447f112a..12e04aad94 100755 --- a/Testing/test/qa-functional/src/org/sleuthkit/autopsy/testing/RegressionTest.java +++ b/Testing/test/qa-functional/src/org/sleuthkit/autopsy/testing/RegressionTest.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2014 Basis Technology Corp. + * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -325,15 +325,11 @@ public class RegressionTest extends TestCase { } public void testGenerateReportToolbar() { - logger.info("Generate Report Toolbars"); - // Force the action if necessary: - //new Action("Tools|Generate Report", null).perform(); - //new Timeout("pausing", 1000).sleep(); MainWindowOperator mwo = MainWindowOperator.getDefault(); JButtonOperator jbo = new JButtonOperator(mwo, "Generate Report"); jbo.pushNoBlock(); - new Timeout("pausing", 1000).sleep(); + new Timeout("pausing", 5000).sleep(); } public void testGenerateReportButton() throws IOException { @@ -355,10 +351,10 @@ public class RegressionTest extends TestCase { JDialog previewDialog = JDialogOperator.waitJDialog("Progress", false, false); screenshot("Progress"); JDialogOperator previewDialogOperator = new JDialogOperator(previewDialog); - JLabel waiter = JLabelOperator.waitJLabel(previewDialog, "Complete", false, false); + JLabelOperator.waitJLabel(previewDialog, "Complete", false, false); JButtonOperator jbo2 = new JButtonOperator(previewDialogOperator, "Close"); jbo2.pushNoBlock(); - new Timeout("pausing", 3000).sleep(); // Give the program a second to idle to be safe + new Timeout("pausing", 10000).sleep(); System.setProperty("ReportStr", datenotime); screenshot("Done Testing"); } diff --git a/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties b/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties index e4717bf430..8eb490c76e 100644 --- a/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties +++ b/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties @@ -1,5 +1,5 @@ #Updated by build script -#Wed, 12 Oct 2016 13:08:27 -0400 +#Mon, 02 Jan 2017 18:41:13 -0500 LBL_splash_window_title=Starting Autopsy SPLASH_HEIGHT=314 SPLASH_WIDTH=538 @@ -8,4 +8,4 @@ SplashRunningTextBounds=0,289,538,18 SplashRunningTextColor=0x0 SplashRunningTextFontSize=19 -currentVersion=Autopsy 4.2.0 +currentVersion=Autopsy 4.3.0 diff --git a/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties b/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties index c4ce9b916d..756018bd64 100644 --- a/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties +++ b/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties @@ -1,4 +1,4 @@ #Updated by build script -#Wed, 12 Oct 2016 13:08:27 -0400 -CTL_MainWindow_Title=Autopsy 4.2.0 -CTL_MainWindow_Title_No_Project=Autopsy 4.2.0 +#Mon, 02 Jan 2017 18:41:13 -0500 +CTL_MainWindow_Title=Autopsy 4.3.0 +CTL_MainWindow_Title_No_Project=Autopsy 4.3.0 diff --git a/build-windows.xml b/build-windows.xml index 41a96016ab..d922476dd4 100644 --- a/build-windows.xml +++ b/build-windows.xml @@ -43,68 +43,40 @@ - - - - + + - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - @@ -115,57 +87,4 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/build.xml b/build.xml index b1e2fdb14a..55951fb079 100755 --- a/build.xml +++ b/build.xml @@ -92,8 +92,6 @@ - - @@ -258,6 +256,9 @@ + + + diff --git a/docs/doxygen-user/Doxyfile b/docs/doxygen-user/Doxyfile index bce35afa67..c3ddd8fc43 100755 --- a/docs/doxygen-user/Doxyfile +++ b/docs/doxygen-user/Doxyfile @@ -38,7 +38,7 @@ PROJECT_NAME = "Autopsy User Documentation" # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 4.2 +PROJECT_NUMBER = 4.3 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a @@ -1025,7 +1025,7 @@ GENERATE_HTML = YES # The default directory is: html. # This tag requires that the tag GENERATE_HTML is set to YES. -HTML_OUTPUT = 4.2 +HTML_OUTPUT = 4.3 # The HTML_FILE_EXTENSION tag can be used to specify the file extension for each # generated HTML page (for example: .htm, .php, .asp). diff --git a/docs/doxygen-user/data_sources.dox b/docs/doxygen-user/data_sources.dox index a09c5b6a55..753c95bcbe 100755 --- a/docs/doxygen-user/data_sources.dox +++ b/docs/doxygen-user/data_sources.dox @@ -1,13 +1,13 @@ /*! \page ds_page Data Sources -A data source the thing you want to analyze. It can be a disk image, some logical files, a local drive, etc. You must open a case prior to adding a data source to Autopsy. +A data source is the thing you want to analyze. It can be a disk image, some logical files, a local disk, etc. You must open a case prior to adding a data source to Autopsy. -Autopsy supports three types of data sources: -- Disk Image: A file (or set of files) that is a byte-for-byte copy of a hard drive or media card. (see \ref ds_img) -- Local Drive: Local storage device (local drive, USB-attached drive, etc.). (see \ref ds_local) +Autopsy supports four types of data sources: +- Disk Image or VM File: A file (or set of files) that is a byte-for-byte copy of a hard drive or media card, or a virtual machine image. (see \ref ds_img) +- Local Disk: Local storage device (local drive, USB-attached drive, etc.). (see \ref ds_local) - Logical Files: Local files or folders. (see \ref ds_log) - +- Unallocated Space Image Files: Any type of file that does not contain a file system but you want to run through ingest (see \ref ds_unalloc) \section ds_add Adding a Data Source @@ -52,7 +52,7 @@ Autopsy supports disk images in the following formats: To add a disk image: --# Choose "Image File" from the pull down. +-# Choose "Disk Image or VM File" from the pull down. -# Browse to the first file in the disk image. You need to specify only the first file and Autopsy will find the rest. -# Choose the timezone that the disk image came from. This is most important for when adding FAT file systems because it does not store timezone information and Autopsy will not know how to normalize to UTC. -# Choose to perform orphan file finding on FAT file systems. This can be a time intensive process because it will require that Autopsy look at each sector in the device. @@ -90,4 +90,13 @@ To add logical files: All of the files that you added in the panel will be grouped together into a single data source, called "LogicalFileSet" in the main UI. +\section ds_unalloc Adding an Unallocated Space Image File + +\image html unallocated_space_options.PNG + +To add unallocated space image files: +-# Choose "Unallocated Space Image File" from the pull down +-# Browse to the file +-# Choose whether to break the image up into chunks. Breaking the image up will give better performance since the chunks can be processed in parallel, but there is a chance that keywords or carved files that span chunk boundaries will be missed. + */ \ No newline at end of file diff --git a/docs/doxygen-user/extension_mismatch.dox b/docs/doxygen-user/extension_mismatch.dox index 77221690e4..2866ecedc0 100755 --- a/docs/doxygen-user/extension_mismatch.dox +++ b/docs/doxygen-user/extension_mismatch.dox @@ -21,7 +21,7 @@ Note that you can get a lot of false positives with this module. You can add you Ingest Settings ------ -In the ingest settings, the user can choose if the module should skip files without extensions and skip text files. Both of these options are enabled by default. +In the ingest settings, the user can choose whether to run on all files, all files except text files, or only multimedia or executable files. Additionally, the user can choose to skip all files without an extension, and to skip any known files identified by the hash lookup module, if it is enabled. \image html extension-mismatch-detected-ingest-settings.PNG diff --git a/docs/doxygen-user/image_gallery.dox b/docs/doxygen-user/image_gallery.dox index 50c5f87c6f..183b72d98d 100644 --- a/docs/doxygen-user/image_gallery.dox +++ b/docs/doxygen-user/image_gallery.dox @@ -12,7 +12,7 @@ Quick Start =========== 1. The Image Gallery tool can be configured to collect data about images/videos as ingest runs or all at once after ingest. To change this setting go to "Tools", "Options", "Image /Video Gallery". This setting is saved per case, but cannot be changed during ingest. See the Options window for more details 2. Create a case as normal and add a disk image (or folder of files) as a data source. Ensure that you have the hash lookup module enabled with NSRL and known bad hashsets, the EXIF module enabled, and the File Type module enabled. -3. Click "Tools", "View Images/Videos" in the menu. This will open the Autopsy Image/Video Analysis tool in a new window. +3. Click the "View Images/Videos" button or select "View Images/Videos" in the "Tools" menu. This will open the Autopsy Image/Video Analysis tool in a new window. 4. Groups of images will be presented as they are analyzed by the background ingest modules. You can later resort and regroup, but it is required to keep it grouped by folder while ingest is still ongoing. 5. As each group is reviewed, the next highest priority group is presented, according to a sorting criteria (the default is the density of hash set hits). 6. Images that were hits from hashsets, will have a dashed border around them. diff --git a/docs/doxygen-user/images/extension-mismatch-detected-ingest-settings.PNG b/docs/doxygen-user/images/extension-mismatch-detected-ingest-settings.PNG index db82252ae9..b0ab22c6d0 100755 Binary files a/docs/doxygen-user/images/extension-mismatch-detected-ingest-settings.PNG and b/docs/doxygen-user/images/extension-mismatch-detected-ingest-settings.PNG differ diff --git a/docs/doxygen-user/images/keyword-search-configuration-dialog.PNG b/docs/doxygen-user/images/keyword-search-configuration-dialog.PNG index 30649c9be4..3923522990 100755 Binary files a/docs/doxygen-user/images/keyword-search-configuration-dialog.PNG and b/docs/doxygen-user/images/keyword-search-configuration-dialog.PNG differ diff --git a/docs/doxygen-user/images/keyword-search-configuration-new-keywords.PNG b/docs/doxygen-user/images/keyword-search-configuration-new-keywords.PNG new file mode 100644 index 0000000000..a1d02bcbe7 Binary files /dev/null and b/docs/doxygen-user/images/keyword-search-configuration-new-keywords.PNG differ diff --git a/docs/doxygen-user/images/screenshot.PNG b/docs/doxygen-user/images/screenshot.PNG index a7a22bfd21..08dadc3cb5 100755 Binary files a/docs/doxygen-user/images/screenshot.PNG and b/docs/doxygen-user/images/screenshot.PNG differ diff --git a/docs/doxygen-user/images/select-data-source-type.PNG b/docs/doxygen-user/images/select-data-source-type.PNG index ab288f8334..662b84d495 100755 Binary files a/docs/doxygen-user/images/select-data-source-type.PNG and b/docs/doxygen-user/images/select-data-source-type.PNG differ diff --git a/docs/doxygen-user/images/tagging-3.PNG b/docs/doxygen-user/images/tagging-3.PNG index 92f5b3c940..7eff9ad481 100755 Binary files a/docs/doxygen-user/images/tagging-3.PNG and b/docs/doxygen-user/images/tagging-3.PNG differ diff --git a/docs/doxygen-user/images/tagging-5.PNG b/docs/doxygen-user/images/tagging-5.PNG new file mode 100644 index 0000000000..b18db5cc04 Binary files /dev/null and b/docs/doxygen-user/images/tagging-5.PNG differ diff --git a/docs/doxygen-user/images/threadcount.PNG b/docs/doxygen-user/images/threadcount.PNG index 5320f6b626..a23977033f 100755 Binary files a/docs/doxygen-user/images/threadcount.PNG and b/docs/doxygen-user/images/threadcount.PNG differ diff --git a/docs/doxygen-user/images/ui-layout-1.PNG b/docs/doxygen-user/images/ui-layout-1.PNG index 8aa266893b..1cc4568e56 100755 Binary files a/docs/doxygen-user/images/ui-layout-1.PNG and b/docs/doxygen-user/images/ui-layout-1.PNG differ diff --git a/docs/doxygen-user/images/unallocated_space_options.PNG b/docs/doxygen-user/images/unallocated_space_options.PNG new file mode 100644 index 0000000000..29742955a4 Binary files /dev/null and b/docs/doxygen-user/images/unallocated_space_options.PNG differ diff --git a/docs/doxygen-user/keyword_search.dox b/docs/doxygen-user/keyword_search.dox index 336996b1af..e743584a22 100644 --- a/docs/doxygen-user/keyword_search.dox +++ b/docs/doxygen-user/keyword_search.dox @@ -31,6 +31,13 @@ Autopsy supports importing Encase tab-delimited lists as well as lists created p Lists tab \n \image html keyword-search-configuration-dialog.PNG +The Lists tab is used to create/import and add content to keyword lists. Once a keyword list is selected, the "New Keywords" button can be used to add one or more entries to the list. + +
+\image html keyword-search-configuration-new-keywords.PNG + +New entries can be typed into the dialog or pasted from the clipboard. All entries added at once must be the same type of match (exact, substring, or regex), but the dialog can be used multiple times to add keywords to the keyword list. +
String extraction setting \n The string extraction setting defines how strings are extracted from files from which text cannot be extracted because their file formats are not supported. This is the case with arbitrary binary files (such as the page file) and chunks of unallocated space that represent deleted files. diff --git a/docs/doxygen-user/tagging.dox b/docs/doxygen-user/tagging.dox index c634486f96..c9cee12eb5 100755 --- a/docs/doxygen-user/tagging.dox +++ b/docs/doxygen-user/tagging.dox @@ -22,12 +22,19 @@ You can create arbitrary tag names. Autopsy remembers your tag names from previo \image html tagging-3.PNG -You can apply tags to groups of items at once. Select multiple items in the Blackboard, right click, and add the appropriate tag. +If you just want to tag the item with the default "Bookmark" tag, you can also use the keyboard shortcut control+B instead of going through the menus. + +You can also apply tags to groups of items at once. Select multiple items in the Blackboard, right click, and add the appropriate tag. Items may have more than one tag. -Tagged results are shown in the "Results" portion of the tree under "Tags". +Tagged results are shown in the "Results" portion of the tree under "Tags". Tagged items are also highlighted in the Results Viewer.
\image html tagging-4.PNG
+ +The list of tags can be edited through the Tags tab on the Options menu. +
+\image html tagging-5.PNG +
*/ diff --git a/docs/doxygen-user/uilayout.dox b/docs/doxygen-user/uilayout.dox index e5b298451b..a8e2b35efe 100644 --- a/docs/doxygen-user/uilayout.dox +++ b/docs/doxygen-user/uilayout.dox @@ -43,7 +43,7 @@ An example of the single file extraction option is shown below. \subsection ui_tree_views Views Views filter all the files in the case by some external property of the file, not by any internal analysis of the file. -- File Type Sorts files by file extension, and shows them in the appropriate group. For example, .mp3 and .wav both end up in the "Audio" group. +- File Type Sorts files by file extension or MIME type, and shows them in the appropriate group. For example, .mp3 and .wav both end up in the "Audio" group. - Recent Files Displays files that are accessed within the last seven days the user had the device. - Deleted Files Displays files that have been deleted but the names have been recovered. - File Size Sorts files based upon size. This can give you an idea where to look for files you are interested in. @@ -55,6 +55,7 @@ Views filter all the files in the case by some external property of the file, no - Hashset Hits: Hashset hits show up here - E-Mail Messages: Email messages show up here - Interesting Items: Things deemed interesting show up here +- Accounts: Credit card accounts show up here - Tags: Any item you tag shows up here so you can find it again easily \subsection ui_tree_reports Reports @@ -73,7 +74,7 @@ The Result Viewer windows are in the upper right area of the interface and displ \subsection right_click_functions Right Click Functions Viewers in Result Viewers have certain right-click functions built-in into them that can be accessed when a node a certain type is selected (a file, directory or a result). Here are some examples that you may see: -\li Open File in External Viewer: Opens the selected file in an "external" application as defined by the local OS. For example, HTML files may be opened by IE or Firefox, depending on what the local system is configured to use. +\li Open File in External Viewer: Opens the selected file in an "external" application as defined by the local OS or through the External Viewer tab on the Options menu. For example, HTML files may be opened by IE or Firefox, depending on what the local system is configured to use. \li View in New Window: Opens the content in a new internal Content Viewer (instead of in the default location in the lower right). \li Extract: Make a local copy of the file or directory for further analysis. \li Search for files with the same MD5 Hash: Searches the entire file-system for any files with the same MD5 Hash as the one selected. diff --git a/docs/doxygen/Doxyfile b/docs/doxygen/Doxyfile index e44aee3a51..654743e639 100755 --- a/docs/doxygen/Doxyfile +++ b/docs/doxygen/Doxyfile @@ -1063,7 +1063,7 @@ GENERATE_HTML = YES # The default directory is: html. # This tag requires that the tag GENERATE_HTML is set to YES. -HTML_OUTPUT = api-docs/4.2/ +HTML_OUTPUT = api-docs/4.3/ # The HTML_FILE_EXTENSION tag can be used to specify the file extension for each # generated HTML page (for example: .htm, .php, .asp). diff --git a/nbproject/project.properties b/nbproject/project.properties index 96b1848b29..7485e33aee 100644 --- a/nbproject/project.properties +++ b/nbproject/project.properties @@ -4,11 +4,12 @@ app.title=Autopsy ### lowercase version of above app.name=${branding.token} ### if left unset, version will default to today's date -app.version=4.2.0 +app.version=4.3.0 ### build.type must be one of: DEVELOPMENT, RELEASE 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 diff --git a/pythonExamples/README.txt b/pythonExamples/README.txt index 43eb78f661..b558f44e4b 100755 --- a/pythonExamples/README.txt +++ b/pythonExamples/README.txt @@ -5,7 +5,7 @@ your needs. See the developer guide for more details and how to use and load the modules. - http://sleuthkit.org/autopsy/docs/api-docs/4.2/index.html + http://sleuthkit.org/autopsy/docs/api-docs/4.3/index.html Each module in this folder should have a brief description about what they can do. diff --git a/test/script/tskdbdiff.py b/test/script/tskdbdiff.py index 89bbcbfd2d..d44274b57d 100755 --- a/test/script/tskdbdiff.py +++ b/test/script/tskdbdiff.py @@ -357,7 +357,7 @@ def normalize_db_entry(line, table): layout_index = line.find('INSERT INTO "tsk_file_layout"') data_source_info_index = line.find('INSERT INTO "data_source_info"') ingest_job_index = line.find('INSERT INTO "ingest_jobs"') - parens = line[line.find('(') + 1 : line.find(')')] + parens = line[line.find('(') + 1 : line.rfind(')')] fields_list = parens.replace(" ", "").split(',') # remove object ID diff --git a/thirdparty/crt/update.bat b/thirdparty/crt/update.bat deleted file mode 100644 index 807264ff07..0000000000 --- a/thirdparty/crt/update.bat +++ /dev/null @@ -1,9 +0,0 @@ -REM Updates the 32-bit and 64-bit VS Runtime dlls -REM Needs to be run from a 64-bit command prompt -REM Otherwise Windows will put 32-bit dlls in system32 -copy c:\windows\system32\msvcr100.dll win64 -copy c:\windows\system32\msvcp100.dll win64 -copy c:\windows\system32\msvcr120.dll win64 -copy c:\windows\sysWoW64\msvcr100.dll win32 -copy c:\windows\sysWow64\msvcp100.dll win32 -copy c:\windows\sysWow64\msvcr120.dll win32 diff --git a/thirdparty/crt/win32/msvcp100.dll b/thirdparty/crt/win32/msvcp100.dll deleted file mode 100755 index 8502dfae5e..0000000000 Binary files a/thirdparty/crt/win32/msvcp100.dll and /dev/null differ diff --git a/thirdparty/crt/win32/msvcr100.dll b/thirdparty/crt/win32/msvcr100.dll deleted file mode 100755 index 3e82b1aeac..0000000000 Binary files a/thirdparty/crt/win32/msvcr100.dll and /dev/null differ diff --git a/thirdparty/crt/win64/msvcp100.dll b/thirdparty/crt/win64/msvcp100.dll deleted file mode 100755 index 6f0cdf160a..0000000000 Binary files a/thirdparty/crt/win64/msvcp100.dll and /dev/null differ diff --git a/thirdparty/crt/win64/msvcr100.dll b/thirdparty/crt/win64/msvcr100.dll deleted file mode 100755 index b1c3a5e77c..0000000000 Binary files a/thirdparty/crt/win64/msvcr100.dll and /dev/null differ diff --git a/thunderbirdparser/nbproject/project.xml b/thunderbirdparser/nbproject/project.xml index f108aa7e5a..84f4663022 100644 --- a/thunderbirdparser/nbproject/project.xml +++ b/thunderbirdparser/nbproject/project.xml @@ -36,7 +36,7 @@ 10 - 10.6 + 10.7
diff --git a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java index ea5827d49e..9784a1b712 100644 --- a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java +++ b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java @@ -81,7 +81,8 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { } //skip unalloc - if (abstractFile.getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS)) { + if ((abstractFile.getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS)) || + (abstractFile.getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.SLACK))) { return ProcessResult.OK; }