diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java index ffd226d6d3..e4402a1b86 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2012-2020 Basis Technology Corp. + * Copyright 2012-2021 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -124,6 +124,7 @@ import org.sleuthkit.autopsy.ingest.IngestServices; import org.sleuthkit.autopsy.ingest.ModuleDataEvent; import org.sleuthkit.autopsy.keywordsearchservice.KeywordSearchService; import org.sleuthkit.autopsy.keywordsearchservice.KeywordSearchServiceException; +import org.sleuthkit.autopsy.machinesettings.UserMachinePreferences; import org.sleuthkit.autopsy.progress.LoggingProgressIndicator; import org.sleuthkit.autopsy.progress.ModalDialogProgressIndicator; import org.sleuthkit.autopsy.progress.ProgressIndicator; @@ -143,7 +144,6 @@ import org.sleuthkit.datamodel.HostManager.HostsUpdateEvent; import org.sleuthkit.datamodel.HostManager.HostsDeletionEvent; import org.sleuthkit.datamodel.Image; import org.sleuthkit.datamodel.OsAccount; -import org.sleuthkit.datamodel.OsAccountManager; import org.sleuthkit.datamodel.OsAccountManager.OsAccountsCreationEvent; import org.sleuthkit.datamodel.OsAccountManager.OsAccountsUpdateEvent; import org.sleuthkit.datamodel.Person; @@ -1478,16 +1478,7 @@ public class Case { * @return The temp subdirectory path. */ public String getTempDirectory() { - // get temp folder scoped to the combination of case name and timestamp - // provided by getName() - Path path = Paths.get(UserPreferences.getAppTempDirectory(), CASE_TEMP_DIR, getName()); - File f = path.toFile(); - // verify that the folder exists - if (!f.exists()) { - f.mkdirs(); - } - - return path.toAbsolutePath().toString(); + return UserMachinePreferences.getTempDirectory(); } /** diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCasesBrowserPanel.form b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCasesBrowserPanel.form index ada4c7f484..9b8c2c8f72 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCasesBrowserPanel.form +++ b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCasesBrowserPanel.form @@ -23,25 +23,4 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCasesBrowserPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCasesBrowserPanel.java index 154e692663..04785cc83d 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCasesBrowserPanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/multiusercasesbrowser/MultiUserCasesBrowserPanel.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2017-2019 Basis Technology Corp. + * Copyright 2017-2021 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,6 +18,7 @@ */ package org.sleuthkit.autopsy.casemodule.multiusercasesbrowser; +import java.awt.BorderLayout; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -68,8 +69,7 @@ public final class MultiUserCasesBrowserPanel extends javax.swing.JPanel impleme outlineView = new org.openide.explorer.view.OutlineView(); outline = this.outlineView.getOutline(); configureOutlineView(); - caseTableScrollPane.add(outlineView); - caseTableScrollPane.setViewportView(outlineView); + add(outlineView, BorderLayout.CENTER); this.setVisible(true); } @@ -146,20 +146,11 @@ public final class MultiUserCasesBrowserPanel extends javax.swing.JPanel impleme // //GEN-BEGIN:initComponents private void initComponents() { - caseTableScrollPane = new javax.swing.JScrollPane(); - setMinimumSize(new java.awt.Dimension(0, 5)); setPreferredSize(new java.awt.Dimension(5, 5)); setLayout(new java.awt.BorderLayout()); - - caseTableScrollPane.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); - caseTableScrollPane.setMinimumSize(new java.awt.Dimension(0, 5)); - caseTableScrollPane.setOpaque(false); - caseTableScrollPane.setPreferredSize(new java.awt.Dimension(500, 500)); - add(caseTableScrollPane, java.awt.BorderLayout.CENTER); }// //GEN-END:initComponents // Variables declaration - do not modify//GEN-BEGIN:variables - private javax.swing.JScrollPane caseTableScrollPane; // End of variables declaration//GEN-END:variables } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/SqliteCentralRepoSettings.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/SqliteCentralRepoSettings.java index a938dd166a..2e52694416 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/SqliteCentralRepoSettings.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/SqliteCentralRepoSettings.java @@ -22,6 +22,8 @@ import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; @@ -41,7 +43,15 @@ public final class SqliteCentralRepoSettings implements CentralRepoDbConnectivit public final static String DEFAULT_DBNAME = "central_repository.db"; // NON-NLS private final static Logger LOGGER = Logger.getLogger(SqliteCentralRepoSettings.class.getName()); + private final Path userConfigDir = Paths.get(PlatformUtil.getUserDirectory().getAbsolutePath()); private final static String DEFAULT_DBDIRECTORY = PlatformUtil.getUserDirectory() + File.separator + "central_repository"; // NON-NLS + + //property names + private static final String PROFILE_NAME = "CentralRepository"; + private static final String DATABASE_NAME = "db.sqlite.dbName"; //NON-NLS + private static final String DATABASE_PATH = "db.sqlite.dbDirectory"; //NON-NLS + private static final String BULK_THRESHOLD = "db.sqlite.bulkThreshold"; //NON-NLS + private final static String JDBC_DRIVER = "org.sqlite.JDBC"; // NON-NLS private final static String JDBC_BASE_URI = "jdbc:sqlite:"; // NON-NLS private final static String VALIDATION_QUERY = "SELECT count(*) from sqlite_master"; // NON-NLS @@ -56,18 +66,18 @@ public final class SqliteCentralRepoSettings implements CentralRepoDbConnectivit } public void loadSettings() { - dbName = ModuleSettings.getConfigSetting("CentralRepository", "db.sqlite.dbName"); // NON-NLS + dbName = ModuleSettings.getConfigSetting(PROFILE_NAME, DATABASE_NAME); // NON-NLS if (dbName == null || dbName.isEmpty()) { dbName = DEFAULT_DBNAME; } - dbDirectory = ModuleSettings.getConfigSetting("CentralRepository", "db.sqlite.dbDirectory"); // NON-NLS + dbDirectory = readDbPath(); // NON-NLS if (dbDirectory == null || dbDirectory.isEmpty()) { dbDirectory = DEFAULT_DBDIRECTORY; } try { - String bulkThresholdString = ModuleSettings.getConfigSetting("CentralRepository", "db.sqlite.bulkThreshold"); // NON-NLS + String bulkThresholdString = ModuleSettings.getConfigSetting(PROFILE_NAME, BULK_THRESHOLD); // NON-NLS if (bulkThresholdString == null || bulkThresholdString.isEmpty()) { this.bulkThreshold = RdbmsCentralRepo.DEFAULT_BULK_THRESHHOLD; } else { @@ -96,9 +106,64 @@ public final class SqliteCentralRepoSettings implements CentralRepoDbConnectivit public void saveSettings() { createDbDirectory(); - ModuleSettings.setConfigSetting("CentralRepository", "db.sqlite.dbName", getDbName()); // NON-NLS - ModuleSettings.setConfigSetting("CentralRepository", "db.sqlite.dbDirectory", getDbDirectory()); // NON-NLS - ModuleSettings.setConfigSetting("CentralRepository", "db.sqlite.bulkThreshold", Integer.toString(getBulkThreshold())); // NON-NLS + ModuleSettings.setConfigSetting(PROFILE_NAME, DATABASE_NAME, getDbName()); // NON-NLS + saveDbPath(getDbDirectory()); // NON-NLS + ModuleSettings.setConfigSetting(PROFILE_NAME, BULK_THRESHOLD, Integer.toString(getBulkThreshold())); // NON-NLS + } + + /** + * Save CR database path. If the path is inside user directory (e.g. + * "C:\Users\USER_NAME\AppData\Roaming\autopsy"), trim that off and save it + * as a relative path (i.e it will not start with a “/” or drive letter). Otherwise, + * full path is saved. See JIRA-7348. + * + * @param fullPath Full path to the SQLite db file. + */ + private void saveDbPath(String fullPath) { + Path relativePath = Paths.get(fullPath); + // check if the path is within user directory + if (Paths.get(fullPath).startsWith(userConfigDir)) { + // relativize the path + relativePath = userConfigDir.relativize(relativePath); + } + // Use properties to persist the logo to use. + ModuleSettings.setConfigSetting(PROFILE_NAME, DATABASE_PATH, relativePath.toString()); + } + + /** + * Read CD database path from preferences file. Reverses the path relativization performed + * in saveDbPath(). If the stored path starts with either “/” or drive letter, + * it is a full path, and is returned to the caller. Otherwise, append current user + * directory to the saved relative path. See JIRA-7348. + * + * @return Full path to the SQLite CR database file. + */ + private String readDbPath() { + + String curPath = ModuleSettings.getConfigSetting(PROFILE_NAME, DATABASE_PATH); + + + //if has been set, validate it's correct, if not set, return null + if (curPath != null && !curPath.isEmpty()) { + + // check if the path is an absolute path (starts with either drive letter or "/") + Path driveLetterOrNetwork = Paths.get(curPath).getRoot(); + if (driveLetterOrNetwork != null) { + // absolute path + return curPath; + } + + // Path is a relative path. Reverse path relativization performed in saveDbPath() + Path absolutePath = userConfigDir.resolve(curPath); + curPath = absolutePath.toString(); + if (new File(curPath).canRead() == false) { + //use default + LOGGER.log(Level.INFO, "Path to SQLite Central Repository database is not valid: {0}", curPath); //NON-NLS + curPath = null; + } + } + + return curPath; } /** @@ -252,9 +317,9 @@ public final class SqliteCentralRepoSettings implements CentralRepoDbConnectivit } boolean isChanged() { - String dbNameString = ModuleSettings.getConfigSetting("CentralRepository", "db.sqlite.dbName"); // NON-NLS - String dbDirectoryString = ModuleSettings.getConfigSetting("CentralRepository", "db.sqlite.dbDirectory"); // NON-NLS - String bulkThresholdString = ModuleSettings.getConfigSetting("CentralRepository", "db.sqlite.bulkThreshold"); // NON-NLS + String dbNameString = ModuleSettings.getConfigSetting(PROFILE_NAME, DATABASE_NAME); // NON-NLS + String dbDirectoryString = readDbPath(); // NON-NLS + String bulkThresholdString = ModuleSettings.getConfigSetting(PROFILE_NAME, BULK_THRESHOLD); // NON-NLS return !dbName.equals(dbNameString) || !dbDirectory.equals(dbDirectoryString) diff --git a/Core/src/org/sleuthkit/autopsy/communications/relationships/MessageViewer.java b/Core/src/org/sleuthkit/autopsy/communications/relationships/MessageViewer.java index f9989c5f55..005689d8a8 100755 --- a/Core/src/org/sleuthkit/autopsy/communications/relationships/MessageViewer.java +++ b/Core/src/org/sleuthkit/autopsy/communications/relationships/MessageViewer.java @@ -192,8 +192,8 @@ final class MessageViewer extends JPanel implements RelationshipsViewer { if (isDescendingFrom(newFocusOwner, rootTablePane)) { proxyLookup.setNewLookups(createLookup(rootTablePane.getExplorerManager(), getActionMap())); } else if (isDescendingFrom(newFocusOwner, this)) { - proxyLookup.setNewLookups(createLookup(currentPanel.getExplorerManager(), getActionMap())); - } + proxyLookup.setNewLookups(createLookup(threadMessagesPanel.getExplorerManager(), getActionMap())); + } } @Override diff --git a/Core/src/org/sleuthkit/autopsy/communications/relationships/MessagesPanel.java b/Core/src/org/sleuthkit/autopsy/communications/relationships/MessagesPanel.java index f409c288f7..1955cc333b 100755 --- a/Core/src/org/sleuthkit/autopsy/communications/relationships/MessagesPanel.java +++ b/Core/src/org/sleuthkit/autopsy/communications/relationships/MessagesPanel.java @@ -113,7 +113,7 @@ class MessagesPanel extends javax.swing.JPanel implements Lookup.Provider { outlineViewPanel.setTableColumnsWidth(5, 10, 10, 15, 50, 10); } - public MessagesPanel(ChildFactory nodeFactory) { + MessagesPanel(ChildFactory nodeFactory) { this(); setChildFactory(nodeFactory); } @@ -122,6 +122,15 @@ class MessagesPanel extends javax.swing.JPanel implements Lookup.Provider { public Lookup getLookup() { return proxyLookup; } + + /** + * Return the explorerManager for the table. + * + * @return The explorer manager for the table. + */ + ExplorerManager getExplorerManager() { + return outlineViewPanel.getExplorerManager(); + } @Override public void addNotify() { diff --git a/Core/src/org/sleuthkit/autopsy/core/Installer.java b/Core/src/org/sleuthkit/autopsy/core/Installer.java index 0612729b61..5bd649e8cb 100644 --- a/Core/src/org/sleuthkit/autopsy/core/Installer.java +++ b/Core/src/org/sleuthkit/autopsy/core/Installer.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2018 Basis Technology Corp. + * Copyright 2011-2021 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -50,6 +50,7 @@ import org.sleuthkit.autopsy.coreutils.ModuleSettings; import org.sleuthkit.autopsy.coreutils.PlatformUtil; import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector; import org.sleuthkit.autopsy.python.JythonModuleLoader; +import org.sleuthkit.autopsy.texttranslation.TextTranslationService; /** * Wrapper over Installers in packages in Core module. This is the main @@ -369,6 +370,7 @@ public class Installer extends ModuleInstall { } logger.log(Level.INFO, "Autopsy Core restore completed"); //NON-NLS preloadJython(); + preloadTranslationServices(); } /** @@ -376,7 +378,7 @@ public class Installer extends ModuleInstall { * because we encountered issues related to file locking when initialization * was performed closer to where the bindings are used. See JIRA-6528. */ - private void initializeSevenZip() { + private static void initializeSevenZip() { try { SevenZip.initSevenZipFromPlatformJAR(); logger.log(Level.INFO, "7zip-java bindings loaded"); //NON-NLS @@ -388,7 +390,7 @@ public class Installer extends ModuleInstall { /** * Runs an initial load of the Jython modules to speed up subsequent loads. */ - private void preloadJython() { + private static void preloadJython() { Runnable loader = () -> { try { JythonModuleLoader.getIngestModuleFactories(); @@ -402,6 +404,22 @@ public class Installer extends ModuleInstall { }; new Thread(loader).start(); } + + /** + * Runs an initial load of the translation services to speed up subsequent loads. + */ + private static void preloadTranslationServices() { + Runnable loader = () -> { + try { + TextTranslationService.getInstance(); + } catch (Exception ex) { + // This is a firewall exception to ensure that any possible exception caused + // by this initial load of the translation modules are caught and logged. + logger.log(Level.SEVERE, "There was an error while doing an initial load of translation services.", ex); + } + }; + new Thread(loader).start(); + } @Override public void validate() throws IllegalStateException { diff --git a/Core/src/org/sleuthkit/autopsy/core/UserPreferences.java b/Core/src/org/sleuthkit/autopsy/core/UserPreferences.java index 537c25c827..395b8cd830 100644 --- a/Core/src/org/sleuthkit/autopsy/core/UserPreferences.java +++ b/Core/src/org/sleuthkit/autopsy/core/UserPreferences.java @@ -674,8 +674,7 @@ public final class UserPreferences { * @return The absolute path to the application temp directory. */ public static String getAppTempDirectory() { - return Paths.get(UserMachinePreferences.getBaseTempDirectory(), getAppName()) - .toAbsolutePath().toString(); + return UserMachinePreferences.getTempDirectory(); } /** diff --git a/Core/src/org/sleuthkit/autopsy/corecomponentinterfaces/CoreComponentControl.java b/Core/src/org/sleuthkit/autopsy/corecomponentinterfaces/CoreComponentControl.java index 06ad27962a..a3b1620b27 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponentinterfaces/CoreComponentControl.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponentinterfaces/CoreComponentControl.java @@ -27,6 +27,7 @@ import org.openide.windows.TopComponent; import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.corecomponents.DataContentTopComponent; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.directorytree.DirectoryTreeTopComponent; /** * Responsible for opening and closing the core windows when a case is opened @@ -49,7 +50,8 @@ final public class CoreComponentControl { * ({@link DataExplorer}, {@link DataResult}, and {@link DataContent}) */ public static void openCoreWindows() { - // TODO: there has to be a better way to do this. + // preload UI components (JIRA-7345). This only takes place the first time Autopsy opens a case. + DirectoryTreeTopComponent dtc = DirectoryTreeTopComponent.findInstance(); // find the data explorer top components Collection dataExplorers = Lookup.getDefault().lookupAll(DataExplorer.class); diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/AutopsyOptionsPanel.form b/Core/src/org/sleuthkit/autopsy/corecomponents/AutopsyOptionsPanel.form index 79746c704e..4c42c44668 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/AutopsyOptionsPanel.form +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/AutopsyOptionsPanel.form @@ -8,6 +8,8 @@ + + @@ -24,12 +26,12 @@ - + - + @@ -418,7 +420,7 @@ - + @@ -444,37 +446,53 @@ - - - + + + + + - + + + + + + + + - + + + + + - + + - + + + - + - + - + @@ -498,6 +516,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/AutopsyOptionsPanel.java b/Core/src/org/sleuthkit/autopsy/corecomponents/AutopsyOptionsPanel.java index 1d4157e97d..193b1836e9 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/AutopsyOptionsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/AutopsyOptionsPanel.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2019 Basis Technology Corp. + * Copyright 2011-2021 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -38,6 +38,7 @@ import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; +import org.apache.commons.lang3.StringUtils; import org.netbeans.spi.options.OptionsPanelController; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; @@ -47,9 +48,9 @@ import org.sleuthkit.autopsy.casemodule.GeneralFilter; import org.sleuthkit.autopsy.machinesettings.UserMachinePreferences; import org.sleuthkit.autopsy.machinesettings.UserMachinePreferencesException; import org.sleuthkit.autopsy.core.UserPreferences; -import org.sleuthkit.autopsy.coreutils.ModuleSettings; import org.sleuthkit.autopsy.coreutils.PlatformUtil; import org.sleuthkit.autopsy.coreutils.Version; +import org.sleuthkit.autopsy.machinesettings.UserMachinePreferences.TempDirChoice; import org.sleuthkit.autopsy.report.ReportBranding; /** @@ -77,7 +78,6 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { private static final long serialVersionUID = 1L; private final JFileChooser logoFileChooser; private final JFileChooser tempDirChooser; - private final TextFieldListener textFieldListener; private static final String ETC_FOLDER_NAME = "etc"; private static final String CONFIG_FILE_EXTENSION = ".conf"; private static final long ONE_BILLION = 1000000000L; //used to roughly convert system memory from bytes to gigabytes @@ -86,6 +86,8 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { private static final int MIN_MEMORY_IN_GB = 2; //the enforced minimum memory in gigabytes private static final Logger logger = Logger.getLogger(AutopsyOptionsPanel.class.getName()); private String initialMemValue = Long.toString(Runtime.getRuntime().maxMemory() / ONE_BILLION); + + private final ReportBranding reportBranding; /** * Instantiate the Autopsy options panel. @@ -116,10 +118,12 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { solrMaxHeapSpinner.setModel(new javax.swing.SpinnerNumberModel(UserPreferences.getMaxSolrVMSize(), JVM_MEMORY_STEP_SIZE_MB, ((int) getSystemMemoryInGB()) * MEGA_IN_GIGA, JVM_MEMORY_STEP_SIZE_MB)); - textFieldListener = new TextFieldListener(); + TextFieldListener textFieldListener = new TextFieldListener(); agencyLogoPathField.getDocument().addDocumentListener(textFieldListener); - tempDirectoryField.getDocument().addDocumentListener(textFieldListener); + tempCustomField.getDocument().addDocumentListener(new TempCustomTextListener()); logFileCount.setText(String.valueOf(UserPreferences.getLogFileCount())); + + reportBranding = new ReportBranding(); } /** @@ -298,21 +302,46 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { } return new String[]{}; } + + private void evaluateTempDirState() { + boolean caseOpen = Case.isCaseOpen(); + boolean customSelected = tempCustomRadio.isSelected(); + + tempDirectoryBrowseButton.setEnabled(!caseOpen && customSelected); + tempCustomField.setEnabled(!caseOpen && customSelected); + + tempOnCustomNoPath.setVisible(customSelected && StringUtils.isBlank(tempCustomField.getText())); + } /** * Load the saved user preferences. */ void load() { - String path = ModuleSettings.getConfigSetting(ReportBranding.MODULE_NAME, ReportBranding.AGENCY_LOGO_PATH_PROP); + String path = reportBranding.getAgencyLogoPath(); boolean useDefault = (path == null || path.isEmpty()); defaultLogoRB.setSelected(useDefault); specifyLogoRB.setSelected(!useDefault); agencyLogoPathField.setEnabled(!useDefault); browseLogosButton.setEnabled(!useDefault); - tempDirectoryField.setText(UserMachinePreferences.getBaseTempDirectory()); + + tempCustomField.setText(UserMachinePreferences.getCustomTempDirectory()); + switch (UserMachinePreferences.getTempDirChoice()) { + case CASE: + tempCaseRadio.setSelected(true); + break; + case CUSTOM: + tempCustomRadio.setSelected(true); + break; + default: + case SYSTEM: + tempLocalRadio.setSelected(true); + break; + } + + evaluateTempDirState(); + logFileCount.setText(String.valueOf(UserPreferences.getLogFileCount())); solrMaxHeapSpinner.setValue(UserPreferences.getMaxSolrVMSize()); - tempDirectoryField.setText(UserMachinePreferences.getBaseTempDirectory()); try { updateAgencyLogo(path); } catch (IOException ex) { @@ -333,9 +362,13 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { private void setTempDirEnabled() { boolean enabled = !Case.isCaseOpen(); - this.tempDirectoryBrowseButton.setEnabled(enabled); - this.tempDirectoryField.setEnabled(enabled); + + this.tempCaseRadio.setEnabled(enabled); + this.tempCustomRadio.setEnabled(enabled); + this.tempLocalRadio.setEnabled(enabled); + this.tempDirectoryWarningLabel.setVisible(!enabled); + evaluateTempDirState(); } /** @@ -367,12 +400,14 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { @Messages({ "AutopsyOptionsPanel_storeTempDir_onError_title=Error Saving Temporary Directory", "# {0} - path", - "AutopsyOptionsPanel_storeTempDir_onError_description=There was an error creating the temporary directory on the filesystem at: {0}.",}) + "AutopsyOptionsPanel_storeTempDir_onError_description=There was an error creating the temporary directory on the filesystem at: {0}.", + "AutopsyOptionsPanel_storeTempDir_onChoiceError_title=Error Saving Temporary Directory Choice", + "AutopsyOptionsPanel_storeTempDir_onChoiceError_description=There was an error updating temporary directory choice selection.",}) private void storeTempDir() { - String tempDirectoryPath = tempDirectoryField.getText(); - if (!UserMachinePreferences.getBaseTempDirectory().equals(tempDirectoryPath)) { + String tempDirectoryPath = tempCustomField.getText(); + if (!UserMachinePreferences.getCustomTempDirectory().equals(tempDirectoryPath)) { try { - UserMachinePreferences.setBaseTempDirectory(tempDirectoryPath); + UserMachinePreferences.setCustomTempDirectory(tempDirectoryPath); } catch (UserMachinePreferencesException ex) { logger.log(Level.WARNING, "There was an error creating the temporary directory defined by the user: " + tempDirectoryPath, ex); SwingUtilities.invokeLater(() -> { @@ -383,6 +418,29 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { }); } } + + TempDirChoice choice; + if (tempCaseRadio.isSelected()) { + choice = TempDirChoice.CASE; + } else if (tempCustomRadio.isSelected()) { + choice = TempDirChoice.CUSTOM; + } else { + choice = TempDirChoice.SYSTEM; + } + + if (!choice.equals(UserMachinePreferences.getTempDirChoice())) { + try { + UserMachinePreferences.setTempDirChoice(choice); + } catch (UserMachinePreferencesException ex) { + logger.log(Level.WARNING, "There was an error updating choice to: " + choice.name(), ex); + SwingUtilities.invokeLater(() -> { + JOptionPane.showMessageDialog(this, + String.format("%s", Bundle.AutopsyOptionsPanel_storeTempDir_onChoiceError_description()), + Bundle.AutopsyOptionsPanel_storeTempDir_onChoiceError_title(), + JOptionPane.ERROR_MESSAGE); + }); + } + } } /** @@ -395,10 +453,10 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { if (!agencyLogoPathField.getText().isEmpty()) { File file = new File(agencyLogoPathField.getText()); if (file.exists()) { - ModuleSettings.setConfigSetting(ReportBranding.MODULE_NAME, ReportBranding.AGENCY_LOGO_PATH_PROP, agencyLogoPathField.getText()); + reportBranding.setAgencyLogoPath(agencyLogoPathField.getText()); } } else { - ModuleSettings.setConfigSetting(ReportBranding.MODULE_NAME, ReportBranding.AGENCY_LOGO_PATH_PROP, ""); + reportBranding.setAgencyLogoPath(""); } UserPreferences.setMaxSolrVMSize((int) solrMaxHeapSpinner.getValue()); if (memField.isEnabled()) { //if the field could of been changed we need to try and save it @@ -550,6 +608,32 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null); } } + + /** + * Listens for changes in the temp directory custom directory text field. + */ + private class TempCustomTextListener extends TextFieldListener { + + @Override + public void changedUpdate(DocumentEvent e) { + evaluateTempDirState(); + super.changedUpdate(e); + } + + @Override + public void removeUpdate(DocumentEvent e) { + evaluateTempDirState(); + super.changedUpdate(e); + } + + @Override + public void insertUpdate(DocumentEvent e) { + evaluateTempDirState(); + super.changedUpdate(e); + } + + + } /** * This method is called from within the constructor to initialize the form. @@ -563,6 +647,7 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { fileSelectionButtonGroup = new javax.swing.ButtonGroup(); displayTimesButtonGroup = new javax.swing.ButtonGroup(); logoSourceButtonGroup = new javax.swing.ButtonGroup(); + tempDirChoiceGroup = new javax.swing.ButtonGroup(); jScrollPane1 = new javax.swing.JScrollPane(); javax.swing.JPanel mainPanel = new javax.swing.JPanel(); logoPanel = new javax.swing.JPanel(); @@ -589,9 +674,13 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { solrMaxHeapSpinner = new javax.swing.JSpinner(); solrJVMHeapWarning = new javax.swing.JLabel(); tempDirectoryPanel = new javax.swing.JPanel(); - tempDirectoryField = new javax.swing.JTextField(); + tempCustomField = new javax.swing.JTextField(); tempDirectoryBrowseButton = new javax.swing.JButton(); tempDirectoryWarningLabel = new javax.swing.JLabel(); + tempLocalRadio = new javax.swing.JRadioButton(); + tempCaseRadio = new javax.swing.JRadioButton(); + tempCustomRadio = new javax.swing.JRadioButton(); + tempOnCustomNoPath = new javax.swing.JLabel(); rdpPanel = new javax.swing.JPanel(); javax.swing.JScrollPane sizingScrollPane = new javax.swing.JScrollPane(); javax.swing.JTextPane sizingTextPane = new javax.swing.JTextPane(); @@ -818,7 +907,7 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { tempDirectoryPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(org.openide.util.NbBundle.getMessage(AutopsyOptionsPanel.class, "AutopsyOptionsPanel.tempDirectoryPanel.border.title"))); // NOI18N tempDirectoryPanel.setName(org.openide.util.NbBundle.getMessage(AutopsyOptionsPanel.class, "AutopsyOptionsPanel.tempDirectoryPanel.name")); // NOI18N - tempDirectoryField.setText(org.openide.util.NbBundle.getMessage(AutopsyOptionsPanel.class, "AutopsyOptionsPanel.tempDirectoryField.text")); // NOI18N + tempCustomField.setText(org.openide.util.NbBundle.getMessage(AutopsyOptionsPanel.class, "AutopsyOptionsPanel.tempCustomField.text")); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(tempDirectoryBrowseButton, org.openide.util.NbBundle.getMessage(AutopsyOptionsPanel.class, "AutopsyOptionsPanel.tempDirectoryBrowseButton.text")); // NOI18N tempDirectoryBrowseButton.addActionListener(new java.awt.event.ActionListener() { @@ -830,6 +919,33 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { tempDirectoryWarningLabel.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/corecomponents/warning16.png"))); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(tempDirectoryWarningLabel, org.openide.util.NbBundle.getMessage(AutopsyOptionsPanel.class, "AutopsyOptionsPanel.tempDirectoryWarningLabel.text")); // NOI18N + tempDirChoiceGroup.add(tempLocalRadio); + org.openide.awt.Mnemonics.setLocalizedText(tempLocalRadio, org.openide.util.NbBundle.getMessage(AutopsyOptionsPanel.class, "AutopsyOptionsPanel.tempLocalRadio.text")); // NOI18N + tempLocalRadio.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + tempLocalRadioActionPerformed(evt); + } + }); + + tempDirChoiceGroup.add(tempCaseRadio); + org.openide.awt.Mnemonics.setLocalizedText(tempCaseRadio, org.openide.util.NbBundle.getMessage(AutopsyOptionsPanel.class, "AutopsyOptionsPanel.tempCaseRadio.text")); // NOI18N + tempCaseRadio.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + tempCaseRadioActionPerformed(evt); + } + }); + + tempDirChoiceGroup.add(tempCustomRadio); + org.openide.awt.Mnemonics.setLocalizedText(tempCustomRadio, org.openide.util.NbBundle.getMessage(AutopsyOptionsPanel.class, "AutopsyOptionsPanel.tempCustomRadio.text")); // NOI18N + tempCustomRadio.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + tempCustomRadioActionPerformed(evt); + } + }); + + tempOnCustomNoPath.setForeground(java.awt.Color.RED); + org.openide.awt.Mnemonics.setLocalizedText(tempOnCustomNoPath, org.openide.util.NbBundle.getMessage(AutopsyOptionsPanel.class, "AutopsyOptionsPanel.tempOnCustomNoPath.text")); // NOI18N + javax.swing.GroupLayout tempDirectoryPanelLayout = new javax.swing.GroupLayout(tempDirectoryPanel); tempDirectoryPanel.setLayout(tempDirectoryPanelLayout); tempDirectoryPanelLayout.setHorizontalGroup( @@ -837,23 +953,37 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { .addGroup(tempDirectoryPanelLayout.createSequentialGroup() .addContainerGap() .addGroup(tempDirectoryPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(tempLocalRadio) + .addComponent(tempCaseRadio) .addComponent(tempDirectoryWarningLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 615, javax.swing.GroupLayout.PREFERRED_SIZE) .addGroup(tempDirectoryPanelLayout.createSequentialGroup() - .addComponent(tempDirectoryField, javax.swing.GroupLayout.PREFERRED_SIZE, 367, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(tempCustomRadio) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(tempDirectoryBrowseButton))) - .addGap(0, 0, Short.MAX_VALUE)) + .addGroup(tempDirectoryPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(tempOnCustomNoPath) + .addGroup(tempDirectoryPanelLayout.createSequentialGroup() + .addComponent(tempCustomField, javax.swing.GroupLayout.PREFERRED_SIZE, 459, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(tempDirectoryBrowseButton))))) + .addContainerGap(158, Short.MAX_VALUE)) ); tempDirectoryPanelLayout.setVerticalGroup( tempDirectoryPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(tempDirectoryPanelLayout.createSequentialGroup() .addContainerGap() + .addComponent(tempLocalRadio) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(tempCaseRadio) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(tempDirectoryPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(tempDirectoryField) + .addComponent(tempCustomRadio) + .addComponent(tempCustomField) .addComponent(tempDirectoryBrowseButton)) - .addGap(18, 18, 18) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(tempOnCustomNoPath) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(tempDirectoryWarningLabel) - .addContainerGap()) + .addGap(14, 14, 14)) ); gridBagConstraints = new java.awt.GridBagConstraints(); @@ -906,11 +1036,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.DEFAULT_SIZE, 648, Short.MAX_VALUE) + .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 860, Short.MAX_VALUE) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 382, Short.MAX_VALUE) + .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 620, Short.MAX_VALUE) ); }// //GEN-END:initComponents @@ -927,7 +1057,7 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { if (!f.exists() && !f.mkdirs()) { throw new InvalidPathException(specifiedPath, "Unable to create parent directories leading to " + specifiedPath); } - tempDirectoryField.setText(specifiedPath); + tempCustomField.setText(specifiedPath); firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null); } catch (InvalidPathException ex) { logger.log(Level.WARNING, "Unable to create temporary directory in " + specifiedPath, ex); @@ -971,7 +1101,7 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { browseLogosButton.setEnabled(true); try { if (agencyLogoPathField.getText().isEmpty()) { - String path = ModuleSettings.getConfigSetting(ReportBranding.MODULE_NAME, ReportBranding.AGENCY_LOGO_PATH_PROP); + String path = reportBranding.getAgencyLogoPath(); if (path != null && !path.isEmpty()) { updateAgencyLogo(path); } @@ -1017,6 +1147,21 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { } }//GEN-LAST:event_browseLogosButtonActionPerformed + private void tempLocalRadioActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_tempLocalRadioActionPerformed + firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null); + evaluateTempDirState(); + }//GEN-LAST:event_tempLocalRadioActionPerformed + + private void tempCaseRadioActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_tempCaseRadioActionPerformed + firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null); + evaluateTempDirState(); + }//GEN-LAST:event_tempCaseRadioActionPerformed + + private void tempCustomRadioActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_tempCustomRadioActionPerformed + firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null); + evaluateTempDirState(); + }//GEN-LAST:event_tempCustomRadioActionPerformed + // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JTextField agencyLogoPathField; private javax.swing.JLabel agencyLogoPathFieldValidationLabel; @@ -1045,10 +1190,15 @@ final class AutopsyOptionsPanel extends javax.swing.JPanel { private javax.swing.JSpinner solrMaxHeapSpinner; private javax.swing.JRadioButton specifyLogoRB; private javax.swing.JLabel systemMemoryTotal; + private javax.swing.JRadioButton tempCaseRadio; + private javax.swing.JTextField tempCustomField; + private javax.swing.JRadioButton tempCustomRadio; + private javax.swing.ButtonGroup tempDirChoiceGroup; private javax.swing.JButton tempDirectoryBrowseButton; - private javax.swing.JTextField tempDirectoryField; private javax.swing.JPanel tempDirectoryPanel; private javax.swing.JLabel tempDirectoryWarningLabel; + private javax.swing.JRadioButton tempLocalRadio; + private javax.swing.JLabel tempOnCustomNoPath; private javax.swing.JLabel totalMemoryLabel; // 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 b880747533..bad8f8ff83 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties @@ -188,12 +188,11 @@ MultiUserSettingsPanel.restartRequiredLabel.text=Application restart required to MultiUserSettingsPanel.MustRestart=Autopsy must be restarted for new configuration to take effect MultiUserSettingsPanel.lbSolrNote1.text=Enter Solr 8 and/or Solr 4 server settings. MultiUserSettingsPanel.lbSolrNote2.text=New text indexing can only be done with Solr 8. -AutopsyOptionsPanel.tempDirectoryField.text= AutopsyOptionsPanel.tempDirectoryBrowseButton.text=Browse AutopsyOptionsPanel.a.AccessibleContext.accessibleName=Temp Directory AutopsyOptionsPanel.tempDirectoryPanel.AccessibleContext.accessibleName=Temp Directory AutopsyOptionsPanel.tempDirectoryPanel.name=Temp Directory -AutopsyOptionsPanel.tempDirectoryPanel.border.title=Temp Directory +AutopsyOptionsPanel.tempDirectoryPanel.border.title=Root Temp Directory AutopsyOptionsPanel.tempDirectoryWarningLabel.text=Close the current case to change the temporary directory. AutopsyOptionsPanel.solrJVMHeapWarning.text=NOTE: Setting this too large may impact overall performance. AutopsyOptionsPanel.maxMemoryUnitsLabel2.text=MB @@ -247,3 +246,8 @@ AutopsyOptionsPanel.agencyLogoPathField.text= AutopsyOptionsPanel.logoPanel.border.title=Logo ViewPreferencesPanel.radioGroupByPersonHost.text=Group by Person/Host ViewPreferencesPanel.radioGroupByDataType.text=Group by Data Type +AutopsyOptionsPanel.tempLocalRadio.text=Local temp directory +AutopsyOptionsPanel.tempCaseRadio.text=Temp folder in case directory +AutopsyOptionsPanel.tempCustomRadio.text=Custom +AutopsyOptionsPanel.tempCustomField.text= +AutopsyOptionsPanel.tempOnCustomNoPath.text=Please select a path for the custom root temp directory. diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties-MERGED index a1520ece04..bcf95a19a9 100755 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties-MERGED @@ -12,6 +12,8 @@ AutopsyOptionsPanel.memFieldValidationLabel.noValueEntered.text=No value entered AutopsyOptionsPanel.memFieldValidationLabel.overMaxMemory.text=Value must be less than the total system memory of {0}GB # {0} - minimumMemory AutopsyOptionsPanel.memFieldValidationLabel.underMinMemory.text=Value must be at least {0}GB +AutopsyOptionsPanel_storeTempDir_onChoiceError_description=There was an error updating temporary directory choice selection. +AutopsyOptionsPanel_storeTempDir_onChoiceError_title=Error Saving Temporary Directory Choice # {0} - path AutopsyOptionsPanel_storeTempDir_onError_description=There was an error creating the temporary directory on the filesystem at: {0}. AutopsyOptionsPanel_storeTempDir_onError_title=Error Saving Temporary Directory @@ -246,12 +248,11 @@ MultiUserSettingsPanel.restartRequiredLabel.text=Application restart required to MultiUserSettingsPanel.MustRestart=Autopsy must be restarted for new configuration to take effect MultiUserSettingsPanel.lbSolrNote1.text=Enter Solr 8 and/or Solr 4 server settings. MultiUserSettingsPanel.lbSolrNote2.text=New text indexing can only be done with Solr 8. -AutopsyOptionsPanel.tempDirectoryField.text= AutopsyOptionsPanel.tempDirectoryBrowseButton.text=Browse AutopsyOptionsPanel.a.AccessibleContext.accessibleName=Temp Directory AutopsyOptionsPanel.tempDirectoryPanel.AccessibleContext.accessibleName=Temp Directory AutopsyOptionsPanel.tempDirectoryPanel.name=Temp Directory -AutopsyOptionsPanel.tempDirectoryPanel.border.title=Temp Directory +AutopsyOptionsPanel.tempDirectoryPanel.border.title=Root Temp Directory AutopsyOptionsPanel.tempDirectoryWarningLabel.text=Close the current case to change the temporary directory. AutopsyOptionsPanel.solrJVMHeapWarning.text=NOTE: Setting this too large may impact overall performance. AutopsyOptionsPanel.maxMemoryUnitsLabel2.text=MB @@ -305,3 +306,8 @@ AutopsyOptionsPanel.agencyLogoPathField.text= AutopsyOptionsPanel.logoPanel.border.title=Logo ViewPreferencesPanel.radioGroupByPersonHost.text=Group by Person/Host ViewPreferencesPanel.radioGroupByDataType.text=Group by Data Type +AutopsyOptionsPanel.tempLocalRadio.text=Local temp directory +AutopsyOptionsPanel.tempCaseRadio.text=Temp folder in case directory +AutopsyOptionsPanel.tempCustomRadio.text=Custom +AutopsyOptionsPanel.tempCustomField.text= +AutopsyOptionsPanel.tempOnCustomNoPath.text=Please select a path for the custom root temp directory. diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/DataModelActionsFactory.java b/Core/src/org/sleuthkit/autopsy/datamodel/DataModelActionsFactory.java index bdb4a394a8..71b99cd282 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/DataModelActionsFactory.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/DataModelActionsFactory.java @@ -35,6 +35,7 @@ import org.sleuthkit.autopsy.actions.DeleteFileContentTagAction; import org.sleuthkit.autopsy.actions.ReplaceBlackboardArtifactTagAction; import org.sleuthkit.autopsy.actions.ReplaceContentTagAction; import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; +import org.sleuthkit.autopsy.datamodel.OsAccounts.OsAccountNode; import org.sleuthkit.autopsy.datamodel.Reports.ReportNode; import org.sleuthkit.autopsy.directorytree.ExportCSVAction; import org.sleuthkit.autopsy.directorytree.ExternalViewerAction; @@ -53,6 +54,7 @@ import org.sleuthkit.datamodel.File; import org.sleuthkit.datamodel.LayoutFile; import org.sleuthkit.datamodel.LocalFile; import org.sleuthkit.datamodel.LocalDirectory; +import org.sleuthkit.datamodel.OsAccount; import org.sleuthkit.datamodel.Report; import org.sleuthkit.datamodel.SlackFile; import org.sleuthkit.datamodel.VirtualDirectory; @@ -446,6 +448,18 @@ public class DataModelActionsFactory { actionsList.addAll(ContextMenuExtensionPoint.getActions()); return actionsList; } + + public static List getActions(OsAccount osAccount) { + List actionsList = new ArrayList<>(); + + OsAccountNode node = new OsAccountNode(osAccount); + actionsList.add(null); // creates a menu separator + actionsList.add(new NewWindowViewAction(VIEW_IN_NEW_WINDOW, node)); + actionsList.add(null); + actionsList.add(ExportCSVAction.getInstance()); + actionsList.addAll(ContextMenuExtensionPoint.getActions()); + return actionsList; + } public static List getActions(Content content, boolean isArtifactSource) { if (content instanceof File) { @@ -464,7 +478,9 @@ public class DataModelActionsFactory { return getActions((SlackFile) content, isArtifactSource); } else if (content instanceof Report) { return getActions((Report) content, isArtifactSource); - } else { + } else if (content instanceof OsAccount) { + return getActions((OsAccount) content, isArtifactSource); + }else { return new ArrayList<>(); } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/DataSourcesByTypeNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/DataSourcesByTypeNode.java index afeb99b8ec..6dae811e99 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/DataSourcesByTypeNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/DataSourcesByTypeNode.java @@ -22,7 +22,9 @@ import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.EnumSet; import java.util.List; +import java.util.Set; import java.util.logging.Level; +import java.util.stream.Collectors; import org.openide.nodes.ChildFactory; import org.openide.nodes.Children; import org.openide.nodes.Node; @@ -48,13 +50,23 @@ public class DataSourcesByTypeNode extends DisplayableItemNode { */ public static class DataSourcesByTypeChildren extends ChildFactory.Detachable { + private static final Set UPDATE_EVTS = EnumSet.of( + Case.Events.DATA_SOURCE_ADDED, + Case.Events.HOSTS_ADDED, + Case.Events.HOSTS_DELETED, + Case.Events.HOSTS_CHANGED); + + private static final Set UPDATE_EVT_STRS = UPDATE_EVTS.stream() + .map(evt -> evt.name()) + .collect(Collectors.toSet()); + private static final Logger logger = Logger.getLogger(DataSourcesByTypeChildren.class.getName()); private final PropertyChangeListener pcl = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { String eventType = evt.getPropertyName(); - if (eventType.equals(Case.Events.DATA_SOURCE_ADDED.toString())) { + if (UPDATE_EVT_STRS.contains(eventType)) { refresh(true); } } @@ -62,12 +74,12 @@ public class DataSourcesByTypeNode extends DisplayableItemNode { @Override protected void addNotify() { - Case.addEventTypeSubscriber(EnumSet.of(Case.Events.DATA_SOURCE_ADDED), pcl); + Case.addEventTypeSubscriber(UPDATE_EVTS, pcl); } @Override protected void removeNotify() { - Case.removeEventTypeSubscriber(EnumSet.of(Case.Events.DATA_SOURCE_ADDED), pcl); + Case.removeEventTypeSubscriber(UPDATE_EVTS, pcl); } @Override @@ -91,7 +103,7 @@ public class DataSourcesByTypeNode extends DisplayableItemNode { } } - + private static final String NAME = Bundle.DataSourcesHostsNode_name(); /** @@ -100,7 +112,7 @@ public class DataSourcesByTypeNode extends DisplayableItemNode { public static String getNameIdentifier() { return NAME; } - + /** * Main constructor. */ diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/HostNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/HostNode.java index 44cb4fad5f..72b65a89c2 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/HostNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/HostNode.java @@ -20,6 +20,7 @@ package org.sleuthkit.autopsy.datamodel; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; +import java.util.ArrayList; import java.util.EnumSet; import java.util.List; import java.util.Optional; @@ -40,6 +41,7 @@ import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.casemodule.events.HostsChangedEvent; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.datamodel.hosts.AssociatePersonsMenuAction; +import org.sleuthkit.autopsy.datamodel.hosts.MergeHostMenuAction; import org.sleuthkit.autopsy.datamodel.hosts.RemoveParentPersonAction; import org.sleuthkit.datamodel.DataSource; import org.sleuthkit.datamodel.Host; @@ -287,10 +289,13 @@ public class HostNode extends DisplayableItemNode { "HostNode_actions_removeFromPerson=Remove from person ({0})"}) public Action[] getActions(boolean context) { - Optional parent = Optional.empty(); - + List actionsList = new ArrayList<>(); + // if there is a host, then provide actions if (this.host != null) { + + // Add the appropriate Person action + Optional parent; try { parent = Case.getCurrentCaseThrows().getSleuthkitCase().getHostManager().getPerson(this.host); } catch (NoCurrentCaseException | TskCoreException ex) { @@ -300,17 +305,14 @@ public class HostNode extends DisplayableItemNode { // if there is a parent, only give option to remove parent person. if (parent.isPresent()) { - return new Action[]{ - new RemoveParentPersonAction(this.host, parent.get()), - null - }; + actionsList.add(new RemoveParentPersonAction(this.host, parent.get())); } else { - return new Action[]{ - new AssociatePersonsMenuAction(this.host), - null - }; + actionsList.add(new AssociatePersonsMenuAction(this.host)); } + + // Add option to merge hosts + actionsList.add(new MergeHostMenuAction(this.host)); } - return new Action[0]; + return actionsList.toArray(new Action[actionsList.size()]); } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/OsAccounts.java b/Core/src/org/sleuthkit/autopsy/datamodel/OsAccounts.java index ade1ef6329..df0d1968aa 100755 --- a/Core/src/org/sleuthkit/autopsy/datamodel/OsAccounts.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/OsAccounts.java @@ -21,12 +21,14 @@ package org.sleuthkit.autopsy.datamodel; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Optional; -import java.util.Set; import java.util.logging.Level; +import javax.swing.Action; import org.openide.nodes.ChildFactory; import org.openide.nodes.Children; import org.openide.nodes.Node; @@ -51,7 +53,7 @@ public final class OsAccounts implements AutopsyVisitableItem { private static final String ICON_PATH = "org/sleuthkit/autopsy/images/os-account.png"; private static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z"); - private final SleuthkitCase skCase; + private SleuthkitCase skCase; private final long filteringDSObjId; public OsAccounts(SleuthkitCase skCase) { @@ -111,34 +113,46 @@ public final class OsAccounts implements AutopsyVisitableItem { private final PropertyChangeListener listener = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { - refresh(true); + String eventType = evt.getPropertyName(); + if(eventType.equals(Case.Events.OS_ACCOUNT_ADDED.toString())) { + refresh(true); + } 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) { + removeNotify(); + skCase = null; + } + } } }; @Override protected void addNotify() { Case.addEventTypeSubscriber(Collections.singleton(Case.Events.OS_ACCOUNT_ADDED), listener); + Case.addEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), listener); } @Override protected void removeNotify() { Case.removeEventTypeSubscriber(Collections.singleton(Case.Events.OS_ACCOUNT_ADDED), listener); + Case.removeEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), listener); } @Override protected boolean createKeys(List list) { - try { - if (filteringDSObjId == 0) { - list.addAll(skCase.getOsAccountManager().getAccounts()); - } else { - Host host = skCase.getHostManager().getHost(skCase.getDataSource(filteringDSObjId)); - list.addAll(skCase.getOsAccountManager().getAccounts(host)); + if(skCase != null) { + try { + if (filteringDSObjId == 0) { + list.addAll(skCase.getOsAccountManager().getAccounts()); + } else { + Host host = skCase.getHostManager().getHost(skCase.getDataSource(filteringDSObjId)); + list.addAll(skCase.getOsAccountManager().getAccounts(host)); + } + } catch (TskCoreException | TskDataException ex) { + logger.log(Level.SEVERE, "Unable to retrieve list of OsAccounts for case", ex); + return false; } - } catch (TskCoreException | TskDataException ex) { - logger.log(Level.SEVERE, "Unable to retrieve list of OsAccounts for case", ex); - return false; } - return true; } @@ -260,5 +274,14 @@ public final class OsAccounts implements AutopsyVisitableItem { return sheet; } + + @Override + public Action[] getActions(boolean popup) { + List actionsList = new ArrayList<>(); + actionsList.addAll(Arrays.asList(super.getActions(popup))); + actionsList.addAll(DataModelActionsFactory.getActions(account)); + + return actionsList.toArray(new Action[actionsList.size()]); + } } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/hosts/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/datamodel/hosts/Bundle.properties-MERGED index 452a98a54d..cec3355913 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/hosts/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/datamodel/hosts/Bundle.properties-MERGED @@ -31,6 +31,21 @@ AddEditHostDialog.okButton.text=OK AddEditHostDialog.cancelButton.text=Cancel AddEditHostDialog.inputTextField.text=jTextField1 ManageHostsDialog_title_text=Manage Hosts +# {0} - sourceHost +# {1} - destHost +MergeHostAction.confirmText=Are you sure you want to merge {0} into {1}?\nThis may include merging OS Accounts and cannot be undone. +MergeHostAction.confirmTitle=Confirmation +MergeHostAction.errorText=An error occurred while merging hosts.\nTry again in a few minutes or check the log for details. +MergeHostAction.errorTitle=Error Merging Hosts +MergeHostAction.progressIndicatorName=Merging Hosts +# {0} - sourceHost +# {1} - destHost +MergeHostAction.progressText=Merging {0} into {1}... +# {0} - sourceHostName +# {1} - destHostName +MergeHostAction_onError_description=There was an error merging host {0} into host {1}. +MergeHostAction_onError_title=Error Merging Hosts +MergeHostMenuAction_menuTitle=Merge Into Other Host OpenHostsAction_displayName=Hosts # {0} - personName RemoveParentPersonAction_menuTitle=Remove from Person ({0}) diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/hosts/MergeHostAction.java b/Core/src/org/sleuthkit/autopsy/datamodel/hosts/MergeHostAction.java new file mode 100644 index 0000000000..921cc638ef --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datamodel/hosts/MergeHostAction.java @@ -0,0 +1,135 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2021 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.hosts; + +import java.awt.event.ActionEvent; +import java.util.logging.Level; +import javax.swing.AbstractAction; +import javax.swing.JOptionPane; +import javax.swing.SwingWorker; +import org.openide.util.NbBundle; +import org.openide.util.NbBundle.Messages; +import org.openide.windows.WindowManager; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.progress.ModalDialogProgressIndicator; +import org.sleuthkit.autopsy.progress.ProgressIndicator; +import org.sleuthkit.datamodel.Host; + +/** + * Menu action to merge a host into another host. + */ +@Messages({ + "MergeHostAction_onError_title=Error Merging Hosts", + "# {0} - sourceHostName", + "# {1} - destHostName", + "MergeHostAction_onError_description=There was an error merging host {0} into host {1}.",}) +public class MergeHostAction extends AbstractAction { + + private static final Logger logger = Logger.getLogger(MergeHostAction.class.getName()); + + private final Host sourceHost; + private final Host destHost; + + /** + * Main constructor. + * + * @param sourceHost The source host. + * @param destHost The destination host. + */ + public MergeHostAction(Host sourceHost, Host destHost) { + super(destHost.getName()); + + this.sourceHost = sourceHost; + this.destHost = destHost; + } + + @NbBundle.Messages({ + "MergeHostAction.progressIndicatorName=Merging Hosts", + "MergeHostAction.confirmTitle=Confirmation", + "# {0} - sourceHost", + "# {1} - destHost", + "MergeHostAction.confirmText=Are you sure you want to merge {0} into {1}?\nThis may include merging OS Accounts and cannot be undone.", + "# {0} - sourceHost", + "# {1} - destHost", + "MergeHostAction.progressText=Merging {0} into {1}..." + }) + @Override + public void actionPerformed(ActionEvent e) { + + // Display confirmation dialog + int response = JOptionPane.showConfirmDialog( + WindowManager.getDefault().getMainWindow(), + NbBundle.getMessage(this.getClass(), "MergeHostAction.confirmText", sourceHost.getName(), destHost.getName()), + NbBundle.getMessage(this.getClass(), "MergeHostAction.confirmTitle"), + JOptionPane.YES_NO_OPTION); + if (response == JOptionPane.NO_OPTION) { + return; + } + + ModalDialogProgressIndicator progressDialog = new ModalDialogProgressIndicator(WindowManager.getDefault().getMainWindow(), + Bundle.MergeHostAction_progressIndicatorName()); + + MergeHostsBackgroundTask mergeTask = new MergeHostsBackgroundTask(sourceHost, destHost, progressDialog); + progressDialog.start(NbBundle.getMessage(this.getClass(), "MergeHostAction.progressText", sourceHost.getName(), destHost.getName())); + mergeTask.execute(); + } + + /** + * Merges the host in a background worker. + */ + private class MergeHostsBackgroundTask extends SwingWorker { + + private final Host sourceHost; + private final Host destHost; + private final ProgressIndicator progress; + + public MergeHostsBackgroundTask(Host sourceHost, Host destHost, ProgressIndicator progress) { + this.sourceHost = sourceHost; + this.destHost = destHost; + this.progress = progress; + } + + @Override + protected Void doInBackground() throws Exception { + Case.getCurrentCaseThrows().getSleuthkitCase().getHostManager().mergeHosts(sourceHost, destHost); + return null; + } + + @NbBundle.Messages({ + "MergeHostAction.errorTitle=Error Merging Hosts", + "MergeHostAction.errorText=An error occurred while merging hosts.\nTry again in a few minutes or check the log for details." + }) + @Override + protected void done() { + progress.finish(); + try { + get(); + } catch (Exception ex) { + logger.log(Level.SEVERE, "Error merging " + sourceHost.getName() + " into " + destHost.getName(), ex); + + JOptionPane.showMessageDialog(WindowManager.getDefault().getMainWindow(), + NbBundle.getMessage(this.getClass(), "MergeHostAction.errorText"), + NbBundle.getMessage(this.getClass(), "MergeHostAction.errorTitle"), + JOptionPane.ERROR_MESSAGE); + } + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/hosts/MergeHostMenuAction.java b/Core/src/org/sleuthkit/autopsy/datamodel/hosts/MergeHostMenuAction.java new file mode 100644 index 0000000000..8b8ddc3f23 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datamodel/hosts/MergeHostMenuAction.java @@ -0,0 +1,93 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2021 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.hosts; + +import java.awt.event.ActionEvent; +import java.util.Collections; +import java.util.List; +import java.util.logging.Level; +import javax.swing.AbstractAction; +import javax.swing.JMenu; +import javax.swing.JMenuItem; +import org.openide.util.NbBundle.Messages; +import org.openide.util.actions.Presenter; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.Host; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * + * JMenu item to show a menu allowing the selected host to be merged into another host. + */ +@Messages({ + "MergeHostMenuAction_menuTitle=Merge Into Other Host",}) +public class MergeHostMenuAction extends AbstractAction implements Presenter.Popup { + + private static final Logger logger = Logger.getLogger(MergeHostMenuAction.class.getName()); + + private final Host sourceHost; + + /** + * Main constructor. + * + * @param host The original host. + */ + public MergeHostMenuAction(Host host) { + super(""); + this.sourceHost = host; + } + + @Override + @SuppressWarnings("NoopMethodInAbstractClass") + public void actionPerformed(ActionEvent event) { + } + + @Override + public JMenuItem getPopupPresenter() { + JMenu menu = new JMenu(Bundle.MergeHostMenuAction_menuTitle()); + + // Get a list of all other hosts + List otherHosts = Collections.emptyList(); + try { + otherHosts = Case.getCurrentCaseThrows().getSleuthkitCase().getHostManager().getHosts(); + otherHosts.remove(sourceHost); + } catch (NoCurrentCaseException | TskCoreException ex) { + logger.log(Level.WARNING, "Error getting hosts for case.", ex); + } + + // If there are no other hosts, disable the menu item. Otherwise add + // the other hosts to the menu. + if (otherHosts.isEmpty()) { + menu.setEnabled(false); + } else { + menu.setEnabled(true); + otherHosts.stream() + .filter(p -> p != null && p.getName() != null) + .sorted((a, b) -> a.getName().compareToIgnoreCase(b.getName())) + .map(p -> new JMenuItem(new MergeHostAction(sourceHost, p))) + .forEach(menu::add); + } + + return menu; + } + +} + diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/BaseDataSourceSummaryPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/BaseDataSourceSummaryPanel.java index 818dc85cdd..35d65db932 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/BaseDataSourceSummaryPanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/BaseDataSourceSummaryPanel.java @@ -47,7 +47,7 @@ import org.sleuthkit.autopsy.datasourcesummary.uiutils.EventUpdateHandler; import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelExport.ExcelExportException; import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelExport.ExcelSheetExport; import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelTableExport; -import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelTableExport.ExcelCellModel; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelCellModel; import org.sleuthkit.autopsy.datasourcesummary.uiutils.GuiCellModel.DefaultMenuItem; import org.sleuthkit.autopsy.datasourcesummary.uiutils.GuiCellModel.MenuItem; import org.sleuthkit.autopsy.datasourcesummary.uiutils.LoadableComponent; diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties-MERGED index 1e4ce97e7d..4a47bcb092 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties-MERGED @@ -6,7 +6,21 @@ AnalysisPanel_keywordHits_tabName=Keyword Hits AnalysisPanel_keywordSearchModuleName=Keyword Search BaseDataSourceSummaryPanel_goToArtifact=View Source Result BaseDataSourceSummaryPanel_goToFile=View Source File in Directory +ContainerPanel_export_acquisitionDetails=Acquisition Details: +ContainerPanel_export_deviceId=Device ID: +ContainerPanel_export_displayName=Display Name: +ContainerPanel_export_filePaths=File Paths: +ContainerPanel_export_imageType=Image Type: +ContainerPanel_export_md5=MD5: +ContainerPanel_export_originalName=Name: +ContainerPanel_export_sectorSize=Sector Size: +ContainerPanel_export_sha1=SHA1: +ContainerPanel_export_sha256=SHA256: +ContainerPanel_export_size=Size: +ContainerPanel_export_timeZone=Time Zone: +ContainerPanel_export_unallocatedSize=Unallocated Space: ContainerPanel_setFieldsForNonImageDataSource_na=N/A +ContainerPanel_tabName=Container CTL_DataSourceSummaryAction=Data Source Summary DataSourceSummaryDialog.closeButton.text=Close ContainerPanel.displayNameLabel.text=Display Name: @@ -73,6 +87,12 @@ GeolocationPanel_mostCommon_tabName=Most Common Cities GeolocationPanel_mostRecent_tabName=Most Recent Cities GeolocationPanel_onNoCrIngest_message=No results will be shown because the GPX Parser was not run. GeolocationPanel_unknownRow_title=Unknown +IngestJobExcelExport_endTimeColumn=End Time +IngestJobExcelExport_ingestStatusTimeColumn=Ingest Status +IngestJobExcelExport_moduleNameTimeColumn=Module Name +IngestJobExcelExport_sheetName=Ingest History +IngestJobExcelExport_startTimeColumn=Start Time +IngestJobExcelExport_versionColumn=Module Version PastCasesPanel_caseColumn_title=Case PastCasesPanel_countColumn_title=Count PastCasesPanel_notableFileTable_tabName=Cases with Common Notable @@ -87,18 +107,19 @@ RecentFilesPanel_attachmentsTable_tabName=Recent Attachments RecentFilesPanel_col_head_date=Date RecentFilesPanel_docsTable_tabName=Recently Opened Documents RecentFilesPanel_downloadsTable_tabName=Recently Downloads -SizeRepresentationUtil_units_bytes=\ bytes -SizeRepresentationUtil_units_gigabytes=\ GB -SizeRepresentationUtil_units_kilobytes=\ kB -SizeRepresentationUtil_units_megabytes=\ MB -SizeRepresentationUtil_units_petabytes=\ PB -SizeRepresentationUtil_units_terabytes=\ TB +SizeRepresentationUtil_units_bytes=bytes +SizeRepresentationUtil_units_gigabytes=GB +SizeRepresentationUtil_units_kilobytes=KB +SizeRepresentationUtil_units_megabytes=MB +SizeRepresentationUtil_units_petabytes=PB +SizeRepresentationUtil_units_terabytes=TB TimelinePanel_earliestLabel_title=Earliest TimelinePanel_latestLabel_title=Latest TimlinePanel_last30DaysChart_artifactEvts_title=Result Events TimlinePanel_last30DaysChart_fileEvts_title=File Events TimlinePanel_last30DaysChart_title=Last 30 Days TypesPanel_artifactsTypesPieChart_title=Artifact Types +TypesPanel_excelTabName=Types TypesPanel_fileMimeTypesChart_audio_title=Audio TypesPanel_fileMimeTypesChart_documents_title=Documents TypesPanel_fileMimeTypesChart_executables_title=Executables diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/ContainerPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/ContainerPanel.java index c19fd2198e..a8e5fef64e 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/ContainerPanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/ContainerPanel.java @@ -19,21 +19,35 @@ package org.sleuthkit.autopsy.datasourcesummary.ui; import java.beans.PropertyChangeEvent; +import java.sql.SQLException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.logging.Level; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.sleuthkit.autopsy.coreutils.Logger; import javax.swing.table.DefaultTableModel; +import org.apache.commons.lang.StringUtils; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.datasourcesummary.datamodel.ContainerSummary; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.SleuthkitCaseProvider.SleuthkitCaseProviderException; +import static org.sleuthkit.autopsy.datasourcesummary.ui.BaseDataSourceSummaryPanel.getFetchResult; import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchResult.ResultType; import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchWorker.DataFetchComponents; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetcher; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DefaultCellModel; import org.sleuthkit.autopsy.datasourcesummary.uiutils.DefaultUpdateGovernor; -import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelExport; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelExport.ExcelSheetExport; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelSpecialFormatExport; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelSpecialFormatExport.ExcelItemExportable; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelSpecialFormatExport.KeyValueItemExportable; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelSpecialFormatExport.SingleCellExportable; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelSpecialFormatExport.TitledExportable; import org.sleuthkit.autopsy.datasourcesummary.uiutils.UpdateGovernor; import org.sleuthkit.datamodel.DataSource; import org.sleuthkit.datamodel.Image; @@ -42,39 +56,184 @@ import org.sleuthkit.datamodel.TskCoreException; /** * Panel to display additional details associated with a specific DataSource */ +@Messages({ + "ContainerPanel_tabName=Container" +}) class ContainerPanel extends BaseDataSourceSummaryPanel { /** - * Data payload for the Container panel. + * View model data for data source images. */ - private static class ContainerPanelData { + private static class ImageViewModel { - private final DataSource dataSource; - private final Long unallocatedFilesSize; + private final long unallocatedSize; + private final long size; + private final long sectorSize; + + private final String timeZone; + private final String imageType; + + private final List paths; + private final String md5Hash; + private final String sha1Hash; + private final String sha256Hash; /** * Main constructor. * - * @param dataSource The original datasource. - * @param unallocatedFilesSize The unallocated file size. + * @param unallocatedSize Size in bytes of unallocated space. + * @param size Total size in bytes. + * @param sectorSize Sector size in bytes. + * @param timeZone The time zone. + * @param imageType The type of image. + * @param paths The source paths for the image. + * @param md5Hash The md5 hash or null. + * @param sha1Hash The sha1 hash or null. + * @param sha256Hash The sha256 hash or null. */ - ContainerPanelData(DataSource dataSource, Long unallocatedFilesSize) { - this.dataSource = dataSource; - this.unallocatedFilesSize = unallocatedFilesSize; + ImageViewModel(long unallocatedSize, long size, long sectorSize, + String timeZone, String imageType, List paths, String md5Hash, + String sha1Hash, String sha256Hash) { + this.unallocatedSize = unallocatedSize; + this.size = size; + this.sectorSize = sectorSize; + this.timeZone = timeZone; + this.imageType = imageType; + this.paths = paths == null ? Collections.emptyList() : new ArrayList<>(paths); + this.md5Hash = md5Hash; + this.sha1Hash = sha1Hash; + this.sha256Hash = sha256Hash; } /** - * @return The original datasource. + * @return Size in bytes of unallocated space. */ - DataSource getDataSource() { - return dataSource; + long getUnallocatedSize() { + return unallocatedSize; } /** - * @return The unallocated file size. + * @return Total size in bytes. */ - Long getUnallocatedFilesSize() { - return unallocatedFilesSize; + long getSize() { + return size; + } + + /** + * @return Sector size in bytes. + */ + long getSectorSize() { + return sectorSize; + } + + /** + * @return The time zone. + */ + String getTimeZone() { + return timeZone; + } + + /** + * @return The type of image. + */ + String getImageType() { + return imageType; + } + + /** + * @return The source paths for the image. + */ + List getPaths() { + return paths; + } + + /** + * @return The md5 hash or null. + */ + String getMd5Hash() { + return md5Hash; + } + + /** + * @return The sha1 hash or null. + */ + String getSha1Hash() { + return sha1Hash; + } + + /** + * @return The sha256 hash or null. + */ + String getSha256Hash() { + return sha256Hash; + } + } + + /** + * View model for container data. + */ + private static class ContainerViewModel { + + private final String displayName; + private final String originalName; + private final String deviceIdValue; + private final String acquisitionDetails; + private final ImageViewModel imageViewModel; + + /** + * Main constructor. + * + * @param displayName The display name for this data source. + * @param originalName The original name for this data source. + * @param deviceIdValue The device id value for this data source. + * @param acquisitionDetails The acquisition details for this data + * source or null. + * @param imageViewModel If the data source is an image, the image view + * model for this data source or null if non-image. + */ + ContainerViewModel(String displayName, String originalName, String deviceIdValue, + String acquisitionDetails, ImageViewModel imageViewModel) { + this.displayName = displayName; + this.originalName = originalName; + this.deviceIdValue = deviceIdValue; + this.acquisitionDetails = acquisitionDetails; + this.imageViewModel = imageViewModel; + } + + /** + * @return The display name for this data source. + */ + String getDisplayName() { + return displayName; + } + + /** + * @return The original name for this data source. + */ + String getOriginalName() { + return originalName; + } + + /** + * @return The device id value for this data source. + */ + String getDeviceId() { + return deviceIdValue; + } + + /** + * @return The acquisition details for this data source or null. + */ + String getAcquisitionDetails() { + return acquisitionDetails; + } + + /** + * @return If the data source is an image, the image view model for this + * data source or null if non-image. + */ + ImageViewModel getImageViewModel() { + return imageViewModel; } } @@ -103,6 +262,7 @@ class ContainerPanel extends BaseDataSourceSummaryPanel { private static final Logger logger = Logger.getLogger(ContainerPanel.class.getName()); private final List> dataFetchComponents; + private final DataFetcher containerDataFetcher; /** * Creates a new form ContainerPanel. @@ -117,21 +277,15 @@ class ContainerPanel extends BaseDataSourceSummaryPanel { ContainerPanel(ContainerSummary containerSummary) { super(containerSummary, CONTAINER_UPDATES); + containerDataFetcher = (dataSource) -> getContainerViewModel(containerSummary, dataSource); + dataFetchComponents = Arrays.asList( new DataFetchComponents<>( - (dataSource) -> { - return new ContainerPanelData( - dataSource, - containerSummary.getSizeOfUnallocatedFiles(dataSource) - ); - }, + containerDataFetcher, (result) -> { if (result != null && result.getResultType() == ResultType.SUCCESS) { - ContainerPanelData data = result.getData(); - DataSource dataSource = (data == null) ? null : data.getDataSource(); - Long unallocatedFileSize = (data == null) ? null : data.getUnallocatedFilesSize(); - - updateDetailsPanelData(dataSource, unallocatedFileSize); + ContainerViewModel data = result.getData(); + updateDetailsPanelData(data); } else { if (result == null) { logger.log(Level.WARNING, "No data fetch result was provided to the ContainerPanel."); @@ -139,8 +293,7 @@ class ContainerPanel extends BaseDataSourceSummaryPanel { logger.log(Level.WARNING, "An exception occurred while attempting to fetch data for the ContainerPanel.", result.getException()); } - - updateDetailsPanelData(null, null); + updateDetailsPanelData(null); } } ) @@ -161,33 +314,113 @@ class ContainerPanel extends BaseDataSourceSummaryPanel { } /** - * Update which DataSource this panel should display details about - * - * @param selectedDataSource the DataSource to display details about. + * A means of retrieving data that could potentially throw an exception. */ - private void updateDetailsPanelData(DataSource selectedDataSource, Long unallocatedFilesSize) { + private interface Retriever { + + /** + * Retrieves data of a certain type and possibly throws an exception. + * + * @return The data type. + * @throws TskCoreException + * @throws SleuthkitCaseProviderException + * @throws SQLException + */ + O retrieve() throws TskCoreException, SleuthkitCaseProviderException, SQLException; + } + + /** + * Retrieves data of a particular type and handles any exceptions that may + * be thrown by logging. + * + * @param retriever The retrieving function. + * @return The retrieved data. + */ + private static O retrieve(Retriever retriever) { + try { + return retriever.retrieve(); + } catch (TskCoreException | SleuthkitCaseProviderException | SQLException ex) { + logger.log(Level.WARNING, "Error while retrieving data.", ex); + return null; + } + } + + /** + * Generates a container view model object containing data to display about + * the data source. + * + * @param containerSummary The service providing data about the data source. + * @param ds The data source. + * @return The generated view model. + */ + private static ContainerViewModel getContainerViewModel(ContainerSummary containerSummary, DataSource ds) { + if (ds == null) { + return null; + } + + return new ContainerViewModel( + ds.getName(), + ds.getName(), + ds.getDeviceId(), + retrieve(() -> ds.getAcquisitionDetails()), + ds instanceof Image ? getImageViewModel(containerSummary, (Image) ds) : null + ); + } + + /** + * Generates an image view model object containing data to display about the + * image. + * + * @param containerSummary The service providing data about the image. + * @param image The image. + * @return The generated view model. + */ + private static ImageViewModel getImageViewModel(ContainerSummary containerSummary, Image image) { + if (image == null) { + return null; + } + + Long unallocSize = retrieve(() -> containerSummary.getSizeOfUnallocatedFiles(image)); + String imageType = image.getType().getName(); + Long size = image.getSize(); + Long sectorSize = image.getSsize(); + String timeZone = image.getTimeZone(); + List paths = image.getPaths() == null ? Collections.emptyList() : Arrays.asList(image.getPaths()); + String md5 = retrieve(() -> image.getMd5()); + String sha1 = retrieve(() -> image.getSha1()); + String sha256 = retrieve(() -> image.getSha256()); + + return new ImageViewModel(unallocSize, size, sectorSize, timeZone, imageType, paths, md5, sha1, sha256); + } + + /** + * Update the swing components with fetched data. + * + * @param viewModel The data source view model data. + */ + private void updateDetailsPanelData(ContainerViewModel viewModel) { clearTableValues(); - if (selectedDataSource != null) { - displayNameValue.setText(selectedDataSource.getName()); - originalNameValue.setText(selectedDataSource.getName()); - deviceIdValue.setText(selectedDataSource.getDeviceId()); + if (viewModel == null) { + return; + } - try { - acquisitionDetailsTextArea.setText(selectedDataSource.getAcquisitionDetails()); - } catch (TskCoreException ex) { - logger.log(Level.WARNING, "Unable to get acquisition details for selected data source", ex); - } + displayNameValue.setText(viewModel.getDisplayName()); + originalNameValue.setText(viewModel.getOriginalName()); + deviceIdValue.setText(viewModel.getDeviceId()); + acquisitionDetailsTextArea.setText(viewModel.getAcquisitionDetails()); - if (selectedDataSource instanceof Image) { - setFieldsForImage((Image) selectedDataSource, unallocatedFilesSize); - } else { - setFieldsForNonImageDataSource(); - } + if (viewModel.getImageViewModel() != null) { + setFieldsForImage(viewModel.getImageViewModel()); + } else { + setFieldsForNonImageDataSource(); } this.repaint(); } + /** + * Sets image-only fields to N/A. + */ @Messages({ "ContainerPanel_setFieldsForNonImageDataSource_na=N/A" }) @@ -208,54 +441,24 @@ class ContainerPanel extends BaseDataSourceSummaryPanel { } /** - * Sets text fields for an image. This should be called after - * clearTableValues and before updateFieldVisibility to ensure the proper - * rendering. + * Sets fields for images. * - * @param selectedImage The selected image. - * @param unallocatedFilesSize Unallocated file size in bytes. + * @param viewModel The image view model data. */ - private void setFieldsForImage(Image selectedImage, Long unallocatedFilesSize) { - unallocatedSizeValue.setText(SizeRepresentationUtil.getSizeString(unallocatedFilesSize)); - imageTypeValue.setText(selectedImage.getType().getName()); - sizeValue.setText(SizeRepresentationUtil.getSizeString(selectedImage.getSize())); - sectorSizeValue.setText(SizeRepresentationUtil.getSizeString(selectedImage.getSsize())); - timeZoneValue.setText(selectedImage.getTimeZone()); + private void setFieldsForImage(ImageViewModel viewModel) { + unallocatedSizeValue.setText(SizeRepresentationUtil.getSizeString(viewModel.getUnallocatedSize())); + imageTypeValue.setText(viewModel.getImageType()); + sizeValue.setText(SizeRepresentationUtil.getSizeString(viewModel.getSize())); + sectorSizeValue.setText(SizeRepresentationUtil.getSizeString(viewModel.getSectorSize())); + timeZoneValue.setText(viewModel.getTimeZone()); - for (String path : selectedImage.getPaths()) { + for (String path : viewModel.getPaths()) { ((DefaultTableModel) filePathsTable.getModel()).addRow(new Object[]{path}); } - try { - //older databases may have null as the hash values - String md5String = selectedImage.getMd5(); - if (md5String == null) { - md5String = ""; - } - md5HashValue.setText(md5String); - } catch (TskCoreException ex) { - logger.log(Level.WARNING, "Unable to get MD5 for selected data source", ex); - } - - try { - String sha1String = selectedImage.getSha1(); - if (sha1String == null) { - sha1String = ""; - } - sha1HashValue.setText(sha1String); - } catch (TskCoreException ex) { - logger.log(Level.WARNING, "Unable to get SHA1 for selected data source", ex); - } - - try { - String sha256String = selectedImage.getSha256(); - if (sha256String == null) { - sha256String = ""; - } - sha256HashValue.setText(sha256String); - } catch (TskCoreException ex) { - logger.log(Level.WARNING, "Unable to get SHA256 for selected data source", ex); - } + md5HashValue.setText(viewModel.getMd5Hash()); + sha1HashValue.setText(viewModel.getSha1Hash()); + sha256HashValue.setText(viewModel.getSha256Hash()); } /** @@ -277,9 +480,82 @@ class ContainerPanel extends BaseDataSourceSummaryPanel { ((DefaultTableModel) filePathsTable.getModel()).setRowCount(0); } + /** + * Divides acquisition details into key/value pairs to be displayed in + * separate cells in an excel export. + * + * @param acquisitionDetails The acquisition details. + * @return The list of key value pairs that can be incorporated into the + * excel export. + */ + private static List getAcquisitionDetails(String acquisitionDetails) { + if (StringUtils.isBlank(acquisitionDetails)) { + return Collections.emptyList(); + } else { + return Stream.of(acquisitionDetails.split("\\r?\\n")) + .map((line) -> (StringUtils.isBlank(line)) ? null : new SingleCellExportable(line)) + .filter(item -> item != null) + .collect(Collectors.toList()); + } + } + @Override - List getExports(DataSource ds) { - return Collections.emptyList(); + @Messages({ + "ContainerPanel_export_displayName=Display Name:", + "ContainerPanel_export_originalName=Name:", + "ContainerPanel_export_deviceId=Device ID:", + "ContainerPanel_export_timeZone=Time Zone:", + "ContainerPanel_export_acquisitionDetails=Acquisition Details:", + "ContainerPanel_export_imageType=Image Type:", + "ContainerPanel_export_size=Size:", + "ContainerPanel_export_sectorSize=Sector Size:", + "ContainerPanel_export_md5=MD5:", + "ContainerPanel_export_sha1=SHA1:", + "ContainerPanel_export_sha256=SHA256:", + "ContainerPanel_export_unallocatedSize=Unallocated Space:", + "ContainerPanel_export_filePaths=File Paths:",}) + protected List getExports(DataSource ds) { + ContainerViewModel result = getFetchResult(containerDataFetcher, "Container sheets", ds); + if (ds == null || result == null) { + return Collections.emptyList(); + } + + String NA = Bundle.ContainerPanel_setFieldsForNonImageDataSource_na(); + DefaultCellModel NACell = new DefaultCellModel<>(NA); + + ImageViewModel imageModel = result.getImageViewModel(); + boolean hasImage = imageModel != null; + + DefaultCellModel timeZone = hasImage ? new DefaultCellModel<>(imageModel.getTimeZone()) : NACell; + DefaultCellModel imageType = hasImage ? new DefaultCellModel<>(imageModel.getImageType()) : NACell; + DefaultCellModel size = hasImage ? SizeRepresentationUtil.getBytesCell(imageModel.getSize()) : NACell; + DefaultCellModel sectorSize = hasImage ? SizeRepresentationUtil.getBytesCell(imageModel.getSectorSize()) : NACell; + DefaultCellModel md5 = hasImage ? new DefaultCellModel<>(imageModel.getMd5Hash()) : NACell; + DefaultCellModel sha1 = hasImage ? new DefaultCellModel<>(imageModel.getSha1Hash()) : NACell; + DefaultCellModel sha256 = hasImage ? new DefaultCellModel<>(imageModel.getSha256Hash()) : NACell; + DefaultCellModel unallocatedSize = hasImage ? SizeRepresentationUtil.getBytesCell(imageModel.getUnallocatedSize()) : NACell; + List paths = result.getImageViewModel() == null ? Collections.singletonList(NA) : result.getImageViewModel().getPaths(); + List cellPaths = paths.stream() + .map(SingleCellExportable::new) + .collect(Collectors.toList()); + + return Arrays.asList( + new ExcelSpecialFormatExport(Bundle.ContainerPanel_tabName(), Arrays.asList( + new KeyValueItemExportable(Bundle.ContainerPanel_export_displayName(), new DefaultCellModel<>(result.getDisplayName())), + new KeyValueItemExportable(Bundle.ContainerPanel_export_originalName(), new DefaultCellModel<>(result.getOriginalName())), + new KeyValueItemExportable(Bundle.ContainerPanel_export_deviceId(), new DefaultCellModel<>(result.getDeviceId())), + new KeyValueItemExportable(Bundle.ContainerPanel_export_timeZone(), timeZone), + new TitledExportable(Bundle.ContainerPanel_export_acquisitionDetails(), getAcquisitionDetails(result.getAcquisitionDetails())), + new KeyValueItemExportable(Bundle.ContainerPanel_export_imageType(), imageType), + new KeyValueItemExportable(Bundle.ContainerPanel_export_size(), size), + new KeyValueItemExportable(Bundle.ContainerPanel_export_sectorSize(), sectorSize), + new KeyValueItemExportable(Bundle.ContainerPanel_export_md5(), md5), + new KeyValueItemExportable(Bundle.ContainerPanel_export_sha1(), sha1), + new KeyValueItemExportable(Bundle.ContainerPanel_export_sha256(), sha256), + new KeyValueItemExportable(Bundle.ContainerPanel_export_unallocatedSize(), unallocatedSize), + new TitledExportable(Bundle.ContainerPanel_export_filePaths(), cellPaths) + ))); + } /** diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryTabbedPane.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryTabbedPane.java index 102a75f89b..4a77a8a485 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryTabbedPane.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryTabbedPane.java @@ -68,7 +68,6 @@ public class DataSourceSummaryTabbedPane extends javax.swing.JPanel { * * @param tabTitle The title of the tab. * @param panel The component to be displayed in the tab. - * @param notifyParentClose Notifies parent to trigger a close. */ DataSourceTab(String tabTitle, BaseDataSourceSummaryPanel panel) { this(tabTitle, panel, panel::setDataSource, panel::getExports, panel::close); @@ -138,10 +137,10 @@ public class DataSourceSummaryTabbedPane extends javax.swing.JPanel { private Runnable notifyParentClose = null; private final IngestJobInfoPanel ingestHistoryPanel = new IngestJobInfoPanel(); - + // create an export panel whose button triggers the export to XLSX action private final ExportPanel exportPanel = new ExportPanel(); - + private final List tabs = Arrays.asList( new DataSourceTab(Bundle.DataSourceSummaryTabbedPane_typesTab_title(), new TypesPanel()), new DataSourceTab(Bundle.DataSourceSummaryTabbedPane_userActivityTab_title(), new UserActivityPanel()), @@ -155,7 +154,7 @@ public class DataSourceSummaryTabbedPane extends javax.swing.JPanel { Bundle.DataSourceSummaryTabbedPane_ingestHistoryTab_title(), ingestHistoryPanel, ingestHistoryPanel::setDataSource, - null, + IngestJobExcelExport::getExports, null), new DataSourceTab(Bundle.DataSourceSummaryTabbedPane_detailsTab_title(), new ContainerPanel()), new DataSourceTab( @@ -165,10 +164,10 @@ public class DataSourceSummaryTabbedPane extends javax.swing.JPanel { null, null) ); - + // the action that does the export private final ExcelExportAction exportAction = new ExcelExportAction(tabs); - + private DataSource dataSource = null; private CardLayout cardLayout; @@ -222,7 +221,7 @@ public class DataSourceSummaryTabbedPane extends javax.swing.JPanel { // set this to no datasource initially cardLayout.show(this, NO_DATASOURCE_PANE); - + // set action for when user requests xlsx export exportPanel.setXlsxExportAction(() -> exportAction.accept(getDataSource())); } @@ -270,7 +269,6 @@ public class DataSourceSummaryTabbedPane extends javax.swing.JPanel { Case.removeEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), caseEventsListener); } - /** * This method is called from within the constructor to initialize the form. * WARNING: Do NOT modify this code. The content of this method is always diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/IngestJobExcelExport.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/IngestJobExcelExport.java new file mode 100644 index 0000000000..3ad757e7be --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/IngestJobExcelExport.java @@ -0,0 +1,253 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2021 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.datasourcesummary.ui; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ColumnModel; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DefaultCellModel; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelExport.ExcelSheetExport; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelTableExport; +import org.sleuthkit.datamodel.DataSource; +import org.sleuthkit.datamodel.IngestJobInfo; +import org.sleuthkit.datamodel.IngestModuleInfo; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Class that handles exporting information in IngestJobInfoPanel to excel. + */ +@Messages({ + "IngestJobExcelExport_startTimeColumn=Start Time", + "IngestJobExcelExport_endTimeColumn=End Time", + "IngestJobExcelExport_ingestStatusTimeColumn=Ingest Status", + "IngestJobExcelExport_moduleNameTimeColumn=Module Name", + "IngestJobExcelExport_versionColumn=Module Version", + "IngestJobExcelExport_sheetName=Ingest History" +}) +class IngestJobExcelExport { + + /** + * An entry to display in an excel export. + */ + private static class IngestJobEntry { + + private final Date startTime; + private final Date endTime; + private final String status; + private final String ingestModule; + private final String ingestModuleVersion; + + /** + * Main constructor. + * + * @param startTime The ingest start time. + * @param endTime The ingest stop time. + * @param status The ingest status. + * @param ingestModule The ingest module. + * @param ingestModuleVersion The ingest module version. + */ + IngestJobEntry(Date startTime, Date endTime, String status, String ingestModule, String ingestModuleVersion) { + this.startTime = startTime; + this.endTime = endTime; + this.status = status; + this.ingestModule = ingestModule; + this.ingestModuleVersion = ingestModuleVersion; + } + + /** + * @return The ingest start time. + */ + Date getStartTime() { + return startTime; + } + + /** + * @return The ingest stop time. + */ + Date getEndTime() { + return endTime; + } + + /** + * @return The ingest status. + */ + String getStatus() { + return status; + } + + /** + * @return The ingest module. + */ + String getIngestModule() { + return ingestModule; + } + + /** + * @return The ingest module version. + */ + String getIngestModuleVersion() { + return ingestModuleVersion; + } + } + + private static final Logger logger = Logger.getLogger(IngestJobExcelExport.class.getName()); + private static final String DATETIME_FORMAT_STR = "yyyy/MM/dd HH:mm:ss"; + private static final DateFormat DATETIME_FORMAT = new SimpleDateFormat(DATETIME_FORMAT_STR, Locale.getDefault()); + + // columns in the excel export table to be created. + private static final List>> COLUMNS = Arrays.asList( + new ColumnModel<>( + Bundle.IngestJobExcelExport_startTimeColumn(), + (entry) -> getDateCell(entry.getStartTime())), + new ColumnModel<>( + Bundle.IngestJobExcelExport_endTimeColumn(), + (entry) -> getDateCell(entry.getEndTime())), + new ColumnModel<>( + Bundle.IngestJobExcelExport_ingestStatusTimeColumn(), + (entry) -> new DefaultCellModel<>(entry.getStatus())), + new ColumnModel<>( + Bundle.IngestJobExcelExport_moduleNameTimeColumn(), + (entry) -> new DefaultCellModel<>(entry.getIngestModule())), + new ColumnModel<>( + Bundle.IngestJobExcelExport_versionColumn(), + (entry) -> new DefaultCellModel<>(entry.getIngestModuleVersion())) + ); + + /** + * Retrieves data for a date cell. + * + * @param date The date. + * @return The data cell to be used in the excel export. + */ + private static DefaultCellModel getDateCell(Date date) { + Function dateParser = (dt) -> dt == null ? "" : DATETIME_FORMAT.format(dt); + return new DefaultCellModel<>(date, dateParser, DATETIME_FORMAT_STR); + } + + /** + * Retrieves all the ingest job modules and versions for a job. + * + * @param job The ingest job. + * @return All of the corresponding entries sorted by module name. + */ + private static List getEntries(IngestJobInfo job) { + List infoList = job.getIngestModuleInfo(); + if (infoList == null) { + return Collections.emptyList(); + } else { + Date startTime = job.getStartDateTime(); + Date endTime = job.getEndDateTime(); + String status = job.getStatus().getDisplayName(); + + return infoList.stream() + .filter(info -> info != null) + .map(info -> new IngestJobEntry(startTime, endTime, status, info.getDisplayName(), info.getVersion())) + .sorted((a, b) -> { + boolean aIsNull = a == null || a.getIngestModule() == null; + boolean bIsNull = b == null || b.getIngestModule() == null; + if (aIsNull || bIsNull) { + return Boolean.compare(aIsNull, bIsNull); + } else { + return a.getIngestModule().compareTo(b.getIngestModule()); + } + }) + .collect(Collectors.toList()); + } + } + + /** + * For output, show ingest job details in first row present. Otherwise, set + * to null. + * + * @param list The list of entries for an ingest job. + * @return The stream of entries to be displayed. + */ + private static Stream showFirstRowOnly(List list) { + return IntStream.range(0, list.size()) + .mapToObj(idx -> { + IngestJobEntry entry = list.get(idx); + if (entry == null || idx == 0) { + return entry; + } else { + return new IngestJobEntry(null, null, null, entry.getIngestModule(), entry.getIngestModuleVersion()); + } + }); + + } + + /** + * Returns a list of sheets to be exported for the Ingest History tab. + * + * @param dataSource The data source. + * @return The list of sheets to be included in an export. + */ + static List getExports(DataSource dataSource) { + if (dataSource == null) { + return Collections.emptyList(); + } + + List info = null; + try { + info = Case.getCurrentCaseThrows().getSleuthkitCase().getIngestJobs(); + } catch (NoCurrentCaseException | TskCoreException ex) { + logger.log(Level.WARNING, "There was an error fetching ingest jobs", ex); + } + + if (info == null) { + info = Collections.emptyList(); + } + + List toDisplay = info.stream() + .filter(job -> job != null && dataSource.getId() == job.getObjectId()) + .sorted((a, b) -> { + // sort ingest jobs by time. + boolean aIsNull = a.getStartDateTime() == null; + boolean bIsNull = b.getStartDateTime() == null; + if (aIsNull || bIsNull) { + return Boolean.compare(aIsNull, bIsNull); + } else { + return a.getStartDateTime().compareTo(b.getStartDateTime()); + } + }) + .map((job) -> getEntries(job)) + .filter(lst -> lst != null) + .flatMap((lst) -> showFirstRowOnly(lst)) + .filter(item -> item != null) + .collect(Collectors.toList()); + + return Arrays.asList(new ExcelTableExport<>(Bundle.IngestJobExcelExport_sheetName(), COLUMNS, toDisplay)); + } + + private IngestJobExcelExport() { + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/SizeRepresentationUtil.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/SizeRepresentationUtil.java index 34c622563d..74b9be06a8 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/SizeRepresentationUtil.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/SizeRepresentationUtil.java @@ -22,6 +22,7 @@ import java.text.DecimalFormat; import java.util.Arrays; import java.util.List; import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DefaultCellModel; /** * This class provides utilities for representing storage size in most relevant @@ -32,14 +33,64 @@ public final class SizeRepresentationUtil { private static final int SIZE_CONVERSION_CONSTANT = 1000; private static final DecimalFormat APPROXIMATE_SIZE_FORMAT = new DecimalFormat("#.##"); - private static List UNITS = Arrays.asList( - Bundle.SizeRepresentationUtil_units_bytes(), - Bundle.SizeRepresentationUtil_units_kilobytes(), - Bundle.SizeRepresentationUtil_units_megabytes(), - Bundle.SizeRepresentationUtil_units_gigabytes(), - Bundle.SizeRepresentationUtil_units_terabytes(), - Bundle.SizeRepresentationUtil_units_petabytes() - ); + /** + * A size unit corresponding to orders of magnitude of bytes (kilobyte, gigabytes, etc.). + */ + @NbBundle.Messages({ + "SizeRepresentationUtil_units_bytes=bytes", + "SizeRepresentationUtil_units_kilobytes=KB", + "SizeRepresentationUtil_units_megabytes=MB", + "SizeRepresentationUtil_units_gigabytes=GB", + "SizeRepresentationUtil_units_terabytes=TB", + "SizeRepresentationUtil_units_petabytes=PB" + }) + enum SizeUnit { + BYTES(Bundle.SizeRepresentationUtil_units_bytes(), "#", 0), + KB(Bundle.SizeRepresentationUtil_units_kilobytes(), "#,##0.00,", 1), + MB(Bundle.SizeRepresentationUtil_units_megabytes(), "#,##0.00,,", 2), + GB(Bundle.SizeRepresentationUtil_units_gigabytes(), "#,##0.00,,,", 3), + TB(Bundle.SizeRepresentationUtil_units_terabytes(), "#,##0.00,,,,", 4), + PB(Bundle.SizeRepresentationUtil_units_petabytes(), "#,##0.00,,,,,", 5); + + private final String suffix; + private final String excelFormatString; + private final long divisor; + + /** + * Main constructor. + * @param suffix The string suffix to use for size unit. + * @param excelFormatString The excel format string to use for this size unit. + * @param power The power of 1000 of bytes for this size unit. + */ + SizeUnit(String suffix, String excelFormatString, int power) { + this.suffix = suffix; + + // based on https://www.mrexcel.com/board/threads/how-do-i-format-cells-to-show-gb-mb-kb.140135/ + this.excelFormatString = String.format("%s \"%s\"", excelFormatString, suffix); + this.divisor = (long) Math.pow(SIZE_CONVERSION_CONSTANT, power); + } + + /** + * @return The string suffix to use for size unit. + */ + public String getSuffix() { + return suffix; + } + + /** + * @return The excel format string to use for this size unit. + */ + public String getExcelFormatString() { + return excelFormatString; + } + + /** + * @return The divisor to convert from bytes to this unit. + */ + public long getDivisor() { + return divisor; + } + } /** * Get a long size in bytes as a string formated to be read by users. @@ -47,49 +98,59 @@ public final class SizeRepresentationUtil { * @param size Long value representing a size in bytes. * * @return Return a string formated with a user friendly version of the size - * as a string, returns empty String when provided empty size. + * as a string, returns empty String when provided empty size. */ - public static String getSizeString(Long size) { + static String getSizeString(Long size) { return getSizeString(size, APPROXIMATE_SIZE_FORMAT, true); } + /** + * Determines the relevant size unit that should be used for a particular size. + * @param size The size in bytes. + * @return The relevant size unit. + */ + static SizeUnit getSizeUnit(Long size) { + if (size == null) { + return SizeUnit.values()[0]; + } + + for (int unitsIndex = 0; unitsIndex < SizeUnit.values().length; unitsIndex++) { + SizeUnit unit = SizeUnit.values()[unitsIndex]; + long result = size / unit.getDivisor(); + if (result < SIZE_CONVERSION_CONSTANT) { + return unit; + } + } + + return SizeUnit.values()[SizeUnit.values().length - 1]; + } + /** * Get a long size in bytes as a string formated to be read by users. * - * @param size Long value representing a size in byte.s - * @param format The means of formatting the number. + * @param size Long value representing a size in byte.s + * @param format The means of formatting the number. * @param showFullSize Optionally show the number of bytes in the - * datasource. + * datasource. * * @return Return a string formated with a user friendly version of the size - * as a string, returns empty String when provided empty size. + * as a string, returns empty String when provided empty size. */ - @NbBundle.Messages({ - "SizeRepresentationUtil_units_bytes= bytes", - "SizeRepresentationUtil_units_kilobytes= kB", - "SizeRepresentationUtil_units_megabytes= MB", - "SizeRepresentationUtil_units_gigabytes= GB", - "SizeRepresentationUtil_units_terabytes= TB", - "SizeRepresentationUtil_units_petabytes= PB" - }) - public static String getSizeString(Long size, DecimalFormat format, boolean showFullSize) { + static String getSizeString(Long size, DecimalFormat format, boolean showFullSize) { if (size == null) { return ""; } - double approximateSize = size; - int unitsIndex = 0; - for (; unitsIndex < UNITS.size(); unitsIndex++) { - if (approximateSize < SIZE_CONVERSION_CONSTANT) { - break; - } else { - approximateSize /= SIZE_CONVERSION_CONSTANT; - } + + SizeUnit sizeUnit = getSizeUnit(size); + if (sizeUnit == null) { + sizeUnit = SizeUnit.BYTES; } - - String fullSize = size + UNITS.get(0); - String closestUnitSize = format.format(approximateSize) + UNITS.get(unitsIndex); - - if (unitsIndex == 0) { + + String closestUnitSize = String.format("%s %s", + format.format(((double) size) / sizeUnit.getDivisor()), sizeUnit.getSuffix()); + + String fullSize = String.format("%d %s", size, SizeUnit.BYTES.getSuffix()); + if (sizeUnit.equals(SizeUnit.BYTES)) { return fullSize; } else if (showFullSize) { return String.format("%s (%s)", closestUnitSize, fullSize); @@ -97,6 +158,24 @@ public final class SizeRepresentationUtil { return closestUnitSize; } } + + /** + * Returns a default cell model using size units. + * @param bytes The number of bytes. + * @return The default cell model. + */ + static DefaultCellModel getBytesCell(Long bytes) { + if (bytes == null) { + return new DefaultCellModel<>(""); + } else { + SizeUnit unit = SizeRepresentationUtil.getSizeUnit(bytes); + if (unit == null) { + unit = SizeUnit.BYTES; + } + + return new DefaultCellModel(bytes, SizeRepresentationUtil::getSizeString, unit.getExcelFormatString()); + } + } private SizeRepresentationUtil() { } diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TypesPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TypesPanel.java index ae5598cb92..97fc63fc51 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TypesPanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TypesPanel.java @@ -31,7 +31,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.coreutils.FileTypeUtils.FileTypeCategory; -import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.datasourcesummary.datamodel.TypesSummary; import org.sleuthkit.autopsy.datasourcesummary.datamodel.ContainerSummary; import org.sleuthkit.autopsy.datasourcesummary.datamodel.MimeTypeSummary; @@ -40,13 +39,16 @@ import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchResult; import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchResult.ResultType; import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchWorker; import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchWorker.DataFetchComponents; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetcher; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DefaultCellModel; import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelExport; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelSpecialFormatExport; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelSpecialFormatExport.KeyValueItemExportable; import org.sleuthkit.autopsy.datasourcesummary.uiutils.IngestRunningLabel; import org.sleuthkit.autopsy.datasourcesummary.uiutils.LoadableComponent; import org.sleuthkit.autopsy.datasourcesummary.uiutils.LoadableLabel; import org.sleuthkit.autopsy.datasourcesummary.uiutils.PieChartPanel; import org.sleuthkit.autopsy.datasourcesummary.uiutils.PieChartPanel.PieChartItem; -import org.sleuthkit.autopsy.modules.filetypeid.FileTypeIdModuleFactory; import org.sleuthkit.datamodel.DataSource; import org.sleuthkit.datamodel.TskCoreException; @@ -72,7 +74,8 @@ import org.sleuthkit.datamodel.TskCoreException; "TypesPanel_fileMimeTypesChart_notAnalyzed_title=Not Analyzed", "TypesPanel_usageLabel_title=Usage", "TypesPanel_osLabel_title=OS", - "TypesPanel_sizeLabel_title=Size"}) + "TypesPanel_sizeLabel_title=Size", + "TypesPanel_excelTabName=Types"}) class TypesPanel extends BaseDataSourceSummaryPanel { /** @@ -167,10 +170,9 @@ class TypesPanel extends BaseDataSourceSummaryPanel { private static final long serialVersionUID = 1L; private static final DecimalFormat INTEGER_SIZE_FORMAT = new DecimalFormat("#"); - private static final DecimalFormat COMMA_FORMATTER = new DecimalFormat("#,###"); - private static final String FILE_TYPE_FACTORY = FileTypeIdModuleFactory.class.getCanonicalName(); - private static final String FILE_TYPE_MODULE_NAME = FileTypeIdModuleFactory.getModuleName(); - private static final Logger logger = Logger.getLogger(TypesPanel.class.getName()); + private static final String COMMA_FORMAT_STR = "#,###"; + + private static final DecimalFormat COMMA_FORMATTER = new DecimalFormat(COMMA_FORMAT_STR); private static final Color IMAGES_COLOR = new Color(156, 39, 176); private static final Color VIDEOS_COLOR = Color.YELLOW; @@ -191,6 +193,15 @@ class TypesPanel extends BaseDataSourceSummaryPanel { new TypesPieCategory(Bundle.TypesPanel_fileMimeTypesChart_unknown_title(), new HashSet<>(Arrays.asList("application/octet-stream")), UNKNOWN_COLOR) ); + private final DataFetcher usageFetcher; + private final DataFetcher osFetcher; + private final DataFetcher sizeFetcher; + + private final DataFetcher allocatedFetcher; + private final DataFetcher unallocatedFetcher; + private final DataFetcher slackFetcher; + private final DataFetcher directoriesFetcher; + private final LoadableLabel usageLabel = new LoadableLabel(Bundle.TypesPanel_usageLabel_title()); private final LoadableLabel osLabel = new LoadableLabel(Bundle.TypesPanel_osLabel_title()); private final LoadableLabel sizeLabel = new LoadableLabel(Bundle.TypesPanel_sizeLabel_title()); @@ -246,42 +257,34 @@ class TypesPanel extends BaseDataSourceSummaryPanel { super(mimeTypeData, typeData, containerData); + this.usageFetcher = containerData::getDataSourceType; + this.osFetcher = containerData::getOperatingSystems; + + this.sizeFetcher = (dataSource) -> dataSource == null ? null : dataSource.getSize(); + + this.allocatedFetcher = (dataSource) -> typeData.getCountOfAllocatedFiles(dataSource); + this.unallocatedFetcher = (dataSource) -> typeData.getCountOfUnallocatedFiles(dataSource); + this.slackFetcher = (dataSource) -> typeData.getCountOfSlackFiles(dataSource); + this.directoriesFetcher = (dataSource) -> typeData.getCountOfDirectories(dataSource); + this.dataFetchComponents = Arrays.asList( - // usage label worker - new DataFetchWorker.DataFetchComponents<>( - containerData::getDataSourceType, - (result) -> usageLabel.showDataFetchResult(result)), - // os label worker - new DataFetchWorker.DataFetchComponents<>( - containerData::getOperatingSystems, - (result) -> osLabel.showDataFetchResult(result)), - // size label worker - new DataFetchWorker.DataFetchComponents<>( - (dataSource) -> { - Long size = dataSource == null ? null : dataSource.getSize(); - return SizeRepresentationUtil.getSizeString(size, INTEGER_SIZE_FORMAT, false); - }, - sizeLabel::showDataFetchResult), - // file types worker + new DataFetchWorker.DataFetchComponents<>(usageFetcher, usageLabel::showDataFetchResult), + new DataFetchWorker.DataFetchComponents<>(osFetcher, osLabel::showDataFetchResult), + new DataFetchWorker.DataFetchComponents<>(sizeFetcher, + (sizeResult) -> sizeLabel.showDataFetchResult( + DataFetchResult.getSubResult(sizeResult, + size -> SizeRepresentationUtil.getSizeString(size, INTEGER_SIZE_FORMAT, false)))), new DataFetchWorker.DataFetchComponents<>( (dataSource) -> getMimeTypeCategoriesModel(mimeTypeData, dataSource), this::showMimeTypeCategories), - // allocated files worker - new DataFetchWorker.DataFetchComponents<>( - (dataSource) -> getStringOrZero(typeData.getCountOfAllocatedFiles(dataSource)), - allocatedLabel::showDataFetchResult), - // unallocated files worker - new DataFetchWorker.DataFetchComponents<>( - (dataSource) -> getStringOrZero(typeData.getCountOfUnallocatedFiles(dataSource)), - unallocatedLabel::showDataFetchResult), - // slack files worker - new DataFetchWorker.DataFetchComponents<>( - (dataSource) -> getStringOrZero(typeData.getCountOfSlackFiles(dataSource)), - slackLabel::showDataFetchResult), - // directories worker - new DataFetchWorker.DataFetchComponents<>( - (dataSource) -> getStringOrZero(typeData.getCountOfDirectories(dataSource)), - directoriesLabel::showDataFetchResult) + new DataFetchWorker.DataFetchComponents<>(allocatedFetcher, + countRes -> allocatedLabel.showDataFetchResult(DataFetchResult.getSubResult(countRes, (count) -> getStringOrZero(count)))), + new DataFetchWorker.DataFetchComponents<>(unallocatedFetcher, + countRes -> unallocatedLabel.showDataFetchResult(DataFetchResult.getSubResult(countRes, (count) -> getStringOrZero(count)))), + new DataFetchWorker.DataFetchComponents<>(slackFetcher, + countRes -> slackLabel.showDataFetchResult(DataFetchResult.getSubResult(countRes, (count) -> getStringOrZero(count)))), + new DataFetchWorker.DataFetchComponents<>(directoriesFetcher, + countRes -> directoriesLabel.showDataFetchResult(DataFetchResult.getSubResult(countRes, (count) -> getStringOrZero(count)))) ); initComponents(); @@ -406,9 +409,53 @@ class TypesPanel extends BaseDataSourceSummaryPanel { return longVal == null ? "0" : COMMA_FORMATTER.format(longVal); } + /** + * Returns a key value pair to be exported in a sheet. + * + * @param fetcher The means of fetching the data. + * @param key The key to use. + * @param dataSource The data source containing the data. + * @return The key value pair to be exported. + */ + private static KeyValueItemExportable getStrExportable(DataFetcher fetcher, String key, DataSource dataSource) { + String result = getFetchResult(fetcher, "Types", dataSource); + return (result == null) ? null : new KeyValueItemExportable(key, new DefaultCellModel<>(result)); + } + + /** + * Returns a key value pair to be exported in a sheet formatting the long + * with commas separated by orders of 1000. + * + * @param fetcher The means of fetching the data. + * @param key The string key for this key value pair. + * @param dataSource The data source. + * @return The key value pair. + */ + private static KeyValueItemExportable getCountExportable(DataFetcher fetcher, String key, DataSource dataSource) { + Long count = getFetchResult(fetcher, "Types", dataSource); + return (count == null) ? null : new KeyValueItemExportable(key, + new DefaultCellModel(count, COMMA_FORMATTER::format, COMMA_FORMAT_STR)); + } + @Override List getExports(DataSource dataSource) { - return Collections.emptyList(); + if (dataSource == null) { + return Collections.emptyList(); + } + + return Arrays.asList(new ExcelSpecialFormatExport(Bundle.TypesPanel_excelTabName(), + Stream.of( + getStrExportable(usageFetcher, Bundle.TypesPanel_usageLabel_title(), dataSource), + getStrExportable(osFetcher, Bundle.TypesPanel_osLabel_title(), dataSource), + new KeyValueItemExportable(Bundle.TypesPanel_sizeLabel_title(), + SizeRepresentationUtil.getBytesCell(getFetchResult(sizeFetcher, "Types", dataSource))), + getCountExportable(allocatedFetcher, Bundle.TypesPanel_filesByCategoryTable_allocatedRow_title(), dataSource), + getCountExportable(unallocatedFetcher, Bundle.TypesPanel_filesByCategoryTable_unallocatedRow_title(), dataSource), + getCountExportable(slackFetcher, Bundle.TypesPanel_filesByCategoryTable_slackRow_title(), dataSource), + getCountExportable(directoriesFetcher, Bundle.TypesPanel_filesByCategoryTable_directoryRow_title(), dataSource)) + .filter(sheet -> sheet != null) + .collect(Collectors.toList()) + )); } /** diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/CellModel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/CellModel.java index 38afc21e2f..60d7e27112 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/CellModel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/CellModel.java @@ -19,6 +19,7 @@ package org.sleuthkit.autopsy.datasourcesummary.uiutils; import javax.swing.JLabel; +import org.apache.poi.ss.usermodel.HorizontalAlignment; /** * Basic interface for a cell model. @@ -29,20 +30,23 @@ public interface CellModel { * Describes the horizontal alignment. */ public enum HorizontalAlign { - LEFT(JLabel.LEFT), - CENTER(JLabel.CENTER), - RIGHT(JLabel.RIGHT); + LEFT(JLabel.LEFT, HorizontalAlignment.LEFT), + CENTER(JLabel.CENTER, HorizontalAlignment.CENTER), + RIGHT(JLabel.RIGHT, HorizontalAlignment.RIGHT); private final int jlabelAlignment; + private final HorizontalAlignment poiAlignment; /** * Constructor for a HorizontalAlign enum. * * @param jlabelAlignment The corresponding JLabel horizontal alignment * number. + * @param poiAlignment Horizontal alignment for Apache POI. */ - HorizontalAlign(int jlabelAlignment) { + HorizontalAlign(int jlabelAlignment, HorizontalAlignment poiAlignment) { this.jlabelAlignment = jlabelAlignment; + this.poiAlignment = poiAlignment; } /** @@ -52,6 +56,13 @@ public interface CellModel { int getJLabelAlignment() { return this.jlabelAlignment; } + + /** + * @return Horizontal alignment for Apache POI. + */ + HorizontalAlignment getPoiAlignment() { + return poiAlignment; + } } /** diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DefaultCellModel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DefaultCellModel.java index a409794387..06165a3023 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DefaultCellModel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DefaultCellModel.java @@ -24,21 +24,21 @@ import java.util.Collections; import java.util.List; import java.util.function.Function; import java.util.function.Supplier; -import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelTableExport.ExcelCellModel; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelCellModel; /** * The default cell model. */ public class DefaultCellModel implements GuiCellModel, ExcelCellModel { - private final T data; - private final Function stringConverter; + final T data; + final Function stringConverter; String tooltip; CellModel.HorizontalAlign horizontalAlignment; Insets insets; List popupMenu; Supplier> menuItemSupplier; - private final String excelFormatString; + final String excelFormatString; /** * Main constructor. @@ -76,6 +76,7 @@ public class DefaultCellModel implements GuiCellModel, ExcelCellModel { this.data = data; this.stringConverter = stringConverter; this.excelFormatString = excelFormatString; + this.tooltip = getText(); } @Override diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ExcelCellModel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ExcelCellModel.java new file mode 100644 index 0000000000..0ca52f12e4 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ExcelCellModel.java @@ -0,0 +1,32 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2021 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.datasourcesummary.uiutils; + +/** + * Basic interface for a cell model. + */ +public interface ExcelCellModel extends CellModel { + + /** + * @return The format string to be used with Apache POI during excel + * export or null if none necessary. + */ + String getExcelFormatString(); + +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ExcelExport.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ExcelExport.java index cf4869ce47..fab6558c4a 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ExcelExport.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ExcelExport.java @@ -21,13 +21,23 @@ package org.sleuthkit.autopsy.datasourcesummary.uiutils; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.CellModel.HorizontalAlign; /** * Class for handling Excel exporting. @@ -59,6 +69,87 @@ public class ExcelExport { } } + /** + * A cell style key that can be used with the WorksheetEnv to generate a + * cell style to be used in a POI excel document. + */ + static class CellStyleKey { + + private final String formatString; + private final CellStyle cellStyle; + private final HorizontalAlign alignment; + + /** + * Main constructor. + * + * @param formatString The format string or null if no special + * formatting. + * @param cellStyle The base cell style or null if default is to be + * used. + * @param alignment The horizontal alignment or null if default is to be + * used. + */ + CellStyleKey(String formatString, CellStyle cellStyle, HorizontalAlign alignment) { + this.formatString = formatString; + this.cellStyle = cellStyle; + this.alignment = alignment; + } + + /** + * @return The format string or null if no special formatting. + */ + String getFormatString() { + return formatString; + } + + /** + * @return The base cell style or null if default is to be used. + */ + CellStyle getCellStyle() { + return cellStyle; + } + + /** + * @return The horizontal alignment or null if default is to be used. + */ + HorizontalAlign getAlignment() { + return alignment; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 29 * hash + Objects.hashCode(this.formatString); + hash = 29 * hash + Objects.hashCode(this.cellStyle); + hash = 29 * hash + Objects.hashCode(this.alignment); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final CellStyleKey other = (CellStyleKey) obj; + if (!Objects.equals(this.formatString, other.formatString)) { + return false; + } + if (!Objects.equals(this.cellStyle, other.cellStyle)) { + return false; + } + if (this.alignment != other.alignment) { + return false; + } + return true; + } + } + /** * Class detailing aspects of the worksheet. */ @@ -66,18 +157,47 @@ public class ExcelExport { private final CellStyle headerStyle; private final Workbook parentWorkbook; + private final CellStyle defaultStyle; + + // maps a data format string / original cell style combination to a created cell style + private final Map cellStyleCache = new HashMap<>(); /** * Main constructor. * * @param headerStyle The cell style to use for headers. + * @param defaultStyle The cell style to use as a default. * @param parentWorkbook The parent workbook. */ - WorksheetEnv(CellStyle headerStyle, Workbook parentWorkbook) { + WorksheetEnv(CellStyle headerStyle, CellStyle defaultStyle, Workbook parentWorkbook) { this.headerStyle = headerStyle; + this.defaultStyle = defaultStyle; this.parentWorkbook = parentWorkbook; } + /** + * Returns a cell style signified by the given cell style key. If the + * key is already present, a cached version is returned. + * + * @param cellStyleKey The key. + * @return The cell style representing this key. + */ + public CellStyle getCellStyle(CellStyleKey cellStyleKey) { + return cellStyleCache.computeIfAbsent(cellStyleKey, (pair) -> { + CellStyle computed = this.parentWorkbook.createCellStyle(); + computed.cloneStyleFrom(cellStyleKey.getCellStyle() == null ? defaultStyle : cellStyleKey.getCellStyle()); + + if (cellStyleKey.getAlignment() != null) { + computed.setAlignment(cellStyleKey.getAlignment().getPoiAlignment()); + } + + if (cellStyleKey.getFormatString() != null) { + computed.setDataFormat(this.parentWorkbook.getCreationHelper().createDataFormat().getFormat(cellStyleKey.getFormatString())); + } + return computed; + }); + } + /** * Returns the cell style to use for headers. * @@ -87,6 +207,15 @@ public class ExcelExport { return headerStyle; } + /** + * Returns the cell style for default items. + * + * @return The cell style for default items. + */ + public CellStyle getDefaultCellStyle() { + return defaultStyle; + } + /** * Returns the parent workbook. * @@ -125,6 +254,7 @@ public class ExcelExport { /** * Retrieves a singleton instance of this class. + * * @return The instance. */ public static ExcelExport getInstance() { @@ -141,10 +271,11 @@ public class ExcelExport { /** * Writes the exports to a workbook. + * * @param exports The sheets to export. * @param path The path to the output file. * @throws IOException - * @throws ExcelExportException + * @throws ExcelExportException */ @Messages({ "# {0} - sheetNumber", @@ -160,10 +291,15 @@ public class ExcelExport { //headerFont.setFontHeightInPoints((short) 14); // Create a CellStyle with the font + HorizontalAlignment alignment = HorizontalAlignment.LEFT; CellStyle headerCellStyle = workbook.createCellStyle(); headerCellStyle.setFont(headerFont); + headerCellStyle.setAlignment(alignment); - WorksheetEnv env = new WorksheetEnv(headerCellStyle, workbook); + CellStyle defaultCellStyle = workbook.createCellStyle(); + defaultCellStyle.setAlignment(alignment); + + WorksheetEnv env = new WorksheetEnv(headerCellStyle, defaultCellStyle, workbook); if (exports != null) { for (int i = 0; i < exports.size(); i++) { @@ -190,4 +326,46 @@ public class ExcelExport { // Closing the workbook workbook.close(); } + + /** + * Creates an excel cell given the model. + * + * @param env The work sheet environment including the workbook. + * @param row The row in the excel document. + * @param colNum The column number (not zero-indexed). + * @param cellModel The model for the cell. + * @param cellStyle The style to use. + * @return The created cell. + */ + static Cell createCell(WorksheetEnv env, Row row, int colNum, ExcelCellModel cellModel, Optional cellStyle) { + CellStyle cellStyleToUse = cellStyle.orElse(env.getDefaultCellStyle()); + + if (cellModel.getExcelFormatString() != null || cellModel.getHorizontalAlignment() != null) { + cellStyleToUse = env.getCellStyle(new CellStyleKey(cellModel.getExcelFormatString(), cellStyleToUse, cellModel.getHorizontalAlignment())); + } + + Object cellData = cellModel.getData(); + Cell cell = row.createCell(colNum); + if (cellData instanceof Calendar) { + cell.setCellValue((Calendar) cellData); + } else if (cellData instanceof Date) { + cell.setCellValue((Date) cellData); + } else if (cellData instanceof Double) { + cell.setCellValue((Double) cellData); + } else if (cellData instanceof String) { + cell.setCellValue((String) cellData); + } else if (cellData instanceof Short) { + cell.setCellValue((Short) cellData); + } else if (cellData instanceof Integer) { + cell.setCellValue((Integer) cellData); + } else if (cellData instanceof Long) { + cell.setCellValue((Long) cellData); + } else if (cellData instanceof Float) { + cell.setCellValue((Float) cellData); + } else { + cell.setCellValue(cellModel.getText()); + } + cell.setCellStyle(cellStyleToUse); + return cell; + } } diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ExcelSpecialFormatExport.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ExcelSpecialFormatExport.java new file mode 100644 index 0000000000..f9c46fa5e2 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ExcelSpecialFormatExport.java @@ -0,0 +1,265 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2021 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.datasourcesummary.uiutils; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelExport.ExcelExportException; + +/** + * An excel export that has special row-by-row formatting. + */ +public class ExcelSpecialFormatExport implements ExcelExport.ExcelSheetExport { + + /** + * The dimensions consumed by an item in an ExcelSpecialFormatExport list of + * items to be rendered. + */ + public static class ItemDimensions { + + private final int rowStart; + private final int rowEnd; + private final int colStart; + private final int colEnd; + + /** + * Main constructor. + * + * @param rowStart The starting excel row of the item. + * @param colStart The starting excel column of the item. + * @param rowEnd The last excel row of the the item. + * @param colEnd The last excel column of the item. + */ + public ItemDimensions(int rowStart, int colStart, int rowEnd, int colEnd) { + this.rowStart = rowStart; + this.colStart = colStart; + this.rowEnd = rowEnd; + this.colEnd = colEnd; + } + + /** + * @return The starting excel row of the item. + */ + public int getRowStart() { + return rowStart; + } + + /** + * @return The last excel row of the the item. + */ + public int getRowEnd() { + return rowEnd; + } + + /** + * @return The starting excel column of the item. + */ + public int getColStart() { + return colStart; + } + + /** + * @return The last excel column of the item. + */ + public int getColEnd() { + return colEnd; + } + } + + /** + * An item to be exported in a specially formatted excel export. + */ + public interface ExcelItemExportable { + + /** + * Writes the item to the sheet in the special format export sheet. + * + * @param sheet The sheet. + * @param rowStart The starting row to start writing. + * @param colStart The starting column to start writing. + * @param env The excel export context. + * @return The dimensions of what has been written. + * @throws ExcelExportException + */ + ItemDimensions write(Sheet sheet, int rowStart, int colStart, ExcelExport.WorksheetEnv env) throws ExcelExportException; + } + + /** + * Writes a string to a single cell in a specially formatted excel export. + */ + public static class SingleCellExportable implements ExcelItemExportable { + + private final ExcelCellModel item; + + /** + * Main constructor. + * + * @param key The text to be written. + */ + public SingleCellExportable(String key) { + this(new DefaultCellModel<>(key)); + } + + /** + * Main constructor. + * + * @param item The cell model to be written. + */ + public SingleCellExportable(ExcelCellModel item) { + this.item = item; + } + + @Override + public ItemDimensions write(Sheet sheet, int rowStart, int colStart, ExcelExport.WorksheetEnv env) throws ExcelExportException { + Row row = sheet.createRow(rowStart); + ExcelExport.createCell(env, row, colStart, item, Optional.empty()); + return new ItemDimensions(rowStart, colStart, rowStart, colStart); + } + } + + /** + * Writes a row consisting of first column as a key and second column as a + * value. + */ + public static class KeyValueItemExportable implements ExcelItemExportable { + + private final ExcelCellModel key; + private final ExcelCellModel value; + + /** + * Main constructor. + * + * @param key The string key to be exported. + * @param value The cell model to be exported. + */ + public KeyValueItemExportable(String key, ExcelCellModel value) { + this(new DefaultCellModel<>(key), value); + } + + /** + * Main constructor. + * + * @param key The cell key to be exported. + * @param value The cell model to be exported. + */ + public KeyValueItemExportable(ExcelCellModel key, ExcelCellModel value) { + this.key = key; + this.value = value; + } + + @Override + public ItemDimensions write(Sheet sheet, int rowStart, int colStart, ExcelExport.WorksheetEnv env) throws ExcelExportException { + Row row = sheet.createRow(rowStart); + ExcelExport.createCell(env, row, colStart, key, Optional.of(env.getHeaderStyle())); + ExcelExport.createCell(env, row, colStart + 1, value, Optional.empty()); + return new ItemDimensions(rowStart, colStart, rowStart, colStart + 1); + } + } + + /** + * A special format excel export item that shows a title and a list of items + * indented one column. + * + * i.e. + *
+     * title
+     *      item 1
+     *      item 2
+     * 
+ */ + public static class TitledExportable implements ExcelItemExportable { + + private static final int DEFAULT_INDENT = 1; + + private final String title; + private final List children; + + /** + * Main constructor. + * + * @param title The title for the export. + * @param children The children to be indented and enumerated. + */ + public TitledExportable(String title, List children) { + this.title = title; + this.children = children; + } + + @Override + public ItemDimensions write(Sheet sheet, int rowStart, int colStart, ExcelExport.WorksheetEnv env) throws ExcelExportException { + ExcelExport.createCell(env, sheet.createRow(rowStart), colStart, new DefaultCellModel<>(title), Optional.of(env.getHeaderStyle())); + int curRow = rowStart + 1; + int maxCol = colStart; + for (ExcelItemExportable export : children) { + if (export == null) { + continue; + } + + ItemDimensions thisItemDim = export.write(sheet, curRow, colStart + DEFAULT_INDENT, env); + curRow = thisItemDim.getRowEnd() + 1; + maxCol = Math.max(thisItemDim.getColEnd(), maxCol); + } + + return new ItemDimensions(rowStart, colStart, curRow - 1, maxCol); + } + } + + private final String sheetName; + private final List exports; + + /** + * Main constructor. + * + * @param sheetName The name of the sheet. + * @param exports The row-by-row items to be exported. + */ + public ExcelSpecialFormatExport(String sheetName, List exports) { + this.sheetName = sheetName; + this.exports = exports == null ? Collections.emptyList() : exports; + } + + @Override + public String getSheetName() { + return sheetName; + } + + @Override + public void renderSheet(Sheet sheet, ExcelExport.WorksheetEnv env) throws ExcelExportException { + int rowStart = 0; + int maxCol = 0; + for (ExcelItemExportable export : exports) { + if (export == null) { + continue; + } + + ItemDimensions dimensions = export.write(sheet, rowStart, 0, env); + rowStart = dimensions.getRowEnd() + 1; + maxCol = Math.max(maxCol, dimensions.getColEnd()); + } + + // Resize all columns to fit the content size + for (int i = 0; i <= maxCol; i++) { + sheet.autoSizeColumn(i); + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ExcelTableExport.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ExcelTableExport.java index 43b8320c49..5592344e2c 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ExcelTableExport.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ExcelTableExport.java @@ -18,40 +18,26 @@ */ package org.sleuthkit.autopsy.datasourcesummary.uiutils; -import java.util.Calendar; import java.util.Collections; -import java.util.Date; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Optional; import org.apache.poi.ss.usermodel.Cell; -import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; -import org.apache.poi.ss.usermodel.Workbook; -import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelTableExport.ExcelCellModel; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelExport.ExcelExportException; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelExport.ExcelSheetExport; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelSpecialFormatExport.ExcelItemExportable; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelSpecialFormatExport.ItemDimensions; /** * An excel sheet export of table data. */ -public class ExcelTableExport implements ExcelExport.ExcelSheetExport { - - /** - * Basic interface for a cell model. - */ - public interface ExcelCellModel extends CellModel { - - /** - * @return The format string to be used with Apache POI during excel - * export or null if none necessary. - */ - String getExcelFormatString(); - } +public class ExcelTableExport implements ExcelSheetExport, ExcelItemExportable { private final String sheetName; private final List> columns; private final List data; + private final int columnIndent; /** * Main constructor. @@ -62,9 +48,23 @@ public class ExcelTableExport implements ExcelExpor * @param data The data to export. */ public ExcelTableExport(String sheetName, List> columns, List data) { + this(sheetName, columns, data, 0); + } + + /** + * Main constructor. + * + * @param sheetName The name of the sheet. NOTE: There can be no duplicates + * in a workbook. + * @param columns The columns of the table. + * @param data The data to export. + * @param columnIndent The column indent. + */ + public ExcelTableExport(String sheetName, List> columns, List data, int columnIndent) { this.sheetName = sheetName; this.columns = columns; this.data = data; + this.columnIndent = columnIndent; } @Override @@ -74,11 +74,20 @@ public class ExcelTableExport implements ExcelExpor @Override public void renderSheet(Sheet sheet, ExcelExport.WorksheetEnv style) throws ExcelExport.ExcelExportException { - renderSheet(sheet, style, columns, data); + renderSheet(sheet, style, 0, columnIndent, columns, data); + // Resize all columns to fit the content size for (int i = 0; i < columns.size(); i++) { sheet.autoSizeColumn(i); } + + } + + @Override + public ItemDimensions write(Sheet sheet, int rowStart, int colStart, ExcelExport.WorksheetEnv env) throws ExcelExportException { + int columnStart = columnIndent + colStart; + int rowsWritten = renderSheet(sheet, env, rowStart, columnStart, columns, data); + return new ItemDimensions(rowStart, columnStart, rowStart + rowsWritten - 1, this.columns == null ? columnStart : columnStart + this.columns.size()); } /** @@ -86,88 +95,44 @@ public class ExcelTableExport implements ExcelExpor * * @param sheet The sheet. * @param worksheetEnv The worksheet environment and preferences. + * @param rowStart The row to start in. + * @param colStart The column to start in. * @param columns The columns. * @param data The data. * @throws ExcelExportException + * @return The number of rows (including the header) written. */ - private static void renderSheet( - Sheet sheet, ExcelExport.WorksheetEnv worksheetEnv, List> columns, List data) + private static int renderSheet( + Sheet sheet, + ExcelExport.WorksheetEnv worksheetEnv, + int rowStart, + int colStart, + List> columns, List data) throws ExcelExport.ExcelExportException { List safeData = data == null ? Collections.emptyList() : data; // Create a header row - Row headerRow = sheet.createRow(0); + Row headerRow = sheet.createRow(rowStart); // Create header cells for (int i = 0; i < columns.size(); i++) { - Cell cell = headerRow.createCell(i); + Cell cell = headerRow.createCell(i + colStart); cell.setCellValue(columns.get(i).getHeaderTitle()); cell.setCellStyle(worksheetEnv.getHeaderStyle()); } // freeze header row sheet.createFreezePane(0, 1); // Create Cell Style for each column (if one is needed) - Map cellStyles = new HashMap<>(); + for (int rowNum = 0; rowNum < safeData.size(); rowNum++) { T rowData = safeData.get(rowNum); - Row row = sheet.createRow(rowNum + 1); + Row row = sheet.createRow(rowNum + rowStart + 1); for (int colNum = 0; colNum < columns.size(); colNum++) { ColumnModel colModel = columns.get(colNum); ExcelCellModel cellModel = colModel.getCellRenderer().apply(rowData); - String formatString = cellModel.getExcelFormatString(); - Optional cellStyle = (formatString == null) - ? Optional.empty() - : Optional.of(cellStyles.computeIfAbsent(formatString, k -> createCellStyle(worksheetEnv.getParentWorkbook(), formatString))); - createCell(row, colNum, cellModel, cellStyle); + ExcelExport.createCell(worksheetEnv, row, colNum + colStart, cellModel, Optional.empty()); } } - } - /** - * Create a cell style in the workbook with the given format string. - * - * @param workbook The workbook. - * @param formatString The format string. - * @return The cell style. - */ - private static CellStyle createCellStyle(Workbook workbook, String formatString) { - CellStyle cellStyle = workbook.createCellStyle(); - cellStyle.setDataFormat(workbook.getCreationHelper().createDataFormat().getFormat(formatString)); - return cellStyle; + return safeData.size() + 1; } - - /** - * Creates an excel cell given the model. - * - * @param row The row in the excel document. - * @param colNum The column number (not zero-indexed). - * @param cellModel The model for the cell. - * @param cellStyle The style to use. - * @return The created cell. - */ - private static Cell createCell(Row row, int colNum, ExcelCellModel cellModel, Optional cellStyle) { - Object cellData = cellModel.getData(); - Cell cell = row.createCell(colNum); - if (cellData instanceof Calendar) { - cell.setCellValue((Calendar) cellData); - } else if (cellData instanceof Date) { - cell.setCellValue((Date) cellData); - } else if (cellData instanceof Double) { - cell.setCellValue((Double) cellData); - } else if (cellData instanceof String) { - cell.setCellValue((String) cellData); - } else if (cellData instanceof Short) { - cell.setCellValue((Short) cellData); - } else if (cellData instanceof Integer) { - cell.setCellValue((Integer) cellData); - } else if (cellData instanceof Long) { - cell.setCellValue((Long) cellData); - } else if (cellData instanceof Float) { - cell.setCellValue((Float) cellData); - } else { - cell.setCellValue(cellModel.getText()); - } - cellStyle.ifPresent(cs -> cell.setCellStyle(cs)); - return cell; - } - } diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/directorytree/Bundle.properties-MERGED index 7e96d8fdaa..c648387016 100755 --- a/Core/src/org/sleuthkit/autopsy/directorytree/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/directorytree/Bundle.properties-MERGED @@ -14,7 +14,7 @@ ExportCSV.saveNodesToCSV.empty=No data to export # {0} - Output file ExportCSV.saveNodesToCSV.fileExists=File {0} already exists ExportCSV.saveNodesToCSV.noCurrentCase=No open case available -ExportCSV.title.text=Export selected rows to CSV +ExportCSV.title.text=Export Selected Rows to CSV ExternalViewerAction.actionPerformed.failure.exe.message=The file is an executable and will not be opened. ExternalViewerAction.actionPerformed.failure.IO.message=There is no associated editor for files of this type or the associated application failed to launch. ExternalViewerAction.actionPerformed.failure.missingFile.message=The file no longer exists. diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/ExportCSVAction.java b/Core/src/org/sleuthkit/autopsy/directorytree/ExportCSVAction.java index 7761e82231..7b312211c4 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/ExportCSVAction.java +++ b/Core/src/org/sleuthkit/autopsy/directorytree/ExportCSVAction.java @@ -85,7 +85,7 @@ public final class ExportCSVAction extends AbstractAction { /** * Private constructor for the action. */ - @NbBundle.Messages({"ExportCSV.title.text=Export selected rows to CSV"}) + @NbBundle.Messages({"ExportCSV.title.text=Export Selected Rows to CSV"}) private ExportCSVAction() { super(Bundle.ExportCSV_title_text()); } diff --git a/Core/src/org/sleuthkit/autopsy/discovery/search/DiscoveryAttributes.java b/Core/src/org/sleuthkit/autopsy/discovery/search/DiscoveryAttributes.java index 4518429232..16e0e80f1b 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/search/DiscoveryAttributes.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/DiscoveryAttributes.java @@ -156,12 +156,11 @@ public class DiscoveryAttributes { public void addAttributeToResults(List results, SleuthkitCase caseDb, CentralRepository centralRepoDb) throws DiscoveryException { try { - Map domainsToCategories = getDomainsWithWebCategories(caseDb); + Map> domainsToCategories = getDomainsWithWebCategories(caseDb); for (Result result : results) { if (result instanceof ResultDomain) { ResultDomain domain = (ResultDomain) result; - String webCategory = domainsToCategories.get(domain.getDomain()); - domain.setWebCategory(webCategory); + domain.addWebCategories(domainsToCategories.get(domain.getDomain())); } } } catch (TskCoreException | InterruptedException ex) { @@ -174,24 +173,23 @@ public class DiscoveryAttributes { * the category name attribute. Each ResultDomain is then parsed and * matched against this map of values. */ - private Map getDomainsWithWebCategories(SleuthkitCase caseDb) throws TskCoreException, InterruptedException { - Map domainToCategory = new HashMap<>(); + private Map> getDomainsWithWebCategories(SleuthkitCase caseDb) throws TskCoreException, InterruptedException { + Map> domainToCategory = new HashMap<>(); for (BlackboardArtifact artifact : caseDb.getBlackboardArtifacts(TSK_WEB_CATEGORIZATION)) { if (Thread.currentThread().isInterrupted()) { throw new InterruptedException(); } - BlackboardAttribute webCategory = artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME)); BlackboardAttribute domain = artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN)); - if (webCategory != null && domain != null) { - String webCatDisplayName = webCategory.getValueString(); String domainDisplayName = domain.getValueString().trim().toLowerCase(); - domainToCategory.put(domainDisplayName, webCatDisplayName); + if (!domainToCategory.containsKey(domainDisplayName)) { + domainToCategory.put(domainDisplayName, new HashSet<>()); + } + domainToCategory.get(domainDisplayName).add(webCategory.getValueString()); } } - return domainToCategory; } } diff --git a/Core/src/org/sleuthkit/autopsy/discovery/search/DiscoveryKeyUtils.java b/Core/src/org/sleuthkit/autopsy/discovery/search/DiscoveryKeyUtils.java index a3c8b4a584..39b3c1b80c 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/search/DiscoveryKeyUtils.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/DiscoveryKeyUtils.java @@ -23,6 +23,8 @@ import java.time.Instant; import java.time.ZonedDateTime; import java.time.temporal.TemporalAdjusters; import java.util.Collections; +import java.util.Set; +import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.TimeZone; @@ -796,18 +798,20 @@ public class DiscoveryKeyUtils { return 0; } } - + /** * Group key representing a domain category (TSK_WEB_CATEGORY artifact). */ static class DomainCategoryGroupKey extends GroupKey { - - private final String webCategory; - + + private final Set webCategories = new HashSet<>(); + private final String displayName; + DomainCategoryGroupKey(Result result) { if (result instanceof ResultDomain) { ResultDomain domain = (ResultDomain) result; - this.webCategory = domain.getWebCategory(); + this.webCategories.addAll(domain.getWebCategories()); + displayName = String.join(",", webCategories); } else { throw new IllegalArgumentException("Input result should be of type ResultDomain"); } @@ -815,7 +819,8 @@ public class DiscoveryKeyUtils { @Override String getDisplayName() { - return this.webCategory; + + return this.displayName; } @Override @@ -828,31 +833,37 @@ public class DiscoveryKeyUtils { @Override public int hashCode() { - return Objects.hash(getWebCategory()); + return Objects.hash(webCategories); } @Override public int compareTo(GroupKey otherGroupKey) { if (otherGroupKey instanceof DomainCategoryGroupKey) { - DomainCategoryGroupKey webCategoryKey = (DomainCategoryGroupKey) otherGroupKey; - return this.webCategory.compareTo(webCategoryKey.getWebCategory()); + if (webCategories.size() != ((DomainCategoryGroupKey) otherGroupKey).getWebCategories().size()) { + return 1; + } + if (webCategories.containsAll(((DomainCategoryGroupKey) otherGroupKey).getWebCategories())) { + return 0; + } else { + return -1; + } } else { return compareClassNames(otherGroupKey); } } - - String getWebCategory() { - return this.webCategory; + + Set getWebCategories() { + return Collections.unmodifiableSet(webCategories); } } - + /** * Key representing a central repository notable status. */ static class PreviouslyNotableGroupKey extends GroupKey { - + private final SearchData.PreviouslyNotable notableStatus; - + PreviouslyNotableGroupKey(Result result) { this.notableStatus = result.getPreviouslyNotableInCR(); } @@ -884,7 +895,7 @@ public class DiscoveryKeyUtils { return compareClassNames(otherGroupKey); } } - + SearchData.PreviouslyNotable getStatus() { return notableStatus; } @@ -1163,7 +1174,7 @@ public class DiscoveryKeyUtils { String getDisplayName() { MonthAbbreviation currentCutOffMonth = MonthAbbreviation.fromMonthValue(currentWeekCutOff.getMonthValue()); return Bundle.DiscoveryAttributes_ActivityDateGroupKey_getDisplayNameTemplate( - currentCutOffMonth.toString(), Integer.toString(currentWeekCutOff.getDayOfMonth()), + currentCutOffMonth.toString(), Integer.toString(currentWeekCutOff.getDayOfMonth()), Integer.toString(currentWeekCutOff.getYear())); } @@ -1196,11 +1207,11 @@ public class DiscoveryKeyUtils { } } } - + /** - * Get the next closed Sunday given an epoch time and timezone. - * Dates for grouping are managed on a weekly basis. Each Sunday - * acts as the boundary and representative for the week. + * Get the next closed Sunday given an epoch time and timezone. Dates for + * grouping are managed on a weekly basis. Each Sunday acts as the boundary + * and representative for the week. */ private static ZonedDateTime getCurrentWeekCutOff(long epochSeconds, ResultDomain domainResult) { Instant startActivityAsInsant = Instant.ofEpochSecond(epochSeconds); @@ -1234,12 +1245,12 @@ public class DiscoveryKeyUtils { throw new IllegalArgumentException("Expected a domain result only."); } } - + @Override String getDisplayName() { MonthAbbreviation currentCutOffMonth = MonthAbbreviation.fromMonthValue(currentWeekCutOff.getMonthValue()); return Bundle.DiscoveryAttributes_ActivityDateGroupKey_getDisplayNameTemplate( - currentCutOffMonth.toString(), Integer.toString(currentWeekCutOff.getDayOfMonth()), + currentCutOffMonth.toString(), Integer.toString(currentWeekCutOff.getDayOfMonth()), Integer.toString(currentWeekCutOff.getYear())); } @@ -1274,9 +1285,8 @@ public class DiscoveryKeyUtils { } /** - * Key representing the number of page views. - * Page views are defined as the number of TSK_WEB_HISTORY artifacts that match - * a domain value. + * Key representing the number of page views. Page views are defined as the + * number of TSK_WEB_HISTORY artifacts that match a domain value. */ static class PageViewsGroupKey extends GroupKey { diff --git a/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearch.java b/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearch.java index 61efc8cfbe..497d622b1c 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearch.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearch.java @@ -95,7 +95,6 @@ public class DomainSearch { final Map> searchResults = searchCache.get( userName, filters, groupAttributeType, groupSortingType, domainSortingMethod, caseDb, centralRepoDb); - // Transform the cached results into a map of group key to group size. final LinkedHashMap groupSizes = new LinkedHashMap<>(); for (GroupKey groupKey : searchResults.keySet()) { @@ -139,7 +138,6 @@ public class DomainSearch { userName, filters, groupAttributeType, groupSortingType, domainSortingMethod, caseDb, centralRepoDb); final List domainsInGroup = searchResults.get(groupKey); - final List page = new ArrayList<>(); for (int i = startingEntry; (i < startingEntry + numberOfEntries) && (i < domainsInGroup.size()); i++) { diff --git a/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchCacheLoader.java b/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchCacheLoader.java index 6c233bd37d..db2ada61de 100755 --- a/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchCacheLoader.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchCacheLoader.java @@ -138,7 +138,7 @@ class DomainSearchCacheLoader extends CacheLoader '' " + " AND (artifact_type_id = " + TSK_WEB_ACCOUNT_TYPE.getTypeID() + ")) " + "GROUP BY artifact_id "; @@ -188,7 +188,7 @@ class DomainSearchCacheLoader extends CacheLoader webCategories = new HashSet<>(); private final Content dataSource; private final long dataSourceId; @@ -60,6 +62,24 @@ public class ResultDomain extends Result { this.accountTypes = accountTypes; } + /** + * Make a copy of the specified ResultDomain, without a category set. + * + * @param resultDomain The ResultDomain to copy + */ + ResultDomain(ResultDomain resultDomain) { + this.domain = resultDomain.getDomain(); + this.dataSource = resultDomain.getDataSource(); + this.dataSourceId = resultDomain.getDataSourceObjectId(); + this.activityStart = resultDomain.getActivityStart(); + this.activityEnd = resultDomain.getActivityEnd(); + this.totalPageViews = resultDomain.getTotalPageViews(); + this.pageViewsInLast60 = resultDomain.getPageViewsInLast60Days(); + this.filesDownloaded = resultDomain.getFilesDownloaded(); + this.countOfKnownAccountTypes = resultDomain.getCountOfKnownAccountTypes(); + this.accountTypes = resultDomain.getAccountTypes(); + } + /** * Get the domain represented as a String. * @@ -123,20 +143,24 @@ public class ResultDomain extends Result { @NbBundle.Messages({ "ResultDomain_getDefaultCategory=Uncategorized" }) - public String getWebCategory() { - if (webCategory == null) { - return Bundle.ResultDomain_getDefaultCategory(); + public Set getWebCategories() { + Set returnList = new HashSet<>(); + if (webCategories.isEmpty()) { + returnList.add(Bundle.ResultDomain_getDefaultCategory()); } else { - return webCategory; + returnList.addAll(webCategories); } + return returnList; } /** - * Set the web category for this domain (derived from TSK_WEB_CATEGORY) + * Add the web categories for this domain (derived from TSK_WEB_CATEGORY) * artifacts. */ - public void setWebCategory(String webCategory) { - this.webCategory = webCategory; + public void addWebCategories(Set categories) { + if (categories != null && !categories.isEmpty()) { + this.webCategories.addAll(categories); + } } /** @@ -144,8 +168,8 @@ public class ResultDomain extends Result { * (TSK_WEB_ACCOUNT_TYPE). */ public boolean hasKnownAccountType() { - return countOfKnownAccountTypes != null - && countOfKnownAccountTypes > 0; + return getCountOfKnownAccountTypes() != null + && getCountOfKnownAccountTypes() > 0; } /** @@ -192,4 +216,11 @@ public class ResultDomain extends Result { + this.pageViewsInLast60 + ", downloads=" + this.filesDownloaded + ", frequency=" + this.getFrequency() + "]"; } + + /** + * @return the countOfKnownAccountTypes + */ + Long getCountOfKnownAccountTypes() { + return countOfKnownAccountTypes; + } } diff --git a/Core/src/org/sleuthkit/autopsy/discovery/search/SearchFiltering.java b/Core/src/org/sleuthkit/autopsy/discovery/search/SearchFiltering.java index d5142c08c2..29a4dd698c 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/search/SearchFiltering.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/SearchFiltering.java @@ -768,7 +768,6 @@ public class SearchFiltering { CentralRepository centralRepoDb) throws DiscoveryException { DiscoveryAttributes.PreviouslyNotableAttribute previouslyNotableAttr = new DiscoveryAttributes.PreviouslyNotableAttribute(); previouslyNotableAttr.addAttributeToResults(currentResults, caseDb, centralRepoDb); - List filteredResults = new ArrayList<>(); for (Result file : currentResults) { if (file.getPreviouslyNotableInCR() == SearchData.PreviouslyNotable.PREVIOUSLY_NOTABLE) { diff --git a/Core/src/org/sleuthkit/autopsy/discovery/search/SearchResults.java b/Core/src/org/sleuthkit/autopsy/discovery/search/SearchResults.java index 8d07b03e22..f3218fe6f3 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/search/SearchResults.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/SearchResults.java @@ -21,10 +21,13 @@ package org.sleuthkit.autopsy.discovery.search; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; import org.sleuthkit.autopsy.discovery.search.DiscoveryKeyUtils.GroupKey; /** @@ -75,15 +78,46 @@ class SearchResults { void add(List results) { for (Result result : results) { // Add the file to the appropriate group, creating it if necessary - GroupKey groupKey = attrType.getGroupKey(result); - - if (!groupMap.containsKey(groupKey)) { - groupMap.put(groupKey, new Group(groupSortingType, groupKey)); + if (result.getType() == SearchData.Type.DOMAIN && attrType instanceof DiscoveryAttributes.DomainCategoryAttribute) { + /** + * This section is to add results to individual groups based on + * the individual Web Categories the domain is part of instead + * of the combination of categories. So that results will show + * up in every group for which they have a category. + */ + for (String category : ((ResultDomain) result).getWebCategories()) { + if (!StringUtils.isBlank(category)) { + ResultDomain currentResult = (ResultDomain) result; + Set newCategorySet = new HashSet<>(); + newCategorySet.add(category); + ResultDomain copyOfResult = new ResultDomain(currentResult); + copyOfResult.addWebCategories(newCategorySet); + GroupKey groupKey = attrType.getGroupKey(copyOfResult); + //purposefully adding original instead of copy so it will display all categories when looking at domain + addResultToGroupMap(groupKey, result); + } + } + } else { + GroupKey groupKey = attrType.getGroupKey(result); + addResultToGroupMap(groupKey, result); } - groupMap.get(groupKey).addResult(result); } } + /** + * Private helper method to add a result to the groupMap with a specified + * key. + * + * @param groupKey The key to add the result under. + * @param result The result to add. + */ + private void addResultToGroupMap(GroupKey groupKey, Result result) { + if (!groupMap.containsKey(groupKey)) { + groupMap.put(groupKey, new Group(groupSortingType, groupKey)); + } + groupMap.get(groupKey).addResult(result); + } + /** * Run after all files have been added to sortGroupsAndFiles the groups and * files. @@ -166,7 +200,6 @@ class SearchResults { for (Group group : groupList) { map.put(group.getGroupKey(), group.getResults()); } - return map; } } diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/discovery/ui/Bundle.properties-MERGED index f24ec80db6..e4e50864dd 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/Bundle.properties-MERGED @@ -54,10 +54,11 @@ DocumentPanel.numberOfImages.noImages=No images DocumentPanel.numberOfImages.text=1 of {0} images DocumentWrapper.previewInitialValue=Preview not generated yet. DomainDetailsPanel.miniTimelineTitle.text=Timeline +DomainDetailsPanel.otherOccurrencesTab.title=Other Occurrences # {0} - startDate # {1} - endDate DomainSummaryPanel.activity.text=Activity: {0} to {1} -DomainSummaryPanel.category.text=Category: +DomainSummaryPanel.category.text=Categories: DomainSummaryPanel.downloads.text=Files downloaded: DomainSummaryPanel.loadingImages.text=Loading thumbnail... DomainSummaryPanel.no.text=No diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainSummaryPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainSummaryPanel.java index 1d5a2f6d99..d78cb5e2f3 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainSummaryPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainSummaryPanel.java @@ -24,7 +24,9 @@ import java.awt.Point; import java.awt.event.MouseEvent; import java.time.Instant; import java.time.ZonedDateTime; +import java.util.HashSet; import java.util.TimeZone; +import java.util.Set; import javax.swing.ImageIcon; import javax.swing.JComponent; import javax.swing.JList; @@ -171,7 +173,7 @@ class DomainSummaryPanel extends javax.swing.JPanel implements ListCellRenderer< "DomainSummaryPanel.downloads.text=Files downloaded: ", "DomainSummaryPanel.notability.text=Previously tagged as notable: ", "DomainSummaryPanel.userRole.text=Account type: ", - "DomainSummaryPanel.category.text=Category: ", + "DomainSummaryPanel.category.text=Categories: ", "DomainSummaryPanel.loadingImages.text=Loading thumbnail...", "DomainSummaryPanel.no.text=No", "DomainSummaryPanel.yes.text=Yes"}) @@ -188,7 +190,9 @@ class DomainSummaryPanel extends javax.swing.JPanel implements ListCellRenderer< notability += Bundle.DomainSummaryPanel_no_text(); } domainNotabilityLabel.setText(notability); - categoryLabel.setText(Bundle.DomainSummaryPanel_category_text() + value.getResultDomain().getWebCategory()); + Set webCategories = new HashSet<>(); + webCategories.addAll(value.getResultDomain().getWebCategories()); + categoryLabel.setText(Bundle.DomainSummaryPanel_category_text() + String.join(",", webCategories)); activityLabel.setText(Bundle.DomainSummaryPanel_activity_text(startDate, endDate)); totalVisitsLabel.setText(Bundle.DomainSummaryPanel_totalPages_text() + value.getResultDomain().getTotalPageViews()); pagesLabel.setText(Bundle.DomainSummaryPanel_pages_text() + value.getResultDomain().getPageViewsInLast60Days()); diff --git a/Core/src/org/sleuthkit/autopsy/ingest/GetRootDirectoryVisitor.java b/Core/src/org/sleuthkit/autopsy/ingest/GetRootDirectoryVisitor.java index 8523fa699f..cf02c8e11b 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/GetRootDirectoryVisitor.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/GetRootDirectoryVisitor.java @@ -29,6 +29,7 @@ import org.sleuthkit.datamodel.FileSystem; import org.sleuthkit.datamodel.LayoutFile; import org.sleuthkit.datamodel.LocalFile; import org.sleuthkit.datamodel.LocalDirectory; +import org.sleuthkit.datamodel.OsAccount; import org.sleuthkit.datamodel.SlackFile; import org.sleuthkit.datamodel.VirtualDirectory; @@ -107,4 +108,9 @@ final class GetRootDirectoryVisitor extends GetFilesContentVisitor { public Collection visit(BlackboardArtifact art) { return getAllFromChildren(art); } + + @Override + public Collection visit(OsAccount art) { + return getAllFromChildren(art); + } } diff --git a/Core/src/org/sleuthkit/autopsy/ingest/IngestManager.java b/Core/src/org/sleuthkit/autopsy/ingest/IngestManager.java index 791a45021c..b778cfcfb7 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/IngestManager.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/IngestManager.java @@ -23,6 +23,7 @@ import java.awt.EventQueue; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -46,6 +47,7 @@ import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.ThreadSafe; import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; import org.netbeans.api.progress.ProgressHandle; import org.openide.util.Cancellable; import org.openide.util.NbBundle; @@ -287,15 +289,16 @@ public class IngestManager implements IngestProgressSnapshotProvider { caseIsOpen = false; clearIngestMessageBox(); } - + /** - * Creates an ingest stream from the given ingest settings for a data source. - * + * Creates an ingest stream from the given ingest settings for a data + * source. + * * @param dataSource The data source * @param settings The ingest job settings. - * + * * @return The newly created ingest stream. - * + * * @throws TskCoreException if there was an error starting the ingest job. */ public IngestStream openIngestStream(DataSource dataSource, IngestJobSettings settings) throws TskCoreException { @@ -313,7 +316,6 @@ public class IngestManager implements IngestProgressSnapshotProvider { } } - /** * Gets the number of file ingest threads the ingest manager is using to do * ingest jobs. @@ -395,6 +397,21 @@ public class IngestManager implements IngestProgressSnapshotProvider { "IngestManager.startupErr.dlgErrorList=Errors:" }) IngestJobStartResult startIngestJob(IngestJob job) { + + // initialize IngestMessageInbox, if it hasn't been initialized yet. This can't be done in + // the constructor because that ends up freezing the UI on startup (JIRA-7345). + if (SwingUtilities.isEventDispatchThread()) { + initIngestMessageInbox(); + } else { + try { + SwingUtilities.invokeAndWait(() -> initIngestMessageInbox()); + } catch (InterruptedException ex) { + // ignore interruptions + } catch (InvocationTargetException ex) { + logger.log(Level.WARNING, "There was an error starting ingest message inbox", ex); + } + } + List errors = null; Case openCase; try { @@ -519,10 +536,11 @@ public class IngestManager implements IngestProgressSnapshotProvider { public void addIngestJobEventListener(final PropertyChangeListener listener) { jobEventPublisher.addSubscriber(INGEST_JOB_EVENT_NAMES, listener); } - + /** - * Adds an ingest job event property change listener for the given event types. - * + * Adds an ingest job event property change listener for the given event + * types. + * * @param eventTypes The event types to listen for * @param listener The PropertyChangeListener to be added */ @@ -540,18 +558,18 @@ public class IngestManager implements IngestProgressSnapshotProvider { public void removeIngestJobEventListener(final PropertyChangeListener listener) { jobEventPublisher.removeSubscriber(INGEST_JOB_EVENT_NAMES, listener); } - + /** * Removes an ingest job event property change listener. * * @param eventTypes The event types to stop listening for - * @param listener The PropertyChangeListener to be removed. + * @param listener The PropertyChangeListener to be removed. */ public void removeIngestJobEventListener(Set eventTypes, final PropertyChangeListener listener) { eventTypes.forEach((IngestJobEvent event) -> { jobEventPublisher.removeSubscriber(event.toString(), listener); }); - } + } /** * Adds an ingest module event property change listener. @@ -563,8 +581,9 @@ public class IngestManager implements IngestProgressSnapshotProvider { } /** - * Adds an ingest module event property change listener for given event types. - * + * Adds an ingest module event property change listener for given event + * types. + * * @param eventTypes The event types to listen for * @param listener The PropertyChangeListener to be removed. */ @@ -573,7 +592,7 @@ public class IngestManager implements IngestProgressSnapshotProvider { moduleEventPublisher.addSubscriber(event.toString(), listener); }); } - + /** * Removes an ingest module event property change listener. * @@ -582,16 +601,16 @@ public class IngestManager implements IngestProgressSnapshotProvider { public void removeIngestModuleEventListener(final PropertyChangeListener listener) { moduleEventPublisher.removeSubscriber(INGEST_MODULE_EVENT_NAMES, listener); } - + /** * Removes an ingest module event property change listener. - * + * * @param eventTypes The event types to stop listening for * @param listener The PropertyChangeListener to be removed. */ public void removeIngestModuleEventListener(Set eventTypes, final PropertyChangeListener listener) { moduleEventPublisher.removeSubscriber(INGEST_MODULE_EVENT_NAMES, listener); - } + } /** * Publishes an ingest job event signifying an ingest job started. @@ -699,8 +718,11 @@ public class IngestManager implements IngestProgressSnapshotProvider { /** * Causes the ingest manager to get the top component used to display ingest - * inbox messages. Called by the custom installer for this package once the - * window system is initialized. + * inbox messages. Used to be called by the custom installer for this + * package once the window system is initialized, but that results in a lot + * of UI components being initialized, which freezes the UI for a long + * period of time(JIRA-7345). Instead we are now initializing + * IngestMessageInbox immediately prior to running first ingest job. */ void initIngestMessageInbox() { synchronized (this.ingestMessageBoxLock) { diff --git a/Core/src/org/sleuthkit/autopsy/ingest/Installer.java b/Core/src/org/sleuthkit/autopsy/ingest/Installer.java index 6738aa0ddb..1117eb82b1 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/Installer.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/Installer.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2014 Basis Technology Corp. + * Copyright 2011-2021 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,7 +19,6 @@ package org.sleuthkit.autopsy.ingest; import org.openide.modules.ModuleInstall; -import org.openide.windows.WindowManager; /** * Initializes ingest manager when the module is loaded @@ -41,15 +40,8 @@ public class Installer extends ModuleInstall { @Override public void restored() { - final IngestManager manager = IngestManager.getInstance(); - WindowManager.getDefault().invokeWhenUIReady(new Runnable() { - @Override - public void run() { - //at this point UI top component is present for sure, ensure manager has it - manager.initIngestMessageInbox(); - } - }); - + // initialize ingest manager + IngestManager.getInstance(); } @Override diff --git a/Core/src/org/sleuthkit/autopsy/machinesettings/UserMachinePreferences.java b/Core/src/org/sleuthkit/autopsy/machinesettings/UserMachinePreferences.java index cc1538c9b7..ef7b138ef0 100644 --- a/Core/src/org/sleuthkit/autopsy/machinesettings/UserMachinePreferences.java +++ b/Core/src/org/sleuthkit/autopsy/machinesettings/UserMachinePreferences.java @@ -20,10 +20,15 @@ package org.sleuthkit.autopsy.machinesettings; import java.io.File; import java.nio.file.Paths; +import java.util.Optional; import java.util.prefs.Preferences; +import java.util.stream.Stream; import org.apache.commons.lang3.StringUtils; import org.openide.util.NbBundle; import org.openide.util.NbPreferences; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.core.UserPreferences; import org.sleuthkit.autopsy.coreutils.FileUtil; /** @@ -33,26 +38,122 @@ public final class UserMachinePreferences { private static final Preferences preferences = NbPreferences.forModule(UserMachinePreferences.class); - private static final String TEMP_DIR_KEY = "TempDirectory"; + /** + * The user specified choice for where the temp directory should be located. + */ + public enum TempDirChoice { + /** + * A custom location specified with an absolute path by the user. + */ + CUSTOM, + /** + * Equivalent to java.io.tmpdir. + */ + SYSTEM, + /** + * If a case is open, a sub directory of the case. + */ + CASE; + + /** + * Returns the temp directory choice that matches the string provided + * (whitespace and case insensitive). + * + * @param val The string value. + * @return The choice or empty if not found. + */ + static Optional getValue(String val) { + if (val == null) { + return Optional.empty(); + } + + return Stream.of(TempDirChoice.values()) + .filter(tempChoice -> tempChoice.name().equalsIgnoreCase(val.trim())) + .findFirst(); + } + } + + private static final String CUSTOM_TEMP_DIR_KEY = "TempDirectory"; + private static final String TEMP_DIR_CHOICE_KEY = "TempDirChoice"; + + private static final String AUTOPSY_SUBDIR = UserPreferences.getAppName(); + private static final String CASE_SUBDIR = "Temp"; + + private static final TempDirChoice DEFAULT_CHOICE = TempDirChoice.SYSTEM; /** - * Retrieves a default temporary directory that is a subdirectory of - * java.io.tmpdir. - * - * @return The absolute path to the temp directory. + * @return A subdirectory of java.io.tmpdir. */ - private static String getDefaultTempDirectory() { - return Paths.get(System.getProperty("java.io.tmpdir")).toAbsolutePath().toString(); + private static File getSystemTempDirFile() { + return Paths.get(System.getProperty("java.io.tmpdir"), AUTOPSY_SUBDIR).toFile(); } /** - * Retrieves the base user-specified temporary directory. + * @return A subdirectory of the open case or getSystemTempDirFile if no + * open case. + */ + private static File getCaseTempDirFile() { + try { + String caseDirStr = Case.getCurrentCaseThrows().getCaseDirectory(); + return Paths.get(caseDirStr, CASE_SUBDIR).toFile(); + } catch (NoCurrentCaseException ex) { + return getSystemTempDirFile(); + } + } + + /** + * Returns the custom directory subdirectory to be used for temp files + * (otherwise java.io.tmpdir subdir). + * + * @return A subdirectory of the custom user-specified path. If no path is + * specified, getSystemTempDirFile() is returned instead. + */ + private static File getCustomTempDirFile() { + String customDirectory = getCustomTempDirectory(); + return (StringUtils.isBlank(customDirectory)) + ? getSystemTempDirFile() : Paths.get(customDirectory, AUTOPSY_SUBDIR).toFile(); + } + + /** + * Returns the temp directory file to use based on user choice. + * + * @return The directory. + */ + private static File getTempDirFile() { + TempDirChoice choice = getTempDirChoice(); + switch (choice) { + case CASE: + return getCaseTempDirFile(); + case CUSTOM: + return getCustomTempDirFile(); + case SYSTEM: + default: + return getSystemTempDirFile(); + } + } + + /** + * Returns the temp directory to use based on settings. This method also + * ensures the temp directory has been created. * * @return The base user-specified temporary directory. */ - public static String getBaseTempDirectory() { - String tempDir = preferences.get(TEMP_DIR_KEY, null); - return StringUtils.isBlank(tempDir) ? getDefaultTempDirectory() : tempDir; + public static String getTempDirectory() { + File dir = getTempDirFile(); + dir = dir == null ? getSystemTempDirFile() : dir; + + if (!dir.exists()) { + dir.mkdirs(); + } + + return dir.getAbsolutePath(); + } + + /** + * @return The user-specified custom temp directory path or empty string. + */ + public static String getCustomTempDirectory() { + return preferences.get(CUSTOM_TEMP_DIR_KEY, ""); } /** @@ -64,8 +165,7 @@ public final class UserMachinePreferences { * @return True if this is a valid location for a temp directory. * * @throws UserMachinePreferencesException If path could not be validated - * due to mkdirs failure or the - * directory is not read/write. + * due to mkdirs failure or the directory is not read/write. */ @NbBundle.Messages({ "# {0} - path", @@ -96,11 +196,33 @@ public final class UserMachinePreferences { * @param path The path to the directory. * * @throws UserMachinePreferencesException If the directory cannot be - * accessed or created. + * accessed or created. */ - public static void setBaseTempDirectory(String path) throws UserMachinePreferencesException { + public static void setCustomTempDirectory(String path) throws UserMachinePreferencesException { validateTempDirectory(path); - preferences.put(TEMP_DIR_KEY, path); + preferences.put(CUSTOM_TEMP_DIR_KEY, path); + } + + /** + * @return The user selection for how the temp directory should be handled + * (temp directory in case folder, in java.io.tmpdir, custom path). + */ + public static TempDirChoice getTempDirChoice() { + return TempDirChoice.getValue(preferences.get(TEMP_DIR_CHOICE_KEY, null)) + .orElse(DEFAULT_CHOICE); + } + + /** + * Sets the temp directory choice (i.e. system, case, custom). + * @param tempDirChoice The choice (must be non-null). + * @throws UserMachinePreferencesException + */ + public static void setTempDirChoice(TempDirChoice tempDirChoice) throws UserMachinePreferencesException { + if (tempDirChoice == null) { + throw new UserMachinePreferencesException("Expected non-null temp dir choice"); + } + + preferences.put(TEMP_DIR_CHOICE_KEY, tempDirChoice.name()); } private UserMachinePreferences() { diff --git a/Core/src/org/sleuthkit/autopsy/modules/leappanalyzers/aleap-artifact-attribute-reference.xml b/Core/src/org/sleuthkit/autopsy/modules/leappanalyzers/aleap-artifact-attribute-reference.xml index d5d0af5d55..27d3bc7263 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/leappanalyzers/aleap-artifact-attribute-reference.xml +++ b/Core/src/org/sleuthkit/autopsy/modules/leappanalyzers/aleap-artifact-attribute-reference.xml @@ -48,7 +48,7 @@ - + @@ -66,13 +66,13 @@ - - + + - + @@ -90,7 +90,7 @@ - + @@ -120,7 +120,7 @@ - + @@ -163,13 +163,13 @@ - - + + - + @@ -209,7 +209,7 @@ - + @@ -225,7 +225,7 @@ - + @@ -236,13 +236,13 @@ - - + + - + @@ -282,7 +282,7 @@ - + @@ -318,7 +318,7 @@ - + @@ -333,7 +333,7 @@ - + @@ -482,8 +482,8 @@ - - + + diff --git a/Core/src/org/sleuthkit/autopsy/modules/leappanalyzers/ileap-artifact-attribute-reference.xml b/Core/src/org/sleuthkit/autopsy/modules/leappanalyzers/ileap-artifact-attribute-reference.xml index 5799161685..16409f42c7 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/leappanalyzers/ileap-artifact-attribute-reference.xml +++ b/Core/src/org/sleuthkit/autopsy/modules/leappanalyzers/ileap-artifact-attribute-reference.xml @@ -72,7 +72,7 @@ - + @@ -237,8 +237,8 @@ - - + + @@ -252,17 +252,15 @@ - - - - + + + + - - @@ -314,9 +312,9 @@ - - - + + + @@ -357,7 +355,7 @@ - + @@ -377,7 +375,7 @@ - + @@ -495,13 +493,6 @@ - - - - - - - @@ -524,7 +515,7 @@ - + @@ -543,14 +534,12 @@ - + - - @@ -714,13 +703,14 @@ - + - + + - + @@ -741,13 +731,13 @@ - + - + - + diff --git a/Core/src/org/sleuthkit/autopsy/report/ReportBranding.java b/Core/src/org/sleuthkit/autopsy/report/ReportBranding.java index 93b4ee0081..e9ebf4216c 100644 --- a/Core/src/org/sleuthkit/autopsy/report/ReportBranding.java +++ b/Core/src/org/sleuthkit/autopsy/report/ReportBranding.java @@ -2,7 +2,7 @@ * * Autopsy Forensic Browser * - * Copyright 2013-2014 Basis Technology Corp. + * Copyright 2013-2021 Basis Technology Corp. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,8 @@ package org.sleuthkit.autopsy.report; import org.sleuthkit.autopsy.report.infrastructure.ReportGenerator; import java.io.File; import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.logging.Level; import org.sleuthkit.autopsy.coreutils.Logger; import org.openide.util.NbBundle; @@ -37,7 +39,7 @@ import org.sleuthkit.autopsy.coreutils.PlatformUtil; public final class ReportBranding implements ReportBrandingProviderI { //property names - public static final String AGENCY_LOGO_PATH_PROP = "AgencyLogoPath"; //NON-NLS + private static final String AGENCY_LOGO_PATH_PROP = "AgencyLogoPath"; //NON-NLS private static final String REPORT_TITLE_PROP = "ReportTitle"; //NON-NLS private static final String REPORT_FOOTER_PROP = "ReportFooter"; //NON-NLS //default settings @@ -46,8 +48,9 @@ public final class ReportBranding implements ReportBrandingProviderI { .getMessage(ReportBranding.class, "ReportBranding.defaultReportTitle.text"); private static final String DEFAULT_REPORT_FOOTER = NbBundle .getMessage(ReportBranding.class, "ReportBranding.defaultReportFooter.text"); - private String reportsBrandingDir; //dir with extracted reports branding resources - public static final String MODULE_NAME = ReportBranding.class.getSimpleName(); + private final String reportsBrandingDir; //dir with extracted reports branding resources + private final Path userConfigDir = Paths.get(PlatformUtil.getUserDirectory().getAbsolutePath()); + private static final String MODULE_NAME = ReportBranding.class.getSimpleName(); private static final Logger logger = Logger.getLogger(ReportBranding.class.getName()); // this is static so that it can be set by another object @@ -109,38 +112,73 @@ public final class ReportBranding implements ReportBrandingProviderI { generatorLogoPath = path; } + /** + * Read logo path from preferences file. Reverses the path relativization performed + * in setAgencyLogoPath(). If the stored path starts with either “/” or drive letter, + * it is a full path, and is returned to the caller. Otherwise, append current user + * directory to the saved relative path. See JIRA-7348. + * + * @return Full path to the logo file. + */ @Override public String getAgencyLogoPath() { - String curPath = null; /* * The agency logo code uses these properties to persist changes in the * logo (within the same process). This is different from the generator * logo that uses a static variable. */ - curPath = ModuleSettings.getConfigSetting(MODULE_NAME, AGENCY_LOGO_PATH_PROP); + String curPath = ModuleSettings.getConfigSetting(MODULE_NAME, AGENCY_LOGO_PATH_PROP); + + //if has been set, validate it's correct, if not set, return null - if (curPath != null && new File(curPath).canRead() == false) { - //use default - logger.log(Level.INFO, "Custom report branding for agency logo is not valid: " + curPath); //NON-NLS - curPath = null; + if (curPath != null && !curPath.isEmpty()) { + + // check if the path is an absolute path (starts with either drive letter or "/") + Path driveLetterOrNetwork = Paths.get(curPath).getRoot(); + if (driveLetterOrNetwork != null) { + // absolute path + return curPath; + } + + // Path is a relative path. Reverse path relativization performed in setAgencyLogoPath() + Path absolutePath = userConfigDir.resolve(curPath); + curPath = absolutePath.toString(); + if (new File(curPath).canRead() == false) { + //use default + logger.log(Level.INFO, "Custom report branding for agency logo is not valid: {0}", curPath); //NON-NLS + curPath = null; + } } return curPath; } + /** + * Save logo path. If the path is inside user directory (e.g. + * "C:\Users\USER_NAME\AppData\Roaming\autopsy"), trim that off and save it + * as a relative path (i.e it will not start with a “/” or drive letter). Otherwise, + * full path is saved. See JIRA-7348. + * + * @param fullPath Full path to the logo file. + */ @Override - public void setAgencyLogoPath(String path) { + public void setAgencyLogoPath(String fullPath) { + + Path relativePath = Paths.get(fullPath); + // check if the path is within user directory + if (Paths.get(fullPath).startsWith(userConfigDir)) { + // relativize the path + relativePath = userConfigDir.relativize(relativePath); + } // Use properties to persist the logo to use. - // Should use static variable instead - ModuleSettings.setConfigSetting(MODULE_NAME, AGENCY_LOGO_PATH_PROP, path); + ModuleSettings.setConfigSetting(MODULE_NAME, AGENCY_LOGO_PATH_PROP, relativePath.toString()); } @Override public String getReportTitle() { - String curTitle = null; - curTitle = ModuleSettings.getConfigSetting(MODULE_NAME, REPORT_TITLE_PROP); + String curTitle = ModuleSettings.getConfigSetting(MODULE_NAME, REPORT_TITLE_PROP); if (curTitle == null || curTitle.isEmpty()) { //use default logger.log(Level.INFO, "Using default report branding for report title"); //NON-NLS @@ -158,9 +196,8 @@ public final class ReportBranding implements ReportBrandingProviderI { @Override public String getReportFooter() { - String curFooter = null; - curFooter = ModuleSettings.getConfigSetting(MODULE_NAME, REPORT_FOOTER_PROP); + String curFooter = ModuleSettings.getConfigSetting(MODULE_NAME, REPORT_FOOTER_PROP); if (curFooter == null) { //use default logger.log(Level.INFO, "Using default report branding for report footer"); //NON-NLS diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineModule.java b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineModule.java index 6d8e5da23c..1dff37d7b1 100755 --- a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineModule.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineModule.java @@ -49,19 +49,19 @@ public class TimeLineModule { } /** - * Get instance of the controller for the current case + * Get instance of the controller for the current case. + * The controller instance is initialized from a case open event. * * @return the controller for the current case. * - * @throws NoCurrentCaseException If there is no case open. * @throws TskCoreException If there was a problem accessing the case * database. * */ - public static TimeLineController getController() throws NoCurrentCaseException, TskCoreException { + public static TimeLineController getController() throws TskCoreException { synchronized (controllerLock) { if (controller == null) { - controller = new TimeLineController(Case.getCurrentCaseThrows()); + throw new TskCoreException("Timeline controller not initialized"); } return controller; } @@ -100,13 +100,22 @@ public class TimeLineModule { } controller = null; } + } else { + // Case is opening - create the controller now + synchronized (controllerLock) { + try { + controller = new TimeLineController(Case.getCurrentCaseThrows()); + } catch (TskCoreException | NoCurrentCaseException ex) { + logger.log(Level.SEVERE, "Error creating Timeline controller", ex); + } + } } } else { try { getController().handleCaseEvent(evt); - } catch (NoCurrentCaseException ignored) { } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error handling application event", ex); + // The call to getController() will only fail due to case closing, so do + // not record the error. } } } @@ -121,12 +130,9 @@ public class TimeLineModule { public void propertyChange(PropertyChangeEvent evt) { try { getController().handleIngestModuleEvent(evt); - } catch (NoCurrentCaseException ex) { - // ignore - return; } catch (TskCoreException ex) { - MessageNotifyUtil.Message.error("Error creating timeline controller."); - logger.log(Level.SEVERE, "Error creating timeline controller", ex); + // The call to getController() will only fail due to case closing, so do + // not record the error. } } } diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AinStatusDashboard.form b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AinStatusDashboard.form index d54cd080a2..1aa6230020 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AinStatusDashboard.form +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AinStatusDashboard.form @@ -11,107 +11,128 @@ + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AinStatusDashboard.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AinStatusDashboard.java index 4c03ca839f..91419ef269 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AinStatusDashboard.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AinStatusDashboard.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2018 Basis Technology Corp. + * Copyright 2018-2021 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,6 +21,8 @@ package org.sleuthkit.autopsy.experimental.autoingest; import com.google.common.util.concurrent.ThreadFactoryBuilder; import java.awt.Cursor; import java.awt.EventQueue; +import java.awt.GridBagConstraints; +import java.awt.Insets; import java.util.Observable; import java.util.Observer; import java.util.concurrent.ScheduledThreadPoolExecutor; @@ -34,6 +36,8 @@ import org.sleuthkit.autopsy.healthmonitor.HealthMonitorDashboard; @SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives final class AinStatusDashboard extends javax.swing.JPanel implements Observer { + private static final long serialVersionUID = 1L; + private final AutoIngestMonitor autoIngestMonitor; private final AinStatusPanel nodesPanel; private final static String AIN_REFRESH_THREAD_NAME = "AID-refresh-jobs-%d"; @@ -50,9 +54,17 @@ final class AinStatusDashboard extends javax.swing.JPanel implements Observer { scheduledRefreshThreadPoolExecutor = new ScheduledThreadPoolExecutor(1, new ThreadFactoryBuilder().setNameFormat(AIN_REFRESH_THREAD_NAME).build()); autoIngestMonitor = monitor; nodesPanel = new AinStatusPanel(); - nodesPanel.setSize(nodesPanel.getSize()); - nodeStatusScrollPane.add(nodesPanel); - nodeStatusScrollPane.setViewportView(nodesPanel); + GridBagConstraints gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = GridBagConstraints.BOTH; + gridBagConstraints.weightx = 1; + gridBagConstraints.weighty = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new Insets(0, 10, 0, 10); + mainPanel.add(nodesPanel, gridBagConstraints); + + refreshTables(); } @@ -79,20 +91,45 @@ final class AinStatusDashboard extends javax.swing.JPanel implements Observer { @SuppressWarnings("unchecked") // //GEN-BEGIN:initComponents private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + javax.swing.JScrollPane mainScrollPane = new javax.swing.JScrollPane(); + mainPanel = new javax.swing.JPanel(); + nodeStatusTableTitle = new javax.swing.JLabel(); + buttonPanel = new javax.swing.JPanel(); refreshButton = new javax.swing.JButton(); clusterMetricsButton = new javax.swing.JButton(); - nodeStatusScrollPane = new javax.swing.JScrollPane(); - nodeStatusTableTitle = new javax.swing.JLabel(); healthMonitorButton = new javax.swing.JButton(); + filler1 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(32767, 0)); + + setLayout(new java.awt.BorderLayout()); + + mainPanel.setLayout(new java.awt.GridBagLayout()); + + nodeStatusTableTitle.setFont(nodeStatusTableTitle.getFont().deriveFont(nodeStatusTableTitle.getFont().getSize()+3f)); + org.openide.awt.Mnemonics.setLocalizedText(nodeStatusTableTitle, org.openide.util.NbBundle.getMessage(AinStatusDashboard.class, "AinStatusDashboard.nodeStatusTableTitle.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.insets = new java.awt.Insets(10, 10, 5, 10); + mainPanel.add(nodeStatusTableTitle, gridBagConstraints); + + buttonPanel.setLayout(new java.awt.GridBagLayout()); org.openide.awt.Mnemonics.setLocalizedText(refreshButton, org.openide.util.NbBundle.getMessage(AinStatusDashboard.class, "AinStatusDashboard.refreshButton.text")); // NOI18N refreshButton.setToolTipText(org.openide.util.NbBundle.getMessage(AinStatusDashboard.class, "AinStatusDashboard.refreshButton.toolTipText")); // NOI18N + refreshButton.setPreferredSize(new java.awt.Dimension(133, 23)); refreshButton.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { refreshButtonActionPerformed(evt); } }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + buttonPanel.add(refreshButton, gridBagConstraints); org.openide.awt.Mnemonics.setLocalizedText(clusterMetricsButton, org.openide.util.NbBundle.getMessage(AinStatusDashboard.class, "AinStatusDashboard.clusterMetricsButton.text")); // NOI18N clusterMetricsButton.addActionListener(new java.awt.event.ActionListener() { @@ -100,56 +137,42 @@ final class AinStatusDashboard extends javax.swing.JPanel implements Observer { clusterMetricsButtonActionPerformed(evt); } }); - - nodeStatusTableTitle.setFont(nodeStatusTableTitle.getFont().deriveFont(nodeStatusTableTitle.getFont().getSize()+3f)); - org.openide.awt.Mnemonics.setLocalizedText(nodeStatusTableTitle, org.openide.util.NbBundle.getMessage(AinStatusDashboard.class, "AinStatusDashboard.nodeStatusTableTitle.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 3; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + buttonPanel.add(clusterMetricsButton, gridBagConstraints); org.openide.awt.Mnemonics.setLocalizedText(healthMonitorButton, org.openide.util.NbBundle.getMessage(AinStatusDashboard.class, "AinStatusDashboard.healthMonitorButton.text")); // NOI18N - healthMonitorButton.setMaximumSize(new java.awt.Dimension(133, 23)); - healthMonitorButton.setMinimumSize(new java.awt.Dimension(133, 23)); healthMonitorButton.setPreferredSize(new java.awt.Dimension(133, 23)); healthMonitorButton.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { healthMonitorButtonActionPerformed(evt); } }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + buttonPanel.add(healthMonitorButton, gridBagConstraints); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 1.0; + buttonPanel.add(filler1, gridBagConstraints); - javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); - this.setLayout(layout); - layout.setHorizontalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addContainerGap() - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(nodeStatusScrollPane) - .addGroup(layout.createSequentialGroup() - .addComponent(nodeStatusTableTitle) - .addGap(0, 0, Short.MAX_VALUE)) - .addGroup(layout.createSequentialGroup() - .addComponent(refreshButton, javax.swing.GroupLayout.PREFERRED_SIZE, 100, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 576, Short.MAX_VALUE) - .addComponent(healthMonitorButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(clusterMetricsButton))) - .addContainerGap()) - ); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(10, 10, 10, 10); + mainPanel.add(buttonPanel, gridBagConstraints); - layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {clusterMetricsButton, refreshButton}); + mainScrollPane.setViewportView(mainPanel); - layout.setVerticalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() - .addGap(40, 40, 40) - .addComponent(nodeStatusTableTitle, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(0, 0, 0) - .addComponent(nodeStatusScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 215, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(382, 382, 382) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(refreshButton) - .addComponent(clusterMetricsButton) - .addComponent(healthMonitorButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addContainerGap()) - ); + add(mainScrollPane, java.awt.BorderLayout.CENTER); }// //GEN-END:initComponents private void refreshButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_refreshButtonActionPerformed @@ -172,9 +195,11 @@ final class AinStatusDashboard extends javax.swing.JPanel implements Observer { // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JPanel buttonPanel; private javax.swing.JButton clusterMetricsButton; + private javax.swing.Box.Filler filler1; private javax.swing.JButton healthMonitorButton; - private javax.swing.JScrollPane nodeStatusScrollPane; + private javax.swing.JPanel mainPanel; private javax.swing.JLabel nodeStatusTableTitle; private javax.swing.JButton refreshButton; // End of variables declaration//GEN-END:variables diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AinStatusDashboardTopComponent.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AinStatusDashboardTopComponent.java index 090932e154..46704560b3 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AinStatusDashboardTopComponent.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AinStatusDashboardTopComponent.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2018 Basis Technology Corp. + * Copyright 2018-2021 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,6 +18,7 @@ */ package org.sleuthkit.autopsy.experimental.autoingest; +import java.awt.BorderLayout; import java.awt.Component; import java.util.List; import java.util.logging.Level; @@ -73,7 +74,8 @@ final class AinStatusDashboardTopComponent extends TopComponent { AinStatusDashboard nodeTab = new AinStatusDashboard(monitor); nodeTab.startUp(); nodeTab.setSize(nodeTab.getPreferredSize()); - tc.add(nodeTab); + tc.setLayout(new BorderLayout()); + tc.add(nodeTab, BorderLayout.CENTER); tc.open(); } } diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestDashboard.form b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestDashboard.form index e6aaa64a77..fb0726febc 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestDashboard.form +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestDashboard.form @@ -22,167 +22,185 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - + + + + + + - - - - - + + - - - - - - + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestDashboard.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestDashboard.java index 9308e55ad2..075ba9395b 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestDashboard.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestDashboard.java @@ -348,8 +348,11 @@ final class AutoIngestDashboard extends JPanel implements Observer { @SuppressWarnings("unchecked") // //GEN-BEGIN:initComponents private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; jButton1 = new javax.swing.JButton(); + mainScrollPane = new javax.swing.JScrollPane(); + mainPanel = new javax.swing.JPanel(); pendingScrollPane = new javax.swing.JScrollPane(); runningScrollPane = new javax.swing.JScrollPane(); completedScrollPane = new javax.swing.JScrollPane(); @@ -362,19 +365,77 @@ final class AutoIngestDashboard extends JPanel implements Observer { org.openide.awt.Mnemonics.setLocalizedText(jButton1, org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.jButton1.text")); // NOI18N + setLayout(new java.awt.BorderLayout()); + + mainPanel.setLayout(new java.awt.GridBagLayout()); + pendingScrollPane.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); pendingScrollPane.setVerticalScrollBarPolicy(javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER); pendingScrollPane.setOpaque(false); - pendingScrollPane.setPreferredSize(new java.awt.Dimension(2, 215)); + pendingScrollPane.setPreferredSize(new java.awt.Dimension(2, 150)); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 10, 10, 10); + mainPanel.add(pendingScrollPane, gridBagConstraints); + + runningScrollPane.setPreferredSize(new java.awt.Dimension(2, 150)); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 5; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 10, 10, 10); + mainPanel.add(runningScrollPane, gridBagConstraints); + + completedScrollPane.setPreferredSize(new java.awt.Dimension(2, 150)); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 7; + gridBagConstraints.gridwidth = 5; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 10, 10, 10); + mainPanel.add(completedScrollPane, gridBagConstraints); lbPending.setFont(lbPending.getFont().deriveFont(lbPending.getFont().getSize()+3f)); org.openide.awt.Mnemonics.setLocalizedText(lbPending, org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.lbPending.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(10, 10, 5, 10); + mainPanel.add(lbPending, gridBagConstraints); lbRunning.setFont(lbRunning.getFont().deriveFont(lbRunning.getFont().getSize()+3f)); org.openide.awt.Mnemonics.setLocalizedText(lbRunning, org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.lbRunning.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 4; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(0, 10, 5, 10); + mainPanel.add(lbRunning, gridBagConstraints); lbCompleted.setFont(lbCompleted.getFont().deriveFont(lbCompleted.getFont().getSize()+3f)); org.openide.awt.Mnemonics.setLocalizedText(lbCompleted, org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.lbCompleted.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 6; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(0, 10, 5, 10); + mainPanel.add(lbCompleted, gridBagConstraints); org.openide.awt.Mnemonics.setLocalizedText(refreshButton, org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.refreshButton.text")); // NOI18N refreshButton.setToolTipText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.refreshButton.toolTipText")); // NOI18N @@ -383,61 +444,39 @@ final class AutoIngestDashboard extends JPanel implements Observer { refreshButtonActionPerformed(evt); } }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 8; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(6, 10, 10, 10); + mainPanel.add(refreshButton, gridBagConstraints); lbServicesStatus.setFont(lbServicesStatus.getFont().deriveFont(lbServicesStatus.getFont().getSize()+3f)); org.openide.awt.Mnemonics.setLocalizedText(lbServicesStatus, org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.lbServicesStatus.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(10, 10, 0, 10); + mainPanel.add(lbServicesStatus, gridBagConstraints); tbServicesStatusMessage.setEditable(false); tbServicesStatusMessage.setFont(tbServicesStatusMessage.getFont().deriveFont(tbServicesStatusMessage.getFont().getStyle() | java.awt.Font.BOLD, tbServicesStatusMessage.getFont().getSize()+1)); tbServicesStatusMessage.setText(org.openide.util.NbBundle.getMessage(AutoIngestDashboard.class, "AutoIngestDashboard.tbServicesStatusMessage.text")); // NOI18N tbServicesStatusMessage.setBorder(null); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(10, 0, 0, 10); + mainPanel.add(tbServicesStatusMessage, gridBagConstraints); - javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); - this.setLayout(layout); - layout.setHorizontalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() - .addContainerGap() - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) - .addComponent(pendingScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(runningScrollPane, javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(completedScrollPane, javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup() - .addComponent(lbServicesStatus) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addComponent(tbServicesStatusMessage, javax.swing.GroupLayout.DEFAULT_SIZE, 861, Short.MAX_VALUE)) - .addGroup(javax.swing.GroupLayout.Alignment.LEADING, layout.createSequentialGroup() - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) - .addComponent(lbPending, javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(lbCompleted, javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(lbRunning, javax.swing.GroupLayout.Alignment.LEADING)) - .addGap(0, 0, Short.MAX_VALUE)) - .addComponent(refreshButton, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.PREFERRED_SIZE, 100, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addContainerGap()) - ); - layout.setVerticalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() - .addContainerGap() - .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) - .addGap(1, 1, 1) - .addComponent(pendingScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addComponent(lbRunning) - .addGap(1, 1, 1) - .addComponent(runningScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 133, Short.MAX_VALUE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addComponent(lbCompleted) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(completedScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 179, Short.MAX_VALUE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(refreshButton) - .addContainerGap()) - ); + mainScrollPane.setViewportView(mainPanel); + + add(mainScrollPane, java.awt.BorderLayout.CENTER); }// //GEN-END:initComponents /** @@ -460,6 +499,8 @@ final class AutoIngestDashboard extends JPanel implements Observer { private javax.swing.JLabel lbPending; private javax.swing.JLabel lbRunning; private javax.swing.JLabel lbServicesStatus; + private javax.swing.JPanel mainPanel; + private javax.swing.JScrollPane mainScrollPane; private javax.swing.JScrollPane pendingScrollPane; private javax.swing.JButton refreshButton; private javax.swing.JScrollPane runningScrollPane; diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestDashboardTopComponent.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestDashboardTopComponent.java index f21d80fb8d..18a5a85d41 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestDashboardTopComponent.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestDashboardTopComponent.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2018 Basis Technology Corp. + * Copyright 2011-2021 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,6 +18,7 @@ */ package org.sleuthkit.autopsy.experimental.autoingest; +import java.awt.BorderLayout; import java.awt.Component; import java.awt.EventQueue; import java.util.List; @@ -49,7 +50,7 @@ public final class AutoIngestDashboardTopComponent extends TopComponent { public final static String PREFERRED_ID = "AutoIngestDashboardTopComponent"; // NON-NLS private static final Logger logger = Logger.getLogger(AutoIngestDashboardTopComponent.class.getName()); private static boolean topComponentInitialized = false; - + @Messages({ "AutoIngestDashboardTopComponent.exceptionMessage.failedToCreateDashboard=Failed to create Auto Ingest Dashboard.",}) public static void openTopComponent() { @@ -69,13 +70,14 @@ public final class AutoIngestDashboardTopComponent extends TopComponent { * dashboard instance so we don't accumulate them. */ tc.removeAll(); + tc.setLayout(new BorderLayout()); /* * Create a new dashboard instance to ensure we're using the * most recent configuration. */ AutoIngestDashboard dashboard = AutoIngestDashboard.createDashboard(); - tc.add(dashboard); + tc.add(dashboard, BorderLayout.CENTER); dashboard.setSize(dashboard.getPreferredSize()); //if the user has administrator access enabled open the Node Status and cases top components as well if (AutoIngestDashboard.isAdminAutoIngestDashboard()) { diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties-MERGED b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties-MERGED index 56a675e256..823399e0d0 100755 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties-MERGED +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties-MERGED @@ -205,7 +205,9 @@ DeleteCaseTask.progress.parsingManifest=Parsing manifest file {0}... DeleteCaseTask.progress.releasingManifestLock=Releasing lock on the manifest file {0}... DeleteCaseTask.progress.startMessage=Starting deletion... DeleteOrphanCaseNodesAction.progressDisplayName=Cleanup Case Znodes +# {0} - item count DeleteOrphanCaseNodesDialog.additionalInit.lblNodeCount.text=Znodes found: {0} +# {0} - item count DeleteOrphanCaseNodesDialog.additionalInit.znodesTextArea.countMessage=ZNODES FOUND: {0} DeleteOrphanCaseNodesTask.progress.connectingToCoordSvc=Connecting to the coordination service # {0} - node path diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CasesDashboardTopComponent.form b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CasesDashboardTopComponent.form index 3ade741f25..4e4452485b 100755 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CasesDashboardTopComponent.form +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CasesDashboardTopComponent.form @@ -13,78 +13,79 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CasesDashboardTopComponent.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CasesDashboardTopComponent.java index dcd7fa1fab..51cc3a805b 100755 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CasesDashboardTopComponent.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/CasesDashboardTopComponent.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2019-2019 Basis Technology Corp. + * Copyright 2019-2021 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,6 +18,7 @@ */ package org.sleuthkit.autopsy.experimental.autoingest; +import java.awt.BorderLayout; import java.util.logging.Level; import org.openide.explorer.ExplorerManager; import org.openide.explorer.ExplorerUtils; @@ -95,8 +96,7 @@ public final class CasesDashboardTopComponent extends TopComponent implements Ex explorerManager = new ExplorerManager(); associateLookup(ExplorerUtils.createLookup(explorerManager, getActionMap())); caseBrowserPanel = new MultiUserCasesBrowserPanel(explorerManager, new CasesDashboardCustomizer()); - caseBrowserScrollPane.add(caseBrowserPanel); - caseBrowserScrollPane.setViewportView(caseBrowserPanel); + mainPanel.add(caseBrowserPanel, BorderLayout.CENTER); } @Override @@ -116,18 +116,31 @@ public final class CasesDashboardTopComponent extends TopComponent implements Ex */ // //GEN-BEGIN:initComponents private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; - refreshButton = new javax.swing.JButton(); caseBrowserScrollPane = new javax.swing.JScrollPane(); + mainPanel = new javax.swing.JPanel(); + bottomPanel = new javax.swing.JPanel(); + buttonPanel = new javax.swing.JPanel(); + refreshButton = new javax.swing.JButton(); deleteOrphanCaseNodesButton = new javax.swing.JButton(); deleteOrphanManifestNodesButton = new javax.swing.JButton(); + setLayout(new java.awt.BorderLayout()); + + mainPanel.setLayout(new java.awt.BorderLayout()); + + bottomPanel.setLayout(new java.awt.GridBagLayout()); + + buttonPanel.setLayout(new java.awt.GridLayout()); + org.openide.awt.Mnemonics.setLocalizedText(refreshButton, org.openide.util.NbBundle.getMessage(CasesDashboardTopComponent.class, "CasesDashboardTopComponent.refreshButton.text")); // NOI18N refreshButton.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { refreshButtonActionPerformed(evt); } }); + buttonPanel.add(refreshButton); org.openide.awt.Mnemonics.setLocalizedText(deleteOrphanCaseNodesButton, org.openide.util.NbBundle.getMessage(CasesDashboardTopComponent.class, "CasesDashboardTopComponent.deleteOrphanCaseNodesButton.text")); // NOI18N deleteOrphanCaseNodesButton.addActionListener(new java.awt.event.ActionListener() { @@ -135,6 +148,7 @@ public final class CasesDashboardTopComponent extends TopComponent implements Ex deleteOrphanCaseNodesButtonActionPerformed(evt); } }); + buttonPanel.add(deleteOrphanCaseNodesButton); org.openide.awt.Mnemonics.setLocalizedText(deleteOrphanManifestNodesButton, org.openide.util.NbBundle.getMessage(CasesDashboardTopComponent.class, "CasesDashboardTopComponent.deleteOrphanManifestNodesButton.text")); // NOI18N deleteOrphanManifestNodesButton.addActionListener(new java.awt.event.ActionListener() { @@ -142,43 +156,21 @@ public final class CasesDashboardTopComponent extends TopComponent implements Ex deleteOrphanManifestNodesButtonActionPerformed(evt); } }); + buttonPanel.add(deleteOrphanManifestNodesButton); - 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(refreshButton) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(deleteOrphanCaseNodesButton) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(deleteOrphanManifestNodesButton) - .addGap(0, 0, Short.MAX_VALUE)) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() - .addComponent(caseBrowserScrollPane) - .addContainerGap()))) - ); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(10, 10, 10, 10); + bottomPanel.add(buttonPanel, gridBagConstraints); - layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {deleteOrphanCaseNodesButton, deleteOrphanManifestNodesButton, refreshButton}); + mainPanel.add(bottomPanel, java.awt.BorderLayout.SOUTH); - layout.setVerticalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addContainerGap() - .addComponent(caseBrowserScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 246, Short.MAX_VALUE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(refreshButton) - .addComponent(deleteOrphanCaseNodesButton) - .addComponent(deleteOrphanManifestNodesButton)) - .addContainerGap()) - ); - - layout.linkSize(javax.swing.SwingConstants.VERTICAL, new java.awt.Component[] {deleteOrphanCaseNodesButton, deleteOrphanManifestNodesButton, refreshButton}); + caseBrowserScrollPane.setViewportView(mainPanel); + add(caseBrowserScrollPane, java.awt.BorderLayout.CENTER); }// //GEN-END:initComponents private void refreshButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_refreshButtonActionPerformed @@ -194,9 +186,12 @@ public final class CasesDashboardTopComponent extends TopComponent implements Ex }//GEN-LAST:event_deleteOrphanManifestNodesButtonActionPerformed // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JPanel bottomPanel; + private javax.swing.JPanel buttonPanel; private javax.swing.JScrollPane caseBrowserScrollPane; private javax.swing.JButton deleteOrphanCaseNodesButton; private javax.swing.JButton deleteOrphanManifestNodesButton; + private javax.swing.JPanel mainPanel; private javax.swing.JButton refreshButton; // End of variables declaration//GEN-END:variables diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/Bundle.properties b/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/Bundle.properties index 1c7d3312da..2de432ff18 100755 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/Bundle.properties +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/Bundle.properties @@ -10,3 +10,5 @@ MemoryDSInputPanel.errorLabel.text=Error Label MemoryDSInputPanel.browseButton.text=Browse MemoryDSInputPanel.timeZoneLabel.text=Timezone: MemoryDSInputPanel.profileLabel.text=Profile: +MemoryDSInputPanel.selectAllButton.text=Select All +MemoryDSInputPanel.deselectAllButton.text=Deselect All diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/Bundle.properties-MERGED b/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/Bundle.properties-MERGED index 4173708524..a454855ebf 100755 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/Bundle.properties-MERGED +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/Bundle.properties-MERGED @@ -17,6 +17,8 @@ MemoryDSInputPanel.errorLabel.text=Error Label MemoryDSInputPanel.browseButton.text=Browse MemoryDSInputPanel.timeZoneLabel.text=Timezone: MemoryDSInputPanel.profileLabel.text=Profile: +MemoryDSInputPanel.selectAllButton.text=Select All +MemoryDSInputPanel.deselectAllButton.text=Deselect All MemoryDSInputPanel_errorMsg_dataSourcePathOnCdrive=Path to multi-user data source is on "C:" drive MemoryDSInputPanel_errorMsg_noOpenCase=No open case MemoryDSProcessor.dataSourceType=Memory Image File (Volatility) diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/MemoryDSInputPanel.form b/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/MemoryDSInputPanel.form index f899eeb503..60191d6f48 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/MemoryDSInputPanel.form +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/MemoryDSInputPanel.form @@ -37,6 +37,11 @@ + + + + + @@ -78,7 +83,12 @@ - + + + + + + @@ -188,5 +198,25 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/MemoryDSInputPanel.java b/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/MemoryDSInputPanel.java index 3d41adf201..ed7b4be7b5 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/MemoryDSInputPanel.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/MemoryDSInputPanel.java @@ -191,6 +191,8 @@ final class MemoryDSInputPanel extends JPanel implements DocumentListener { pluginTable = new javax.swing.JTable(); profileLabel = new javax.swing.JLabel(); profileComboBox = new javax.swing.JComboBox<>(); + selectAllButton = new javax.swing.JButton(); + deselectAllButton = new javax.swing.JButton(); org.openide.awt.Mnemonics.setLocalizedText(pathLabel, org.openide.util.NbBundle.getMessage(MemoryDSInputPanel.class, "MemoryDSInputPanel.pathLabel.text")); // NOI18N @@ -238,6 +240,20 @@ final class MemoryDSInputPanel extends JPanel implements DocumentListener { } }); + org.openide.awt.Mnemonics.setLocalizedText(selectAllButton, org.openide.util.NbBundle.getMessage(MemoryDSInputPanel.class, "MemoryDSInputPanel.selectAllButton.text")); // NOI18N + selectAllButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + selectAllButtonActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(deselectAllButton, org.openide.util.NbBundle.getMessage(MemoryDSInputPanel.class, "MemoryDSInputPanel.deselectAllButton.text")); // NOI18N + deselectAllButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + deselectAllButtonActionPerformed(evt); + } + }); + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( @@ -256,7 +272,11 @@ final class MemoryDSInputPanel extends JPanel implements DocumentListener { .addComponent(listsScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 248, javax.swing.GroupLayout.PREFERRED_SIZE) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING, false) .addComponent(profileComboBox, javax.swing.GroupLayout.Alignment.LEADING, 0, 243, Short.MAX_VALUE) - .addComponent(timeZoneComboBox, javax.swing.GroupLayout.Alignment.LEADING, 0, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))))) + .addComponent(timeZoneComboBox, javax.swing.GroupLayout.Alignment.LEADING, 0, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addGroup(layout.createSequentialGroup() + .addComponent(selectAllButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(deselectAllButton))))) .addGap(0, 163, Short.MAX_VALUE)) .addGroup(layout.createSequentialGroup() .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) @@ -287,7 +307,11 @@ final class MemoryDSInputPanel extends JPanel implements DocumentListener { .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(PluginsToRunLabel) .addComponent(listsScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 122, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addContainerGap(73, Short.MAX_VALUE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(selectAllButton) + .addComponent(deselectAllButton)) + .addContainerGap(44, Short.MAX_VALUE)) ); pathLabel.getAccessibleContext().setAccessibleName(org.openide.util.NbBundle.getMessage(MemoryDSInputPanel.class, "MemoryDSInputPanel.pathLabel.AccessibleContext.accessibleName")); // NOI18N @@ -316,9 +340,24 @@ final class MemoryDSInputPanel extends JPanel implements DocumentListener { // TODO add your handling code here: }//GEN-LAST:event_pathTextFieldActionPerformed + private void selectAllButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_selectAllButtonActionPerformed + for(String name : pluginListStates.keySet()) { + pluginListStates.put(name, Boolean.TRUE); + } + tableModel.fireTableDataChanged(); + }//GEN-LAST:event_selectAllButtonActionPerformed + + private void deselectAllButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_deselectAllButtonActionPerformed + for(String name : pluginListStates.keySet()) { + pluginListStates.put(name, Boolean.FALSE); + } + tableModel.fireTableDataChanged(); + }//GEN-LAST:event_deselectAllButtonActionPerformed + // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JLabel PluginsToRunLabel; private javax.swing.JButton browseButton; + private javax.swing.JButton deselectAllButton; private javax.swing.JLabel errorLabel; private javax.swing.ButtonGroup infileTypeButtonGroup; private javax.swing.JScrollPane listsScrollPane; @@ -327,6 +366,7 @@ final class MemoryDSInputPanel extends JPanel implements DocumentListener { private javax.swing.JTable pluginTable; private javax.swing.JComboBox profileComboBox; private javax.swing.JLabel profileLabel; + private javax.swing.JButton selectAllButton; private javax.swing.JComboBox timeZoneComboBox; private javax.swing.JLabel timeZoneLabel; // End of variables declaration//GEN-END:variables diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java index 9513163587..4ecb35a455 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java @@ -192,12 +192,15 @@ public final class ImageGalleryController { * @param theCase The case. */ static void shutDownController(Case theCase) { + ImageGalleryController controller = null; synchronized (controllersByCaseLock) { if (controllersByCase.containsKey(theCase.getName())) { - ImageGalleryController controller = controllersByCase.remove(theCase.getName()); - controller.shutDown(); + controller = controllersByCase.remove(theCase.getName()); } } + if (controller != null) { + controller.shutDown(); + } } /** diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableFile.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableFile.java index 112c7ab6ec..e520e2d0ec 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableFile.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableFile.java @@ -100,13 +100,9 @@ public abstract class DrawableFile { private String model; - private final CategoryManager categoryManager; - protected DrawableFile(AbstractFile file, Boolean analyzed) { this.analyzed = new SimpleBooleanProperty(analyzed); this.file = file; - - categoryManager = ImageGalleryController.getController(Case.getCurrentCase()).getCategoryManager(); } public abstract boolean isVideo(); @@ -245,13 +241,19 @@ public abstract class DrawableFile { /** * Update the category property. */ - private void updateCategory() { + private void updateCategory() { try { + ImageGalleryController controllerForCase = ImageGalleryController.getController(Case.getCurrentCaseThrows()); + if (controllerForCase == null) { + // This can only happen during case closing, so return without generating an error. + return; + } + List contentTags = getContentTags(); TagName tag = null; for (ContentTag ct : contentTags) { TagName tagName = ct.getName(); - if (categoryManager.isCategoryTagName(tagName)) { + if (controllerForCase.getCategoryManager().isCategoryTagName(tagName)) { tag = tagName; break; } @@ -259,7 +261,7 @@ public abstract class DrawableFile { categoryTagName.set(tag); } catch (TskCoreException ex) { LOGGER.log(Level.WARNING, "problem looking up category for " + this.getContentPathSafe(), ex); //NON-NLS - } catch (IllegalStateException ex) { + } catch (IllegalStateException | NoCurrentCaseException ex) { // We get here many times if the case is closed during ingest, so don't print out a ton of warnings. } } diff --git a/build-windows-installer.xml b/build-windows-installer.xml index e07d110568..5bf9bdb67a 100644 --- a/build-windows-installer.xml +++ b/build-windows-installer.xml @@ -173,7 +173,7 @@ - + diff --git a/docs/doxygen-user/images/solr/solr_config_monitoring.png b/docs/doxygen-user/images/solr/solr_config_monitoring.png new file mode 100644 index 0000000000..d6116cd256 Binary files /dev/null and b/docs/doxygen-user/images/solr/solr_config_monitoring.png differ diff --git a/docs/doxygen-user/images/solr/solr_config_param.png b/docs/doxygen-user/images/solr/solr_config_param.png index dbc81d0cb1..3960fcf2e8 100644 Binary files a/docs/doxygen-user/images/solr/solr_config_param.png and b/docs/doxygen-user/images/solr/solr_config_param.png differ diff --git a/docs/doxygen-user/multi-user/installSolr.dox b/docs/doxygen-user/multi-user/installSolr.dox index 8bfa2c712e..8e52b5ba11 100644 --- a/docs/doxygen-user/multi-user/installSolr.dox +++ b/docs/doxygen-user/multi-user/installSolr.dox @@ -53,9 +53,8 @@ Follow these steps to configure Solr: Required Solr Configuration Parameters:
  • JAVA_HOME – path to 64-bit JRE installation. For example \c "JAVA_HOME=C:\Program Files\Java\jre1.8.0_151" or \c "JAVA_HOME=C:\Program Files\ojdkbuild\java-1.8.0-openjdk-1.8.0.222-1" -
  • DEFAULT_CONFDIR – path to Autopsy configuration directory. If the Solr archive was extracted into \c "C:\solr-8.6.3" directory, then this path will be \c "C:\ solr-8.6.3\server\solr\configsets\AutopsyConfig\conf". -
  • Dbootstrap_confdir – same path as DEFAULT_CONFDIR -
  • SOLR_JAVA_MEM - Solr JVM heap size should be somewhere between one third and one half of the total RAM available on the machine. A rule of thumb would be use \c "set SOLR_JAVA_MEM=-Xms2G -Xmx14G" for a machine with 32GB of RAM or more, and \c "set SOLR_JAVA_MEM=-Xms2G -Xmx8G" for a machine with 16GB of RAM. +
  • DEFAULT_CONFDIR – path to Autopsy configuration directory. If the Solr archive was extracted into \c "C:\solr-8.6.3" directory, then this path will be \c "C:\ solr-8.6.3\server\solr\configsets\AutopsyConfig\conf". Do not include quotes around the path. +
  • SOLR_JAVA_MEM - Solr JVM heap size should be as large as the Solr machine’s resources allow, at least half of the total RAM available on the machine. A rule of thumb would be use "set SOLR_JAVA_MEM=-Xms2G -Xmx40G" for a machine with 64GB of RAM, "set SOLR_JAVA_MEM=-Xms2G -Xmx20G" for a machine with 32GB of RAM, and "set SOLR_JAVA_MEM=-Xms2G -Xmx8G" for a machine with 16GB of RAM. Please see the \ref install_solr_heap_usage "troubleshooting section" for more info regarding Solr heap usage and troubleshooting information.
  • SOLR_DATA_HOME – location where Solr indexes will be stored. If this is not configured, the indexes will be stored in the \c "C:\solr-8.6.3\server\solr" directory. NOTE: for Autopsy cases consisting of large number of data sources, Solr indexes can get very large (hundreds of GBs, or TBs) so they should probably be stored on a larger network share.
@@ -208,8 +207,57 @@ Solr creates two types of data that need to be backed up:
  1. In a default installation that data is stored in \c "C:\solr-8.6.3\server\solr zoo_data" (assuming that the Solr package ZIP was extracted into \c "C:\solr-8.6.3" directory).
-\section install_solr_delayed_start Delayed Start Problems With Large Number Of Solr Collections +\section Troubleshooting + +\subsection install_solr_delayed_start Delayed Start Problems With Large Number Of Solr Collections In our testing, we have encountered an issue when a very large number (thousands) of Autopsy multi-user cases was created. Each new Autopsy multi-user case creates a Solr "collection" that contains the Solr text index. With 2,000 existing collections, when Solr service is restarted, Solr appears to internally be "loading" roughly 250 collections per minute (in chronological order, starting with oldest collections). After 4 minutes roughly half of the 2,000 collections were loaded. Users are able to search the collections that have been loaded, but they are unable to open or search the collections that have not yet been internally loaded by Solr. After 7-8 minutes all collections were loaded. These numbers will vary depending on the specific cluster configuration, text index file location (network or local storage), network throughput, number of Solr servers, etc. +\subsection install_solr_heap_usage Solr Heap Usage and Recommendations + +Solr JVM heap plays especially important role if you are going to create a large number of Autopsy cases (i.e. Solr collections). Here are some “rule of thumb” Solr heap usage stats that we identified during our internal testing: +
    +
  • For very small cases/collections, our tests show that Solr uses an absolute minimum of 7-10 MB of heap per collection. +
  • For larger cases/collections (50-100GB input E01 size) Solr uses at least 65 MB per collection +
  • For large cases/collections (1.5TB input E01 size) Solr uses at least 850 MB per collection +
+ +\subsubsection install_solr_heap_troublshooting Troubleshooting Solr Heap Issues + +Once the Solr JVM uses all of its available heap and is unable to free up any memory via garbage collection, the Solr service will not be able to create new collections or may become completely unresponsive, resulting in Autopsy being unable to create new text indexes. Below is a list of some of the errors that you might see as a result of this in the Solr (not Autopsy) service logs and/or the Solr admin console: + +
    +
  • org.apache.solr.common.SolrException: Could not register as the leader because creating the ephemeral registration node in ZooKeeper failed +
  • RequestHandlerBase org.apache.solr.common.SolrException: Failed to get config from zookeeper +
  • RecoveryStrategy Error while trying to recover. org.apache.solr.common.SolrException: Cloud state still says we are leader. +
  • RequestHandlerBase org.apache.solr.common.SolrException: Could not load collection from ZK +
  • org.apache.solr.common.SolrException: Error CREATEing SolrCore: Unable to create core. Caused by: There are no more files +
  • org.apache.solr.common.SolrException: java.io.IOException: There are no more files +
  • org.apache.solr.common.SolrException: Cannot unload non-existent core +
  • ZkIndexSchemaReader Error creating ZooKeeper watch for the managed schema +
+ +You may also see the following ZooKeeper errors: +
    +
  • org.apache.zookeeper.KeeperException$NodeExistsException: KeeperErrorCode = NodeExists +
  • org.apache.zookeeper.KeeperException$BadVersionException: KeeperErrorCode = BadVersion for (collection_name)/state.json +
  • org.apache.zookeeper.KeeperException$SessionExpiredException: KeeperErrorCode = Session expired for /roles.json +
  • org.apache.zookeeper.KeeperException$SessionExpiredException: KeeperErrorCode = Session expired for /configs/AutopsyConfig/managed-schema +
+ +The common theme among most of these errors is the breakdown in communication between Solr and ZooKeeper, especially when using an embedded ZooKeeper server. It is important to note that these errors may potentially occur for other reasons and are not unique to Solr heap issues. + +\subsubsection install_solr_monitoring Monitoring Solr Heap Usage + +The simplest way to see current Solr heap usage is to check the Solr Admin Console web page. To access the Solr admin console, on the Solr machine navigate to http://localhost:8983/solr/#/ . There you will be able to see the Solr memory usage: + +\image html solr_config_monitoring.png + +However, the dashboard does not show enough detail to know when Solr is out of heap, so it should only be used to identify that you are NOT having heap issues. Even if the dashboard shows that the Solr heap is fully used, it may or may not be an issue. It is best to use profiling tools like Java VisualVM. In order for VisualVM to connect to Solr, you will need to enable the JMX interface for Solr’s Java process. The details are described here: +
  • https://solr.apache.org/guide/8_3/using-jmx-with-solr.html#using-jmx-with-solr
+ +Solr heap and other performance tuning is described in the following article: +
  • https://cwiki.apache.org/confluence/display/SOLR/SolrPerformanceProblems
+ + */ diff --git a/test/script/tskdbdiff.py b/test/script/tskdbdiff.py index 85087892b1..9452b335d9 100644 --- a/test/script/tskdbdiff.py +++ b/test/script/tskdbdiff.py @@ -444,6 +444,7 @@ def normalize_db_entry(line, files_table, vs_parts_table, vs_info_table, fs_info ig_groups_seen_index = line.find('INSERT INTO "image_gallery_groups_seen"') > -1 or line.find('INSERT INTO image_gallery_groups_seen ') > -1 os_account_index = line.find('INSERT INTO "tsk_os_accounts"') > -1 or line.find('INSERT INTO tsk_os_accounts') > -1 os_account_attr_index = line.find('INSERT INTO "tsk_os_account_attributes"') > -1 or line.find('INSERT INTO tsk_os_account_attributes') > -1 + os_account_instances_index = line.find('INSERT INTO "tsk_os_account_instances"') > -1 or line.find('INSERT INTO tsk_os_account_instances') > -1 parens = line[line.find('(') + 1 : line.rfind(')')] no_space_parens = parens.replace(" ", "") @@ -664,6 +665,11 @@ def normalize_db_entry(line, files_table, vs_parts_table, vs_info_table, fs_info fields_list[3] = "NULL" newLine = ('INSERT INTO "tsk_os_account_attributes" VALUES(' + ','.join(fields_list[1:]) + ');') # remove id return newLine + elif os_account_instances_index: + os_account_id = int(fields_list[1]) + fields_list[1] = accounts_table[os_account_id] + newLine = ('INSERT INTO "tsk_os_account_instances" VALUES(' + ','.join(fields_list[1:]) + ');') # remove id + return newLine else: return line