diff --git a/Core/build.xml b/Core/build.xml index 32fff13e6e..bbf612c3d9 100644 --- a/Core/build.xml +++ b/Core/build.xml @@ -48,6 +48,11 @@ + + + + + diff --git a/Core/ivy.xml b/Core/ivy.xml index 32438b5971..bce73a903d 100644 --- a/Core/ivy.xml +++ b/Core/ivy.xml @@ -46,6 +46,9 @@ + + + diff --git a/Core/manifest.mf b/Core/manifest.mf index a2c2843839..da946df7b8 100644 --- a/Core/manifest.mf +++ b/Core/manifest.mf @@ -2,7 +2,7 @@ Manifest-Version: 1.0 OpenIDE-Module: org.sleuthkit.autopsy.core/10 OpenIDE-Module-Localizing-Bundle: org/sleuthkit/autopsy/core/Bundle.properties OpenIDE-Module-Layer: org/sleuthkit/autopsy/core/layer.xml -OpenIDE-Module-Implementation-Version: 33 +OpenIDE-Module-Implementation-Version: 34 OpenIDE-Module-Requires: org.openide.windows.WindowManager AutoUpdate-Show-In-Client: true AutoUpdate-Essential-Module: true diff --git a/Core/nbproject/project.properties b/Core/nbproject/project.properties index 30ca03d03b..8e0aabafaf 100644 --- a/Core/nbproject/project.properties +++ b/Core/nbproject/project.properties @@ -11,6 +11,8 @@ file.reference.bcpkix-jdk15on-1.54.jar=release\\modules\\ext\\bcpkix-jdk15on-1.5 file.reference.bcprov-ext-jdk15on-1.54.jar=release\\modules\\ext\\bcprov-ext-jdk15on-1.54.jar file.reference.bcprov-jdk15on-1.52.jar=release\\modules\\ext\\bcprov-jdk15on-1.52.jar file.reference.bcprov-jdk15on-1.54.jar=release\\modules\\ext\\bcprov-jdk15on-1.54.jar +file.reference.byte-buddy-1.10.13.jar=release\\modules\\ext\\byte-buddy-1.10.13.jar +file.reference.byte-buddy-agent-1.10.13.jar=release\\modules\\ext\\byte-buddy-agent-1.10.13.jar file.reference.c3p0-0.9.5.jar=release\\modules\\ext\\c3p0-0.9.5.jar file.reference.checker-compat-qual-2.5.3.jar=release\\modules\\ext\\checker-compat-qual-2.5.3.jar file.reference.commons-beanutils-1.9.2.jar=release\\modules\\ext\\commons-beanutils-1.9.2.jar @@ -70,9 +72,11 @@ file.reference.jai_core-1.1.3.jar=release\\modules\\ext\\jai_core-1.1.3.jar file.reference.jai_imageio-1.1.jar=release\\modules\\ext\\jai_imageio-1.1.jar file.reference.javax.annotation-api-1.3.2.jar=release\\modules\\ext\\javax.annotation-api-1.3.2.jar file.reference.javax.ws.rs-api-2.0.jar=release\\modules\\ext\\javax.ws.rs-api-2.0.jar +file.reference.jcommon-1.0.23.jar=release/modules/ext/jcommon-1.0.23.jar file.reference.jdom-2.0.5-contrib.jar=release\\modules\\ext\\jdom-2.0.5-contrib.jar file.reference.jdom-2.0.5.jar=release\\modules\\ext\\jdom-2.0.5.jar file.reference.jericho-html-3.3.jar=release\\modules\\ext\\jericho-html-3.3.jar +file.reference.jfreechart-1.0.19.jar=release/modules/ext/jfreechart-1.0.19.jar file.reference.jgraphx-4.1.0.jar=release\\modules\\ext\\jgraphx-4.1.0.jar file.reference.jline-0.9.94.jar=release\\modules\\ext\\jline-0.9.94.jar file.reference.jsoup-1.10.3.jar=release\\modules\\ext\\jsoup-1.10.3.jar @@ -86,7 +90,9 @@ file.reference.listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar=re file.reference.log4j-1.2.16.jar=release\\modules\\ext\\log4j-1.2.16.jar file.reference.mchange-commons-java-0.2.9.jar=release\\modules\\ext\\mchange-commons-java-0.2.9.jar file.reference.metadata-extractor-2.11.0.jar=release\\modules\\ext\\metadata-extractor-2.11.0.jar +file.reference.mockito-core-3.5.7.jar=release\\modules\\ext\\mockito-core-3.5.7.jar file.reference.netty-3.7.0.Final.jar=release\\modules\\ext\\netty-3.7.0.Final.jar +file.reference.objenesis-3.1.jar=release\\modules\\ext\\objenesis-3.1.jar file.reference.okhttp-2.7.5.jar=release\\modules\\ext\\okhttp-2.7.5.jar file.reference.okio-1.6.0.jar=release\\modules\\ext\\okio-1.6.0.jar file.reference.opencensus-api-0.19.2.jar=release\\modules\\ext\\opencensus-api-0.19.2.jar @@ -102,8 +108,8 @@ file.reference.protobuf-java-util-3.7.0.jar=release\\modules\\ext\\protobuf-java file.reference.Rejistry-1.1-SNAPSHOT.jar=release\\modules\\ext\\Rejistry-1.1-SNAPSHOT.jar file.reference.sevenzipjbinding-AllPlatforms.jar=release\\modules\\ext\\sevenzipjbinding-AllPlatforms.jar file.reference.sevenzipjbinding.jar=release\\modules\\ext\\sevenzipjbinding.jar -file.reference.sleuthkit-4.10.0.jar=release\\modules\\ext\\sleuthkit-4.10.0.jar -file.reference.sleuthkit-caseuco-4.10.0.jar=release\\modules\\ext\\sleuthkit-caseuco-4.10.0.jar +file.reference.sleuthkit-4.10.1.jar=release/modules/ext/sleuthkit-4.10.1.jar +file.reference.sleuthkit-caseuco-4.10.1.jar=release/modules/ext/sleuthkit-caseuco-4.10.1.jar file.reference.slf4j-api-1.7.6.jar=release\\modules\\ext\\slf4j-api-1.7.6.jar file.reference.slf4j-log4j12-1.7.6.jar=release\\modules\\ext\\slf4j-log4j12-1.7.6.jar file.reference.SparseBitSet-1.1.jar=release\\modules\\ext\\SparseBitSet-1.1.jar @@ -120,5 +126,4 @@ nbm.homepage=http://www.sleuthkit.org/ nbm.module.author=Brian Carrier nbm.needs.restart=true source.reference.curator-recipes-2.8.0.jar=release/modules/ext/curator-recipes-2.8.0-sources.jar -spec.version.base=10.21 - +spec.version.base=10.22 diff --git a/Core/nbproject/project.xml b/Core/nbproject/project.xml index 1945cdeff4..30cbbc7aa5 100644 --- a/Core/nbproject/project.xml +++ b/Core/nbproject/project.xml @@ -399,6 +399,10 @@ ext/proto-google-cloud-translate-v3beta1-0.53.0.jar release\modules\ext\proto-google-cloud-translate-v3beta1-0.53.0.jar + + ext/byte-buddy-1.10.13.jar + release\modules\ext\byte-buddy-1.10.13.jar + ext/error_prone_annotations-2.3.2.jar release\modules\ext\error_prone_annotations-2.3.2.jar @@ -439,14 +443,6 @@ ext/jxmapviewer2-2.4.jar release\modules\ext\jxmapviewer2-2.4.jar - - ext/jfreechart-1.0.19.jar - release/modules/ext/jfreechart-1.0.19.jar - - - ext/jcommon-1.0.23.jar - release/modules/ext/jcommon-1.0.23.jar - ext/jdom-2.0.5-contrib.jar release\modules\ext\jdom-2.0.5-contrib.jar @@ -552,24 +548,16 @@ release\modules\ext\checker-compat-qual-2.5.3.jar - ext/sleuthkit-4.10.0.jar - release\modules\ext\sleuthkit-4.10.0.jar + ext/sleuthkit-4.10.1.jar + release/modules/ext/sleuthkit-4.10.1.jar ext/animal-sniffer-annotations-1.17.jar release\modules\ext\animal-sniffer-annotations-1.17.jar - ext/sleuthkit-caseuco-4.10.0.jar - release\modules\ext\sleuthkit-caseuco-4.10.0.jar - - - ext/sleuthkit-4.10.0.jar - release/modules/ext/sleuthkit-4.10.0.jar - - - ext/sleuthkit-caseuco-4.10.0.jar - release/modules/ext/sleuthkit-caseuco-4.10.0.jar + ext/sleuthkit-caseuco-4.10.1.jar + release/modules/ext/sleuthkit-caseuco-4.10.1.jar ext/gax-1.44.0.jar @@ -611,6 +599,10 @@ ext/decodetect-core-0.3.jar release\modules\ext\decodetect-core-0.3.jar + + ext/mockito-core-3.5.7.jar + release\modules\ext\mockito-core-3.5.7.jar + ext/httpclient-4.5.5.jar release\modules\ext\httpclient-4.5.5.jar @@ -623,6 +615,10 @@ ext/jackson-annotations-2.9.0.jar release\modules\ext\jackson-annotations-2.9.0.jar + + ext/objenesis-3.1.jar + release\modules\ext\objenesis-3.1.jar + ext/jackson-core-2.9.7.jar release\modules\ext\jackson-core-2.9.7.jar @@ -715,6 +711,10 @@ ext/netty-3.7.0.Final.jar release\modules\ext\netty-3.7.0.Final.jar + + ext/jfreechart-1.0.19.jar + release/modules/ext/jfreechart-1.0.19.jar + ext/opencensus-contrib-grpc-metrics-0.19.2.jar release\modules\ext\opencensus-contrib-grpc-metrics-0.19.2.jar @@ -743,6 +743,10 @@ ext/javax.ws.rs-api-2.0.jar release\modules\ext\javax.ws.rs-api-2.0.jar + + ext/jcommon-1.0.23.jar + release/modules/ext/jcommon-1.0.23.jar + ext/icepdf-core-6.2.2.jar release\modules\ext\icepdf-core-6.2.2.jar @@ -795,6 +799,10 @@ ext/jutf7-1.0.0.jar release\modules\ext\jutf7-1.0.0.jar + + ext/byte-buddy-agent-1.10.13.jar + release\modules\ext\byte-buddy-agent-1.10.13.jar + ext/batik-awt-util-1.6.jar release\modules\ext\batik-awt-util-1.6.jar diff --git a/Core/src/org/sleuthkit/autopsy/actions/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/actions/Bundle.properties-MERGED index a3a13c0cff..507e079cad 100755 --- a/Core/src/org/sleuthkit/autopsy/actions/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/actions/Bundle.properties-MERGED @@ -14,6 +14,7 @@ AddContentTagAction.taggingErr=Tagging Error AddContentTagAction.unableToTag.msg=Unable to tag {0}, not a regular file. # {0} - fileName AddContentTagAction.unableToTag.msg2=Unable to tag {0}. +CTL_DumpThreadAction=Thread Dump CTL_ShowIngestProgressSnapshotAction=Ingest Status Details DeleteBlackboardArtifactTagAction.deleteTag=Remove Selected Tag(s) DeleteBlackboardArtifactTagAction.tagDelErr=Tag Deletion Error diff --git a/Core/src/org/sleuthkit/autopsy/actions/OpenLogFolderAction.java b/Core/src/org/sleuthkit/autopsy/actions/OpenLogFolderAction.java index fb178c797d..92b04140c3 100644 --- a/Core/src/org/sleuthkit/autopsy/actions/OpenLogFolderAction.java +++ b/Core/src/org/sleuthkit/autopsy/actions/OpenLogFolderAction.java @@ -44,7 +44,7 @@ import org.sleuthkit.autopsy.coreutils.Logger; * This action should only be invoked in the event dispatch thread (EDT). */ @ActionRegistration(displayName = "#CTL_OpenLogFolder", iconInMenu = true) -@ActionReference(path = "Menu/Help", position = 1750) +@ActionReference(path = "Menu/Help", position = 2000) @ActionID(id = "org.sleuthkit.autopsy.actions.OpenLogFolderAction", category = "Help") public final class OpenLogFolderAction implements ActionListener { diff --git a/Core/src/org/sleuthkit/autopsy/actions/ThreadDumpAction.java b/Core/src/org/sleuthkit/autopsy/actions/ThreadDumpAction.java new file mode 100755 index 0000000000..d74511996c --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/actions/ThreadDumpAction.java @@ -0,0 +1,156 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.actions; + +import java.awt.Desktop; +import java.awt.event.ActionListener; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadInfo; +import java.lang.management.ThreadMXBean; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import java.util.stream.Collectors; +import javax.swing.SwingWorker; +import org.openide.awt.ActionID; +import org.openide.awt.ActionReference; +import org.openide.awt.ActionRegistration; +import org.openide.util.HelpCtx; +import org.openide.util.NbBundle.Messages; +import org.openide.util.actions.CallableSystemAction; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.PlatformUtil; + +/** + * Action class for the Thread Dump help menu item. If there is no case open the + * dump file will be created in PlatformUtil.getLogDirectory() otherwise the + * file will be created in Case.getCurrentCase().getLogDirectoryPath() + */ +@ActionID(category = "Help", id = "org.sleuthkit.autopsy.actions.ThreadDumpAction") +@ActionRegistration(displayName = "#CTL_DumpThreadAction", lazy = false) +@ActionReference(path = "Menu/Help", position = 1750) +@Messages({ + "CTL_DumpThreadAction=Thread Dump" +}) +public final class ThreadDumpAction extends CallableSystemAction implements ActionListener { + + private static final long serialVersionUID = 1L; + private static final Logger logger = Logger.getLogger(ThreadDumpAction.class.getName()); + + private static final DateFormat DATE_FORMAT = new SimpleDateFormat("MM-dd-yyyy-HH-mm-ss-SSSS"); + + @Override + public void performAction() { + (new ThreadDumper()).run(); + } + + @Override + public String getName() { + return Bundle.CTL_DumpThreadAction(); + } + + @Override + public HelpCtx getHelpCtx() { + return HelpCtx.DEFAULT_HELP; + } + + /** + * SwingWorker to that will create the thread dump file. Once the file is + * created it will be opened in an external viewer. + */ + private final class ThreadDumper extends SwingWorker { + + @Override + protected File doInBackground() throws Exception { + return createThreadDump(); + } + + @Override + protected void done() { + File dumpFile = null; + try { + dumpFile = get(); + Desktop.getDesktop().open(dumpFile); + } catch (ExecutionException | InterruptedException ex) { + logger.log(Level.SEVERE, "Failure occurred while creating thread dump file", ex); + } catch (IOException ex) { + if (dumpFile != null) { + logger.log(Level.WARNING, "Failed to open thread dump file in external viewer: " + dumpFile.getAbsolutePath(), ex); + } else { + logger.log(Level.SEVERE, "Failed to create thread dump file.", ex); + } + } + } + + /** + * Create the thread dump file. + * + * @throws IOException + */ + private File createThreadDump() throws IOException { + File dumpFile = createFilePath().toFile(); + try (BufferedWriter writer = new BufferedWriter(new FileWriter(dumpFile, true))) { + ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); + ThreadInfo[] threadInfos = threadMXBean.getThreadInfo(threadMXBean.getAllThreadIds(), 100); + for (ThreadInfo threadInfo : threadInfos) { + writer.write(threadInfo.toString()); + writer.write("\n"); + } + + long[] deadlockThreadIds = threadMXBean.findDeadlockedThreads(); + if (deadlockThreadIds != null) { + writer.write("-------------------List of Deadlocked Thread IDs ---------------------"); + String idsList = (Arrays + .stream(deadlockThreadIds) + .boxed() + .collect(Collectors.toList())) + .stream().map(n -> String.valueOf(n)) + .collect(Collectors.joining("-", "{", "}")); + writer.write(idsList); + } + } + + return dumpFile; + } + + /** + * Create the dump file path. + * + * @return Path for dump file. + */ + private Path createFilePath() { + String fileName = "ThreadDump_" + DATE_FORMAT.format(new Date()) + ".txt"; + if (Case.isCaseOpen()) { + return Paths.get(Case.getCurrentCase().getLogDirectoryPath(), fileName); + } + return Paths.get(PlatformUtil.getLogDirectory(), fileName); + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java index 1e05403dd8..395310e600 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java @@ -107,7 +107,7 @@ import org.sleuthkit.autopsy.coreutils.Version; import org.sleuthkit.autopsy.events.AutopsyEvent; import org.sleuthkit.autopsy.events.AutopsyEventException; import org.sleuthkit.autopsy.events.AutopsyEventPublisher; -import org.sleuthkit.autopsy.discovery.OpenDiscoveryAction; +import org.sleuthkit.autopsy.discovery.ui.OpenDiscoveryAction; import org.sleuthkit.autopsy.ingest.IngestJob; import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.autopsy.ingest.IngestServices; @@ -1468,9 +1468,16 @@ public class Case { */ public boolean hasData() { boolean hasDataSources = false; - try { - hasDataSources = (getDataSources().size() > 0); - } catch (TskCoreException ex) { + String query = "SELECT count(*) AS count FROM tsk_objects WHERE par_obj_id IS NULL"; + try (SleuthkitCase.CaseDbQuery dbQuery = caseDb.executeQuery(query)) { + ResultSet resultSet = dbQuery.getResultSet(); + if (resultSet.next()) { + long numDataSources = resultSet.getLong("count"); + if (numDataSources > 0) { + hasDataSources = true; + } + } + } catch (TskCoreException | SQLException ex) { logger.log(Level.SEVERE, "Error accessing case database", ex); //NON-NLS } return hasDataSources; diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/IngestJobInfoPanel.form b/Core/src/org/sleuthkit/autopsy/casemodule/IngestJobInfoPanel.form index a67b768492..c183e0de36 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/IngestJobInfoPanel.form +++ b/Core/src/org/sleuthkit/autopsy/casemodule/IngestJobInfoPanel.form @@ -1,6 +1,11 @@
+ + + + + @@ -13,104 +18,154 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - + - + + + + + + + - + - - + + - - + + - - + + - - - - - - - - - - - - - - - - - - - - - + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/IngestJobInfoPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/IngestJobInfoPanel.java index 6381acd32d..9faa17f51c 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/IngestJobInfoPanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/IngestJobInfoPanel.java @@ -241,65 +241,84 @@ public final class IngestJobInfoPanel extends javax.swing.JPanel { @SuppressWarnings("unchecked") // //GEN-BEGIN:initComponents private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; - jScrollPane1 = new javax.swing.JScrollPane(); + javax.swing.JScrollPane mainScrollPane = new javax.swing.JScrollPane(); + javax.swing.JPanel contentPanel = new javax.swing.JPanel(); + javax.swing.JScrollPane ingestJobsScrollPane = new javax.swing.JScrollPane(); ingestJobTable = new javax.swing.JTable(); - jLabel1 = new javax.swing.JLabel(); - jLabel2 = new javax.swing.JLabel(); - jScrollPane2 = new javax.swing.JScrollPane(); + javax.swing.JLabel jLabel1 = new javax.swing.JLabel(); + javax.swing.JLabel jLabel2 = new javax.swing.JLabel(); + javax.swing.JScrollPane ingestModulesScrollPane = new javax.swing.JScrollPane(); ingestModuleTable = new javax.swing.JTable(); - jScrollPane1.setBorder(null); + setMaximumSize(new java.awt.Dimension(32767, 32767)); + setLayout(new java.awt.BorderLayout()); + + contentPanel.setMaximumSize(new java.awt.Dimension(32767, 32767)); + contentPanel.setMinimumSize(new java.awt.Dimension(625, 150)); + contentPanel.setPreferredSize(new java.awt.Dimension(625, 150)); + contentPanel.setLayout(new java.awt.GridBagLayout()); + + ingestJobsScrollPane.setBorder(null); + ingestJobsScrollPane.setMinimumSize(new java.awt.Dimension(16, 16)); + ingestJobsScrollPane.setPreferredSize(null); ingestJobTable.setModel(ingestJobTableModel); ingestJobTable.getTableHeader().setReorderingAllowed(false); - jScrollPane1.setViewportView(ingestJobTable); + ingestJobsScrollPane.setViewportView(ingestJobTable); ingestJobTable.getColumnModel().getSelectionModel().setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(2, 10, 10, 0); + contentPanel.add(ingestJobsScrollPane, gridBagConstraints); + org.openide.awt.Mnemonics.setLocalizedText(jLabel1, org.openide.util.NbBundle.getMessage(IngestJobInfoPanel.class, "IngestJobInfoPanel.jLabel1.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(10, 10, 0, 0); + contentPanel.add(jLabel1, gridBagConstraints); org.openide.awt.Mnemonics.setLocalizedText(jLabel2, org.openide.util.NbBundle.getMessage(IngestJobInfoPanel.class, "IngestJobInfoPanel.jLabel2.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, 0); + contentPanel.add(jLabel2, gridBagConstraints); + + ingestModulesScrollPane.setMaximumSize(new java.awt.Dimension(254, 32767)); + ingestModulesScrollPane.setMinimumSize(new java.awt.Dimension(254, 16)); + ingestModulesScrollPane.setPreferredSize(new java.awt.Dimension(254, 16)); ingestModuleTable.setModel(ingestModuleTableModel); - jScrollPane2.setViewportView(ingestModuleTable); + ingestModulesScrollPane.setViewportView(ingestModuleTable); - javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); - this.setLayout(layout); - layout.setHorizontalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addGap(15, 15, 15) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(jLabel2) - .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 485, Short.MAX_VALUE)) - .addGap(8, 8, 8) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(jScrollPane2, javax.swing.GroupLayout.PREFERRED_SIZE, 254, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(jLabel1)) - .addContainerGap()) - ); - layout.setVerticalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addGap(8, 8, 8) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(jLabel1) - .addComponent(jLabel2)) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 162, Short.MAX_VALUE) - .addComponent(jScrollPane2, javax.swing.GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE)) - .addGap(10, 10, 10)) - ); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.fill = java.awt.GridBagConstraints.VERTICAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(2, 8, 10, 10); + contentPanel.add(ingestModulesScrollPane, gridBagConstraints); + + mainScrollPane.setViewportView(contentPanel); + + add(mainScrollPane, java.awt.BorderLayout.CENTER); }// //GEN-END:initComponents // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JTable ingestJobTable; private javax.swing.JTable ingestModuleTable; - private javax.swing.JLabel jLabel1; - private javax.swing.JLabel jLabel2; - private javax.swing.JScrollPane jScrollPane1; - private javax.swing.JScrollPane jScrollPane2; // End of variables declaration//GEN-END:variables } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesDSProcessor.java b/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesDSProcessor.java index 4d5e3eac36..a1629609ce 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesDSProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/LocalFilesDSProcessor.java @@ -193,7 +193,7 @@ public class LocalFilesDSProcessor implements DataSourceProcessor, AutoIngestDat List command = new ArrayList<>(); for (final String l01Path : logicalEvidenceFilePaths) { command.clear(); - command.add(ewfexportPath.toAbsolutePath().toString()); + command.add(String.format("\"%s\"", ewfexportPath.toAbsolutePath().toString())); command.add("-f"); command.add("files"); command.add("-t"); @@ -203,8 +203,8 @@ public class LocalFilesDSProcessor implements DataSourceProcessor, AutoIngestDat } Path dirPath = Paths.get(FilenameUtils.getBaseName(l01Path) + UNIQUENESS_CONSTRAINT_SEPERATOR + System.currentTimeMillis()); - command.add(dirPath.toString()); - command.add(l01Path); + command.add(String.format("\"%s\"", dirPath.toString())); + command.add(String.format("\"%s\"", l01Path)); ProcessBuilder processBuilder = new ProcessBuilder(command); processBuilder.directory(l01Dir); try { diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/UnpackagePortableCaseProgressDialog.java b/Core/src/org/sleuthkit/autopsy/casemodule/UnpackagePortableCaseProgressDialog.java index b5f495806b..914f88afb1 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/UnpackagePortableCaseProgressDialog.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/UnpackagePortableCaseProgressDialog.java @@ -173,10 +173,10 @@ class UnpackagePortableCaseProgressDialog extends javax.swing.JDialog implements throw new TskCoreException("Error finding 7-Zip executable"); // NON-NLS } - String outputFolderSwitch = "-o" + outputFolder; // NON-NLS + String outputFolderSwitch = String.format("\"-o%s\"",outputFolder); // NON-NLS ProcessBuilder procBuilder = new ProcessBuilder(); procBuilder.command( - sevenZipExe.getAbsolutePath(), + String.format("\"%s\"",sevenZipExe.getAbsolutePath()), "x", // Extract packagedCase, outputFolderSwitch diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeUtil.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeUtil.java index 9af6fcde3b..88f76c393c 100755 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeUtil.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationAttributeUtil.java @@ -19,6 +19,7 @@ package org.sleuthkit.autopsy.centralrepository.datamodel; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -46,6 +47,7 @@ import org.sleuthkit.datamodel.TskData; public class CorrelationAttributeUtil { private static final Logger logger = Logger.getLogger(CorrelationAttributeUtil.class.getName()); + private static final List domainsToSkip = Arrays.asList("localhost", "127.0.0.1"); /** * Gets a string that is expected to be the same string that is stored in @@ -148,8 +150,11 @@ public class CorrelationAttributeUtil { || artifactTypeID == ARTIFACT_TYPE.TSK_WEB_COOKIE.getTypeID() || artifactTypeID == ARTIFACT_TYPE.TSK_WEB_DOWNLOAD.getTypeID() || artifactTypeID == ARTIFACT_TYPE.TSK_WEB_HISTORY.getTypeID()) { - makeCorrAttrFromArtifactAttr(correlationAttrs, sourceArtifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN, CorrelationAttributeInstance.DOMAIN_TYPE_ID); - + BlackboardAttribute domainAttr = sourceArtifact.getAttribute(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DOMAIN)); + if ((domainAttr != null) + && ! domainsToSkip.contains(domainAttr.getValueString())) { + makeCorrAttrFromArtifactAttr(correlationAttrs, sourceArtifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN, CorrelationAttributeInstance.DOMAIN_TYPE_ID); + } } else if (artifactTypeID == ARTIFACT_TYPE.TSK_DEVICE_ATTACHED.getTypeID()) { makeCorrAttrFromArtifactAttr(correlationAttrs, sourceArtifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DEVICE_ID, CorrelationAttributeInstance.USBID_TYPE_ID); makeCorrAttrFromArtifactAttr(correlationAttrs, sourceArtifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_MAC_ADDRESS, CorrelationAttributeInstance.MAC_TYPE_ID); diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonasTopComponent.java b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonasTopComponent.java index e051529f11..40ecdaf05f 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonasTopComponent.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/persona/PersonasTopComponent.java @@ -129,15 +129,7 @@ public final class PersonasTopComponent extends TopComponent { handleSelectionChange(e); } }); - - searchNameRadio.addActionListener((ActionEvent e) -> { - searchField.setText(""); - }); - - searchAccountRadio.addActionListener((ActionEvent e) -> { - searchField.setText(""); - }); - + createAccountBtn.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { diff --git a/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineOptionProcessor.java b/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineOptionProcessor.java index 350e99c598..ad84bdcd0f 100755 --- a/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineOptionProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineOptionProcessor.java @@ -49,8 +49,7 @@ public class CommandLineOptionProcessor extends OptionProcessor { private final Option dataSourceObjectIdOption = Option.requiredArgument('i', "dataSourceObjectId"); private final Option addDataSourceCommandOption = Option.withoutArgument('a', "addDataSource"); private final Option caseDirOption = Option.requiredArgument('d', "caseDir"); - private final Option runIngestCommandOption = Option.withoutArgument('r', "runIngest"); - private final Option ingestProfileOption = Option.requiredArgument('p', "ingestProfile"); + private final Option runIngestCommandOption = Option.optionalArgument('r', "runIngest"); private final Option listAllDataSourcesCommandOption = Option.withoutArgument('l', "listAllDataSources"); private final Option generateReportsOption = Option.optionalArgument('g', "generateReports"); private final Option defaultArgument = Option.defaultArguments(); @@ -76,7 +75,6 @@ public class CommandLineOptionProcessor extends OptionProcessor { set.add(dataSourceObjectIdOption); set.add(caseDirOption); set.add(runIngestCommandOption); - set.add(ingestProfileOption); set.add(listAllDataSourcesCommandOption); set.add(generateReportsOption); set.add(defaultArgument); @@ -205,21 +203,6 @@ public class CommandLineOptionProcessor extends OptionProcessor { } } - String ingestProfile = ""; - if (values.containsKey(ingestProfileOption)) { - - argDirs = values.get(ingestProfileOption); - if (argDirs.length < 1) { - handleError("Argument missing from 'ingestProfile' option"); - } - ingestProfile = argDirs[0]; - - // verify inputs - if (ingestProfile == null || ingestProfile.isEmpty()) { - handleError("Missing argument 'ingestProfile'"); - } - } - // Create commands in order in which they should be executed: // First create the "CREATE_CASE" command, if present if (values.containsKey(createCaseCommandOption)) { @@ -263,9 +246,15 @@ public class CommandLineOptionProcessor extends OptionProcessor { runFromCommandLine = true; } + String ingestProfile = ""; // Add RUN_INGEST command, if present if (values.containsKey(runIngestCommandOption)) { + argDirs = values.get(runIngestCommandOption); + if(argDirs != null && argDirs.length > 0) { + ingestProfile = argDirs[0]; + } + // 'caseDir' must only be specified if the case is not being created during the current run if (!values.containsKey(createCaseCommandOption) && caseDir.isEmpty()) { // new case is not being created during this run, so 'caseDir' should have been specified diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaFileViewer.java b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaFileViewer.java index 9decc79175..002ec72578 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaFileViewer.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaFileViewer.java @@ -134,7 +134,7 @@ class MediaFileViewer extends javax.swing.JPanel implements FileTypeViewer { mediaPlayerPanel.loadFile(file); this.showVideoPanel(); } else if (imagePanelInited && imagePanel.isSupported(file)) { - imagePanel.showImageFx(file); + imagePanel.loadFile(file); this.showImagePanel(); } } catch (Exception e) { diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaViewImagePanel.java b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaViewImagePanel.java index 55163535cb..09182144bf 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/MediaViewImagePanel.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/MediaViewImagePanel.java @@ -59,6 +59,7 @@ import javafx.scene.transform.Translate; import javax.imageio.ImageIO; import javax.swing.JFileChooser; import javafx.scene.Node; +import javax.annotation.concurrent.Immutable; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; @@ -87,6 +88,7 @@ import org.sleuthkit.autopsy.corelibs.OpenCvLoader; import org.sleuthkit.autopsy.coreutils.ImageUtils; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.PlatformUtil; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.datamodel.FileNode; import org.sleuthkit.autopsy.directorytree.ExternalViewerAction; import org.sleuthkit.datamodel.AbstractFile; @@ -94,8 +96,9 @@ import org.sleuthkit.datamodel.ContentTag; import org.sleuthkit.datamodel.TskCoreException; /** - * Image viewer part of the Media View layered pane. Uses JavaFX to display the - * image. + * A media image file viewer implemented as a Swing panel that uses JavaFX (JFX) + * components in a child JFX panel to render the image. Images can be zoomed and + * rotated and a "rubber band box" can be used to select and tag regions. */ @NbBundle.Messages({ "MediaViewImagePanel.externalViewerButton.text=Open in External Viewer Ctrl+E", @@ -107,56 +110,100 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan private static final long serialVersionUID = 1L; private static final Logger logger = Logger.getLogger(MediaViewImagePanel.class.getName()); + private static final SortedSet supportedMimes = ImageUtils.getSupportedImageMimeTypes(); + private static final List supportedExtensions = ImageUtils.getSupportedImageExtensions().stream() + .map("."::concat) //NOI18N + .collect(Collectors.toList()); private static final double[] ZOOM_STEPS = { 0.0625, 0.125, 0.25, 0.375, 0.5, 0.75, 1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10}; private static final double MIN_ZOOM_RATIO = 0.0625; // 6.25% private static final double MAX_ZOOM_RATIO = 10.0; // 1000% - private static final Image externalImage = new Image(MediaViewImagePanel.class.getResource("/org/sleuthkit/autopsy/images/external.png").toExternalForm()); - private static final SortedSet supportedMimes = ImageUtils.getSupportedImageMimeTypes(); - private static final List supportedExtensions = ImageUtils.getSupportedImageExtensions().stream() - .map("."::concat) //NOI18N - .collect(Collectors.toList()); - - /* - * JFX components - */ - private final ProgressBar progressBar = new ProgressBar(); - private final MaskerPane maskerPane = new MaskerPane(); - private Group masterGroup; - private ImageTagsGroup tagsGroup; - private ImageTagCreator imageTagCreator; - private ImageView fxImageView; - private ScrollPane scrollPane; - private Task readImageTask; - - /* - * Swing components - */ - private final JPopupMenu imageTaggingOptions = new JPopupMenu(); - private final JMenuItem createTagMenuItem; - private final JMenuItem deleteTagMenuItem; - private final JMenuItem hideTagsMenuItem; - private final JMenuItem exportTagsMenuItem; - private final JFileChooser exportChooser; + private static final Image openInExternalViewerButtonImage = new Image(MediaViewImagePanel.class.getResource("/org/sleuthkit/autopsy/images/external.png").toExternalForm()); //NOI18N + private final boolean jfxIsInited = org.sleuthkit.autopsy.core.Installer.isJavaFxInited(); private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); - private JFXPanel fxPanel; - + /* - * State + * Threading policy: JFX UI components, must be accessed in JFX thread only. */ - private final boolean fxInited; - private double zoomRatio; - private double rotation; // Can be 0, 90, 180, and 270. - private boolean autoResize = true; // Auto resize when the user changes the size of the content viewer unless the user has used the zoom buttons. - private AbstractFile file; + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + private final ProgressBar progressBar = new ProgressBar(); + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + private final MaskerPane maskerPane = new MaskerPane(); + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + private Group masterGroup; + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + private ImageTagsGroup tagsGroup; + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + private ImageTagCreator imageTagCreator; + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + private ImageView fxImageView; + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + private ScrollPane scrollPane; + + /* + * Threading policy: Swing UI components, must be accessed in EDT only. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + private final JPopupMenu imageTaggingOptions; + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + private final JMenuItem createTagMenuItem; + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + private final JMenuItem deleteTagMenuItem; + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + private final JMenuItem hideTagsMenuItem; + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + private final JMenuItem exportTagsMenuItem; + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + private final JFileChooser exportChooser; + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + private final JFXPanel fxPanel; + + /* + * Panel state variables threading policy: + * + * imageFile: The loadFile() method kicks off a JFX background task to read + * the content of the currently selected file into a JFX Image object. If + * the task succeeds and is not cancelled, the AbstractFile reference is + * saved as imageFile. The reference is used for tagging operations which + * are done in the JFX thread. IMPORTANT: Thread confinement is maintained + * by capturing the reference in a local variable before dispatching a tag + * export task to the SwingWorker thread pool. The imageFile field should + * not be read directly in the JFX thread. + * + * readImageFileTask: This is a reference to a JFX background task that + * reads the content of the currently selected file into a JFX Image object. + * A reference is maintained so that the task can be cancelled if it is + * running when the selected image file changes. Only accessed in the JFX + * thread. + * + * imageTransforms: These values are mostly written in the EDT based on user + * interactions with Swing components and then read in the JFX thread when + * rendering the image. The exception is recalculation of the zoom ratio + * based on the image size when a) the selected image file is changed, b) + * the panel is resized or c) the user pushes the reset button to clear any + * transforms they have specified. In these three cases, the zoom ratio + * update happens in the JFX thread since the image must be accessed. + * IMPORTANT: The image transforms are bundled as atomic state and a + * snapshot should be captured for each rendering operation on the JFX + * thread so that the image transforms do not change during rendering due to + * user interactions in the EDT. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + private AbstractFile imageFile; + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + private Task readImageFileTask; + private volatile ImageTransforms imageTransforms; static { ImageIO.scanForPlugins(); } /** - * Creates new form MediaViewImagePanel + * Constructs a media image file viewer implemented as a Swing panel that + * uses JavaFX (JFX) components in a child JFX panel to render the image. + * Images can be zoomed and rotated and a "rubber band box" can be used to + * select and tag regions. */ @NbBundle.Messages({ "MediaViewImagePanel.createTagOption=Create", @@ -166,12 +213,14 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan }) MediaViewImagePanel() { initComponents(); - fxInited = org.sleuthkit.autopsy.core.Installer.isJavaFxInited(); + + imageTransforms = new ImageTransforms(0, 0, true); exportChooser = new JFileChooser(); exportChooser.setDialogTitle(Bundle.MediaViewImagePanel_fileChooserTitle()); //Build popupMenu when Tags Menu button is pressed. + imageTaggingOptions = new JPopupMenu(); createTagMenuItem = new JMenuItem(Bundle.MediaViewImagePanel_createTagOption()); createTagMenuItem.addActionListener((event) -> createTag()); imageTaggingOptions.add(createTagMenuItem); @@ -202,7 +251,8 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan imageTaggingOptions.setEnabled(false); } - if (fxInited) { + fxPanel = new JFXPanel(); + if (isInited()) { Platform.runLater(new Runnable() { @Override public void run() { @@ -217,6 +267,14 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan } }); + /* + * RC: I'm not sure exactly why this is located precisely + * here. At least putting this call outside of the + * constructor avoids leaking the "this" reference of a + * partially constructed instance of this class that is + * given to the PropertyChangeSupport object created at the + * very beginning of construction. + */ subscribeTagMenuItemsToStateChanges(); masterGroup.getChildren().add(tagsGroup); @@ -246,7 +304,6 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan scrollPane.setVbarPolicy(ScrollBarPolicy.AS_NEEDED); scrollPane.setHbarPolicy(ScrollBarPolicy.AS_NEEDED); - fxPanel = new JFXPanel(); // bridge jfx-swing Scene scene = new Scene(scrollPane); //root of jfx tree scene.getStylesheets().add(MediaViewImagePanel.class.getResource("MediaViewImagePanel.css").toExternalForm()); //NOI18N fxPanel.setScene(scene); @@ -272,57 +329,75 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan State currentState = (State) event.getNewValue(); switch (currentState) { case CREATE: - createTagMenuItem.setEnabled(true); - deleteTagMenuItem.setEnabled(false); - hideTagsMenuItem.setEnabled(true); - exportTagsMenuItem.setEnabled(true); + SwingUtilities.invokeLater(() -> { + createTagMenuItem.setEnabled(true); + deleteTagMenuItem.setEnabled(false); + hideTagsMenuItem.setEnabled(true); + exportTagsMenuItem.setEnabled(true); + }); break; case SELECTED: - if (masterGroup.getChildren().contains(imageTagCreator)) { - imageTagCreator.disconnect(); - masterGroup.getChildren().remove(imageTagCreator); - } - createTagMenuItem.setEnabled(false); - deleteTagMenuItem.setEnabled(true); - hideTagsMenuItem.setEnabled(true); - exportTagsMenuItem.setEnabled(true); + Platform.runLater(() -> { + if (masterGroup.getChildren().contains(imageTagCreator)) { + imageTagCreator.disconnect(); + masterGroup.getChildren().remove(imageTagCreator); + } + SwingUtilities.invokeLater(() -> { + createTagMenuItem.setEnabled(false); + deleteTagMenuItem.setEnabled(true); + hideTagsMenuItem.setEnabled(true); + exportTagsMenuItem.setEnabled(true); + }); + }); break; case HIDDEN: - createTagMenuItem.setEnabled(false); - deleteTagMenuItem.setEnabled(false); - hideTagsMenuItem.setEnabled(true); - hideTagsMenuItem.setText(DisplayOptions.SHOW_TAGS.getName()); - exportTagsMenuItem.setEnabled(false); + SwingUtilities.invokeLater(() -> { + createTagMenuItem.setEnabled(false); + deleteTagMenuItem.setEnabled(false); + hideTagsMenuItem.setEnabled(true); + hideTagsMenuItem.setText(DisplayOptions.SHOW_TAGS.getName()); + exportTagsMenuItem.setEnabled(false); + }); break; case VISIBLE: - createTagMenuItem.setEnabled(true); - deleteTagMenuItem.setEnabled(false); - hideTagsMenuItem.setEnabled(true); - hideTagsMenuItem.setText(DisplayOptions.HIDE_TAGS.getName()); - exportTagsMenuItem.setEnabled(true); + SwingUtilities.invokeLater(() -> { + createTagMenuItem.setEnabled(true); + deleteTagMenuItem.setEnabled(false); + hideTagsMenuItem.setEnabled(true); + hideTagsMenuItem.setText(DisplayOptions.HIDE_TAGS.getName()); + exportTagsMenuItem.setEnabled(true); + }); break; case DEFAULT: case EMPTY: - if (masterGroup.getChildren().contains(imageTagCreator)) { - imageTagCreator.disconnect(); - } - createTagMenuItem.setEnabled(true); - deleteTagMenuItem.setEnabled(false); - hideTagsMenuItem.setEnabled(false); - hideTagsMenuItem.setText(DisplayOptions.HIDE_TAGS.getName()); - exportTagsMenuItem.setEnabled(false); + Platform.runLater(() -> { + if (masterGroup.getChildren().contains(imageTagCreator)) { + imageTagCreator.disconnect(); + } + SwingUtilities.invokeLater(() -> { + createTagMenuItem.setEnabled(true); + deleteTagMenuItem.setEnabled(false); + hideTagsMenuItem.setEnabled(false); + hideTagsMenuItem.setText(DisplayOptions.HIDE_TAGS.getName()); + exportTagsMenuItem.setEnabled(false); + }); + }); break; case NONEMPTY: - createTagMenuItem.setEnabled(true); - deleteTagMenuItem.setEnabled(false); - hideTagsMenuItem.setEnabled(true); - exportTagsMenuItem.setEnabled(true); + SwingUtilities.invokeLater(() -> { + createTagMenuItem.setEnabled(true); + deleteTagMenuItem.setEnabled(false); + hideTagsMenuItem.setEnabled(true); + exportTagsMenuItem.setEnabled(true); + }); break; case DISABLE: - createTagMenuItem.setEnabled(false); - deleteTagMenuItem.setEnabled(false); - hideTagsMenuItem.setEnabled(false); - exportTagsMenuItem.setEnabled(false); + SwingUtilities.invokeLater(() -> { + createTagMenuItem.setEnabled(false); + deleteTagMenuItem.setEnabled(false); + hideTagsMenuItem.setEnabled(false); + exportTagsMenuItem.setEnabled(false); + }); break; default: break; @@ -330,14 +405,18 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan }); } - public boolean isInited() { - return fxInited; + /* + * Indicates whether or not the panel can be used, i.e., JavaFX has been + * intitialized. + */ + final boolean isInited() { + return jfxIsInited; } /** - * Clear the displayed image + * Clear the displayed image. */ - public void reset() { + final void reset() { Platform.runLater(() -> { fxImageView.setViewport(new Rectangle2D(0, 0, 0, 0)); fxImageView.setImage(null); @@ -349,111 +428,157 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan }); } - private void showErrorNode(String errorMessage, AbstractFile file) { - final Button externalViewerButton = new Button(Bundle.MediaViewImagePanel_externalViewerButton_text(), new ImageView(externalImage)); - /* - * Tie a Swing action (ExternalViewerAction) to a JFX button action. - */ - externalViewerButton.setOnAction(actionEvent -> - new ExternalViewerAction(Bundle.MediaViewImagePanel_externalViewerButton_text(), new FileNode(file)) - .actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "")) + /** + * Displays a button with an error message label and a view in external + * viewer action. + * + * @param errorMessage The error message. + * @param file The file that could not be viewed. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + private void showErrorButton(String errorMessage, AbstractFile file) { + ensureInJfxThread(); + final Button externalViewerButton = new Button(Bundle.MediaViewImagePanel_externalViewerButton_text(), new ImageView(openInExternalViewerButtonImage)); + externalViewerButton.setOnAction(actionEvent + -> new ExternalViewerAction(Bundle.MediaViewImagePanel_externalViewerButton_text(), new FileNode(file)) + .actionPerformed(new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "")) ); - final VBox errorNode = new VBox(10, new Label(errorMessage), externalViewerButton); errorNode.setAlignment(Pos.CENTER); } /** - * Show the contents of the given AbstractFile as a visual image. + * Loads an image file into this panel. * - * @param file image file to show + * @param file The image file. */ - void showImageFx(final AbstractFile file) { - if (!fxInited) { + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + final void loadFile(final AbstractFile file) { + ensureInSwingThread(); + if (!isInited()) { + return; + } + + final double panelWidth = fxPanel.getWidth(); + final double panelHeight = fxPanel.getHeight(); + Platform.runLater(() -> { + /* + * Set up a new task to get the contents of the image file in + * displayable form and cancel any previous task in progress. + */ + if (readImageFileTask != null) { + readImageFileTask.cancel(); + } + readImageFileTask = ImageUtils.newReadImageTask(file); + readImageFileTask.setOnSucceeded(succeeded -> { + onReadImageTaskSucceeded(file, panelWidth, panelHeight); + }); + readImageFileTask.setOnFailed(failed -> { + onReadImageTaskFailed(file); + }); + + /* + * Update the JFX components to a "task in progress" state and start + * the task. + */ + maskerPane.setProgressNode(progressBar); + progressBar.progressProperty().bind(readImageFileTask.progressProperty()); + maskerPane.textProperty().bind(readImageFileTask.messageProperty()); + scrollPane.setContent(null); // Prevent content display issues. + scrollPane.setCursor(Cursor.WAIT); + new Thread(readImageFileTask).start(); + }); + } + + /** + * Implements a JFX background task state change handler called on read + * image task success that loads the file content into this panel. + * + * @param file The file. + * @param panelWidth The width of the child panel that contains the JFX + * components of this panel. + * @param panelHeight The height of the child panel that contains the JFX + * components of this panel. + */ + private void onReadImageTaskSucceeded(AbstractFile file, double panelWidth, double panelHeight) { + if (!Case.isCaseOpen()) { + /* + * Handle the in-between condition when case is being closed and an + * image was previously selected + * + * NOTE: I think this is unnecessary -jm + */ + reset(); return; } Platform.runLater(() -> { - if (readImageTask != null) { - readImageTask.cancel(); - } - readImageTask = ImageUtils.newReadImageTask(file); - readImageTask.setOnSucceeded(succeeded -> { - if (!Case.isCaseOpen()) { - /* - * Handle the in-between condition when case is being closed - * and an image was previously selected - * - * NOTE: I think this is unnecessary -jm - */ - reset(); - return; - } + try { + Image fxImage = readImageFileTask.get(); + masterGroup.getChildren().clear(); + tagsGroup.getChildren().clear(); + this.imageFile = file; + if (nonNull(fxImage)) { + // We have a non-null image, so let's show it. + fxImageView.setImage(fxImage); + resetView(panelWidth, panelHeight); + masterGroup.getChildren().add(fxImageView); + masterGroup.getChildren().add(tagsGroup); - try { - autoResize = true; - Image fxImage = readImageTask.get(); - masterGroup.getChildren().clear(); - tagsGroup.getChildren().clear(); - this.file = file; - if (nonNull(fxImage)) { - // We have a non-null image, so let's show it. - fxImageView.setImage(fxImage); - resetView(); - masterGroup.getChildren().add(fxImageView); - masterGroup.getChildren().add(tagsGroup); + try { + List tags = Case.getCurrentCase().getServices() + .getTagsManager().getContentTagsByContent(file); - try { - List tags = Case.getCurrentCase().getServices() - .getTagsManager().getContentTagsByContent(file); - - List> contentViewerTags = getContentViewerTags(tags); - //Add all image tags - tagsGroup = buildImageTagsGroup(contentViewerTags); - if (!tagsGroup.getChildren().isEmpty()) { - pcs.firePropertyChange(new PropertyChangeEvent(this, - "state", null, State.NONEMPTY)); - } - } catch (TskCoreException | NoCurrentCaseException ex) { - logger.log(Level.WARNING, "Could not retrieve image tags for file in case db", ex); //NON-NLS + List> contentViewerTags = getContentViewerTags(tags); + //Add all image tags + tagsGroup = buildImageTagsGroup(contentViewerTags); + if (!tagsGroup.getChildren().isEmpty()) { + pcs.firePropertyChange(new PropertyChangeEvent(this, + "state", null, State.NONEMPTY)); } - scrollPane.setContent(masterGroup); - } else { - showErrorNode(Bundle.MediaViewImagePanel_errorLabel_text(), file); + } catch (TskCoreException | NoCurrentCaseException ex) { + logger.log(Level.WARNING, "Could not retrieve image tags for file in case db", ex); //NON-NLS } - } catch (InterruptedException | ExecutionException ex) { - showErrorNode(Bundle.MediaViewImagePanel_errorLabel_text(), file); - } - scrollPane.setCursor(Cursor.DEFAULT); - }); - readImageTask.setOnFailed(failed -> { - if (!Case.isCaseOpen()) { - /* - * Handle in-between condition when case is being closed and - * an image was previously selected - * - * NOTE: I think this is unnecessary -jm - */ - reset(); - return; - } - Throwable exception = readImageTask.getException(); - if (exception instanceof OutOfMemoryError - && exception.getMessage().contains("Java heap space")) { - showErrorNode(Bundle.MediaViewImagePanel_errorLabel_OOMText(), file); + scrollPane.setContent(masterGroup); } else { - showErrorNode(Bundle.MediaViewImagePanel_errorLabel_text(), file); + showErrorButton(Bundle.MediaViewImagePanel_errorLabel_text(), file); } + } catch (InterruptedException | ExecutionException ex) { + showErrorButton(Bundle.MediaViewImagePanel_errorLabel_text(), file); + } + scrollPane.setCursor(Cursor.DEFAULT); + }); + } - scrollPane.setCursor(Cursor.DEFAULT); - }); + /** + * Implements a JFX background task state change handler called on read + * image file task failure that displays a button with an error message + * label and a view in external viewer action. + * + * @param file The image file. + */ + private void onReadImageTaskFailed(AbstractFile file) { + if (!Case.isCaseOpen()) { + /* + * Handle in-between condition when case is being closed and an + * image was previously selected + * + * NOTE: I think this is unnecessary -jm + */ + reset(); + return; + } - maskerPane.setProgressNode(progressBar); - progressBar.progressProperty().bind(readImageTask.progressProperty()); - maskerPane.textProperty().bind(readImageTask.messageProperty()); - scrollPane.setContent(null); // Prevent content display issues. - scrollPane.setCursor(Cursor.WAIT); - new Thread(readImageTask).start(); + Platform.runLater(() -> { + Throwable exception = readImageFileTask.getException(); + if (exception instanceof OutOfMemoryError + && exception.getMessage().contains("Java heap space")) { //NON-NLS + showErrorButton(Bundle.MediaViewImagePanel_errorLabel_OOMText(), file); + } else { + showErrorButton(Bundle.MediaViewImagePanel_errorLabel_text(), file); + } + + scrollPane.setCursor(Cursor.DEFAULT); }); } @@ -495,7 +620,7 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan * @throws NoCurrentCaseException */ private ImageTagsGroup buildImageTagsGroup(List> contentViewerTags) { - + ensureInJfxThread(); contentViewerTags.forEach(contentViewerTag -> { /** * Build the image tag, add an edit event call back to persist all @@ -503,7 +628,6 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan */ tagsGroup.getChildren().add(buildImageTag(contentViewerTag)); }); - return tagsGroup; } @@ -511,7 +635,7 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan * @return supported mime types */ @Override - public List getSupportedMimeTypes() { + final public List getSupportedMimeTypes() { return Collections.unmodifiableList(Lists.newArrayList(supportedMimes)); } @@ -521,7 +645,7 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan * @return */ @Override - public List getSupportedExtensions() { + final public List getSupportedExtensions() { return getExtensions(); } @@ -530,12 +654,12 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan * * @return */ - public List getExtensions() { + final public List getExtensions() { return Collections.unmodifiableList(supportedExtensions); } @Override - public boolean isSupported(AbstractFile file) { + final public boolean isSupported(AbstractFile file) { return ImageUtils.isImageThumbnailSupported(file); } @@ -687,53 +811,90 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan }// //GEN-END:initComponents private void rotateLeftButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_rotateLeftButtonActionPerformed - autoResize = false; - - rotation = (rotation + 270) % 360; - updateView(); + rotateImage(270); }//GEN-LAST:event_rotateLeftButtonActionPerformed private void rotateRightButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_rotateRightButtonActionPerformed - autoResize = false; - - rotation = (rotation + 90) % 360; - updateView(); + rotateImage(90); }//GEN-LAST:event_rotateRightButtonActionPerformed + private void rotateImage(int angle) { + final double panelWidth = fxPanel.getWidth(); + final double panelHeight = fxPanel.getHeight(); + ImageTransforms currentTransforms = imageTransforms; + double newRotation = (currentTransforms.getRotation() + angle) % 360; + final ImageTransforms newTransforms = new ImageTransforms(currentTransforms.getZoomRatio(), newRotation, false); + imageTransforms = newTransforms; + Platform.runLater(() -> { + updateView(panelWidth, panelHeight, newTransforms); + }); + } + private void zoomInButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_zoomInButtonActionPerformed - autoResize = false; - // Find the next zoom step. - for (int i = 0; i < ZOOM_STEPS.length; i++) { - if (zoomRatio < ZOOM_STEPS[i]) { - zoomRatio = ZOOM_STEPS[i]; - break; - } - } - updateView(); + zoomImage(ZoomDirection.IN); }//GEN-LAST:event_zoomInButtonActionPerformed private void zoomOutButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_zoomOutButtonActionPerformed - autoResize = false; - // Find the next zoom step. - for (int i = ZOOM_STEPS.length - 1; i >= 0; i--) { - if (zoomRatio > ZOOM_STEPS[i]) { - zoomRatio = ZOOM_STEPS[i]; + zoomImage(ZoomDirection.OUT); + }//GEN-LAST:event_zoomOutButtonActionPerformed + + private void zoomImage(ZoomDirection direction) { + ensureInSwingThread(); + final double panelWidth = fxPanel.getWidth(); + final double panelHeight = fxPanel.getHeight(); + final ImageTransforms currentTransforms = imageTransforms; + double newZoomRatio; + if (direction == ZoomDirection.IN) { + newZoomRatio = zoomImageIn(currentTransforms.getZoomRatio()); + } else { + newZoomRatio = zoomImageOut(currentTransforms.getZoomRatio()); + } + final ImageTransforms newTransforms = new ImageTransforms(newZoomRatio, currentTransforms.getRotation(), false); + imageTransforms = newTransforms; + Platform.runLater(() -> { + updateView(panelWidth, panelHeight, newTransforms); + }); + } + + private double zoomImageIn(double zoomRatio) { + double newZoomRatio = zoomRatio; + for (int i = 0; i < ZOOM_STEPS.length; i++) { + if (newZoomRatio < ZOOM_STEPS[i]) { + newZoomRatio = ZOOM_STEPS[i]; break; } } - updateView(); - }//GEN-LAST:event_zoomOutButtonActionPerformed + return newZoomRatio; + } + + private double zoomImageOut(double zoomRatio) { + double newZoomRatio = zoomRatio; + for (int i = ZOOM_STEPS.length - 1; i >= 0; i--) { + if (newZoomRatio > ZOOM_STEPS[i]) { + newZoomRatio = ZOOM_STEPS[i]; + break; + } + } + return newZoomRatio; + } private void zoomResetButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_zoomResetButtonActionPerformed - autoResize = true; + final ImageTransforms currentTransforms = imageTransforms; + final ImageTransforms newTransforms = new ImageTransforms(0, currentTransforms.getRotation(), true); + imageTransforms = newTransforms; resetView(); }//GEN-LAST:event_zoomResetButtonActionPerformed private void formComponentResized(java.awt.event.ComponentEvent evt) {//GEN-FIRST:event_formComponentResized - if (autoResize) { + final ImageTransforms currentTransforms = imageTransforms; + if (currentTransforms.shouldAutoResize()) { resetView(); } else { - updateView(); + final double panelWidth = fxPanel.getWidth(); + final double panelHeight = fxPanel.getHeight(); + Platform.runLater(() -> { + updateView(panelWidth, panelHeight, currentTransforms); + }); } }//GEN-LAST:event_formComponentResized @@ -772,42 +933,44 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan private void createTag() { pcs.firePropertyChange(new PropertyChangeEvent(this, "state", null, State.DISABLE)); - imageTagCreator = new ImageTagCreator(fxImageView); + Platform.runLater(() -> { + imageTagCreator = new ImageTagCreator(fxImageView); - PropertyChangeListener newTagListener = (event) -> { - SwingUtilities.invokeLater(() -> { - ImageTagRegion tag = (ImageTagRegion) event.getNewValue(); - //Ask the user for tag name and comment - TagNameAndComment result = GetTagNameAndCommentDialog.doDialog(); - if (result != null) { - //Persist and build image tag - Platform.runLater(() -> { - try { - scrollPane.setCursor(Cursor.WAIT); - ContentViewerTag contentViewerTag = storeImageTag(tag, result); - ImageTag imageTag = buildImageTag(contentViewerTag); - tagsGroup.getChildren().add(imageTag); - } catch (TskCoreException | SerializationException | NoCurrentCaseException ex) { - logger.log(Level.WARNING, "Could not save new image tag in case db", ex); //NON-NLS - } + PropertyChangeListener newTagListener = (event) -> { + SwingUtilities.invokeLater(() -> { + ImageTagRegion tag = (ImageTagRegion) event.getNewValue(); + //Ask the user for tag name and comment + TagNameAndComment result = GetTagNameAndCommentDialog.doDialog(); + if (result != null) { + //Persist and build image tag + Platform.runLater(() -> { + try { + scrollPane.setCursor(Cursor.WAIT); + ContentViewerTag contentViewerTag = storeImageTag(tag, result); + ImageTag imageTag = buildImageTag(contentViewerTag); + tagsGroup.getChildren().add(imageTag); + } catch (TskCoreException | SerializationException | NoCurrentCaseException ex) { + logger.log(Level.WARNING, "Could not save new image tag in case db", ex); //NON-NLS + } - scrollPane.setCursor(Cursor.DEFAULT); - }); - } + scrollPane.setCursor(Cursor.DEFAULT); + }); + } - pcs.firePropertyChange(new PropertyChangeEvent(this, - "state", null, State.CREATE)); - }); + pcs.firePropertyChange(new PropertyChangeEvent(this, + "state", null, State.CREATE)); + }); - //Remove image tag creator from panel - Platform.runLater(() -> { - imageTagCreator.disconnect(); - masterGroup.getChildren().remove(imageTagCreator); - }); - }; + //Remove image tag creator from panel + Platform.runLater(() -> { + imageTagCreator.disconnect(); + masterGroup.getChildren().remove(imageTagCreator); + }); + }; - imageTagCreator.addNewTagListener(newTagListener); - Platform.runLater(() -> masterGroup.getChildren().add(imageTagCreator)); + imageTagCreator.addNewTagListener(newTagListener); + masterGroup.getChildren().add(imageTagCreator); + }); } /** @@ -818,6 +981,7 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan * @return */ private ImageTag buildImageTag(ContentViewerTag contentViewerTag) { + ensureInJfxThread(); ImageTag imageTag = new ImageTag(contentViewerTag, fxImageView); //Automatically persist edits made by user @@ -841,12 +1005,12 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan * @param data * @param result */ - private ContentViewerTag storeImageTag(ImageTagRegion data, TagNameAndComment result) - throws TskCoreException, SerializationException, NoCurrentCaseException { + private ContentViewerTag storeImageTag(ImageTagRegion data, TagNameAndComment result) throws TskCoreException, SerializationException, NoCurrentCaseException { + ensureInJfxThread(); scrollPane.setCursor(Cursor.WAIT); try { ContentTag contentTag = Case.getCurrentCaseThrows().getServices().getTagsManager() - .addContentTag(file, result.getTagName(), result.getComment()); + .addContentTag(imageFile, result.getTagName(), result.getComment()); return ContentViewerTagManager.saveTag(contentTag, data); } finally { scrollPane.setCursor(Cursor.DEFAULT); @@ -883,41 +1047,46 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan "MediaViewImagePanel.fileChooserTitle=Choose a save location" }) private void exportTags() { - tagsGroup.clearFocus(); - exportChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); - //Always base chooser location to export folder - exportChooser.setCurrentDirectory(new File(Case.getCurrentCase().getExportDirectory())); - int returnVal = exportChooser.showDialog(this, Bundle.MediaViewImagePanel_exportSaveText()); - if (returnVal == JFileChooser.APPROVE_OPTION) { - new SwingWorker() { - @Override - protected Void doInBackground() { - try { - //Retrieve content viewer tags - List tags = Case.getCurrentCase().getServices() - .getTagsManager().getContentTagsByContent(file); - List> contentViewerTags = getContentViewerTags(tags); + Platform.runLater(() -> { + final AbstractFile file = imageFile; + tagsGroup.clearFocus(); + SwingUtilities.invokeLater(() -> { + exportChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + //Always base chooser location to export folder + exportChooser.setCurrentDirectory(new File(Case.getCurrentCase().getExportDirectory())); + int returnVal = exportChooser.showDialog(this, Bundle.MediaViewImagePanel_exportSaveText()); + if (returnVal == JFileChooser.APPROVE_OPTION) { + new SwingWorker() { + @Override + protected Void doInBackground() { + try { + //Retrieve content viewer tags + List tags = Case.getCurrentCase().getServices() + .getTagsManager().getContentTagsByContent(file); + List> contentViewerTags = getContentViewerTags(tags); - //Pull out image tag regions - Collection regions = contentViewerTags.stream() - .map(cvTag -> cvTag.getDetails()).collect(Collectors.toList()); + //Pull out image tag regions + Collection regions = contentViewerTags.stream() + .map(cvTag -> cvTag.getDetails()).collect(Collectors.toList()); - //Apply tags to image and write to file - BufferedImage taggedImage = ImageTagsUtil.getImageWithTags(file, regions); - Path output = Paths.get(exportChooser.getSelectedFile().getPath(), - FilenameUtils.getBaseName(file.getName()) + "-with_tags.png"); //NON-NLS - ImageIO.write(taggedImage, "png", output.toFile()); + //Apply tags to image and write to file + BufferedImage taggedImage = ImageTagsUtil.getImageWithTags(file, regions); + Path output = Paths.get(exportChooser.getSelectedFile().getPath(), + FilenameUtils.getBaseName(file.getName()) + "-with_tags.png"); //NON-NLS + ImageIO.write(taggedImage, "png", output.toFile()); - JOptionPane.showMessageDialog(null, Bundle.MediaViewImagePanel_successfulExport()); - } catch (Exception ex) { //Runtime exceptions may spill out of ImageTagsUtil from JavaFX. - //This ensures we (devs and users) have something when it doesn't work. - logger.log(Level.WARNING, "Unable to export tagged image to disk", ex); //NON-NLS - JOptionPane.showMessageDialog(null, Bundle.MediaViewImagePanel_unsuccessfulExport()); - } - return null; + JOptionPane.showMessageDialog(null, Bundle.MediaViewImagePanel_successfulExport()); + } catch (Exception ex) { //Runtime exceptions may spill out of ImageTagsUtil from JavaFX. + //This ensures we (devs and users) have something when it doesn't work. + logger.log(Level.WARNING, "Unable to export tagged image to disk", ex); //NON-NLS + JOptionPane.showMessageDialog(null, Bundle.MediaViewImagePanel_unsuccessfulExport()); + } + return null; + } + }.execute(); } - }.execute(); - } + }); + }); } private void tagsMenuMousePressed(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_tagsMenuMousePressed @@ -929,7 +1098,7 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan /** * Display states for the show/hide tags button. */ - enum DisplayOptions { + private enum DisplayOptions { HIDE_TAGS("Hide"), SHOW_TAGS("Show"); @@ -948,7 +1117,7 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan * Different states that the content viewer can be in. These states drive * which buttons are enabled for tagging. */ - enum State { + private enum State { HIDDEN, VISIBLE, SELECTED, @@ -977,14 +1146,31 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan // End of variables declaration//GEN-END:variables /** - * Reset the zoom and rotation values to their defaults. The zoom level gets - * defaulted to the current size of the panel. The rotation will be set to - * zero. - * - * Note: This method will make a call to 'updateView()' after the values - * have been reset. + * Gets the dimensions of the Swing container of the JFX components and then + * resets the components used to display the image to their default state. */ private void resetView() { + ensureInSwingThread(); + final double panelWidth = fxPanel.getWidth(); + final double panelHeight = fxPanel.getHeight(); + Platform.runLater(() -> { + resetView(panelWidth, panelHeight); + }); + } + + /** + * Resets the zoom and rotation values to their defaults. The zoom level + * gets defaulted to the current size of the panel. The rotation will be set + * to zero. + * + * @param panelWidth The width of the child panel that contains the JFX + * components of this panel. + * @param panelHeight The height of the child panel that contains the JFX + * components of this panel. + */ + private void resetView(double panelWidth, double panelHeight) { + ensureInJfxThread(); + Image image = fxImageView.getImage(); if (image == null) { return; @@ -992,24 +1178,23 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan double imageWidth = image.getWidth(); double imageHeight = image.getHeight(); - double scrollPaneWidth = fxPanel.getWidth(); - double scrollPaneHeight = fxPanel.getHeight(); + double scrollPaneWidth = panelWidth; + double scrollPaneHeight = panelHeight; double zoomRatioWidth = scrollPaneWidth / imageWidth; double zoomRatioHeight = scrollPaneHeight / imageHeight; - - // Use the smallest ratio size to fit the entire image in the view area. - zoomRatio = zoomRatioWidth < zoomRatioHeight ? zoomRatioWidth : zoomRatioHeight; - - rotation = 0; + double newZoomRatio = zoomRatioWidth < zoomRatioHeight ? zoomRatioWidth : zoomRatioHeight; // Use the smallest ratio size to fit the entire image in the view area. + final ImageTransforms newTransforms = new ImageTransforms(newZoomRatio, 0, true); + imageTransforms = newTransforms; scrollPane.setHvalue(0); scrollPane.setVvalue(0); - updateView(); + updateView(panelWidth, panelHeight, newTransforms); } /** - * Update the image to use the current zoom and rotation values. + * Updates the display of the image to use the current zoom and rotation + * values. * * Note: For zoom levels less than 100%, special accomodations are made in * order to keep the image fully visible. This is because the viewport size @@ -1017,8 +1202,20 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan * the image. So the viewport is kept the same size. Scrolling adjustments * are also made to try and ensure when the user zooms out, they don't find * themselves looking at an entire screen of dead space. + * + * @param panelWidth The width of the child panel that contains the JFX + * components of this panel. + * @param panelHeight The height of the child panel that contains the JFX + * components of this panel. */ - private void updateView() { + /** + * + * @param panelWidth + * @param panelHeight + * @param imageTransforms + */ + private void updateView(double panelWidth, double panelHeight, ImageTransforms imageTransforms) { + ensureInJfxThread(); Image image = fxImageView.getImage(); if (image == null) { return; @@ -1029,17 +1226,14 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan double imageHeight = image.getHeight(); // Image dimensions with zooming applied - double adjustedImageWidth = imageWidth * zoomRatio; - double adjustedImageHeight = imageHeight * zoomRatio; + double currentZoomRatio = imageTransforms.getZoomRatio(); + double adjustedImageWidth = imageWidth * currentZoomRatio; + double adjustedImageHeight = imageHeight * currentZoomRatio; // ImageView viewport dimensions double viewportWidth; double viewportHeight; - // Panel dimensions - double panelWidth = fxPanel.getWidth(); - double panelHeight = fxPanel.getHeight(); - // Coordinates to center the image on the panel double centerOffsetX = (panelWidth / 2) - (imageWidth / 2); double centerOffsetY = (panelHeight / 2) - (imageHeight / 2); @@ -1057,7 +1251,8 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan double maxScrollY; // Set viewport size and translation offsets. - if ((rotation % 180) == 0) { + final double currentRotation = imageTransforms.getRotation(); + if ((currentRotation % 180) == 0) { // Rotation is 0 or 180. viewportWidth = adjustedImageWidth; viewportHeight = adjustedImageHeight; @@ -1095,8 +1290,8 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan // Step 1: Zoom Scale scale = new Scale(); - scale.setX(zoomRatio); - scale.setY(zoomRatio); + scale.setX(currentZoomRatio); + scale.setY(currentZoomRatio); scale.setPivotX(imageWidth / 2); scale.setPivotY(imageHeight / 2); @@ -1104,7 +1299,7 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan Rotate rotate = new Rotate(); rotate.setPivotX(imageWidth / 2); rotate.setPivotY(imageHeight / 2); - rotate.setAngle(rotation); + rotate.setAngle(currentRotation); // Step 3: Position Translate translate = new Translate(); @@ -1125,10 +1320,96 @@ class MediaViewImagePanel extends JPanel implements MediaFileViewer.MediaViewPan scrollPane.setVvalue(scrollY); } - // Update all image controls to reflect the current values. - zoomOutButton.setEnabled(zoomRatio > MIN_ZOOM_RATIO); - zoomInButton.setEnabled(zoomRatio < MAX_ZOOM_RATIO); - rotationTextField.setText((int) rotation + "°"); - zoomTextField.setText((Math.round(zoomRatio * 100.0)) + "%"); + /* + * RC: There is a race condition here, but it will probably be corrected + * so fast the user will never see it. See Jira-6848 for details and a + * solution that will simplify this class greatly in terms of thread + * safety. + */ + SwingUtilities.invokeLater(() -> { + // Update all image controls to reflect the current values. + zoomOutButton.setEnabled(currentZoomRatio > MIN_ZOOM_RATIO); + zoomInButton.setEnabled(currentZoomRatio < MAX_ZOOM_RATIO); + rotationTextField.setText((int) currentRotation + "°"); + zoomTextField.setText((Math.round(currentZoomRatio * 100.0)) + "%"); + }); } + + /** + * Checks that the calling code is running in the JFX thread and throws an + * IllegalStateException if it is not. The intent of this method is to make + * thread confinement errors obvious at development time. + */ + private void ensureInJfxThread() { + if (!Platform.isFxApplicationThread()) { + throw new IllegalStateException("Attempt to execute JFX code outside of JFX thread"); //NON-NLS + } + } + + /** + * Checks that the calling code is running in the JFX thread and throws an + * IllegalStateException if it is not. The intent of this method is to make + * thread confinement errors obvious at development time. + */ + private void ensureInSwingThread() { + if (!SwingUtilities.isEventDispatchThread()) { + throw new IllegalStateException("Attempt to execute Swing code outside of EDT"); //NON-NLS + } + } + + /** + * Used to idicate zoom direction. + */ + private enum ZoomDirection { + IN, OUT + }; + + /** + * Records a snapshot of the image transforms specified by the user. + */ + @Immutable + private static class ImageTransforms { + + private final double zoomRatio; + private final double rotation; + private final boolean autoResize; + + ImageTransforms(double zoomRatio, double rotation, boolean autoResize) { + this.zoomRatio = zoomRatio; + this.rotation = rotation; + this.autoResize = autoResize; + } + + /** + * Gets the current zoom ratio. + * + * @return The zoom ratio. + */ + private double getZoomRatio() { + return zoomRatio; + } + + /** + * Gets the current image rotation value. Can be 0, 90, 180, or 270 + * degrees. + * + * @return The rotaion, in degrees. + */ + private double getRotation() { + return rotation; + } + + /** + * Indicates whether or not auto resizing is in effect when the user + * resizes the panel. Should always be true unless the user has used the + * zoom buttons. + * + * @return True or false. + */ + private boolean shouldAutoResize() { + return autoResize; + } + + } + } diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/Bundle.properties-MERGED index d5c75e81a3..0673318668 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/Bundle.properties-MERGED @@ -65,6 +65,7 @@ MessageAccountPanel.account.justification=Account found in Message artifact MessageAccountPanel_button_create_label=Create MessageAccountPanel_button_view_label=View MessageAccountPanel_contact_label=Contact: +MessageAccountPanel_copy_label=Copy MessageAccountPanel_no_matches=No matches found. MessageAccountPanel_persona_label=Persona: MessageAccountPanel_unknown_label=Unknown diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/CommunicationArtifactViewerHelper.java b/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/CommunicationArtifactViewerHelper.java index e65550f88c..a911ac20d7 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/CommunicationArtifactViewerHelper.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/CommunicationArtifactViewerHelper.java @@ -34,6 +34,7 @@ import javax.swing.JMenuItem; import javax.swing.JComponent; import javax.swing.JPanel; import javax.swing.JPopupMenu; +import javax.swing.JTextPane; import javax.swing.SwingUtilities; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; @@ -60,10 +61,10 @@ final class CommunicationArtifactViewerHelper { /** * Adds a new heading to the panel. * - * @param panel Panel to update. + * @param panel Panel to update. * @param gridbagLayout Layout to use. - * @param constraints Constrains to use. - * @param headerString Heading string to display. + * @param constraints Constrains to use. + * @param headerString Heading string to display. * * @return JLabel Heading label added. */ @@ -113,10 +114,10 @@ final class CommunicationArtifactViewerHelper { * * Caller must know what it's doing and set up all the constraints properly. * - * @param panel Panel to update. + * @param panel Panel to update. * @param gridbagLayout Layout to use. - * @param constraints Constrains to use. - * @param component Component to add. + * @param constraints Constrains to use. + * @param component Component to add. */ static void addComponent(JPanel panel, GridBagLayout gridbagLayout, GridBagConstraints constraints, JComponent component) { @@ -129,9 +130,9 @@ final class CommunicationArtifactViewerHelper { * Adds a filler/glue at the end of the line to keep the other columns * aligned, in case the panel is resized. * - * @param panel Panel to update. + * @param panel Panel to update. * @param gridbagLayout Layout to use. - * @param constraints Constrains to use. + * @param constraints Constrains to use. */ static void addLineEndGlue(JPanel panel, GridBagLayout gridbagLayout, GridBagConstraints constraints) { // Place the filler just past the last column. @@ -156,9 +157,9 @@ final class CommunicationArtifactViewerHelper { * Adds a filler/glue at the bottom of the panel to keep the data rows * aligned, in case the panel is resized. * - * @param panel Panel to update. + * @param panel Panel to update. * @param gridbagLayout Layout to use. - * @param constraints Constrains to use. + * @param constraints Constrains to use. */ static void addPageEndGlue(JPanel panel, GridBagLayout gridbagLayout, GridBagConstraints constraints) { @@ -182,9 +183,9 @@ final class CommunicationArtifactViewerHelper { /** * Adds a blank line to the panel. * - * @param panel Panel to update. + * @param panel Panel to update. * @param gridbagLayout Layout to use. - * @param constraints Constrains to use. + * @param constraints Constrains to use. */ static void addBlankLine(JPanel panel, GridBagLayout gridbagLayout, GridBagConstraints constraints) { constraints.gridy++; @@ -200,10 +201,10 @@ final class CommunicationArtifactViewerHelper { /** * Adds a label/key to the panel at col 0. * - * @param panel Panel to update. + * @param panel Panel to update. * @param gridbagLayout Layout to use. - * @param constraints Constrains to use. - * @param keyString Key name to display. + * @param constraints Constrains to use. + * @param keyString Key name to display. * * @return Label added. */ @@ -214,11 +215,11 @@ final class CommunicationArtifactViewerHelper { /** * Adds a label/key to the panel at specified column. * - * @param panel Panel to update. + * @param panel Panel to update. * @param gridbagLayout Layout to use. - * @param constraints Constrains to use. - * @param keyString Key name to display. - * @param gridx column index, must be less than MAX_COLS - 1. + * @param constraints Constrains to use. + * @param keyString Key name to display. + * @param gridx column index, must be less than MAX_COLS - 1. * * @return Label added. */ @@ -243,38 +244,41 @@ final class CommunicationArtifactViewerHelper { /** * Adds a value string to the panel at col 1. * - * @param panel Panel to update. + * @param panel Panel to update. * @param gridbagLayout Layout to use. - * @param constraints Constrains to use. - * @param keyString Value string to display. + * @param constraints Constrains to use. + * @param keyString Value string to display. * * @return Label added. */ - static JLabel addValue(JPanel panel, GridBagLayout gridbagLayout, GridBagConstraints constraints, String valueString) { + static JTextPane addValue(JPanel panel, GridBagLayout gridbagLayout, GridBagConstraints constraints, String valueString) { return addValueAtCol(panel, gridbagLayout, constraints, valueString, 1); } /** * Adds a value string to the panel at specified column. * - * @param panel Panel to update. + * @param panel Panel to update. * @param gridbagLayout Layout to use. - * @param constraints Constrains to use. - * @param keyString Value string to display. - * @param gridx Column index, must be less than MAX_COLS; + * @param constraints Constrains to use. + * @param keyString Value string to display. + * @param gridx Column index, must be less than MAX_COLS; * * @return Label added. */ - static JLabel addValueAtCol(JPanel panel, GridBagLayout gridbagLayout, GridBagConstraints constraints, String valueString, int gridx) { + static JTextPane addValueAtCol(JPanel panel, GridBagLayout gridbagLayout, GridBagConstraints constraints, String valueString, int gridx) { // create label, - javax.swing.JLabel valueField = new javax.swing.JLabel(); + JTextPane valueField = new JTextPane(); + valueField.setEditable(false); + valueField.setOpaque(false); constraints.gridx = gridx < MAX_COLS ? gridx : MAX_COLS - 1; - int savedGridwidth = constraints.gridwidth; + GridBagConstraints cloneConstraints = (GridBagConstraints) constraints.clone(); // let the value span 2 cols - constraints.gridwidth = 2; + cloneConstraints.gridwidth = 2; + cloneConstraints.fill = GridBagConstraints.BOTH; // set text valueField.setText(valueString); @@ -288,12 +292,9 @@ final class CommunicationArtifactViewerHelper { }); // add label to panel - gridbagLayout.setConstraints(valueField, constraints); + gridbagLayout.setConstraints(valueField, cloneConstraints); panel.add(valueField); - // restore constraints - constraints.gridwidth = savedGridwidth; - // end the line addLineEndGlue(panel, gridbagLayout, constraints); @@ -304,9 +305,9 @@ final class CommunicationArtifactViewerHelper { * Displays a message string, starting at column 0, and spanning the entire * row. * - * @param panel Panel to show. + * @param panel Panel to show. * @param gridbagLayout Layout to use. - * @param constraints Constraints to use. + * @param constraints Constraints to use. * * @param messageString Message to display. * @@ -320,9 +321,9 @@ final class CommunicationArtifactViewerHelper { * Displays a message string, starting at specified column, and spanning the * entire row. * - * @param panel Panel to show. + * @param panel Panel to show. * @param gridbagLayout Layout to use. - * @param constraints Constraints to use. + * @param constraints Constraints to use. * * @param messageString Message to display. * @@ -364,12 +365,12 @@ final class CommunicationArtifactViewerHelper { * * If CentralRepostory is disabled, just displays 'Unknown' persona name. * - * @param panel Panel to update. - * @param gridbagLayout Layout to use. - * @param constraints Constrains to use. + * @param panel Panel to update. + * @param gridbagLayout Layout to use. + * @param constraints Constrains to use. * @param accountIdentifier Account identifier to search the persona. * - * @return List of AccountPersonaSearcherData objects. + * @return List of AccountPersonaSearcherData objects. */ @NbBundle.Messages({ "CommunicationArtifactViewerHelper_persona_label=Persona: ", @@ -378,7 +379,7 @@ final class CommunicationArtifactViewerHelper { "CommunicationArtifactViewerHelper_persona_button_view=View", "CommunicationArtifactViewerHelper_persona_button_create=Create" }) - + static List addPersonaRow(JPanel panel, GridBagLayout gridbagLayout, GridBagConstraints constraints, String accountIdentifier) { List dataList = new ArrayList<>(); @@ -428,7 +429,7 @@ final class CommunicationArtifactViewerHelper { return dataList; } - + /** * Adds a contact row to the panel. * @@ -444,7 +445,7 @@ final class CommunicationArtifactViewerHelper { "CommunicationArtifactViewerHelper_contact_label=Contact: {0}", "CommunicationArtifactViewerHelper_contact_label_unknown=Unknown" }) - static JLabel addContactRow(JPanel panel, GridBagLayout gridbagLayout, GridBagConstraints constraints, String contactId) { + static JComponent addContactRow(JPanel panel, GridBagLayout gridbagLayout, GridBagConstraints constraints, String contactId) { // Increase the y value because we are not calling the addKey constraints.gridy++; //Don't change the origian constraints, just make a copy to modify @@ -462,13 +463,13 @@ final class CommunicationArtifactViewerHelper { * Event handler for mouse click event. Attaches a 'Copy' menu item to right * click. * - * @param evt Event to check. + * @param evt Event to check. * @param valueLabel Label to attach the menu item to. */ @NbBundle.Messages({ "CommunicationArtifactViewerHelper_menuitem_copy=Copy" }) - private static void valueLabelMouseClicked(java.awt.event.MouseEvent evt, JLabel valueLabel) { + private static void valueLabelMouseClicked(java.awt.event.MouseEvent evt, JTextPane valueLabel) { if (SwingUtilities.isRightMouseButton(evt)) { JPopupMenu popup = new JPopupMenu(); @@ -477,7 +478,6 @@ final class CommunicationArtifactViewerHelper { @Override public void actionPerformed(ActionEvent e) { Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(valueLabel.getText()), null); - } }); diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/MessageAccountPanel.java b/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/MessageAccountPanel.java index 1e63876cfd..2e338d3d86 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/MessageAccountPanel.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/MessageAccountPanel.java @@ -18,8 +18,13 @@ */ package org.sleuthkit.autopsy.contentviewers.artifactviewers; +import java.awt.Dimension; +import java.awt.Toolkit; +import java.awt.datatransfer.StringSelection; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -32,7 +37,10 @@ import javax.swing.GroupLayout.ParallelGroup; import javax.swing.GroupLayout.SequentialGroup; import javax.swing.JButton; import javax.swing.JLabel; +import javax.swing.JMenuItem; import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JTextPane; import javax.swing.LayoutStyle.ComponentPlacement; import javax.swing.SwingUtilities; import javax.swing.SwingWorker; @@ -120,7 +128,7 @@ final class MessageAccountPanel extends JPanel { return new ArrayList<>(); } - if ( !((DataSource)(artifact.getDataSource())).getDeviceId().equals(account.getTypeSpecificID())) { + if (!((DataSource) (artifact.getDataSource())).getDeviceId().equals(account.getTypeSpecificID())) { List contactList = ContactCache.getContacts(account); BlackboardArtifact contact = null; @@ -276,13 +284,20 @@ final class MessageAccountPanel extends JPanel { private Persona persona = null; private final String contactName; - private JLabel accountLabel; + private JTextPane accountLabel; private JLabel personaHeader; - private JLabel personaDisplayName; + private JTextPane personaDisplayName; private JLabel contactHeader; - private JLabel contactDisplayName; + private JTextPane contactDisplayName; private JButton button; + private JMenuItem contactCopyMenuItem; + private JMenuItem personaCopyMenuItem; + private JMenuItem accountCopyMenuItem; + JPopupMenu contactPopupMenu = new JPopupMenu(); + JPopupMenu personaPopupMenu = new JPopupMenu(); + JPopupMenu accountPopupMenu = new JPopupMenu(); + /** * Construct a new AccountContainer * @@ -304,24 +319,103 @@ final class MessageAccountPanel extends JPanel { "MessageAccountPanel_unknown_label=Unknown", "MessageAccountPanel_button_view_label=View", "MessageAccountPanel_button_create_label=Create", - "MessageAccountPanel_contact_label=Contact:" + "MessageAccountPanel_contact_label=Contact:", + "MessageAccountPanel_copy_label=Copy" }) /** * Swing components will not be initialized until this method is called. */ private void initalizeSwingControls() { - accountLabel = new JLabel(); + accountLabel = new JTextPane(); + accountLabel.setEditable(false); + accountLabel.setOpaque(false); personaHeader = new JLabel(Bundle.MessageAccountPanel_persona_label()); contactHeader = new JLabel(Bundle.MessageAccountPanel_contact_label()); - personaDisplayName = new JLabel(); - contactDisplayName = new JLabel(); + personaDisplayName = new JTextPane(); + personaDisplayName.setOpaque(false); + personaDisplayName.setEditable(false); + personaDisplayName.setPreferredSize(new Dimension(100, 26)); + personaDisplayName.setMaximumSize(new Dimension(100, 26)); + contactDisplayName = new JTextPane(); + contactDisplayName.setOpaque(false); + contactDisplayName.setEditable(false); + contactDisplayName.setPreferredSize(new Dimension(100, 26)); button = new JButton(); button.addActionListener(new PersonaButtonListener(this)); accountLabel.setText(account.getTypeSpecificID()); contactDisplayName.setText(contactName); personaDisplayName.setText(persona != null ? persona.getName() : Bundle.MessageAccountPanel_unknown_label()); + + //This is a bit of a hack to size the JTextPane correctly, but it gets the job done. + personaDisplayName.setMaximumSize((new JLabel(personaDisplayName.getText()).getMaximumSize())); + contactDisplayName.setMaximumSize((new JLabel(contactDisplayName.getText()).getMaximumSize())); + accountLabel.setMaximumSize((new JLabel(accountLabel.getText()).getMaximumSize())); + button.setText(persona != null ? Bundle.MessageAccountPanel_button_view_label() : Bundle.MessageAccountPanel_button_create_label()); + + initalizePopupMenus(); + } + + /** + * Initialize the copy popup menus for the persona and the contact label. + */ + private void initalizePopupMenus() { + contactCopyMenuItem = new JMenuItem(Bundle.MessageAccountPanel_copy_label()); + personaCopyMenuItem = new JMenuItem(Bundle.MessageAccountPanel_copy_label()); + accountCopyMenuItem = new JMenuItem(Bundle.MessageAccountPanel_copy_label()); + personaPopupMenu.add(personaCopyMenuItem); + contactPopupMenu.add(contactCopyMenuItem); + accountPopupMenu.add(accountCopyMenuItem); + + personaDisplayName.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent evt) { + if (SwingUtilities.isRightMouseButton(evt)) { + personaPopupMenu.show(personaDisplayName, evt.getX(), evt.getY()); + } + } + }); + + personaCopyMenuItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(personaDisplayName.getText()), null); + } + }); + + contactDisplayName.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent evt) { + if (SwingUtilities.isRightMouseButton(evt)) { + contactPopupMenu.show(contactDisplayName, evt.getX(), evt.getY()); + } + } + }); + + contactCopyMenuItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(contactDisplayName.getText()), null); + } + }); + + accountLabel.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent evt) { + if (SwingUtilities.isRightMouseButton(evt)) { + accountPopupMenu.show(accountLabel, evt.getX(), evt.getY()); + } + } + }); + + accountCopyMenuItem.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(accountLabel.getText()), null); + } + }); + } private String getNameFromContactArtifact(BlackboardArtifact contactArtifact) throws TskCoreException { @@ -349,6 +443,9 @@ final class MessageAccountPanel extends JPanel { public void run() { personaDisplayName.setText(persona != null ? persona.getName() : Bundle.MessageAccountPanel_unknown_label()); button.setText(persona != null ? Bundle.MessageAccountPanel_button_view_label() : Bundle.MessageAccountPanel_button_create_label()); + + //This is a bit of a hack to size the JTextPane correctly, but it gets the job done. + personaDisplayName.setMaximumSize((new JLabel(personaDisplayName.getText()).getMaximumSize())); revalidate(); repaint(); } @@ -378,7 +475,7 @@ final class MessageAccountPanel extends JPanel { * * @return JLabel object */ - private JLabel getAccountLabel() { + private JTextPane getAccountLabel() { return accountLabel; } @@ -408,8 +505,8 @@ final class MessageAccountPanel extends JPanel { return group; } - - private SequentialGroup getContactSequentialGroup(GroupLayout layout) { + + private SequentialGroup getContactSequentialGroup(GroupLayout layout) { SequentialGroup group = layout.createSequentialGroup(); group @@ -428,14 +525,21 @@ final class MessageAccountPanel extends JPanel { * @return A group for the personal controls. */ private ParallelGroup getPersonLineVerticalGroup(GroupLayout layout) { - return layout.createParallelGroup(Alignment.BASELINE) + return layout.createParallelGroup(Alignment.CENTER) .addComponent(personaHeader) .addComponent(personaDisplayName) .addComponent(button); } - + + /** + * Generates the vertical layout code for the contact line. + * + * @param layout Instance of GroupLayout to update. + * + * @return A group for the personal controls. + */ private ParallelGroup getContactLineVerticalGroup(GroupLayout layout) { - return layout.createParallelGroup(Alignment.BASELINE) + return layout.createParallelGroup(Alignment.CENTER) .addComponent(contactHeader) .addComponent(contactDisplayName); } diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/AboutWindowAction.java b/Core/src/org/sleuthkit/autopsy/corecomponents/AboutWindowAction.java index db0710ee2d..00f0e7217a 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/AboutWindowAction.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/AboutWindowAction.java @@ -33,7 +33,7 @@ import org.openide.util.NbBundle; */ @ActionID(id = "org.sleuthkit.autopsy.corecomponents.AboutWindowAction", category = "Help") @ActionRegistration(displayName = "#CTL_CustomAboutAction", iconInMenu = true, lazy = false) -@ActionReference(path = "Menu/Help", position = 3000) +@ActionReference(path = "Menu/Help", position = 3000, separatorBefore = 2999) public class AboutWindowAction extends AboutAction { @Override diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties index c109aad513..38c6af268b 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties @@ -135,25 +135,6 @@ OptionsCategory_Name_View=View OptionsCategory_Keywords_View=View ViewPreferencesPanel.currentSessionSettingsPanel.border.title=Current Session Settings ViewPreferencesPanel.hideRejectedResultsCheckbox.text=Hide rejected results -ViewPreferencesPanel.selectFileLabel.text=When selecting a file: -ViewPreferencesPanel.globalSettingsPanel.border.title=Global Settings -ViewPreferencesPanel.translateTextLabel.text=Translate text: -ViewPreferencesPanel.hideOtherUsersTagsLabel.text=Hide other users' tags in the: -ViewPreferencesPanel.hideOtherUsersTagsCheckbox.text=Tags area in the tree -ViewPreferencesPanel.useAnotherTimeRadioButton.text=Use another time zone -ViewPreferencesPanel.useLocalTimeRadioButton.text=Use local time zone -ViewPreferencesPanel.displayTimeLabel.text=When displaying times: -ViewPreferencesPanel.viewsHideSlackCheckbox.text=Views area -ViewPreferencesPanel.dataSourcesHideSlackCheckbox.text=Data Sources area (the directory hierarchy) -ViewPreferencesPanel.hideSlackFilesLabel.text=Hide slack files in the: -ViewPreferencesPanel.viewsHideKnownCheckbox.text=Views area -ViewPreferencesPanel.dataSourcesHideKnownCheckbox.text=Data Sources area (the directory hierarchy) -ViewPreferencesPanel.hideKnownFilesLabel.text=Hide known files (i.e. those in the NIST NSRL) in the: -ViewPreferencesPanel.keepCurrentViewerRadioButton.toolTipText=For example, stay in Hex view when a JPEG is selected. -ViewPreferencesPanel.keepCurrentViewerRadioButton.text=Stay on the same file viewer -ViewPreferencesPanel.useBestViewerRadioButton.toolTipText=For example, change from Hex to Media when a JPEG is selected. -ViewPreferencesPanel.useBestViewerRadioButton.text=Change to the most specific file viewer -ViewPreferencesPanel.fileNameTranslationColumnCheckbox.text=Add column in result viewer for file name translation DataContentViewerHex.launchHxDButton.text=Launch in HxD ExternalViewerGlobalSettingsPanel.jButton2.text=jButton2 ExternalViewerGlobalSettingsPanel.newRuleButton1.text=New Rule @@ -193,12 +174,7 @@ DataResultViewerTable.pagePrevButton.text= DataResultViewerTable.pagesLabel.text=Pages: DataResultViewerTable.pageNumLabel.text= DataResultViewerTable.pageLabel.text=Page: -ViewPreferencesPanel.maxResultsLabel.text=Maximum number of Results to show in table: -ViewPreferencesPanel.maxResultsLabel.toolTipText=\nSetting this value to 0 will display all results in the results table.\n
Note that setting this value to 0 may result in poor UI responsiveness when there are large numbers of results.\n DataResultViewerTable.exportCSVButton.text=Save Table as CSV -ViewPreferencesPanel.scoColumnsCheckbox.text=S(core), C(omments), and O(ccurences) -ViewPreferencesPanel.scoColumnsWrapAroundText.text=to reduce loading times -ViewPreferencesPanel.scoColumnsLabel.text=Do not add columns for: AutopsyOptionsPanel.logoPanel.border.title=Logo AutopsyOptionsPanel.agencyLogoPathFieldValidationLabel.text= AutopsyOptionsPanel.specifyLogoRB.text=Specify a logo @@ -223,3 +199,27 @@ AutopsyOptionsPanel.totalMemoryLabel.text=Total System Memory: AutopsyOptionsPanel.maxMemoryUnitsLabel.text=GB AutopsyOptionsPanel.maxMemoryLabel.text=Maximum JVM Memory: AutopsyOptionsPanel.runtimePanel.border.title=Runtime +ViewPreferencesPanel.maxResultsLabel.toolTipText=\nSetting this value to 0 will display all results in the results table.\n
Note that setting this value to 0 may result in poor UI responsiveness when there are large numbers of results.\n +ViewPreferencesPanel.maxResultsLabel.text=Maximum number of Results to show in table: +ViewPreferencesPanel.fileNameTranslationColumnCheckbox.text=Add column in result viewer for file name translation +ViewPreferencesPanel.scoColumnsWrapAroundText.text=to reduce loading times +ViewPreferencesPanel.translateTextLabel.text=Translate text: +ViewPreferencesPanel.scoColumnsCheckbox.text=S(core), C(omments), and O(ccurences) +ViewPreferencesPanel.scoColumnsLabel.text=Do not add columns for: +ViewPreferencesPanel.hideOtherUsersTagsLabel.text=Hide other users' tags in the: +ViewPreferencesPanel.hideOtherUsersTagsCheckbox.text=Tags area in the tree +ViewPreferencesPanel.useAnotherTimeRadioButton.text=Use another time zone +ViewPreferencesPanel.useLocalTimeRadioButton.text=Use local time zone +ViewPreferencesPanel.displayTimeLabel.text=When displaying times: +ViewPreferencesPanel.viewsHideSlackCheckbox.text=Views area +ViewPreferencesPanel.dataSourcesHideSlackCheckbox.text=Data Sources area (the directory hierarchy) +ViewPreferencesPanel.hideSlackFilesLabel.text=Hide slack files in the: +ViewPreferencesPanel.viewsHideKnownCheckbox.text=Views area +ViewPreferencesPanel.dataSourcesHideKnownCheckbox.text=Data Sources area (the directory hierarchy) +ViewPreferencesPanel.hideKnownFilesLabel.text=Hide known files (i.e. those in the NIST NSRL) in the: +ViewPreferencesPanel.keepCurrentViewerRadioButton.toolTipText=For example, stay in Hex view when a JPEG is selected. +ViewPreferencesPanel.keepCurrentViewerRadioButton.text=Stay on the same file viewer +ViewPreferencesPanel.useBestViewerRadioButton.toolTipText=For example, change from Hex to Media when a JPEG is selected. +ViewPreferencesPanel.useBestViewerRadioButton.text=Change to the most specific file viewer +ViewPreferencesPanel.selectFileLabel.text=When selecting a file: +ViewPreferencesPanel.globalSettingsPanel.border.title=Global Settings diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties-MERGED index c86bc5278a..26a86074a4 100755 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties-MERGED @@ -192,25 +192,6 @@ OptionsCategory_Name_View=View OptionsCategory_Keywords_View=View ViewPreferencesPanel.currentSessionSettingsPanel.border.title=Current Session Settings ViewPreferencesPanel.hideRejectedResultsCheckbox.text=Hide rejected results -ViewPreferencesPanel.selectFileLabel.text=When selecting a file: -ViewPreferencesPanel.globalSettingsPanel.border.title=Global Settings -ViewPreferencesPanel.translateTextLabel.text=Translate text: -ViewPreferencesPanel.hideOtherUsersTagsLabel.text=Hide other users' tags in the: -ViewPreferencesPanel.hideOtherUsersTagsCheckbox.text=Tags area in the tree -ViewPreferencesPanel.useAnotherTimeRadioButton.text=Use another time zone -ViewPreferencesPanel.useLocalTimeRadioButton.text=Use local time zone -ViewPreferencesPanel.displayTimeLabel.text=When displaying times: -ViewPreferencesPanel.viewsHideSlackCheckbox.text=Views area -ViewPreferencesPanel.dataSourcesHideSlackCheckbox.text=Data Sources area (the directory hierarchy) -ViewPreferencesPanel.hideSlackFilesLabel.text=Hide slack files in the: -ViewPreferencesPanel.viewsHideKnownCheckbox.text=Views area -ViewPreferencesPanel.dataSourcesHideKnownCheckbox.text=Data Sources area (the directory hierarchy) -ViewPreferencesPanel.hideKnownFilesLabel.text=Hide known files (i.e. those in the NIST NSRL) in the: -ViewPreferencesPanel.keepCurrentViewerRadioButton.toolTipText=For example, stay in Hex view when a JPEG is selected. -ViewPreferencesPanel.keepCurrentViewerRadioButton.text=Stay on the same file viewer -ViewPreferencesPanel.useBestViewerRadioButton.toolTipText=For example, change from Hex to Media when a JPEG is selected. -ViewPreferencesPanel.useBestViewerRadioButton.text=Change to the most specific file viewer -ViewPreferencesPanel.fileNameTranslationColumnCheckbox.text=Add column in result viewer for file name translation DataContentViewerHex.launchHxDButton.text=Launch in HxD ExternalViewerGlobalSettingsPanel.jButton2.text=jButton2 ExternalViewerGlobalSettingsPanel.newRuleButton1.text=New Rule @@ -250,12 +231,7 @@ DataResultViewerTable.pagePrevButton.text= DataResultViewerTable.pagesLabel.text=Pages: DataResultViewerTable.pageNumLabel.text= DataResultViewerTable.pageLabel.text=Page: -ViewPreferencesPanel.maxResultsLabel.text=Maximum number of Results to show in table: -ViewPreferencesPanel.maxResultsLabel.toolTipText=\nSetting this value to 0 will display all results in the results table.\n
Note that setting this value to 0 may result in poor UI responsiveness when there are large numbers of results.\n DataResultViewerTable.exportCSVButton.text=Save Table as CSV -ViewPreferencesPanel.scoColumnsCheckbox.text=S(core), C(omments), and O(ccurences) -ViewPreferencesPanel.scoColumnsWrapAroundText.text=to reduce loading times -ViewPreferencesPanel.scoColumnsLabel.text=Do not add columns for: AutopsyOptionsPanel.logoPanel.border.title=Logo AutopsyOptionsPanel.agencyLogoPathFieldValidationLabel.text= AutopsyOptionsPanel.specifyLogoRB.text=Specify a logo @@ -280,3 +256,27 @@ AutopsyOptionsPanel.totalMemoryLabel.text=Total System Memory: AutopsyOptionsPanel.maxMemoryUnitsLabel.text=GB AutopsyOptionsPanel.maxMemoryLabel.text=Maximum JVM Memory: AutopsyOptionsPanel.runtimePanel.border.title=Runtime +ViewPreferencesPanel.maxResultsLabel.toolTipText=\nSetting this value to 0 will display all results in the results table.\n
Note that setting this value to 0 may result in poor UI responsiveness when there are large numbers of results.\n +ViewPreferencesPanel.maxResultsLabel.text=Maximum number of Results to show in table: +ViewPreferencesPanel.fileNameTranslationColumnCheckbox.text=Add column in result viewer for file name translation +ViewPreferencesPanel.scoColumnsWrapAroundText.text=to reduce loading times +ViewPreferencesPanel.translateTextLabel.text=Translate text: +ViewPreferencesPanel.scoColumnsCheckbox.text=S(core), C(omments), and O(ccurences) +ViewPreferencesPanel.scoColumnsLabel.text=Do not add columns for: +ViewPreferencesPanel.hideOtherUsersTagsLabel.text=Hide other users' tags in the: +ViewPreferencesPanel.hideOtherUsersTagsCheckbox.text=Tags area in the tree +ViewPreferencesPanel.useAnotherTimeRadioButton.text=Use another time zone +ViewPreferencesPanel.useLocalTimeRadioButton.text=Use local time zone +ViewPreferencesPanel.displayTimeLabel.text=When displaying times: +ViewPreferencesPanel.viewsHideSlackCheckbox.text=Views area +ViewPreferencesPanel.dataSourcesHideSlackCheckbox.text=Data Sources area (the directory hierarchy) +ViewPreferencesPanel.hideSlackFilesLabel.text=Hide slack files in the: +ViewPreferencesPanel.viewsHideKnownCheckbox.text=Views area +ViewPreferencesPanel.dataSourcesHideKnownCheckbox.text=Data Sources area (the directory hierarchy) +ViewPreferencesPanel.hideKnownFilesLabel.text=Hide known files (i.e. those in the NIST NSRL) in the: +ViewPreferencesPanel.keepCurrentViewerRadioButton.toolTipText=For example, stay in Hex view when a JPEG is selected. +ViewPreferencesPanel.keepCurrentViewerRadioButton.text=Stay on the same file viewer +ViewPreferencesPanel.useBestViewerRadioButton.toolTipText=For example, change from Hex to Media when a JPEG is selected. +ViewPreferencesPanel.useBestViewerRadioButton.text=Change to the most specific file viewer +ViewPreferencesPanel.selectFileLabel.text=When selecting a file: +ViewPreferencesPanel.globalSettingsPanel.border.title=Global Settings diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle_ja.properties b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle_ja.properties index 1eb5cb06fc..9dc1ebdd53 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle_ja.properties +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle_ja.properties @@ -233,29 +233,29 @@ ViewOptionsController.moduleErr=\u5024\u306e\u5909\u66f4\u3092\u51e6\u7406\u4e2d ViewOptionsController.moduleErr.msg=\u5024\u306e\u5909\u66f4\u306e\u51e6\u7406\u306b\u5931\u6557\u3057\u307e\u3057\u305f\u3002 ViewPreferencesPanel.currentCaseSettingsPanel.border.title=\u73fe\u5728\u306e\u30b1\u30fc\u30b9\u8a2d\u5b9a ViewPreferencesPanel.currentSessionSettingsPanel.border.title=\u73fe\u5728\u306e\u30bb\u30c3\u30b7\u30e7\u30f3\u8a2d\u5b9a -ViewPreferencesPanel.dataSourcesHideKnownCheckbox.text=\u30c7\u30fc\u30bf\u30bd\u30fc\u30b9\u9818\u57df(\u30c7\u30a3\u30ec\u30af\u30c8\u30ea\u30fc\u968e\u5c64) -ViewPreferencesPanel.dataSourcesHideSlackCheckbox.text=\u30c7\u30fc\u30bf\u30bd\u30fc\u30b9\u9818\u57df(\u30c7\u30a3\u30ec\u30af\u30c8\u30ea\u30fc\u968e\u5c64) -ViewPreferencesPanel.displayTimeLabel.text=\u6642\u523b\u8868\u793a\u6642\: -ViewPreferencesPanel.fileNameTranslationColumnCheckbox.text=\u7d50\u679c\u30d3\u30e5\u30fc\u30ef\u30fc\u306b\u30d5\u30a1\u30a4\u30eb\u540d\u7ffb\u8a33\u7528\u5217\u3092\u8ffd\u52a0 -ViewPreferencesPanel.globalSettingsPanel.border.title=\u30b0\u30ed\u30fc\u30d0\u30eb\u8a2d\u5b9a ViewPreferencesPanel.groupByDataSourceCheckbox.text=\u30c7\u30fc\u30bf\u30bd\u30fc\u30b9\u5225\u306b\u30b0\u30eb\u30fc\u30d7\u5316 -ViewPreferencesPanel.hideKnownFilesLabel.text=\u6b21\u306e\u65e2\u77e5\u306e\u30d5\u30a1\u30a4\u30eb(NIST NSRL\u5185\u306e\u30d5\u30a1\u30a4\u30eb)\u3092\u975e\u8868\u793a\u306b\u3059\u308b\: -ViewPreferencesPanel.hideOtherUsersTagsCheckbox.text=\u30c4\u30ea\u30fc\u5185\u306e\u30bf\u30b0\u9818\u57df -ViewPreferencesPanel.hideOtherUsersTagsLabel.text=\u6b21\u306e\u305d\u306e\u4ed6\u306e\u30e6\u30fc\u30b6\u30fc\u306e\u30bf\u30b0\u3092\u975e\u8868\u793a\u306b\u3059\u308b\: ViewPreferencesPanel.hideRejectedResultsCheckbox.text=\u62d2\u5426\u3055\u308c\u305f\u7d50\u679c\u3092\u975e\u8868\u793a\u306b\u3059\u308b -ViewPreferencesPanel.hideSlackFilesLabel.text=\u6b21\u306e\u30b9\u30e9\u30c3\u30af\u30d5\u30a1\u30a4\u30eb\u3092\u975e\u8868\u793a\u306b\u3059\u308b\: -ViewPreferencesPanel.keepCurrentViewerRadioButton.text=\u540c\u3058\u30d5\u30a1\u30a4\u30eb\u30d3\u30e5\u30fc\u306e\u307e\u307e\u306b\u3059\u308b -ViewPreferencesPanel.keepCurrentViewerRadioButton.toolTipText=\u305f\u3068\u3048\u3070\u3001JPEG\u9078\u629e\u6642\u306f16\u9032\u30d3\u30e5\u30fc\u306e\u307e\u307e\u306b\u3057\u307e\u3059\u3002 -ViewPreferencesPanel.maxResultsLabel.text=\u30c6\u30fc\u30d6\u30eb\u3067\u8868\u793a\u3059\u308b\u6700\u5927\u7d50\u679c\u6570\: ViewPreferencesPanel.maxResultsLabel.toolTipText=\n\u3053\u306e\u5024\u30920 \u306b\u8a2d\u5b9a\u3059\u308b\u3068\u3001\u3059\u3079\u3066\u306e\u7d50\u679c\u304c\u7d50\u679c\u30c6\u30fc\u30d6\u30eb\u306b\u8868\u793a\u3055\u308c\u307e\u3059\u3002\n
\u3053\u306e\u5024\u30920 \u306b\u8a2d\u5b9a\u3059\u308b\u3068\u3001\u7d50\u679c\u6570\u304c\u591a\u3044\u5834\u5408UI\u306e\u5fdc\u7b54\u6027\u304c\u60aa\u304f\u306a\u308b\u3053\u3068\u304c\u3042\u308a\u307e\u3059 \u3002\n -ViewPreferencesPanel.scoColumnsCheckbox.text=S(\u30b9\u30b3\u30a2)\u3001C(\u30b3\u30e1\u30f3\u30c8)\u3001O(\u767a\u751f) -ViewPreferencesPanel.scoColumnsLabel.text=\u6b21\u306e\u305f\u3081\u306e\u5217\u3092\u8ffd\u52a0\u3057\u306a\u3044\: +ViewPreferencesPanel.maxResultsLabel.text=\u30c6\u30fc\u30d6\u30eb\u3067\u8868\u793a\u3059\u308b\u6700\u5927\u7d50\u679c\u6570: +ViewPreferencesPanel.fileNameTranslationColumnCheckbox.text=\u7d50\u679c\u30d3\u30e5\u30fc\u30ef\u30fc\u306b\u30d5\u30a1\u30a4\u30eb\u540d\u7ffb\u8a33\u7528\u5217\u3092\u8ffd\u52a0 ViewPreferencesPanel.scoColumnsWrapAroundText.text=\u975e\u8868\u793a\u306b\u3059\u308b\u3068\u8aad\u8fbc\u307f\u304c\u65e9\u304f\u306a\u308b\u53ef\u80fd\u6027\u304c\u3042\u308a\u307e\u3059\u3002 -ViewPreferencesPanel.selectFileLabel.text=\u30d5\u30a1\u30a4\u30eb\u9078\u629e\u6642\: -ViewPreferencesPanel.translateTextLabel.text=\u30c6\u30ad\u30b9\u30c8\u3092\u7ffb\u8a33\: +ViewPreferencesPanel.translateTextLabel.text=\u30c6\u30ad\u30b9\u30c8\u3092\u7ffb\u8a33: +ViewPreferencesPanel.scoColumnsCheckbox.text=S(\u30b9\u30b3\u30a2)\u3001C(\u30b3\u30e1\u30f3\u30c8)\u3001O(\u767a\u751f) +ViewPreferencesPanel.scoColumnsLabel.text=\u6b21\u306e\u305f\u3081\u306e\u5217\u3092\u8ffd\u52a0\u3057\u306a\u3044: +ViewPreferencesPanel.hideOtherUsersTagsLabel.text=\u6b21\u306e\u305d\u306e\u4ed6\u306e\u30e6\u30fc\u30b6\u30fc\u306e\u30bf\u30b0\u3092\u975e\u8868\u793a\u306b\u3059\u308b: +ViewPreferencesPanel.hideOtherUsersTagsCheckbox.text=\u30c4\u30ea\u30fc\u5185\u306e\u30bf\u30b0\u9818\u57df ViewPreferencesPanel.useAnotherTimeRadioButton.text=\u5225\u306e\u30bf\u30a4\u30e0\u30be\u30fc\u30f3\u3092\u4f7f\u7528 -ViewPreferencesPanel.useBestViewerRadioButton.text=\u6700\u3082\u56fa\u6709\u306e\u30d5\u30a1\u30a4\u30eb\u30d3\u30e5\u30fc\u306b\u5207\u308a\u66ff\u3048\u308b -ViewPreferencesPanel.useBestViewerRadioButton.toolTipText=\u305f\u3068\u3048\u3070\u3001JPEG\u9078\u629e\u6642\u306f16\u9032\u304b\u3089\u30e1\u30c7\u30a3\u30a2\u306b\u5207\u308a\u66ff\u3048\u307e\u3059\u3002 ViewPreferencesPanel.useLocalTimeRadioButton.text=\u30ed\u30fc\u30ab\u30eb\u30bf\u30a4\u30e0\u30be\u30fc\u30f3\u3092\u4f7f\u7528 -ViewPreferencesPanel.viewsHideKnownCheckbox.text=\u30d3\u30e5\u30fc\u9818\u57df +ViewPreferencesPanel.displayTimeLabel.text=\u6642\u523b\u8868\u793a\u6642: ViewPreferencesPanel.viewsHideSlackCheckbox.text=\u30d3\u30e5\u30fc\u9818\u57df +ViewPreferencesPanel.dataSourcesHideSlackCheckbox.text=\u30c7\u30fc\u30bf\u30bd\u30fc\u30b9\u9818\u57df(\u30c7\u30a3\u30ec\u30af\u30c8\u30ea\u30fc\u968e\u5c64) +ViewPreferencesPanel.hideSlackFilesLabel.text=\u6b21\u306e\u30b9\u30e9\u30c3\u30af\u30d5\u30a1\u30a4\u30eb\u3092\u975e\u8868\u793a\u306b\u3059\u308b: +ViewPreferencesPanel.viewsHideKnownCheckbox.text=\u30d3\u30e5\u30fc\u9818\u57df +ViewPreferencesPanel.dataSourcesHideKnownCheckbox.text=\u30c7\u30fc\u30bf\u30bd\u30fc\u30b9\u9818\u57df(\u30c7\u30a3\u30ec\u30af\u30c8\u30ea\u30fc\u968e\u5c64) +ViewPreferencesPanel.hideKnownFilesLabel.text=\u6b21\u306e\u65e2\u77e5\u306e\u30d5\u30a1\u30a4\u30eb(NIST NSRL\u5185\u306e\u30d5\u30a1\u30a4\u30eb)\u3092\u975e\u8868\u793a\u306b\u3059\u308b: +ViewPreferencesPanel.keepCurrentViewerRadioButton.toolTipText=\u305f\u3068\u3048\u3070\u3001JPEG\u9078\u629e\u6642\u306f16\u9032\u30d3\u30e5\u30fc\u306e\u307e\u307e\u306b\u3057\u307e\u3059\u3002 +ViewPreferencesPanel.keepCurrentViewerRadioButton.text=\u540c\u3058\u30d5\u30a1\u30a4\u30eb\u30d3\u30e5\u30fc\u306e\u307e\u307e\u306b\u3059\u308b +ViewPreferencesPanel.useBestViewerRadioButton.toolTipText=\u305f\u3068\u3048\u3070\u3001JPEG\u9078\u629e\u6642\u306f16\u9032\u304b\u3089\u30e1\u30c7\u30a3\u30a2\u306b\u5207\u308a\u66ff\u3048\u307e\u3059\u3002 +ViewPreferencesPanel.useBestViewerRadioButton.text=\u6700\u3082\u56fa\u6709\u306e\u30d5\u30a1\u30a4\u30eb\u30d3\u30e5\u30fc\u306b\u5207\u308a\u66ff\u3048\u308b +ViewPreferencesPanel.selectFileLabel.text=\u30d5\u30a1\u30a4\u30eb\u9078\u629e\u6642: +ViewPreferencesPanel.globalSettingsPanel.border.title=\u30b0\u30ed\u30fc\u30d0\u30eb\u8a2d\u5b9a diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/ViewPreferencesPanel.form b/Core/src/org/sleuthkit/autopsy/corecomponents/ViewPreferencesPanel.form index 83e04a9d1b..b2a6d9307c 100755 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/ViewPreferencesPanel.form +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/ViewPreferencesPanel.form @@ -96,100 +96,49 @@ - - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - + + + + + + + + + - + @@ -401,6 +350,14 @@ + + + + + + + + @@ -483,9 +440,9 @@ - - - + + + @@ -526,9 +483,9 @@ - - - + + + diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/ViewPreferencesPanel.java b/Core/src/org/sleuthkit/autopsy/corecomponents/ViewPreferencesPanel.java index acb6446c9d..0f2bae4f00 100755 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/ViewPreferencesPanel.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/ViewPreferencesPanel.java @@ -272,6 +272,9 @@ public class ViewPreferencesPanel extends JPanel implements OptionsPanel { } }); + jScrollPane1.setMaximumSize(new java.awt.Dimension(150, 130)); + jScrollPane1.setPreferredSize(new java.awt.Dimension(150, 130)); + timeZoneList.addListSelectionListener(new javax.swing.event.ListSelectionListener() { public void valueChanged(javax.swing.event.ListSelectionEvent evt) { timeZoneListValueChanged(evt); @@ -308,74 +311,40 @@ public class ViewPreferencesPanel extends JPanel implements OptionsPanel { .addContainerGap() .addGroup(globalSettingsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(globalSettingsPanelLayout.createSequentialGroup() - .addComponent(maxResultsLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(maxResultsSpinner, javax.swing.GroupLayout.PREFERRED_SIZE, 74, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(325, 325, 325)) + .addComponent(maxResultsLabel) + .addGap(13, 13, 13) + .addComponent(maxResultsSpinner, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) .addGroup(globalSettingsPanelLayout.createSequentialGroup() - .addGroup(globalSettingsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) - .addGroup(globalSettingsPanelLayout.createSequentialGroup() - .addGroup(globalSettingsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(hideKnownFilesLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGroup(globalSettingsPanelLayout.createSequentialGroup() - .addComponent(hideOtherUsersTagsLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGap(77, 77, 77)) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, globalSettingsPanelLayout.createSequentialGroup() - .addGroup(globalSettingsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) - .addComponent(scoColumnsLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGroup(javax.swing.GroupLayout.Alignment.LEADING, globalSettingsPanelLayout.createSequentialGroup() - .addGap(10, 10, 10) - .addComponent(hideOtherUsersTagsCheckbox, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))) - .addGap(99, 99, 99)) - .addGroup(globalSettingsPanelLayout.createSequentialGroup() - .addGap(32, 32, 32) - .addComponent(scoColumnsWrapAroundText, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGap(93, 93, 93)) - .addGroup(globalSettingsPanelLayout.createSequentialGroup() - .addGap(10, 10, 10) - .addComponent(scoColumnsCheckbox, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGap(14, 14, 14))) - .addGap(10, 10, 10)) - .addGroup(javax.swing.GroupLayout.Alignment.LEADING, globalSettingsPanelLayout.createSequentialGroup() - .addComponent(hideSlackFilesLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGap(51, 51, 51)) - .addGroup(globalSettingsPanelLayout.createSequentialGroup() - .addGap(10, 10, 10) - .addGroup(globalSettingsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) - .addComponent(dataSourcesHideSlackCheckbox, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGroup(javax.swing.GroupLayout.Alignment.LEADING, globalSettingsPanelLayout.createSequentialGroup() - .addComponent(viewsHideKnownCheckbox, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGap(103, 103, 103)) - .addComponent(dataSourcesHideKnownCheckbox, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGroup(globalSettingsPanelLayout.createSequentialGroup() - .addComponent(viewsHideSlackCheckbox, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGap(116, 116, 116))) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED))) .addGroup(globalSettingsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(globalSettingsPanelLayout.createSequentialGroup() - .addComponent(displayTimeLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGap(91, 91, 91)) - .addGroup(globalSettingsPanelLayout.createSequentialGroup() - .addComponent(translateTextLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGap(46, 46, 46)) - .addGroup(globalSettingsPanelLayout.createSequentialGroup() - .addComponent(selectFileLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGap(90, 90, 90)) + .addGap(10, 10, 10) + .addGroup(globalSettingsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addComponent(viewsHideKnownCheckbox) + .addComponent(dataSourcesHideKnownCheckbox, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(hideOtherUsersTagsCheckbox) + .addComponent(viewsHideSlackCheckbox) + .addComponent(hideOtherUsersTagsLabel) + .addComponent(scoColumnsLabel) + .addComponent(scoColumnsCheckbox) + .addComponent(dataSourcesHideSlackCheckbox, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(scoColumnsWrapAroundText))) + .addComponent(hideSlackFilesLabel) + .addComponent(hideKnownFilesLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 289, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(globalSettingsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(globalSettingsPanelLayout.createSequentialGroup() .addGap(10, 10, 10) .addGroup(globalSettingsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(globalSettingsPanelLayout.createSequentialGroup() - .addGap(24, 24, 24) - .addComponent(jScrollPane1)) - .addComponent(fileNameTranslationColumnCheckbox, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(useAnotherTimeRadioButton, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGroup(globalSettingsPanelLayout.createSequentialGroup() - .addGroup(globalSettingsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(useLocalTimeRadioButton, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(keepCurrentViewerRadioButton, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(useBestViewerRadioButton, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) - .addGap(44, 44, 44))))))) - .addContainerGap()) + .addComponent(useLocalTimeRadioButton) + .addComponent(keepCurrentViewerRadioButton) + .addComponent(fileNameTranslationColumnCheckbox) + .addComponent(useAnotherTimeRadioButton) + .addComponent(useBestViewerRadioButton) + .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 246, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addComponent(displayTimeLabel) + .addComponent(selectFileLabel) + .addComponent(translateTextLabel)))) + .addContainerGap(107, Short.MAX_VALUE)) ); globalSettingsPanelLayout.setVerticalGroup( globalSettingsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) @@ -444,8 +413,8 @@ public class ViewPreferencesPanel extends JPanel implements OptionsPanel { currentCaseSettingsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(currentCaseSettingsPanelLayout.createSequentialGroup() .addContainerGap() - .addComponent(groupByDataSourceCheckbox, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGap(474, 474, 474)) + .addComponent(groupByDataSourceCheckbox) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); currentCaseSettingsPanelLayout.setVerticalGroup( currentCaseSettingsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) @@ -469,8 +438,8 @@ public class ViewPreferencesPanel extends JPanel implements OptionsPanel { currentSessionSettingsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(currentSessionSettingsPanelLayout.createSequentialGroup() .addContainerGap() - .addComponent(hideRejectedResultsCheckbox, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGap(418, 418, 418)) + .addComponent(hideRejectedResultsCheckbox) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); currentSessionSettingsPanelLayout.setVerticalGroup( currentSessionSettingsPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) @@ -532,6 +501,22 @@ public class ViewPreferencesPanel extends JPanel implements OptionsPanel { } }//GEN-LAST:event_hideRejectedResultsCheckboxActionPerformed + private void maxResultsSpinnerStateChanged(javax.swing.event.ChangeEvent evt) {//GEN-FIRST:event_maxResultsSpinnerStateChanged + if (immediateUpdates) { + UserPreferences.setResultsTablePageSize((int)maxResultsSpinner.getValue()); + } else { + firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null); + } + }//GEN-LAST:event_maxResultsSpinnerStateChanged + + private void fileNameTranslationColumnCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_fileNameTranslationColumnCheckboxActionPerformed + if (immediateUpdates) { + UserPreferences.setDisplayTranslatedFileNames(fileNameTranslationColumnCheckbox.isSelected()); + } else { + firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null); + } + }//GEN-LAST:event_fileNameTranslationColumnCheckboxActionPerformed + private void timeZoneListValueChanged(javax.swing.event.ListSelectionEvent evt) {//GEN-FIRST:event_timeZoneListValueChanged if (immediateUpdates && useAnotherTimeRadioButton.isSelected()) { UserPreferences.setTimeZoneForDisplays(timeZoneList.getSelectedValue().substring(11).trim()); @@ -630,22 +615,6 @@ public class ViewPreferencesPanel extends JPanel implements OptionsPanel { } }//GEN-LAST:event_useBestViewerRadioButtonActionPerformed - private void fileNameTranslationColumnCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_fileNameTranslationColumnCheckboxActionPerformed - if (immediateUpdates) { - UserPreferences.setDisplayTranslatedFileNames(fileNameTranslationColumnCheckbox.isSelected()); - } else { - firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null); - } - }//GEN-LAST:event_fileNameTranslationColumnCheckboxActionPerformed - - private void maxResultsSpinnerStateChanged(javax.swing.event.ChangeEvent evt) {//GEN-FIRST:event_maxResultsSpinnerStateChanged - if (immediateUpdates) { - UserPreferences.setResultsTablePageSize((int)maxResultsSpinner.getValue()); - } else { - firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null); - } - }//GEN-LAST:event_maxResultsSpinnerStateChanged - // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JPanel currentCaseSettingsPanel; diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java b/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java index 5f1653bf7a..6ae521de1e 100644 --- a/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java +++ b/Core/src/org/sleuthkit/autopsy/coreutils/ImageUtils.java @@ -38,6 +38,7 @@ import java.util.Collections; import java.util.EnumSet; import java.util.Iterator; import java.util.List; +import java.util.Objects; import static java.util.Objects.nonNull; import java.util.SortedSet; import java.util.TreeSet; @@ -58,7 +59,6 @@ import javax.imageio.ImageReadParam; import javax.imageio.ImageReader; import javax.imageio.event.IIOReadProgressListener; import javax.imageio.stream.ImageInputStream; -import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.concurrent.BasicThreadFactory; import org.openide.util.NbBundle; @@ -962,7 +962,7 @@ public class ImageUtils { LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT, ImageUtils.getContentPathSafe(file)); } else if (fxImage.isError()) { //if there was somekind of error, log it - LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT + ": " + ObjectUtils.toString(fxImage.getException()), ImageUtils.getContentPathSafe(file)); + LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT + ": " + Objects.toString(fxImage.getException()), ImageUtils.getContentPathSafe(file)); } } catch (InterruptedException | ExecutionException ex) { failed(); @@ -972,7 +972,7 @@ public class ImageUtils { @Override protected void failed() { super.failed(); - LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT + ": " + ObjectUtils.toString(getException()), ImageUtils.getContentPathSafe(file)); + LOGGER.log(Level.WARNING, IMAGEIO_COULD_NOT_READ_UNSUPPORTED_OR_CORRUPT + ": " + Objects.toString(getException()), ImageUtils.getContentPathSafe(file)); } @Override diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/utils/IconsUtil.java b/Core/src/org/sleuthkit/autopsy/datamodel/utils/IconsUtil.java index e4fbb55ce2..88dc335b5d 100755 --- a/Core/src/org/sleuthkit/autopsy/datamodel/utils/IconsUtil.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/utils/IconsUtil.java @@ -89,6 +89,20 @@ public final class IconsUtil { imageFile = "gps_trackpoint.png"; //NON-NLS } else if (typeID == ARTIFACT_TYPE.TSK_REMOTE_DRIVE.getTypeID()) { imageFile = "drive_network.png"; //NON-NLS + } else if (typeID == ARTIFACT_TYPE.TSK_OS_ACCOUNT.getTypeID()) { + imageFile = "os-account.png"; //NON-NLS + } else if (typeID == ARTIFACT_TYPE.TSK_OBJECT_DETECTED.getTypeID()) { + imageFile = "objects.png"; //NON-NLS + } else if (typeID == ARTIFACT_TYPE.TSK_WEB_FORM_AUTOFILL.getTypeID()) { + imageFile = "web-form.png"; //NON-NLS + } else if (typeID == ARTIFACT_TYPE.TSK_WEB_CACHE.getTypeID()) { + imageFile = "cache.png"; //NON-NLS + } else if (typeID == ARTIFACT_TYPE.TSK_USER_CONTENT_SUSPECTED.getTypeID()) { + imageFile = "user-content.png"; //NON-NLS + } else if (typeID == ARTIFACT_TYPE.TSK_METADATA.getTypeID()) { + imageFile = "metadata.png"; //NON-NLS + } else if (typeID == ARTIFACT_TYPE.TSK_CLIPBOARD_CONTENT.getTypeID()) { + imageFile = "clipboard.png"; //NON-NLS } else if (typeID == ARTIFACT_TYPE.TSK_FACE_DETECTED.getTypeID()) { imageFile = "face.png"; //NON-NLS } else if (typeID == ARTIFACT_TYPE.TSK_WIFI_NETWORK.getTypeID()) { @@ -103,6 +117,8 @@ public final class IconsUtil { imageFile = "devices.png"; //NON-NLS } else if (typeID == ARTIFACT_TYPE.TSK_VERIFICATION_FAILED.getTypeID()) { imageFile = "validationFailed.png"; //NON-NLS + } else if (typeID == ARTIFACT_TYPE.TSK_WEB_ACCOUNT_TYPE.getTypeID()) { + imageFile = "web-account-type.png"; //NON-NLS } else { imageFile = "artifact-icon.png"; //NON-NLS } diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceAnalysisSummary.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/AnalysisSummary.java similarity index 88% rename from Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceAnalysisSummary.java rename to Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/AnalysisSummary.java index 86aa21c7a1..9d35e3d28d 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceAnalysisSummary.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/AnalysisSummary.java @@ -18,8 +18,10 @@ */ package org.sleuthkit.autopsy.datasourcesummary.datamodel; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DefaultArtifactUpdateGovernor; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -40,7 +42,7 @@ import org.sleuthkit.datamodel.TskCoreException; /** * Providing data for the data source analysis tab. */ -public class DataSourceAnalysisSummary { +public class AnalysisSummary implements DefaultArtifactUpdateGovernor { private static final BlackboardAttribute.Type TYPE_SET_NAME = new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_SET_NAME); @@ -52,12 +54,19 @@ public class DataSourceAnalysisSummary { "CREDIT CARD NUMBERS" )); + private static final Set ARTIFACT_UPDATE_TYPE_IDS = new HashSet<>(Arrays.asList( + ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT.getTypeID(), + ARTIFACT_TYPE.TSK_INTERESTING_ARTIFACT_HIT.getTypeID(), + ARTIFACT_TYPE.TSK_HASHSET_HIT.getTypeID(), + ARTIFACT_TYPE.TSK_KEYWORD_HIT.getTypeID() + )); + private final SleuthkitCaseProvider provider; /** * Main constructor. */ - public DataSourceAnalysisSummary() { + public AnalysisSummary() { this(SleuthkitCaseProvider.DEFAULT); } @@ -66,10 +75,15 @@ public class DataSourceAnalysisSummary { * * @param provider The means of obtaining a sleuthkit case. */ - public DataSourceAnalysisSummary(SleuthkitCaseProvider provider) { + public AnalysisSummary(SleuthkitCaseProvider provider) { this.provider = provider; } + @Override + public Set getArtifactTypeIdsForRefresh() { + return ARTIFACT_UPDATE_TYPE_IDS; + } + /** * Gets counts for hashset hits. * @@ -133,6 +147,11 @@ public class DataSourceAnalysisSummary { */ private List> getCountsData(DataSource dataSource, BlackboardAttribute.Type keyType, ARTIFACT_TYPE... artifactTypes) throws SleuthkitCaseProviderException, TskCoreException { + + if (dataSource == null) { + return Collections.emptyList(); + } + List artifacts = new ArrayList<>(); SleuthkitCase skCase = provider.get(); diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/Bundle.properties-MERGED index 2e019a0248..8d62e88381 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/Bundle.properties-MERGED @@ -1,2 +1,3 @@ DataSourceUserActivitySummary_getRecentAccounts_calllogMessage=Call Log DataSourceUserActivitySummary_getRecentAccounts_emailMessage=Email Message +IngestModuleCheckUtil_recentActivityModuleName=Recent Activity diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceDetailsSummary.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/ContainerSummary.java similarity index 61% rename from Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceDetailsSummary.java rename to Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/ContainerSummary.java index 3dd50131e5..8129911500 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceDetailsSummary.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/ContainerSummary.java @@ -18,21 +18,62 @@ */ package org.sleuthkit.autopsy.datasourcesummary.datamodel; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DefaultArtifactUpdateGovernor; import java.sql.SQLException; -import java.util.logging.Level; -import org.sleuthkit.autopsy.coreutils.Logger; -import static org.sleuthkit.autopsy.datasourcesummary.datamodel.DataSourceInfoUtilities.getBaseQueryResult; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.SleuthkitCaseProvider.SleuthkitCaseProviderException; +import org.sleuthkit.autopsy.ingest.ModuleContentEvent; +import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardAttribute; import org.sleuthkit.datamodel.DataSource; +import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData; /** * Provides methods to query for data source overview details. */ -public class DataSourceDetailsSummary { +public class ContainerSummary implements DefaultArtifactUpdateGovernor { - private static final Logger logger = Logger.getLogger(DataSourceDetailsSummary.class.getName()); + private static final Set ARTIFACT_UPDATE_TYPE_IDS = new HashSet<>(Arrays.asList( + BlackboardArtifact.ARTIFACT_TYPE.TSK_OS_INFO.getTypeID(), + BlackboardArtifact.ARTIFACT_TYPE.TSK_DATA_SOURCE_USAGE.getTypeID() + )); + + private final SleuthkitCaseProvider provider; + + /** + * Main constructor. + */ + public ContainerSummary() { + this(SleuthkitCaseProvider.DEFAULT); + } + + /** + * Main constructor. + * + * @param provider The means of obtaining a sleuthkit case. + */ + public ContainerSummary(SleuthkitCaseProvider provider) { + this.provider = provider; + } + + @Override + public boolean isRefreshRequired(ModuleContentEvent evt) { + return true; + } + + @Override + public boolean isRefreshRequired(AbstractFile file) { + return true; + } + + @Override + public Set getArtifactTypeIdsForRefresh() { + return ARTIFACT_UPDATE_TYPE_IDS; + } /** * Gets the size of unallocated files in a particular datasource. @@ -40,8 +81,13 @@ public class DataSourceDetailsSummary { * @param currentDataSource The data source. * * @return The size or null if the query could not be executed. + * + * @throws SleuthkitCaseProviderException + * @throws TskCoreException + * @throws SQLException */ - public static Long getSizeOfUnallocatedFiles(DataSource currentDataSource) { + public Long getSizeOfUnallocatedFiles(DataSource currentDataSource) + throws SleuthkitCaseProvider.SleuthkitCaseProviderException, TskCoreException, SQLException { if (currentDataSource == null) { return null; } @@ -66,9 +112,8 @@ public class DataSourceDetailsSummary { return null; } }; - String errorMessage = "Unable to get size of unallocated files; returning null."; - return DataSourceInfoUtilities.getBaseQueryResult(query, handler, errorMessage); + return DataSourceInfoUtilities.getBaseQueryResult(provider.get(), query, handler); } /** @@ -79,8 +124,14 @@ public class DataSourceDetailsSummary { * * @return The concatenated value or null if the query could not be * executed. + * + * @throws SleuthkitCaseProviderException + * @throws TskCoreException + * @throws SQLException */ - public static String getOperatingSystems(DataSource dataSource) { + public String getOperatingSystems(DataSource dataSource) + throws SleuthkitCaseProvider.SleuthkitCaseProviderException, TskCoreException, SQLException { + if (dataSource == null) { return null; } @@ -98,8 +149,14 @@ public class DataSourceDetailsSummary { * * @return The concatenated value or null if the query could not be * executed. + * + * @throws SleuthkitCaseProviderException + * @throws TskCoreException + * @throws SQLException */ - public static String getDataSourceType(DataSource dataSource) { + public String getDataSourceType(DataSource dataSource) + throws SleuthkitCaseProvider.SleuthkitCaseProviderException, TskCoreException, SQLException { + if (dataSource == null) { return null; } @@ -116,35 +173,33 @@ public class DataSourceDetailsSummary { * @param query The query. * @param valueParam The parameter for the value in the result set. * @param separator The string separator used in concatenation. - * @param errorMessage The error message if the result set could not - * be received. - * @param singleErrorMessage The error message if a single result could not - * be obtained. * * @return The concatenated string or null if the query could not be * executed. + * + * @throws SleuthkitCaseProviderException + * @throws TskCoreException + * @throws SQLException */ - private static String getConcattedStringsResult(String query, String valueParam, String separator, String errorMessage, String singleErrorMessage) { + private String getConcattedStringsResult(String query, String valueParam, String separator) + throws SleuthkitCaseProvider.SleuthkitCaseProviderException, TskCoreException, SQLException { + DataSourceInfoUtilities.ResultSetHandler handler = (resultSet) -> { String toRet = ""; boolean first = true; while (resultSet.next()) { - try { - if (first) { - first = false; - } else { - toRet += separator; - } - toRet += resultSet.getString(valueParam); - } catch (SQLException ex) { - logger.log(Level.WARNING, singleErrorMessage, ex); + if (first) { + first = false; + } else { + toRet += separator; } + toRet += resultSet.getString(valueParam); } return toRet; }; - return getBaseQueryResult(query, handler, errorMessage); + return DataSourceInfoUtilities.getBaseQueryResult(provider.get(), query, handler); } /** @@ -157,8 +212,14 @@ public class DataSourceDetailsSummary { * * @return The concatenated value or null if the query could not be * executed. + * + * @throws SleuthkitCaseProviderException + * @throws TskCoreException + * @throws SQLException */ - private static String getConcattedAttrValue(long dataSourceId, int artifactTypeId, int attributeTypeId) { + private String getConcattedAttrValue(long dataSourceId, int artifactTypeId, int attributeTypeId) + throws SleuthkitCaseProvider.SleuthkitCaseProviderException, TskCoreException, SQLException { + final String valueParam = "concatted_attribute_value"; String query = "SELECT attr.value_text AS " + valueParam + " FROM blackboard_artifacts bba " @@ -167,12 +228,7 @@ public class DataSourceDetailsSummary { + " AND bba.artifact_type_id = " + artifactTypeId + " AND attr.attribute_type_id = " + attributeTypeId; - String errorMessage = "Unable to execute query to retrieve concatted attribute values."; - String singleErrorMessage = "There was an error retrieving one of the results. That result will be omitted from concatted value."; String separator = ", "; - return getConcattedStringsResult(query, valueParam, separator, errorMessage, singleErrorMessage); - } - - private DataSourceDetailsSummary() { + return getConcattedStringsResult(query, valueParam, separator); } } diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceCountsSummary.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceCountsSummary.java deleted file mode 100644 index 0f2826636b..0000000000 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceCountsSummary.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2019 - 2020 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.datamodel; - -import java.sql.SQLException; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.logging.Level; -import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.datasourcesummary.datamodel.DataSourceInfoUtilities.ResultSetHandler; -import org.sleuthkit.datamodel.DataSource; -import org.sleuthkit.datamodel.TskData; - -/** - * Provides information for the DataSourceSummaryCountsPanel. - */ -public class DataSourceCountsSummary { - - private static final Logger logger = Logger.getLogger(DataSourceCountsSummary.class.getName()); - - /** - * Get count of regular files (not directories) in a data source. - * - * @param currentDataSource The data source. - * - * @return The count. - */ - public static Long getCountOfFiles(DataSource currentDataSource) { - return DataSourceInfoUtilities.getCountOfRegularFiles(currentDataSource, null, - "Unable to get count of files, providing empty results"); - } - - /** - * Get count of allocated files in a data source. - * - * @param currentDataSource The data source. - * - * @return The count. - */ - public static Long getCountOfAllocatedFiles(DataSource currentDataSource) { - return DataSourceInfoUtilities.getCountOfRegularFiles(currentDataSource, - DataSourceInfoUtilities.getMetaFlagsContainsStatement(TskData.TSK_FS_META_FLAG_ENUM.ALLOC), - "Unable to get counts of unallocated files for datasource, providing empty results"); - } - - /** - * Get count of unallocated files in a data source. - * - * @param currentDataSource The data source. - * - * @return The count. - */ - public static Long getCountOfUnallocatedFiles(DataSource currentDataSource) { - return DataSourceInfoUtilities.getCountOfRegularFiles(currentDataSource, - DataSourceInfoUtilities.getMetaFlagsContainsStatement(TskData.TSK_FS_META_FLAG_ENUM.UNALLOC) - + " AND type<>" + TskData.TSK_DB_FILES_TYPE_ENUM.SLACK.getFileType(), - "Unable to get counts of unallocated files for datasource, providing empty results"); - } - - /** - * Get count of directories in a data source. - * - * @param currentDataSource The data source. - * - * @return The count. - */ - public static Long getCountOfDirectories(DataSource currentDataSource) { - return DataSourceInfoUtilities.getCountOfTskFiles(currentDataSource, - "meta_type=" + TskData.TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_DIR.getValue() - + " AND type<>" + TskData.TSK_DB_FILES_TYPE_ENUM.VIRTUAL_DIR.getFileType(), - "Unable to get count of directories for datasource, providing empty results"); - } - - /** - * Get count of slack files in a data source. - * - * @param currentDataSource The data source. - * - * @return The count. - */ - public static Long getCountOfSlackFiles(DataSource currentDataSource) { - return DataSourceInfoUtilities.getCountOfRegularFiles(currentDataSource, - DataSourceInfoUtilities.getMetaFlagsContainsStatement(TskData.TSK_FS_META_FLAG_ENUM.UNALLOC) - + " AND type=" + TskData.TSK_DB_FILES_TYPE_ENUM.SLACK.getFileType(), - "Unable to get count of slack files for datasources, providing empty results"); - } - - /** - * Retrieves counts for each artifact type in a data source. - * - * @param selectedDataSource The data source. - * - * @return A mapping of artifact type name to the counts or null if there - * was an error executing the query. - */ - public static Map getCountsOfArtifactsByType(DataSource selectedDataSource) { - if (selectedDataSource == null) { - return Collections.emptyMap(); - } - - final String nameParam = "name"; - final String valueParam = "value"; - String query - = "SELECT bbt.display_name AS " + nameParam + ", COUNT(*) AS " + valueParam - + " FROM blackboard_artifacts bba " - + " INNER JOIN blackboard_artifact_types bbt ON bba.artifact_type_id = bbt.artifact_type_id" - + " WHERE bba.data_source_obj_id =" + selectedDataSource.getId() - + " GROUP BY bbt.display_name"; - - String errorMessage = "Unable to get artifact type counts; returning null."; - return DataSourceInfoUtilities.getBaseQueryResult(query, - getStringLongResultSetHandler(nameParam, valueParam), - errorMessage); - } - - /** - * Generates a result set handler that will return a map of string to long. - * - * @param keyParam The named parameter in the result set representing the - * key. - * @param valueParam The named parameter in the result set representing the - * value. - * - * @return The result set handler to generate the map of string to long. - */ - private static ResultSetHandler> getStringLongResultSetHandler(String keyParam, String valueParam) { - return (resultSet) -> { - LinkedHashMap toRet = new LinkedHashMap<>(); - while (resultSet.next()) { - try { - toRet.put(resultSet.getString(keyParam), resultSet.getLong(valueParam)); - } catch (SQLException ex) { - logger.log(Level.WARNING, "Failed to get a result pair from the result set.", ex); - } - } - - return toRet; - }; - } - - private DataSourceCountsSummary() { - } -} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceInfoUtilities.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceInfoUtilities.java index 4480b207e1..649c15940a 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceInfoUtilities.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceInfoUtilities.java @@ -26,19 +26,14 @@ import java.util.Date; import java.util.List; import java.util.SortedMap; import java.util.TreeMap; -import java.util.logging.Level; -import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TskCoreException; import org.apache.commons.lang.StringUtils; -import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardAttribute; -import org.sleuthkit.autopsy.datasourcesummary.datamodel.SleuthkitCaseProvider.SleuthkitCaseProviderException; import org.sleuthkit.datamodel.BlackboardAttribute.Type; -import org.sleuthkit.datamodel.TskData; import org.sleuthkit.datamodel.DataSource; +import org.sleuthkit.datamodel.TskData.TSK_DB_FILES_TYPE_ENUM; import org.sleuthkit.datamodel.TskData.TSK_FS_META_FLAG_ENUM; import org.sleuthkit.datamodel.TskData.TSK_FS_META_TYPE_ENUM; @@ -48,54 +43,73 @@ import org.sleuthkit.datamodel.TskData.TSK_FS_META_TYPE_ENUM; */ final class DataSourceInfoUtilities { - private static final Logger logger = Logger.getLogger(DataSourceInfoUtilities.class.getName()); - /** - * Gets a count of tsk_files for a particular datasource where dir_type is - * not a virtual directory and has a name. + * Gets a count of tsk_files for a particular datasource. * + * @param skCase The current SleuthkitCase. * @param currentDataSource The datasource. * @param additionalWhere Additional sql where clauses. - * @param onError The message to log on error. * * @return The count of files or null on error. + * + * @throws TskCoreException + * @throws SQLException */ - static Long getCountOfTskFiles(DataSource currentDataSource, String additionalWhere, String onError) { + static Long getCountOfTskFiles(SleuthkitCase skCase, DataSource currentDataSource, String additionalWhere) + throws TskCoreException, SQLException { if (currentDataSource != null) { - try { - SleuthkitCase skCase = Case.getCurrentCaseThrows().getSleuthkitCase(); - return skCase.countFilesWhere( - "data_source_obj_id=" + currentDataSource.getId() - + " AND dir_type<>" + TskData.TSK_FS_NAME_TYPE_ENUM.VIRT_DIR.getValue() - + " AND name<>''" - + (StringUtils.isBlank(additionalWhere) ? "" : (" AND " + additionalWhere))); - } catch (TskCoreException | NoCurrentCaseException ex) { - logger.log(Level.WARNING, onError, ex); - //unable to get count of files for the specified types cell will be displayed as empty - } + return skCase.countFilesWhere( + "data_source_obj_id=" + currentDataSource.getId() + + (StringUtils.isBlank(additionalWhere) ? "" : (" AND " + additionalWhere))); } return null; } /** - * Gets a count of regular files for a particular datasource where the - * dir_type and type are not a virtual directory and has a name. + * Gets a count of regular files for a particular datasource. * + * @param skCase The current SleuthkitCase. * @param currentDataSource The datasource. * @param additionalWhere Additional sql where clauses. - * @param onError The message to log on error. * * @return The count of files or null on error. + * + * @throws TskCoreException + * @throws SQLException */ - static Long getCountOfRegularFiles(DataSource currentDataSource, String additionalWhere, String onError) { - String whereClause = "meta_type=" + TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_REG.getValue() - + " AND type<>" + TskData.TSK_DB_FILES_TYPE_ENUM.VIRTUAL_DIR.getFileType(); + static Long getCountOfRegularFiles(SleuthkitCase skCase, DataSource currentDataSource, String additionalWhere) + throws TskCoreException, SQLException { + String whereClause = "meta_type=" + TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_REG.getValue(); if (StringUtils.isNotBlank(additionalWhere)) { whereClause += " AND " + additionalWhere; } - return getCountOfTskFiles(currentDataSource, whereClause, onError); + return getCountOfTskFiles(skCase, currentDataSource, whereClause); + } + + /** + * Gets a count of regular non-slack files for a particular datasource. + * + * @param skCase The current SleuthkitCase. + * @param currentDataSource The datasource. + * @param additionalWhere Additional sql where clauses. + * + * @return The count of files or null on error. + * + * @throws TskCoreException + * @throws SQLException + */ + static Long getCountOfRegNonSlackFiles(SleuthkitCase skCase, DataSource currentDataSource, String additionalWhere) + throws TskCoreException, SQLException { + String whereClause = "meta_type=" + TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_REG.getValue() + + " AND type<>" + TSK_DB_FILES_TYPE_ENUM.SLACK.getFileType(); + + if (StringUtils.isNotBlank(additionalWhere)) { + whereClause += " AND " + additionalWhere; + } + + return getCountOfTskFiles(skCase, currentDataSource, whereClause); } /** @@ -109,42 +123,22 @@ final class DataSourceInfoUtilities { /** * Retrieves a result based on the provided query. * - * @param query The query. - * @param processor The result set handler. - * @param errorMessage The error message to display if there is an error - * retrieving the resultset. + * @param skCase The current SleuthkitCase. + * @param query The query. + * @param processor The result set handler. * * @return The ResultSetHandler value or null if no ResultSet could be * obtained. - */ - static T getBaseQueryResult(String query, ResultSetHandler processor, String errorMessage) { - return getBaseQueryResult(SleuthkitCaseProvider.DEFAULT, query, processor, errorMessage); - } - - /** - * Retrieves a result based on the provided query. * - * @param provider The means of obtaining a SleuthkitCase. - * @param query The query. - * @param processor The result set handler. - * @param errorMessage The error message to display if there is an error - * retrieving the resultset. - * - * @return The ResultSetHandler value or null if no ResultSet could be - * obtained. + * @throws TskCoreException + * @throws SQLException */ - static T getBaseQueryResult(SleuthkitCaseProvider provider, String query, ResultSetHandler processor, String errorMessage) { - try (SleuthkitCase.CaseDbQuery dbQuery = provider.get().executeQuery(query)) { + static T getBaseQueryResult(SleuthkitCase skCase, String query, ResultSetHandler processor) + throws TskCoreException, SQLException { + try (SleuthkitCase.CaseDbQuery dbQuery = skCase.executeQuery(query)) { ResultSet resultSet = dbQuery.getResultSet(); - try { - return processor.process(resultSet); - } catch (SQLException ex) { - logger.log(Level.WARNING, errorMessage, ex); - } - } catch (TskCoreException | SleuthkitCaseProviderException ex) { - logger.log(Level.WARNING, errorMessage, ex); + return processor.process(resultSet); } - return null; } /** @@ -317,7 +311,7 @@ final class DataSourceInfoUtilities { @Override public int compare(BlackboardAttribute attribute1, BlackboardAttribute attribute2) { - if (attribute1.getAttributeType() != attribute2.getAttributeType()) { + if (!attribute1.getAttributeType().equals(attribute2.getAttributeType())) { throw new IllegalArgumentException("Unable to compare attributes of different types"); } @@ -344,7 +338,7 @@ final class DataSourceInfoUtilities { private int compare(BlackboardAttribute.Type type, BlackboardAttribute attribute1, BlackboardAttribute attribute2) { switch (type.getValueType()) { case STRING: - return attribute1.getValueString().compareTo(attribute2.getValueString()); + return attribute1.getValueString().compareToIgnoreCase(attribute2.getValueString()); case INTEGER: return Integer.compare(attribute1.getValueInt(), attribute2.getValueInt()); case LONG: diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceMimeTypeSummary.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceMimeTypeSummary.java deleted file mode 100644 index add28c0364..0000000000 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceMimeTypeSummary.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2020 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.datamodel; - -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; -import org.sleuthkit.datamodel.DataSource; - -/** - * Provides methods to query for datasource files by mime type. - */ -public class DataSourceMimeTypeSummary { - - /** - * Get the number of files in the case database for the current data source - * which have the specified mimetypes. - * - * @param currentDataSource the data source which we are finding a file - * count - * - * @param setOfMimeTypes the set of mime types which we are finding the - * number of occurences of - * - * @return a Long value which represents the number of occurrences of the - * specified mime types in the current case for the specified data - * source, null if no count was retrieved - */ - public static Long getCountOfFilesForMimeTypes(DataSource currentDataSource, Set setOfMimeTypes) { - return DataSourceInfoUtilities.getCountOfRegularFiles(currentDataSource, - "mime_type IN " + getSqlSet(setOfMimeTypes), - "Unable to get count of files for specified mime types"); - } - - /** - * Get the number of files in the case database for the current data source - * which do not have the specified mimetypes. - * - * @param currentDataSource the data source which we are finding a file - * count - * - * @param setOfMimeTypes the set of mime types that should be excluded. - * - * @return a Long value which represents the number of files that do not - * have the specific mime type, but do have a mime type. - */ - public static Long getCountOfFilesNotInMimeTypes(DataSource currentDataSource, Set setOfMimeTypes) { - return DataSourceInfoUtilities.getCountOfRegularFiles(currentDataSource, - "mime_type NOT IN " + getSqlSet(setOfMimeTypes) - + " AND mime_type IS NOT NULL AND mime_type <> '' ", - "Unable to get count of files without specified mime types"); - } - - /** - * Gets the number of files in the data source with no assigned mime type. - * - * @param currentDataSource The data source. - * - * @return The number of files with no mime type or null if there is an - * issue searching the data source. - * - */ - public static Long getCountOfFilesWithNoMimeType(DataSource currentDataSource) { - return DataSourceInfoUtilities.getCountOfRegularFiles(currentDataSource, - "(mime_type IS NULL OR mime_type = '') ", - "Unable to get count of files without a mime type"); - } - - /** - * Derives a sql set string (i.e. "('val1', 'val2', 'val3')"). A naive - * attempt is made to sanitize the strings by removing single quotes from - * values. - * - * @param setValues The values that should be present in the set. Single - * quotes are removed. - * - * @return The sql set string. - */ - private static String getSqlSet(Set setValues) { - List quotedValues = setValues - .stream() - .map(str -> String.format("'%s'", str.replace("'", ""))) - .collect(Collectors.toList()); - - String commaSeparatedQuoted = String.join(", ", quotedValues); - return String.format("(%s) ", commaSeparatedQuoted); - } - - private DataSourceMimeTypeSummary() { - } -} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/IngestModuleCheckUtil.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/IngestModuleCheckUtil.java new file mode 100644 index 0000000000..3477dd5ff2 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/IngestModuleCheckUtil.java @@ -0,0 +1,145 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.datamodel; + +import java.util.Map; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.SleuthkitCaseProvider.SleuthkitCaseProviderException; +import org.sleuthkit.datamodel.DataSource; +import org.sleuthkit.datamodel.IngestJobInfo; +import org.sleuthkit.datamodel.IngestModuleInfo; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Utilities for checking if an ingest module has been run on a datasource. + */ +@Messages({ + "IngestModuleCheckUtil_recentActivityModuleName=Recent Activity", + +}) +public class IngestModuleCheckUtil { + public static final String RECENT_ACTIVITY_FACTORY = "org.sleuthkit.autopsy.recentactivity.RecentActivityExtracterModuleFactory"; + public static final String RECENT_ACTIVITY_MODULE_NAME = Bundle.IngestModuleCheckUtil_recentActivityModuleName(); + + // IngestModuleInfo separator for unique_name + private static final String UNIQUE_NAME_SEPARATOR = "-"; + + private final SleuthkitCaseProvider caseProvider; + + /** + * Main constructor. + */ + public IngestModuleCheckUtil() { + this(SleuthkitCaseProvider.DEFAULT); + + } + + /** + * Main constructor with external dependencies specified. This constructor + * is designed with unit testing in mind since mocked dependencies can be + * utilized. + * + * @param provider The object providing the current SleuthkitCase. + */ + public IngestModuleCheckUtil(SleuthkitCaseProvider provider) { + + this.caseProvider = provider; + } + + + /** + * Gets the fully qualified factory from the IngestModuleInfo. + * @param info The IngestJobInfo. + * @return The fully qualified factory. + */ + private static String getFullyQualifiedFactory(IngestModuleInfo info) { + if (info == null) { + return null; + } + + String qualifiedName = info.getUniqueName(); + if (StringUtils.isBlank(qualifiedName)) { + return null; + } + + return qualifiedName.split(UNIQUE_NAME_SEPARATOR)[0]; + } + + + /** + * Whether or not the ingest job info contains the ingest modulename. + * @param info The IngestJobInfo. + * @param fullyQualifiedFactory The fully qualified classname of the relevant factory. + * @return True if the ingest module name is contained in the data. + */ + private static boolean hasIngestModule(IngestJobInfo info, String fullyQualifiedFactory) { + if (info == null || info.getIngestModuleInfo() == null || StringUtils.isBlank(fullyQualifiedFactory)) { + return false; + } + + return info.getIngestModuleInfo().stream() + .anyMatch((moduleInfo) -> { + String thisQualifiedFactory = getFullyQualifiedFactory(moduleInfo); + return fullyQualifiedFactory.equalsIgnoreCase(thisQualifiedFactory); + }); + } + + /** + * Whether or not a data source has been ingested with a particular ingest module. + * @param dataSource The datasource. + * @param fullyQualifiedFactory The fully qualified classname of the relevant factory. + * @return Whether or not a data source has been ingested with a particular ingest module. + * @throws TskCoreException + * @throws SleuthkitCaseProviderException + */ + public boolean isModuleIngested(DataSource dataSource, String fullyQualifiedFactory) + throws TskCoreException, SleuthkitCaseProviderException { + if (dataSource == null) { + return false; + } + + long dataSourceId = dataSource.getId(); + + return caseProvider.get().getIngestJobs().stream() + .anyMatch((ingestJob) -> { + return ingestJob != null + && ingestJob.getObjectId() == dataSourceId + && hasIngestModule(ingestJob, fullyQualifiedFactory); + }); + + } + + /** + * Get a mapping of fully qualified factory name to display name. + * @param skCase The SleuthkitCase. + * @return The mapping of fully qualified factory name to display name. + * @throws TskCoreException + */ + public static Map getFactoryDisplayNames(SleuthkitCase skCase) throws TskCoreException { + return skCase.getIngestJobs().stream() + .flatMap(ingestJob -> ingestJob.getIngestModuleInfo().stream()) + .collect(Collectors.toMap( + (moduleInfo) -> getFullyQualifiedFactory(moduleInfo), + (moduleInfo) -> moduleInfo.getDisplayName(), + (a,b) -> a)); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/MimeTypeSummary.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/MimeTypeSummary.java new file mode 100644 index 0000000000..e753a44a76 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/MimeTypeSummary.java @@ -0,0 +1,194 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.datamodel; + +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DefaultUpdateGovernor; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.SleuthkitCaseProvider.SleuthkitCaseProviderException; +import org.sleuthkit.autopsy.ingest.IngestManager; +import org.sleuthkit.autopsy.ingest.ModuleContentEvent; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.DataSource; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Provides methods to query for datasource files by mime type. + */ +public class MimeTypeSummary implements DefaultUpdateGovernor { + + private final SleuthkitCaseProvider provider; + + private static final Set INGEST_JOB_EVENTS = new HashSet<>( + Arrays.asList(IngestManager.IngestJobEvent.COMPLETED, IngestManager.IngestJobEvent.CANCELLED)); + + /** + * Main constructor. + */ + public MimeTypeSummary() { + this(SleuthkitCaseProvider.DEFAULT); + } + + /** + * Main constructor. + * + * @param provider The means of obtaining a sleuthkit case. + */ + public MimeTypeSummary(SleuthkitCaseProvider provider) { + this.provider = provider; + } + + @Override + public boolean isRefreshRequired(ModuleContentEvent evt) { + return true; + } + + @Override + public boolean isRefreshRequired(AbstractFile file) { + return true; + } + + @Override + public boolean isRefreshRequired(IngestManager.IngestJobEvent evt) { + return (evt != null && INGEST_JOB_EVENTS.contains(evt)); + } + + @Override + public Set getIngestJobEventUpdates() { + return INGEST_JOB_EVENTS; + } + + /** + * Get the number of files in the case database for the current data source + * which have the specified mimetypes. + * + * @param currentDataSource the data source which we are finding a file + * count + * + * @param setOfMimeTypes the set of mime types which we are finding the + * number of occurences of + * + * @return a Long value which represents the number of occurrences of the + * specified mime types in the current case for the specified data + * source, null if no count was retrieved + * + * @throws SleuthkitCaseProviderException + * @throws TskCoreException + * @throws SQLException + */ + public Long getCountOfFilesForMimeTypes(DataSource currentDataSource, Set setOfMimeTypes) + throws SleuthkitCaseProvider.SleuthkitCaseProviderException, TskCoreException, SQLException { + + return DataSourceInfoUtilities.getCountOfRegNonSlackFiles( + provider.get(), + currentDataSource, + "mime_type IN " + getSqlSet(setOfMimeTypes) + ); + } + + /** + * Get the number of files in the case database for the current data source + * which do not have the specified mimetypes. + * + * @param currentDataSource the data source which we are finding a file + * count + * + * @param setOfMimeTypes the set of mime types that should be excluded. + * + * @return a Long value which represents the number of files that do not + * have the specific mime type, but do have a mime type. + * + * @throws SleuthkitCaseProviderException + * @throws TskCoreException + * @throws SQLException + */ + public Long getCountOfFilesNotInMimeTypes(DataSource currentDataSource, Set setOfMimeTypes) + throws SleuthkitCaseProvider.SleuthkitCaseProviderException, TskCoreException, SQLException { + + return DataSourceInfoUtilities.getCountOfRegNonSlackFiles( + provider.get(), + currentDataSource, + "mime_type NOT IN " + getSqlSet(setOfMimeTypes) + + " AND mime_type IS NOT NULL AND mime_type <> '' " + ); + } + + /** + * Get a count of all regular files in a datasource. + * + * @param dataSource The datasource. + * + * @return The count of regular files. + * + * @throws SleuthkitCaseProviderException + * @throws TskCoreException + * @throws SQLException + */ + public Long getCountOfAllRegularFiles(DataSource dataSource) + throws SleuthkitCaseProvider.SleuthkitCaseProviderException, TskCoreException, SQLException { + + return DataSourceInfoUtilities.getCountOfRegNonSlackFiles(provider.get(), dataSource, null); + } + + /** + * Gets the number of files in the data source with no assigned mime type. + * + * @param currentDataSource The data source. + * + * @return The number of files with no mime type or null if there is an + * issue searching the data source. + * + * @throws SleuthkitCaseProviderException + * @throws TskCoreException + * @throws SQLException + */ + public Long getCountOfFilesWithNoMimeType(DataSource currentDataSource) + throws SleuthkitCaseProvider.SleuthkitCaseProviderException, TskCoreException, SQLException { + + return DataSourceInfoUtilities.getCountOfRegNonSlackFiles( + provider.get(), + currentDataSource, + "(mime_type IS NULL OR mime_type = '') " + ); + } + + /** + * Derives a sql set string (i.e. "('val1', 'val2', 'val3')"). A naive + * attempt is made to sanitize the strings by removing single quotes from + * values. + * + * @param setValues The values that should be present in the set. Single + * quotes are removed. + * + * @return The sql set string. + */ + private String getSqlSet(Set setValues) { + List quotedValues = setValues + .stream() + .map(str -> String.format("'%s'", str.replace("'", ""))) + .collect(Collectors.toList()); + + String commaSeparatedQuoted = String.join(", ", quotedValues); + return String.format("(%s) ", commaSeparatedQuoted); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/PastCasesSummary.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/PastCasesSummary.java new file mode 100644 index 0000000000..ea6c089fca --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/PastCasesSummary.java @@ -0,0 +1,332 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourcesummary.datamodel; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +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.apache.commons.lang3.tuple.Pair; +import org.sleuthkit.autopsy.centralrepository.ingestmodule.CentralRepoIngestModuleFactory; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.SleuthkitCaseProvider.SleuthkitCaseProviderException; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DefaultArtifactUpdateGovernor; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE; +import org.sleuthkit.datamodel.DataSource; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Provides information about how a datasource relates to a previous case. NOTE: + * This code is fragile and has certain expectations about how the central + * repository handles creating artifacts. So, if the central repository changes + * ingest process, this code could break. This code expects that the central + * repository ingest module: + * + * a) Creates a TSK_INTERESTING_FILE_HIT artifact for a file whose hash is in + * the central repository as a notable file. + * + * b) Creates a TSK_INTERESTING_ARTIFACT_HIT artifact for a matching id in the + * central repository. + * + * c) The created artifact will have a TSK_COMMENT attribute attached where one + * of the sources for the attribute matches + * CentralRepoIngestModuleFactory.getModuleName(). The module display name at + * time of ingest will match CentralRepoIngestModuleFactory.getModuleName() as + * well. + * + * d) The content of that TSK_COMMENT attribute will be of the form "Previous + * Case: case1,case2...caseN" + */ +public class PastCasesSummary implements DefaultArtifactUpdateGovernor { + + /** + * Return type for results items in the past cases tab. + */ + public static class PastCasesResult { + + private final List> sameIdsResults; + private final List> taggedNotable; + + /** + * Main constructor. + * + * @param sameIdsResults Data for the cases with same id table. + * @param taggedNotable Data for the tagged notable table. + */ + public PastCasesResult(List> sameIdsResults, List> taggedNotable) { + this.sameIdsResults = sameIdsResults; + this.taggedNotable = taggedNotable; + } + + /** + * @return Data for the cases with same id table. + */ + public List> getSameIdsResults() { + return sameIdsResults; + } + + /** + * @return Data for the tagged notable table. + */ + public List> getTaggedNotable() { + return taggedNotable; + } + } + + private static final Set ARTIFACT_UPDATE_TYPE_IDS = new HashSet<>(Arrays.asList( + ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT.getTypeID(), + ARTIFACT_TYPE.TSK_INTERESTING_ARTIFACT_HIT.getTypeID() + )); + + private static final String CENTRAL_REPO_INGEST_NAME = CentralRepoIngestModuleFactory.getModuleName().toUpperCase().trim(); + private static final BlackboardAttribute.Type TYPE_COMMENT = new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_COMMENT); + private static final BlackboardAttribute.Type TYPE_ASSOCIATED_ARTIFACT = new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT); + + private static final Set CR_DEVICE_TYPE_IDS = new HashSet<>(Arrays.asList( + ARTIFACT_TYPE.TSK_DEVICE_ATTACHED.getTypeID(), + ARTIFACT_TYPE.TSK_DEVICE_INFO.getTypeID(), + ARTIFACT_TYPE.TSK_SIM_ATTACHED.getTypeID(), + ARTIFACT_TYPE.TSK_WIFI_NETWORK_ADAPTER.getTypeID() + )); + + private static final String CASE_SEPARATOR = ","; + private static final String PREFIX_END = ":"; + + private final SleuthkitCaseProvider caseProvider; + private final java.util.logging.Logger logger; + + /** + * Main constructor. + */ + public PastCasesSummary() { + this( + SleuthkitCaseProvider.DEFAULT, + org.sleuthkit.autopsy.coreutils.Logger.getLogger(PastCasesSummary.class.getName()) + ); + + } + + /** + * Main constructor with external dependencies specified. This constructor + * is designed with unit testing in mind since mocked dependencies can be + * utilized. + * + * @param provider The object providing the current SleuthkitCase. + * @param logger The logger to use. + */ + public PastCasesSummary( + SleuthkitCaseProvider provider, + java.util.logging.Logger logger) { + + this.caseProvider = provider; + this.logger = logger; + } + + @Override + public Set getArtifactTypeIdsForRefresh() { + return ARTIFACT_UPDATE_TYPE_IDS; + } + + /** + * Given the provided sources for an attribute, aims to determine if one of + * those sources is the Central Repository Ingest Module. + * + * @param sources The list of sources found on an attribute. + * + * @return Whether or not this attribute (and subsequently the parent + * artifact) is created by the Central Repository Ingest Module. + */ + private static boolean isCentralRepoGenerated(List sources) { + if (sources == null) { + return false; + } + + return sources.stream().anyMatch((str) -> { + return str != null && CENTRAL_REPO_INGEST_NAME.equalsIgnoreCase(str.trim()); + }); + } + + /** + * Gets a list of cases from the TSK_COMMENT of an artifact. The cases + * string is expected to be of a form of "Previous Case: + * case1,case2...caseN". + * + * @param artifact The artifact. + * + * @return The list of cases if found or empty list if not. + */ + private static List getCasesFromArtifact(BlackboardArtifact artifact) { + if (artifact == null) { + return Collections.emptyList(); + } + + BlackboardAttribute commentAttr = null; + try { + commentAttr = artifact.getAttribute(TYPE_COMMENT); + } catch (TskCoreException ignored) { + // ignore if no attribute can be found + } + + if (commentAttr == null) { + return Collections.emptyList(); + } + + if (!isCentralRepoGenerated(commentAttr.getSources())) { + return Collections.emptyList(); + } + + String commentStr = commentAttr.getValueString(); + + int prefixCharIdx = commentStr.indexOf(PREFIX_END); + if (prefixCharIdx < 0 || prefixCharIdx >= commentStr.length() - 1) { + return Collections.emptyList(); + } + + String justCasesStr = commentStr.substring(prefixCharIdx + 1).trim(); + return Stream.of(justCasesStr.split(CASE_SEPARATOR)) + .map(String::trim) + .collect(Collectors.toList()); + + } + + /** + * Given a stream of case ids, groups the strings in a case-insensitive + * manner, and then provides a list of cases and the occurrence count sorted + * from max to min. + * + * @param cases A stream of cases. + * + * @return The list of unique cases and their occurrences sorted from max to + * min. + */ + private List> getCaseCounts(Stream cases) { + Collection> groupedCases = cases + // group by case insensitive compare of cases + .collect(Collectors.groupingBy((caseStr) -> caseStr.toUpperCase().trim())) + .values(); + + return groupedCases + .stream() + // get any cases where an actual case is found + .filter((lst) -> lst != null && lst.size() > 0) + // get non-normalized (i.e. not all caps) case name and number of items found + .map((lst) -> Pair.of(lst.get(0), (long) lst.size())) + // sorted descending + .sorted((a, b) -> -Long.compare(a.getValue(), b.getValue())) + .collect(Collectors.toList()); + } + + /** + * Given an artifact with a TYPE_ASSOCIATED_ARTIFACT attribute, retrieves + * the related artifact. + * + * @param artifact The artifact with the TYPE_ASSOCIATED_ARTIFACT attribute. + * + * @return The artifact if found or null if not. + * + * @throws SleuthkitCaseProviderException + */ + private BlackboardArtifact getParentArtifact(BlackboardArtifact artifact) throws SleuthkitCaseProviderException { + Long parentId = DataSourceInfoUtilities.getLongOrNull(artifact, TYPE_ASSOCIATED_ARTIFACT); + if (parentId == null) { + return null; + } + + SleuthkitCase skCase = caseProvider.get(); + try { + return skCase.getArtifactByArtifactId(parentId); + } catch (TskCoreException ex) { + logger.log(Level.WARNING, + String.format("There was an error fetching the parent artifact of a TSK_INTERESTING_ARTIFACT_HIT (parent id: %d)", parentId), + ex); + return null; + } + } + + /** + * Returns true if the artifact has an associated artifact of a device type. + * + * @param artifact The artifact. + * + * @return True if there is a device associated artifact. + * + * @throws SleuthkitCaseProviderException + */ + private boolean hasDeviceAssociatedArtifact(BlackboardArtifact artifact) throws SleuthkitCaseProviderException { + BlackboardArtifact parent = getParentArtifact(artifact); + if (parent == null) { + return false; + } + + return CR_DEVICE_TYPE_IDS.contains(parent.getArtifactTypeID()); + } + + /** + * Returns the past cases data to be shown in the past cases tab. + * + * @param dataSource The data source. + * + * @return The retrieved data or null if null dataSource. + * + * @throws SleuthkitCaseProviderException + * @throws TskCoreException + */ + public PastCasesResult getPastCasesData(DataSource dataSource) + throws SleuthkitCaseProvider.SleuthkitCaseProviderException, TskCoreException { + + if (dataSource == null) { + return null; + } + + SleuthkitCase skCase = caseProvider.get(); + + List deviceArtifactCases = new ArrayList<>(); + List nonDeviceArtifactCases = new ArrayList<>(); + + for (BlackboardArtifact artifact : skCase.getBlackboard().getArtifacts(ARTIFACT_TYPE.TSK_INTERESTING_ARTIFACT_HIT.getTypeID(), dataSource.getId())) { + List cases = getCasesFromArtifact(artifact); + if (cases == null || cases.isEmpty()) { + continue; + } + + if (hasDeviceAssociatedArtifact(artifact)) { + deviceArtifactCases.addAll(cases); + } else { + nonDeviceArtifactCases.addAll(cases); + } + } + + Stream filesCases = skCase.getBlackboard().getArtifacts(ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT.getTypeID(), dataSource.getId()).stream() + .flatMap((art) -> getCasesFromArtifact(art).stream()); + + return new PastCasesResult( + getCaseCounts(deviceArtifactCases.stream()), + getCaseCounts(Stream.concat(filesCases, nonDeviceArtifactCases.stream())) + ); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/RecentFilesSummary.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/RecentFilesSummary.java index 42c15d3ff7..0a47e0ea6c 100755 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/RecentFilesSummary.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/RecentFilesSummary.java @@ -18,32 +18,34 @@ */ package org.sleuthkit.autopsy.datasourcesummary.datamodel; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DefaultArtifactUpdateGovernor; import java.nio.file.Paths; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Objects; +import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; -import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_EMAIL_MSG; -import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_MESSAGE; -import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_RECENT_OBJECT; -import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_DOWNLOAD; import org.sleuthkit.datamodel.BlackboardAttribute; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.DataSource; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.autopsy.datasourcesummary.datamodel.SleuthkitCaseProvider.SleuthkitCaseProviderException; +import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE; /** * Helper class for getting data for the Recent Files Data Summary tab. */ -public class RecentFilesSummary { +public class RecentFilesSummary implements DefaultArtifactUpdateGovernor { private final static BlackboardAttribute.Type DATETIME_ACCESSED_ATT = new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_ACCESSED); private final static BlackboardAttribute.Type DOMAIN_ATT = new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN); @@ -52,10 +54,18 @@ public class RecentFilesSummary { private final static BlackboardAttribute.Type ASSOCATED_ATT = new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT); private final static BlackboardAttribute.Type EMAIL_FROM_ATT = new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_EMAIL_FROM); private final static BlackboardAttribute.Type MSG_DATEIME_SENT_ATT = new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_SENT); - private final static BlackboardArtifact.Type ASSOCATED_OBJ_ART = new BlackboardArtifact.Type(BlackboardArtifact.ARTIFACT_TYPE.TSK_ASSOCIATED_OBJECT); + private final static BlackboardArtifact.Type ASSOCATED_OBJ_ART = new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_ASSOCIATED_OBJECT); private static final DateFormat DATETIME_FORMAT = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.getDefault()); + private static final Set ARTIFACT_UPDATE_TYPE_IDS = new HashSet<>(Arrays.asList( + ARTIFACT_TYPE.TSK_RECENT_OBJECT.getTypeID(), + ARTIFACT_TYPE.TSK_WEB_DOWNLOAD.getTypeID(), + ARTIFACT_TYPE.TSK_ASSOCIATED_OBJECT.getTypeID(), + ARTIFACT_TYPE.TSK_EMAIL_MSG.getTypeID(), + ARTIFACT_TYPE.TSK_MESSAGE.getTypeID() + )); + private final SleuthkitCaseProvider provider; /** @@ -78,6 +88,11 @@ public class RecentFilesSummary { this.provider = provider; } + @Override + public Set getArtifactTypeIdsForRefresh() { + return ARTIFACT_UPDATE_TYPE_IDS; + } + /** * Return a list of the most recently opened documents based on the * TSK_RECENT_OBJECT artifact. @@ -94,12 +109,12 @@ public class RecentFilesSummary { */ public List getRecentlyOpenedDocuments(DataSource dataSource, int maxCount) throws SleuthkitCaseProviderException, TskCoreException { if (dataSource == null) { - throw new IllegalArgumentException("Failed to get recently opened documents given data source was null"); + return Collections.emptyList(); } List artifactList = DataSourceInfoUtilities.getArtifacts(provider.get(), - new BlackboardArtifact.Type(TSK_RECENT_OBJECT), + new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_RECENT_OBJECT), dataSource, DATETIME_ATT, DataSourceInfoUtilities.SortOrder.DESCENDING, @@ -145,9 +160,13 @@ public class RecentFilesSummary { * @throws SleuthkitCaseProviderException */ public List getRecentDownloads(DataSource dataSource, int maxCount) throws TskCoreException, SleuthkitCaseProviderException { + if (dataSource == null) { + return Collections.emptyList(); + } + List artifactList = DataSourceInfoUtilities.getArtifacts(provider.get(), - new BlackboardArtifact.Type(TSK_WEB_DOWNLOAD), + new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_WEB_DOWNLOAD), dataSource, DATETIME_ACCESSED_ATT, DataSourceInfoUtilities.SortOrder.DESCENDING, @@ -192,6 +211,10 @@ public class RecentFilesSummary { * @throws TskCoreException */ public List getRecentAttachments(DataSource dataSource, int maxCount) throws SleuthkitCaseProviderException, TskCoreException { + if (dataSource == null) { + return Collections.emptyList(); + } + return createListFromMap(buildAttachmentMap(dataSource), maxCount); } @@ -247,7 +270,7 @@ public class RecentFilesSummary { sortedMap.put(date, list); } RecentAttachmentDetails details = new RecentAttachmentDetails(path, date, sender); - if(!list.contains(details)) { + if (!list.contains(details)) { list.add(details); } } @@ -302,8 +325,8 @@ public class RecentFilesSummary { */ private boolean isMessageArtifact(BlackboardArtifact nodeArtifact) { final int artifactTypeID = nodeArtifact.getArtifactTypeID(); - return artifactTypeID == TSK_EMAIL_MSG.getTypeID() - || artifactTypeID == TSK_MESSAGE.getTypeID(); + return artifactTypeID == ARTIFACT_TYPE.TSK_EMAIL_MSG.getTypeID() + || artifactTypeID == ARTIFACT_TYPE.TSK_MESSAGE.getTypeID(); } /** @@ -334,10 +357,10 @@ public class RecentFilesSummary { public String getDateAsString() { return DATETIME_FORMAT.format(date * 1000); } - + /** * Returns the date as the seconds from java epoch. - * + * * @return Seconds from java epoch. */ Long getDateAsLong() { @@ -415,17 +438,17 @@ public class RecentFilesSummary { public String getSender() { return sender; } - + @Override public boolean equals(Object obj) { - if(!(obj instanceof RecentAttachmentDetails)) { + if (!(obj instanceof RecentAttachmentDetails)) { return false; } - RecentAttachmentDetails compareObj = (RecentAttachmentDetails)obj; - - return compareObj.getSender().equals(this.sender) && - compareObj.getPath().equals(this.getPath()) && - compareObj.getDateAsLong().equals(this.getDateAsLong()); + RecentAttachmentDetails compareObj = (RecentAttachmentDetails) obj; + + return compareObj.getSender().equals(this.sender) + && compareObj.getPath().equals(this.getPath()) + && compareObj.getDateAsLong().equals(this.getDateAsLong()); } @Override diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/TopDomainsResult.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/TopDomainsResult.java deleted file mode 100644 index e843e51c4c..0000000000 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/TopDomainsResult.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2020 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.datamodel; - -import java.util.Date; - -/** - * Describes a result of a program run on a datasource. - */ -public class TopDomainsResult { - - private final String domain; - private final String url; - private final Long visitTimes; - private final Date lastVisit; - - /** - * Describes a top domain result. - * - * @param domain The domain. - * @param url The url. - * @param visitTimes The number of times it was visited. - * @param lastVisit The date of the last visit. - */ - public TopDomainsResult(String domain, String url, Long visitTimes, Date lastVisit) { - this.domain = domain; - this.url = url; - this.visitTimes = visitTimes; - this.lastVisit = lastVisit; - } - - /** - * @return The domain for the result. - */ - public String getDomain() { - return domain; - } - - /** - * @return The url for the result. - */ - public String getUrl() { - return url; - } - - /** - * @return The number of times this site is visited. - */ - public Long getVisitTimes() { - return visitTimes; - } - - /** - * @return The date of the last visit. - */ - public Date getLastVisit() { - return lastVisit; - } - -} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/TopProgramsResult.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/TopProgramsResult.java deleted file mode 100644 index 7f682c3efb..0000000000 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/TopProgramsResult.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2020 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.datamodel; - -import java.util.Date; - -/** - * Describes a result of a program run on a datasource. - */ -public class TopProgramsResult { - - private final String programName; - private final String programPath; - private final Long runTimes; - private final Date lastRun; - - /** - * Main constructor. - * - * @param programName The name of the program. - * @param programPath The path of the program. - * @param runTimes The number of runs. - */ - TopProgramsResult(String programName, String programPath, Long runTimes, Date lastRun) { - this.programName = programName; - this.programPath = programPath; - this.runTimes = runTimes; - this.lastRun = lastRun; - } - - /** - * @return The name of the program - */ - public String getProgramName() { - return programName; - } - - /** - * @return The path of the program. - */ - public String getProgramPath() { - return programPath; - } - - /** - * @return The number of run times or null if not present. - */ - public Long getRunTimes() { - return runTimes; - } - - /** - * @return The last time the program was run or null if not present. - */ - public Date getLastRun() { - return lastRun; - } -} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceTopProgramsSummary.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/TopProgramsSummary.java similarity index 74% rename from Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceTopProgramsSummary.java rename to Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/TopProgramsSummary.java index 495f706f0b..e984b9efca 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceTopProgramsSummary.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/TopProgramsSummary.java @@ -18,29 +18,36 @@ */ package org.sleuthkit.autopsy.datasourcesummary.datamodel; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DefaultArtifactUpdateGovernor; import java.io.File; +import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.function.Function; -import java.util.logging.Level; import java.util.stream.Collectors; import org.apache.commons.lang.StringUtils; -import org.sleuthkit.autopsy.coreutils.Logger; -import static org.sleuthkit.autopsy.datasourcesummary.datamodel.DataSourceInfoUtilities.getBaseQueryResult; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.SleuthkitCaseProvider.SleuthkitCaseProviderException; import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE; import org.sleuthkit.datamodel.BlackboardAttribute; import org.sleuthkit.datamodel.DataSource; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; /** * Provides information to populate Top Programs Summary queries. */ -public class DataSourceTopProgramsSummary { +public class TopProgramsSummary implements DefaultArtifactUpdateGovernor { - private static final Logger logger = Logger.getLogger(DataSourceTopProgramsSummary.class.getName()); + private static final Set ARTIFACT_UPDATE_TYPE_IDS = new HashSet<>(Arrays.asList( + ARTIFACT_TYPE.TSK_PROG_RUN.getTypeID() + )); /** * A SQL join type. @@ -95,7 +102,7 @@ public class DataSourceTopProgramsSummary { return null; } ); - + /** * Creates a sql statement querying the blackboard attributes table for a * particular attribute type and returning a specified value. That query @@ -168,18 +175,22 @@ public class DataSourceTopProgramsSummary { private static String getLikeClause(String column, String likeString, boolean isLike) { return column + (isLike ? "" : " NOT") + " LIKE '" + likeString + "'"; } - - + private final SleuthkitCaseProvider provider; - - public DataSourceTopProgramsSummary() { + + public TopProgramsSummary() { this(SleuthkitCaseProvider.DEFAULT); } - - public DataSourceTopProgramsSummary(SleuthkitCaseProvider provider) { + + public TopProgramsSummary(SleuthkitCaseProvider provider) { this.provider = provider; } + @Override + public Set getArtifactTypeIdsForRefresh() { + return ARTIFACT_UPDATE_TYPE_IDS; + } + /** * Retrieves a list of the top programs used on the data source. Currently * determines this based off of which prefetch results return the highest @@ -188,9 +199,14 @@ public class DataSourceTopProgramsSummary { * @param dataSource The data source. * @param count The number of programs to return. * - * @return + * @return The top results objects found. + * + * @throws SleuthkitCaseProviderException + * @throws TskCoreException + * @throws SQLException */ - public List getTopPrograms(DataSource dataSource, int count) { + public List getTopPrograms(DataSource dataSource, int count) + throws SleuthkitCaseProviderException, TskCoreException, SQLException { if (dataSource == null || count <= 0) { return Collections.emptyList(); } @@ -231,49 +247,46 @@ public class DataSourceTopProgramsSummary { + " MAX(" + getFullKey(lastRunParam) + ") DESC,\n" + " " + getFullKey(nameParam) + " ASC"; - final String errorMessage = "Unable to get top program results; returning null."; - DataSourceInfoUtilities.ResultSetHandler> handler = (resultSet) -> { List progResults = new ArrayList<>(); boolean quitAtCount = false; while (resultSet.next() && (!quitAtCount || progResults.size() < count)) { - try { - long lastRunEpoch = resultSet.getLong(lastRunParam); - Date lastRun = (resultSet.wasNull()) ? null : new Date(lastRunEpoch * 1000); + long lastRunEpoch = resultSet.getLong(lastRunParam); + Date lastRun = (resultSet.wasNull()) ? null : new Date(lastRunEpoch * 1000); - Long runCount = resultSet.getLong(runCountParam); - if (resultSet.wasNull()) { - runCount = null; - } - - if (lastRun != null || runCount != null) { - quitAtCount = true; - } - - progResults.add(new TopProgramsResult( - resultSet.getString(nameParam), - resultSet.getString(pathParam), - runCount, - lastRun)); - - } catch (SQLException ex) { - logger.log(Level.WARNING, "Failed to get a top program result from the result set.", ex); + Long runCount = resultSet.getLong(runCountParam); + if (resultSet.wasNull()) { + runCount = null; } + + if (lastRun != null || runCount != null) { + quitAtCount = true; + } + + progResults.add(new TopProgramsResult( + resultSet.getString(nameParam), + resultSet.getString(pathParam), + runCount, + lastRun)); } return progResults; }; - return getBaseQueryResult(provider, query, handler, errorMessage); + try (SleuthkitCase.CaseDbQuery dbQuery = provider.get().executeQuery(query); + ResultSet resultSet = dbQuery.getResultSet()) { + + return handler.process(resultSet); + } } - /** * Determines a short folder name if any. Otherwise, returns empty string. * - * @param strPath The string path. + * @param strPath The string path. + * @param applicationName The application name. * * @return The short folder name or empty string if not found. */ @@ -301,4 +314,57 @@ public class DataSourceTopProgramsSummary { return ""; } + + /** + * Describes a result of a program run on a datasource. + */ + public static class TopProgramsResult { + + private final String programName; + private final String programPath; + private final Long runTimes; + private final Date lastRun; + + /** + * Main constructor. + * + * @param programName The name of the program. + * @param programPath The path of the program. + * @param runTimes The number of runs. + */ + TopProgramsResult(String programName, String programPath, Long runTimes, Date lastRun) { + this.programName = programName; + this.programPath = programPath; + this.runTimes = runTimes; + this.lastRun = lastRun; + } + + /** + * @return The name of the program + */ + public String getProgramName() { + return programName; + } + + /** + * @return The path of the program. + */ + public String getProgramPath() { + return programPath; + } + + /** + * @return The number of run times or null if not present. + */ + public Long getRunTimes() { + return runTimes; + } + + /** + * @return The last time the program was run or null if not present. + */ + public Date getLastRun() { + return lastRun; + } + } } diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/TypesSummary.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/TypesSummary.java new file mode 100644 index 0000000000..ff4bcae0a0 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/TypesSummary.java @@ -0,0 +1,172 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 - 2020 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.datamodel; + +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DefaultUpdateGovernor; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.SleuthkitCaseProvider.SleuthkitCaseProviderException; +import org.sleuthkit.autopsy.ingest.IngestManager; +import org.sleuthkit.autopsy.ingest.ModuleContentEvent; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.DataSource; +import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.TskData; + +/** + * Provides information for the DataSourceSummaryCountsPanel. + */ +public class TypesSummary implements DefaultUpdateGovernor { + + private static final Set INGEST_JOB_EVENTS = new HashSet<>( + Arrays.asList(IngestManager.IngestJobEvent.COMPLETED, IngestManager.IngestJobEvent.CANCELLED)); + + private final SleuthkitCaseProvider provider; + + /** + * Main constructor. + */ + public TypesSummary() { + this(SleuthkitCaseProvider.DEFAULT); + } + + /** + * Main constructor. + * + * @param provider The means of obtaining a sleuthkit case. + */ + public TypesSummary(SleuthkitCaseProvider provider) { + this.provider = provider; + } + + @Override + public boolean isRefreshRequired(ModuleContentEvent evt) { + return true; + } + + @Override + public boolean isRefreshRequired(AbstractFile file) { + return true; + } + + @Override + public boolean isRefreshRequired(IngestManager.IngestJobEvent evt) { + return (evt != null && INGEST_JOB_EVENTS.contains(evt)); + } + + @Override + public Set getIngestJobEventUpdates() { + return INGEST_JOB_EVENTS; + } + + /** + * Get count of regular files (not directories) in a data source. + * + * @param currentDataSource The data source. + * + * @return The count. + * + * @throws SleuthkitCaseProviderException + * @throws TskCoreException + * @throws SQLException + */ + public Long getCountOfFiles(DataSource currentDataSource) + throws SleuthkitCaseProvider.SleuthkitCaseProviderException, TskCoreException, SQLException { + return DataSourceInfoUtilities.getCountOfRegularFiles( + provider.get(), + currentDataSource, + null + ); + } + + /** + * Get count of allocated files in a data source. + * + * @param currentDataSource The data source. + * + * @return The count. + * + * @throws SleuthkitCaseProviderException + * @throws TskCoreException + * @throws SQLException + */ + public Long getCountOfAllocatedFiles(DataSource currentDataSource) + throws SleuthkitCaseProvider.SleuthkitCaseProviderException, TskCoreException, SQLException { + + return DataSourceInfoUtilities.getCountOfRegNonSlackFiles(provider.get(), currentDataSource, + DataSourceInfoUtilities.getMetaFlagsContainsStatement(TskData.TSK_FS_META_FLAG_ENUM.ALLOC)); + } + + /** + * Get count of unallocated files in a data source. + * + * @param currentDataSource The data source. + * + * @return The count. + * + * @throws SleuthkitCaseProviderException + * @throws TskCoreException + * @throws SQLException + */ + public Long getCountOfUnallocatedFiles(DataSource currentDataSource) + throws SleuthkitCaseProvider.SleuthkitCaseProviderException, TskCoreException, SQLException { + + return DataSourceInfoUtilities.getCountOfRegNonSlackFiles(provider.get(), currentDataSource, + DataSourceInfoUtilities.getMetaFlagsContainsStatement(TskData.TSK_FS_META_FLAG_ENUM.UNALLOC)); + } + + /** + * Get count of directories in a data source. + * + * @param currentDataSource The data source. + * + * @return The count. + * + * @throws SleuthkitCaseProviderException + * @throws TskCoreException + * @throws SQLException + */ + public Long getCountOfDirectories(DataSource currentDataSource) + throws SleuthkitCaseProvider.SleuthkitCaseProviderException, TskCoreException, SQLException { + + return DataSourceInfoUtilities.getCountOfTskFiles(provider.get(), currentDataSource, + "meta_type=" + TskData.TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_DIR.getValue() + + " AND type<>" + TskData.TSK_DB_FILES_TYPE_ENUM.VIRTUAL_DIR.getFileType()); + } + + /** + * Get count of slack files in a data source. + * + * @param currentDataSource The data source. + * + * @return The count. + * + * @throws SleuthkitCaseProviderException + * @throws TskCoreException + * @throws SQLException + */ + public Long getCountOfSlackFiles(DataSource currentDataSource) + throws SleuthkitCaseProvider.SleuthkitCaseProviderException, TskCoreException, SQLException { + + return DataSourceInfoUtilities.getCountOfRegularFiles(provider.get(), currentDataSource, + "type=" + TskData.TSK_DB_FILES_TYPE_ENUM.SLACK.getFileType()); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceUserActivitySummary.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/UserActivitySummary.java similarity index 70% rename from Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceUserActivitySummary.java rename to Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/UserActivitySummary.java index 1ae108c496..481840776f 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceUserActivitySummary.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/UserActivitySummary.java @@ -18,15 +18,23 @@ */ package org.sleuthkit.autopsy.datasourcesummary.datamodel; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DefaultArtifactUpdateGovernor; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.Comparator; import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.logging.Level; import java.util.stream.Collectors; -import java.util.stream.IntStream; import java.util.stream.Stream; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.datasourcesummary.datamodel.SleuthkitCaseProvider.SleuthkitCaseProviderException; import org.sleuthkit.datamodel.BlackboardArtifact; @@ -44,9 +52,10 @@ import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE; * time, the data being provided for domains is fictitious and is done as a * placeholder. */ -public class DataSourceUserActivitySummary { +public class UserActivitySummary implements DefaultArtifactUpdateGovernor { private static final BlackboardArtifact.Type TYPE_DEVICE_ATTACHED = new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_DEVICE_ATTACHED); + private static final BlackboardArtifact.Type TYPE_WEB_HISTORY = new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_WEB_HISTORY); private static final BlackboardAttribute.Type TYPE_DATETIME = new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DATETIME); private static final BlackboardAttribute.Type TYPE_DATETIME_ACCESSED = new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DATETIME_ACCESSED); @@ -55,24 +64,76 @@ public class DataSourceUserActivitySummary { private static final BlackboardAttribute.Type TYPE_DEVICE_MODEL = new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DEVICE_MODEL); private static final BlackboardAttribute.Type TYPE_MESSAGE_TYPE = new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_MESSAGE_TYPE); private static final BlackboardAttribute.Type TYPE_TEXT = new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_TEXT); - private static final BlackboardAttribute.Type TYPE_DATETIME_RCVD = new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DATETIME_RCVD); private static final BlackboardAttribute.Type TYPE_DATETIME_SENT = new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DATETIME_SENT); private static final BlackboardAttribute.Type TYPE_DATETIME_START = new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DATETIME_START); private static final BlackboardAttribute.Type TYPE_DATETIME_END = new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DATETIME_END); + private static final BlackboardAttribute.Type TYPE_DOMAIN = new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DOMAIN); private static final Comparator TOP_ACCOUNT_RESULT_DATE_COMPARE = (a, b) -> a.getLastAccess().compareTo(b.getLastAccess()); private static final Comparator TOP_WEBSEARCH_RESULT_DATE_COMPARE = (a, b) -> a.getDateAccessed().compareTo(b.getDateAccessed()); - private static final String ROOT_HUB_IDENTIFIER = "ROOT_HUB"; - private static final long SLEEP_TIME = 5000; + private static final Set ARTIFACT_UPDATE_TYPE_IDS = new HashSet<>(Arrays.asList( + ARTIFACT_TYPE.TSK_WEB_SEARCH_QUERY.getTypeID(), + ARTIFACT_TYPE.TSK_MESSAGE.getTypeID(), + ARTIFACT_TYPE.TSK_EMAIL_MSG.getTypeID(), + ARTIFACT_TYPE.TSK_CALLLOG.getTypeID(), + ARTIFACT_TYPE.TSK_DEVICE_ATTACHED.getTypeID(), + ARTIFACT_TYPE.TSK_WEB_HISTORY.getTypeID() + )); + + private static final Set DEVICE_EXCLUDE_LIST = new HashSet<>(Arrays.asList("ROOT_HUB", "ROOT_HUB20")); + private static final Set DOMAIN_EXCLUDE_LIST = new HashSet<>(Arrays.asList("127.0.0.1", "LOCALHOST")); + + private static final long MS_PER_DAY = 1000 * 60 * 60 * 24; + private static final long DOMAIN_WINDOW_DAYS = 30; + private static final long DOMAIN_WINDOW_MS = DOMAIN_WINDOW_DAYS * MS_PER_DAY; + + private final SleuthkitCaseProvider caseProvider; + private final TextTranslationService translationService; + private final java.util.logging.Logger logger; /** - * A function to calculate a result from 2 parameters. + * Main constructor. */ - interface Function2 { + public UserActivitySummary() { + this(SleuthkitCaseProvider.DEFAULT, TextTranslationService.getInstance(), + org.sleuthkit.autopsy.coreutils.Logger.getLogger(UserActivitySummary.class.getName())); + } - O apply(A1 a1, A2 a2); + /** + * Main constructor with external dependencies specified. This constructor + * is designed with unit testing in mind since mocked dependencies can be + * utilized. + * + * @param provider The object providing the current SleuthkitCase. + * @param translationService The translation service. + * @param logger The logger to use. + */ + public UserActivitySummary( + SleuthkitCaseProvider provider, + TextTranslationService translationService, + java.util.logging.Logger logger) { + + this.caseProvider = provider; + this.translationService = translationService; + this.logger = logger; + } + + @Override + public Set getArtifactTypeIdsForRefresh() { + return ARTIFACT_UPDATE_TYPE_IDS; + } + + /** + * Throws an IllegalArgumentException if count <= 0. + * + * @param count The count being checked. + */ + private void assertValidCount(int count) { + if (count <= 0) { + throw new IllegalArgumentException("Count must be greater than 0"); + } } /** @@ -85,59 +146,134 @@ public class DataSourceUserActivitySummary { * * @throws InterruptedException */ - public List getRecentDomains(DataSource dataSource, int count) throws InterruptedException { - Thread.sleep(SLEEP_TIME); - final String dId = Long.toString(dataSource.getId()); - final Function2 getId = (s, idx) -> String.format("d:%s, f:%s, i:%d", dId, s, idx); - return IntStream.range(0, count) - .mapToObj(num -> new TopDomainsResult( - getId.apply("domain", num), - getId.apply("url", num), - (long) num, - new Date(((long) num) * 1000 * 60 * 60 * 24) - )) + public List getRecentDomains(DataSource dataSource, int count) throws TskCoreException, SleuthkitCaseProviderException { + assertValidCount(count); + + if (dataSource == null) { + return Collections.emptyList(); + } + + Pair>> mostRecentAndGroups = getDomainGroupsAndMostRecent(dataSource); + // if no recent domains, return accordingly + if (mostRecentAndGroups.getKey() == null || mostRecentAndGroups.getValue().size() == 0) { + return Collections.emptyList(); + } + + final long mostRecentMs = mostRecentAndGroups.getLeft(); + Map> groups = mostRecentAndGroups.getRight(); + + return groups.entrySet().stream() + .map(entry -> getDomainsResult(entry.getKey(), entry.getValue(), mostRecentMs)) + .filter(result -> result != null) + // sort by number of visit times in those 30 days (max to min) + .sorted((a, b) -> -Long.compare(a.getVisitTimes(), b.getVisitTimes())) + // limit the result number to the parameter provided + .limit(count) .collect(Collectors.toList()); } - private final SleuthkitCaseProvider caseProvider; - private final TextTranslationService translationService; - private final java.util.logging.Logger logger; - /** - * Main constructor. + * Creates a TopDomainsResult from data or null if no visit date exists + * within DOMAIN_WINDOW_MS of mostRecentMs. + * + * @param domain The domain. + * @param visits The number of visits. + * @param mostRecentMs The most recent visit of any domain. + * + * @return The TopDomainsResult or null if no visits to this domain within + * 30 days of mostRecentMs. */ - public DataSourceUserActivitySummary() { - this(SleuthkitCaseProvider.DEFAULT, TextTranslationService.getInstance(), - org.sleuthkit.autopsy.coreutils.Logger.getLogger(DataSourceUserActivitySummary.class.getName())); + private TopDomainsResult getDomainsResult(String domain, List visits, long mostRecentMs) { + long visitCount = 0; + Long thisMostRecentMs = null; + + for (Long visitMs : visits) { + // make sure that visit is within window of mostRecentMS; otherwise skip it. + if (visitMs + DOMAIN_WINDOW_MS < mostRecentMs) { + continue; + } + + // if visit is within window, increment the count and get most recent + visitCount++; + thisMostRecentMs = getMax(thisMostRecentMs, visitMs); + } + + // if there are no visits within the window, return null + if (visitCount <= 0 || thisMostRecentMs == null) { + return null; + } else { + // create a top domain result with the domain, count, and most recent visit date + return new TopDomainsResult(domain, visitCount, new Date(thisMostRecentMs)); + } } /** - * Main constructor with external dependencies specified. This constructor - * is designed with unit testing in mind since mocked dependencies can be - * utilized. + * Queries TSK_WEB_HISTORY artifacts and returning the latest web history + * date accessed and a mapping of domains to all of their visits. * - * @param provider The object providing the current SleuthkitCase. - * @param translationService The translation service. - * @param logger The logger to use. + * @param dataSource The datasource. + * + * @return A tuple where the first value is the latest web history accessed + * date in milliseconds and the second value maps normalized + * (lowercase; trimmed) domain names to when those domains were + * visited. + * + * @throws TskCoreException + * @throws SleuthkitCaseProviderException */ - public DataSourceUserActivitySummary( - SleuthkitCaseProvider provider, - TextTranslationService translationService, - java.util.logging.Logger logger) { + private Pair>> getDomainGroupsAndMostRecent(DataSource dataSource) throws TskCoreException, SleuthkitCaseProviderException { + List artifacts = DataSourceInfoUtilities.getArtifacts(caseProvider.get(), TYPE_WEB_HISTORY, + dataSource, TYPE_DATETIME_ACCESSED, DataSourceInfoUtilities.SortOrder.DESCENDING, 0); - this.caseProvider = provider; - this.translationService = translationService; - this.logger = logger; + Long mostRecentMs = null; + Map> domainVisits = new HashMap<>(); + + for (BlackboardArtifact art : artifacts) { + Long artifactDateSecs = DataSourceInfoUtilities.getLongOrNull(art, TYPE_DATETIME_ACCESSED); + String domain = DataSourceInfoUtilities.getStringOrNull(art, TYPE_DOMAIN); + + // if there isn't a last access date or domain for this artifact, it can be ignored. + // Also, ignore the loopback address. + if (artifactDateSecs == null || StringUtils.isBlank(domain) || DOMAIN_EXCLUDE_LIST.contains(domain.toUpperCase().trim())) { + continue; + } + + Long artifactDateMs = artifactDateSecs * 1000; + + // update the most recent visit date overall + mostRecentMs = getMax(mostRecentMs, artifactDateMs); + + //Normalize the domain to lower case. + domain = domain.toLowerCase().trim(); + + // add this visit date to the list of dates for the domain + List domainVisitList = domainVisits.get(domain); + if (domainVisitList == null) { + domainVisitList = new ArrayList<>(); + domainVisits.put(domain, domainVisitList); + } + + domainVisitList.add(artifactDateMs); + } + + return Pair.of(mostRecentMs, domainVisits); } /** - * Throws an IllegalArgumentException if count <= 0. + * Returns the maximum value given two longs handling possible null values. * - * @param count The count being checked. + * @param num1 The first number. + * @param num2 The second number. + * + * @return The maximum non-null number or null if both numbers are null. */ - private void assertValidCount(int count) { - if (count <= 0) { - throw new IllegalArgumentException("Count must be greater than 0"); + private static Long getMax(Long num1, Long num2) { + if (num1 == null) { + return num2; + } else if (num2 == null) { + return num1; + } else { + return num2 > num1 ? num2 : num1; } } @@ -175,6 +311,10 @@ public class DataSourceUserActivitySummary { public List getMostRecentWebSearches(DataSource dataSource, int count) throws SleuthkitCaseProviderException, TskCoreException { assertValidCount(count); + if (dataSource == null) { + return Collections.emptyList(); + } + // get the artifacts List webSearchArtifacts = caseProvider.get().getBlackboard() .getArtifacts(ARTIFACT_TYPE.TSK_WEB_SEARCH_QUERY.getTypeID(), dataSource.getId()); @@ -183,7 +323,7 @@ public class DataSourceUserActivitySummary { Collection> resultGroups = webSearchArtifacts .stream() // get items where search string and date is not null - .map(DataSourceUserActivitySummary::getWebSearchResult) + .map(UserActivitySummary::getWebSearchResult) // remove null records .filter(result -> result != null) // get these messages grouped by search to string @@ -259,6 +399,10 @@ public class DataSourceUserActivitySummary { public List getRecentDevices(DataSource dataSource, int count) throws SleuthkitCaseProviderException, TskCoreException { assertValidCount(count); + if (dataSource == null) { + return Collections.emptyList(); + } + return DataSourceInfoUtilities.getArtifacts(caseProvider.get(), TYPE_DEVICE_ATTACHED, dataSource, TYPE_DATETIME, DataSourceInfoUtilities.SortOrder.DESCENDING, 0) .stream() @@ -273,7 +417,7 @@ public class DataSourceUserActivitySummary { // remove Root Hub identifier .filter(result -> { return result.getDeviceModel() == null - || !result.getDeviceModel().trim().toUpperCase().equals(ROOT_HUB_IDENTIFIER); + || !DEVICE_EXCLUDE_LIST.contains(result.getDeviceModel().trim().toUpperCase()); }) .limit(count) .collect(Collectors.toList()); @@ -344,6 +488,10 @@ public class DataSourceUserActivitySummary { public List getRecentAccounts(DataSource dataSource, int count) throws SleuthkitCaseProviderException, TskCoreException { assertValidCount(count); + if (dataSource == null) { + return Collections.emptyList(); + } + Stream messageResults = caseProvider.get().getBlackboard().getArtifacts(ARTIFACT_TYPE.TSK_MESSAGE.getTypeID(), dataSource.getId()) .stream() .map((art) -> getMessageAccountResult(art)); @@ -530,4 +678,48 @@ public class DataSourceUserActivitySummary { return lastAccess; } } + + /** + * Describes a result of a program run on a datasource. + */ + public static class TopDomainsResult { + + private final String domain; + private final Long visitTimes; + private final Date lastVisit; + + /** + * Describes a top domain result. + * + * @param domain The domain. + * @param visitTimes The number of times it was visited. + * @param lastVisit The date of the last visit. + */ + public TopDomainsResult(String domain, Long visitTimes, Date lastVisit) { + this.domain = domain; + this.visitTimes = visitTimes; + this.lastVisit = lastVisit; + } + + /** + * @return The domain for the result. + */ + public String getDomain() { + return domain; + } + + /** + * @return The number of times this site is visited. + */ + public Long getVisitTimes() { + return visitTimes; + } + + /** + * @return The date of the last visit. + */ + public Date getLastVisit() { + return lastVisit; + } + } } diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/AnalysisPanel.form b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/AnalysisPanel.form index 4016b539a7..402b709b38 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/AnalysisPanel.form +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/AnalysisPanel.form @@ -57,6 +57,27 @@
+ + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/AnalysisPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/AnalysisPanel.java index 16de061385..34dfa97a13 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/AnalysisPanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/AnalysisPanel.java @@ -20,16 +20,17 @@ package org.sleuthkit.autopsy.datasourcesummary.ui; import java.util.Arrays; import java.util.List; -import java.util.stream.Collectors; +import java.util.function.Function; import org.apache.commons.lang3.tuple.Pair; import org.openide.util.NbBundle.Messages; -import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.datasourcesummary.datamodel.DataSourceAnalysisSummary; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.AnalysisSummary; import org.sleuthkit.autopsy.datasourcesummary.uiutils.CellModelTableCellRenderer.DefaultCellModel; -import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchResult; import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchWorker; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.IngestRunningLabel; import org.sleuthkit.autopsy.datasourcesummary.uiutils.JTablePanel; import org.sleuthkit.autopsy.datasourcesummary.uiutils.JTablePanel.ColumnModel; +import org.sleuthkit.autopsy.modules.hashdatabase.HashLookupModuleFactory; +import org.sleuthkit.autopsy.modules.interestingitems.InterestingItemsIngestModuleFactory; import org.sleuthkit.datamodel.DataSource; /** @@ -38,12 +39,22 @@ import org.sleuthkit.datamodel.DataSource; */ @Messages({ "AnalysisPanel_keyColumn_title=Name", - "AnalysisPanel_countColumn_title=Count" + "AnalysisPanel_countColumn_title=Count", + "AnalysisPanel_keywordSearchModuleName=Keyword Search" }) public class AnalysisPanel extends BaseDataSourceSummaryPanel { private static final long serialVersionUID = 1L; + private static final String KEYWORD_SEARCH_MODULE_NAME = Bundle.AnalysisPanel_keywordSearchModuleName(); + private static final String KEYWORD_SEARCH_FACTORY = "org.sleuthkit.autopsy.keywordsearch.KeywordSearchModuleFactory"; + + private static final String INTERESTING_ITEM_MODULE_NAME = new InterestingItemsIngestModuleFactory().getModuleDisplayName(); + private static final String INTERESTING_ITEM_FACTORY = InterestingItemsIngestModuleFactory.class.getCanonicalName(); + + private static final String HASHSET_MODULE_NAME = HashLookupModuleFactory.getModuleName(); + private static final String HASHSET_FACTORY = HashLookupModuleFactory.class.getCanonicalName(); + /** * Default Column definitions for each table */ @@ -60,17 +71,28 @@ public class AnalysisPanel extends BaseDataSourceSummaryPanel { ) ); - private final JTablePanel> hashsetHitsTable = JTablePanel.getJTablePanel(DEFAULT_COLUMNS); + private static final Function, String> DEFAULT_KEY_PROVIDER = (pair) -> pair.getKey(); - private final JTablePanel> keywordHitsTable = JTablePanel.getJTablePanel(DEFAULT_COLUMNS); + private final JTablePanel> hashsetHitsTable + = JTablePanel.getJTablePanel(DEFAULT_COLUMNS) + .setKeyFunction(DEFAULT_KEY_PROVIDER); - private final JTablePanel> interestingItemsTable = JTablePanel.getJTablePanel(DEFAULT_COLUMNS); + private final JTablePanel> keywordHitsTable + = JTablePanel.getJTablePanel(DEFAULT_COLUMNS) + .setKeyFunction(DEFAULT_KEY_PROVIDER); + + private final JTablePanel> interestingItemsTable + = JTablePanel.getJTablePanel(DEFAULT_COLUMNS) + .setKeyFunction(DEFAULT_KEY_PROVIDER); private final List> tables = Arrays.asList( hashsetHitsTable, keywordHitsTable, interestingItemsTable ); + + private final IngestRunningLabel ingestRunningLabel = new IngestRunningLabel(); + /** * All of the components necessary for data fetch swing workers to load data @@ -82,50 +104,47 @@ public class AnalysisPanel extends BaseDataSourceSummaryPanel { * Creates a new DataSourceUserActivityPanel. */ public AnalysisPanel() { - this(new DataSourceAnalysisSummary()); + this(new AnalysisSummary()); } - public AnalysisPanel(DataSourceAnalysisSummary analysisData) { + public AnalysisPanel(AnalysisSummary analysisData) { + super(analysisData); + // set up data acquisition methods dataFetchComponents = Arrays.asList( // hashset hits loading components new DataFetchWorker.DataFetchComponents<>( (dataSource) -> analysisData.getHashsetCounts(dataSource), - (result) -> hashsetHitsTable.showDataFetchResult(result)), + (result) -> showResultWithModuleCheck(hashsetHitsTable, result, HASHSET_FACTORY, HASHSET_MODULE_NAME)), // keyword hits loading components new DataFetchWorker.DataFetchComponents<>( (dataSource) -> analysisData.getKeywordCounts(dataSource), - (result) -> keywordHitsTable.showDataFetchResult(result)), + (result) -> showResultWithModuleCheck(keywordHitsTable, result, KEYWORD_SEARCH_FACTORY, KEYWORD_SEARCH_MODULE_NAME)), // interesting item hits loading components new DataFetchWorker.DataFetchComponents<>( (dataSource) -> analysisData.getInterestingItemCounts(dataSource), - (result) -> interestingItemsTable.showDataFetchResult(result)) + (result) -> showResultWithModuleCheck(interestingItemsTable, result, INTERESTING_ITEM_FACTORY, INTERESTING_ITEM_MODULE_NAME)) ); initComponents(); } + + @Override + public void close() { + ingestRunningLabel.unregister(); + super.close(); + } + + + @Override + protected void fetchInformation(DataSource dataSource) { + fetchInformation(dataFetchComponents, dataSource); + } + @Override protected void onNewDataSource(DataSource dataSource) { - // if no data source is present or the case is not open, - // set results for tables to null. - if (dataSource == null || !Case.isCaseOpen()) { - this.dataFetchComponents.forEach((item) -> item.getResultHandler() - .accept(DataFetchResult.getSuccessResult(null))); - - } else { - // set tables to display loading screen - this.tables.forEach((table) -> table.showDefaultLoadingMessage()); - - // create swing workers to run for each table - List> workers = dataFetchComponents - .stream() - .map((components) -> new DataFetchWorker<>(components, dataSource)) - .collect(Collectors.toList()); - - // submit swing workers to run - submit(workers); - } + onNewDataSource(dataFetchComponents, tables, dataSource); } /** @@ -139,6 +158,7 @@ public class AnalysisPanel extends BaseDataSourceSummaryPanel { javax.swing.JScrollPane mainScrollPane = new javax.swing.JScrollPane(); javax.swing.JPanel mainContentPanel = new javax.swing.JPanel(); + javax.swing.JPanel ingestRunningPanel = ingestRunningLabel; javax.swing.JLabel hashsetHitsLabel = new javax.swing.JLabel(); javax.swing.Box.Filler filler1 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 2), new java.awt.Dimension(0, 2), new java.awt.Dimension(32767, 2)); javax.swing.JPanel hashSetHitsPanel = hashsetHitsTable; @@ -157,6 +177,12 @@ public class AnalysisPanel extends BaseDataSourceSummaryPanel { mainContentPanel.setMinimumSize(new java.awt.Dimension(200, 452)); mainContentPanel.setLayout(new javax.swing.BoxLayout(mainContentPanel, javax.swing.BoxLayout.PAGE_AXIS)); + ingestRunningPanel.setAlignmentX(0.0F); + ingestRunningPanel.setMaximumSize(new java.awt.Dimension(32767, 25)); + ingestRunningPanel.setMinimumSize(new java.awt.Dimension(10, 25)); + ingestRunningPanel.setPreferredSize(new java.awt.Dimension(10, 25)); + mainContentPanel.add(ingestRunningPanel); + org.openide.awt.Mnemonics.setLocalizedText(hashsetHitsLabel, org.openide.util.NbBundle.getMessage(AnalysisPanel.class, "AnalysisPanel.hashsetHitsLabel.text")); // NOI18N mainContentPanel.add(hashsetHitsLabel); mainContentPanel.add(filler1); @@ -204,6 +230,7 @@ public class AnalysisPanel extends BaseDataSourceSummaryPanel { ); }// //GEN-END:initComponents + // Variables declaration - do not modify//GEN-BEGIN:variables // End of variables declaration//GEN-END:variables } diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/BaseDataSourceSummaryPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/BaseDataSourceSummaryPanel.java index d28a734e34..35f3b921fd 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/BaseDataSourceSummaryPanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/BaseDataSourceSummaryPanel.java @@ -18,11 +18,37 @@ */ package org.sleuthkit.autopsy.datasourcesummary.ui; +import java.beans.PropertyChangeEvent; +import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.Set; +import java.util.function.Predicate; +import java.util.logging.Level; +import java.util.stream.Collectors; import javax.swing.JPanel; import javax.swing.SwingWorker; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.IngestModuleCheckUtil; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.SleuthkitCaseProvider.SleuthkitCaseProviderException; +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.EventUpdateHandler; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.LoadableComponent; import org.sleuthkit.autopsy.datasourcesummary.uiutils.SwingWorkerSequentialExecutor; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.UpdateGovernor; +import org.sleuthkit.autopsy.ingest.IngestManager.IngestJobEvent; +import org.sleuthkit.autopsy.ingest.ModuleContentEvent; +import org.sleuthkit.autopsy.ingest.ModuleDataEvent; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.DataSource; +import org.sleuthkit.datamodel.TskCoreException; /** * Base class from which other tabs in data source summary derive. @@ -31,9 +57,184 @@ abstract class BaseDataSourceSummaryPanel extends JPanel { private static final long serialVersionUID = 1L; + private static final Logger logger = Logger.getLogger(BaseDataSourceSummaryPanel.class.getName()); + private final SwingWorkerSequentialExecutor executor = new SwingWorkerSequentialExecutor(); + private final IngestModuleCheckUtil ingestModuleCheck = new IngestModuleCheckUtil(); + private final EventUpdateHandler updateHandler; + private final List governors; + private DataSource dataSource; + /** + * In charge of determining when an update is necessary. In instances where + * a datasource is concerned, this checks to see if the datasource is the + * current datasource. Otherwise, it delegates to the underlying governors + * for the object. + */ + private final UpdateGovernor updateGovernor = new UpdateGovernor() { + /** + * Checks to see if artifact is from a datasource. + * + * @param art The artifact. + * @param ds The datasource. + * + * @return True if in datasource; false if not or exception. + */ + private boolean isInDataSource(BlackboardArtifact art, DataSource ds) { + try { + + return (art.getDataSource() != null && art.getDataSource().getId() == ds.getId()); + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "There was an error fetching datasource for artifact.", ex); + return false; + } + } + + @Override + public boolean isRefreshRequired(ModuleDataEvent evt) { + DataSource ds = getDataSource(); + // make sure there is an event. + if (ds == null || evt == null) { + return false; + } + + //if there are no artifacts with matching datasource, return + // if no artifacts are present, pass it on just in case there was something wrong with ModuleDataEvent + if (evt.getArtifacts() != null + && !evt.getArtifacts().isEmpty() + && !evt.getArtifacts().stream().anyMatch((art) -> isInDataSource(art, ds))) { + return false; + } + + // otherwise, see if there is something that wants updates + for (UpdateGovernor governor : governors) { + if (governor.isRefreshRequired(evt)) { + return true; + } + } + + return false; + } + + @Override + public boolean isRefreshRequired(ModuleContentEvent evt) { + DataSource ds = getDataSource(); + // make sure there is an event. + if (ds == null || evt == null) { + return false; + } + + try { + // if the underlying content has a datasource and that datasource != the + // current datasource, return false + if (evt.getSource() instanceof Content + && ((Content) evt.getSource()).getDataSource() != null + && ((Content) evt.getSource()).getDataSource().getId() != ds.getId()) { + return false; + } + } catch (TskCoreException ex) { + // on an exception, keep going for tolerance sake + logger.log(Level.WARNING, "There was an error fetching datasource for content.", ex); + } + + for (UpdateGovernor governor : governors) { + if (governor.isRefreshRequired(evt)) { + return true; + } + } + + return false; + } + + @Override + public boolean isRefreshRequired(AbstractFile file) { + DataSource currentDataSource = getDataSource(); + if (currentDataSource == null || file == null) { + return false; + } + + // make sure the file is for the current data source + Long fileDsId = null; + try { + Content fileDataSource = file.getDataSource(); + fileDsId = fileDataSource.getId(); + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "Unable to get the datasource for newly added file", ex); + } + + if (fileDsId != null && currentDataSource.getId() == fileDsId) { + for (UpdateGovernor governor : governors) { + if (governor.isRefreshRequired(file)) { + return true; + } + } + } + + return false; + } + + @Override + public boolean isRefreshRequired(IngestJobEvent evt) { + for (UpdateGovernor governor : governors) { + if (governor.isRefreshRequired(evt)) { + return true; + } + } + + return false; + } + + @Override + public boolean isRefreshRequiredForCaseEvent(PropertyChangeEvent evt) { + for (UpdateGovernor governor : governors) { + if (governor.isRefreshRequiredForCaseEvent(evt)) { + return true; + } + } + + return false; + } + + @Override + public Set getCaseEventUpdates() { + // return the union of all case events sets from delegates. + return governors.stream() + .filter(governor -> governor.getCaseEventUpdates() != null) + .flatMap(governor -> governor.getCaseEventUpdates().stream()) + .collect(Collectors.toSet()); + } + + @Override + public Set getIngestJobEventUpdates() { + // return the union of all case events sets from delegates. + return governors.stream() + .filter(governor -> governor.getIngestJobEventUpdates() != null) + .flatMap(governor -> governor.getIngestJobEventUpdates().stream()) + .collect(Collectors.toSet()); + } + }; + + /** + * Main constructor. + * + * @param governors The items governing when this panel should receive + * updates. + */ + protected BaseDataSourceSummaryPanel(UpdateGovernor... governors) { + this.governors = (governors == null) ? Collections.emptyList() : Arrays.asList(governors); + this.updateHandler = new EventUpdateHandler(this::onRefresh, updateGovernor); + this.updateHandler.register(); + } + + /** + * Closes listeners and resources. + */ + public void close() { + executor.cancelRunning(); + updateHandler.unregister(); + } + /** * Sets datasource to visualize in the panel. * @@ -45,6 +246,13 @@ abstract class BaseDataSourceSummaryPanel extends JPanel { onNewDataSource(this.dataSource); } + /** + * @return The current data source. + */ + protected synchronized DataSource getDataSource() { + return this.dataSource; + } + /** * Submits the following swing workers for execution in sequential order. If * there are any previous workers, those workers are cancelled. @@ -55,10 +263,155 @@ abstract class BaseDataSourceSummaryPanel extends JPanel { executor.submit(workers); } + /** + * When a data source is updated this function is triggered. + * + * @param dataSource The data source. + */ + synchronized void onRefresh() { + // trigger on new data source with the current data source + fetchInformation(this.dataSource); + } + + /** + * Action that is called when information needs to be retrieved (on refresh + * or on new data source). + * + * @param dataSource The datasource to fetch information about. + */ + protected abstract void fetchInformation(DataSource dataSource); + + /** + * Utility method to be called when solely updating information (not showing + * a loading screen) that creates swing workers from the data source + * argument and data fetch components and then submits them to run. + * + * @param dataFetchComponents The components to be run. + * @param dataSource The data source argument. + */ + protected void fetchInformation(List> dataFetchComponents, DataSource dataSource) { + if (dataSource == null || !Case.isCaseOpen()) { + dataFetchComponents.forEach((item) -> item.getResultHandler() + .accept(DataFetchResult.getSuccessResult(null))); + } else { + // create swing workers to run for each loadable item + List> workers = dataFetchComponents + .stream() + .map((components) -> new DataFetchWorker<>(components, dataSource)) + .collect(Collectors.toList()); + + // submit swing workers to run + if (!workers.isEmpty()) { + submit(workers); + } + } + } + /** * When a new dataSource is added, this method is called. * * @param dataSource The new dataSource. */ protected abstract void onNewDataSource(DataSource dataSource); + + /** + * Utility method that shows a loading screen with loadable components, + * create swing workers from the datafetch components and data source + * argument and submits them to be executed. + * + * @param dataFetchComponents The components to register. + * @param loadableComponents The components to set to a loading screen. + * @param dataSource The data source argument. + */ + protected void onNewDataSource( + List> dataFetchComponents, + List> loadableComponents, + DataSource dataSource) { + // if no data source is present or the case is not open, + // set results for tables to null. + if (dataSource == null || !Case.isCaseOpen()) { + dataFetchComponents.forEach((item) -> item.getResultHandler() + .accept(DataFetchResult.getSuccessResult(null))); + + } else { + // set tables to display loading screen + loadableComponents.forEach((table) -> table.showDefaultLoadingMessage()); + + fetchInformation(dataSource); + } + } + + /** + * Get default message when there is a NotIngestedWithModuleException. + * + * @param moduleName The moduleName. + * + * @return Message specifying that the ingest module was not run. + */ + @Messages({ + "# {0} - module name", + "BaseDataSourceSummaryPanel_defaultNotIngestMessage=The {0} ingest module has not been run on this data source." + }) + protected String getDefaultNoIngestMessage(String moduleName) { + return Bundle.BaseDataSourceSummaryPanel_defaultNotIngestMessage(moduleName); + } + + /** + * Utility method to return the IngestModuleCheckUtil. + * + * @return The IngestModuleCheckUtil. + */ + protected IngestModuleCheckUtil getIngestModuleCheckUtil() { + return this.ingestModuleCheck; + } + + /** + * Utility method that in the event of a) there are no results and b) a + * relevant ingest module has not been run on this datasource, then a + * message indicating the unrun ingest module will be shown. Otherwise, the + * default LoadableComponent.showDataFetchResult behavior will be used. + * + * @param component The component. + * @param result The data result. + * @param factoryClass The fully qualified class name of the relevant + * factory. + * @param moduleName The name of the ingest module (i.e. 'Keyword + * Search'). + */ + protected void showResultWithModuleCheck(LoadableComponent> component, DataFetchResult> result, String factoryClass, String moduleName) { + Predicate> hasResults = (lst) -> lst != null && !lst.isEmpty(); + showResultWithModuleCheck(component, result, hasResults, factoryClass, moduleName); + } + + /** + * Utility method that in the event of a) there are no results and b) a + * relevant ingest module has not been run on this datasource, then a + * message indicating the unrun ingest module will be shown. Otherwise, the + * default LoadableComponent.showDataFetchResult behavior will be used. + * + * @param component The component. + * @param result The data result. + * @param hasResults Given the data type, will provide whether or not the + * data contains any actual results. + * @param factoryClass The fully qualified class name of the relevant + * factory. + * @param moduleName The name of the ingest module (i.e. 'Keyword + * Search'). + */ + protected void showResultWithModuleCheck(LoadableComponent component, DataFetchResult result, + Predicate hasResults, String factoryClass, String moduleName) { + + if (result != null && result.getResultType() == ResultType.SUCCESS && !hasResults.test(result.getData())) { + try { + if (!ingestModuleCheck.isModuleIngested(getDataSource(), factoryClass)) { + component.showMessage(getDefaultNoIngestMessage(moduleName)); + return; + } + } catch (TskCoreException | SleuthkitCaseProviderException ex) { + logger.log(Level.WARNING, "There was an error while checking for ingest modules for datasource.", ex); + } + } + + component.showDataFetchResult(result); + } } diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties index b88a008e10..94b497d2ff 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties @@ -1,43 +1,44 @@ DataSourceSummaryDialog.closeButton.text=Close -DataSourceSummaryDetailsPanel.displayNameLabel.text=Display Name: -DataSourceSummaryDetailsPanel.originalNameLabel.text=Name: -DataSourceSummaryDetailsPanel.deviceIdLabel.text=Device ID: -DataSourceSummaryDetailsPanel.timeZoneLabel.text=Time Zone: -DataSourceSummaryDetailsPanel.imageTypeLabel.text=Image Type: -DataSourceSummaryDetailsPanel.sizeLabel.text=Size: -DataSourceSummaryDetailsPanel.sectorSizeLabel.text=Sector Size: -DataSourceSummaryDetailsPanel.md5HashLabel.text=MD5: -DataSourceSummaryDetailsPanel.sha1HashLabel.text=SHA1: -DataSourceSummaryDetailsPanel.sha256HashLabel.text=SHA256: -DataSourceSummaryDetailsPanel.filePathsLabel.text=File Paths: -DataSourceSummaryDetailsPanel.displayNameValue.text= -DataSourceSummaryDetailsPanel.originalNameValue.text= -DataSourceSummaryDetailsPanel.deviceIdValue.toolTipText= -DataSourceSummaryDetailsPanel.deviceIdValue.text= -DataSourceSummaryDetailsPanel.timeZoneValue.text= -DataSourceSummaryDetailsPanel.imageTypeValue.toolTipText= -DataSourceSummaryDetailsPanel.imageTypeValue.text= -DataSourceSummaryDetailsPanel.sizeValue.text= -DataSourceSummaryDetailsPanel.sectorSizeValue.text= -DataSourceSummaryDetailsPanel.md5HashValue.toolTipText= -DataSourceSummaryDetailsPanel.md5HashValue.text= -DataSourceSummaryDetailsPanel.sha1HashValue.text= -DataSourceSummaryDetailsPanel.sha256HashValue.text= -DataSourceSummaryDetailsPanel.filePathsTable.columnModel.title0= -DataSourceSummaryDetailsPanel.acquisitionDetailsTextArea.text= -DataSourceSummaryDetailsPanel.acquisitionDetailsLabel.text=Acquisition Details: -DataSourceSummaryDetailsPanel.unallocatedSizeLabel.text=Unallocated Space: -DataSourceSummaryDetailsPanel.unallocatedSizeValue.text= -DataSourceSummaryCountsPanel.byCategoryLabel.text=Files by Category -DataSourceSummaryCountsPanel.resultsByTypeLabel.text=Results by Type -DataSourceSummaryUserActivityPanel.programsRunLabel.text=Recent Programs -DataSourceSummaryUserActivityPanel.recentAccountsLabel.text=Recent Accounts -DataSourceSummaryUserActivityPanel.topWebSearchLabel.text=Recent Web Searches -DataSourceSummaryUserActivityPanel.topDevicesAttachedLabel.text=Recent Devices Attached -DataSourceSummaryUserActivityPanel.recentDomainsLabel.text=Recent Domains +ContainerPanel.displayNameLabel.text=Display Name: +ContainerPanel.originalNameLabel.text=Name: +ContainerPanel.deviceIdLabel.text=Device ID: +ContainerPanel.timeZoneLabel.text=Time Zone: +ContainerPanel.imageTypeLabel.text=Image Type: +ContainerPanel.sizeLabel.text=Size: +ContainerPanel.sectorSizeLabel.text=Sector Size: +ContainerPanel.md5HashLabel.text=MD5: +ContainerPanel.sha1HashLabel.text=SHA1: +ContainerPanel.sha256HashLabel.text=SHA256: +ContainerPanel.filePathsLabel.text=File Paths: +ContainerPanel.displayNameValue.text= +ContainerPanel.originalNameValue.text= +ContainerPanel.deviceIdValue.toolTipText= +ContainerPanel.deviceIdValue.text= +ContainerPanel.timeZoneValue.text= +ContainerPanel.imageTypeValue.toolTipText= +ContainerPanel.imageTypeValue.text= +ContainerPanel.sizeValue.text= +ContainerPanel.sectorSizeValue.text= +ContainerPanel.md5HashValue.toolTipText= +ContainerPanel.md5HashValue.text= +ContainerPanel.sha1HashValue.text= +ContainerPanel.sha256HashValue.text= +ContainerPanel.filePathsTable.columnModel.title0= +ContainerPanel.acquisitionDetailsTextArea.text= +ContainerPanel.acquisitionDetailsLabel.text=Acquisition Details: +ContainerPanel.unallocatedSizeLabel.text=Unallocated Space: +ContainerPanel.unallocatedSizeValue.text= +UserActivityPanel.programsRunLabel.text=Recent Programs +UserActivityPanel.recentAccountsLabel.text=Recent Account Types Used +UserActivityPanel.topWebSearchLabel.text=Recent Web Searches +UserActivityPanel.topDevicesAttachedLabel.text=Recent Devices Attached +UserActivityPanel.recentDomainsLabel.text=Recent Domains AnalysisPanel.hashsetHitsLabel.text=Hashset Hits AnalysisPanel.keywordHitsLabel.text=Keyword Hits AnalysisPanel.interestingItemLabel.text=Interesting Item Hits RecentFilesPanel.openDocsLabel.text=Recently Opened Documents RecentFilesPanel.downloadLabel.text=Recent Downloads -RecentFilesPanel.attachmentLabel.text=Recent Attachements +RecentFilesPanel.attachmentLabel.text=Recent Attachments +PastCasesPanel.notableFileLabel.text=Cases with Common Items That Were Tagged as Notable +PastCasesPanel.sameIdLabel.text=Past Cases with the Same Device IDs +DataSourceSummaryTabbedPane.noDataSourceLabel.text=No data source has been selected. 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 f3325f2e46..f03f55df38 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties-MERGED @@ -1,63 +1,39 @@ AnalysisPanel_countColumn_title=Count AnalysisPanel_keyColumn_title=Name +AnalysisPanel_keywordSearchModuleName=Keyword Search +# {0} - module name +BaseDataSourceSummaryPanel_defaultNotIngestMessage=The {0} ingest module has not been run on this data source. CTL_DataSourceSummaryAction=Data Source Summary -DataSourceSummaryCountsPanel.ArtifactCountsTableModel.count.header=Count -DataSourceSummaryCountsPanel.ArtifactCountsTableModel.type.header=Result Type -DataSourceSummaryCountsPanel.byMimeTypeLabel.text=Files by MIME Type -DataSourceSummaryCountsPanel.FilesByCategoryTableModel.all.row=All -DataSourceSummaryCountsPanel.FilesByCategoryTableModel.allocated.row=Allocated -DataSourceSummaryCountsPanel.FilesByCategoryTableModel.count.header=Count -DataSourceSummaryCountsPanel.FilesByCategoryTableModel.directory.row=Directory -DataSourceSummaryCountsPanel.FilesByCategoryTableModel.slack.row=Slack -DataSourceSummaryCountsPanel.FilesByCategoryTableModel.type.header=File Type -DataSourceSummaryCountsPanel.FilesByCategoryTableModel.unallocated.row=Unallocated -DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.audio.row=Audio -DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.documents.row=Documents -DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.executables.row=Executables -DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.images.row=Images -DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.videos.row=Videos -DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_notAnalyzed_label=Not Analyzed -DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_other_label=Other -DataSourceSummaryDetailsPanel.getDataSources.error.text=Failed to get the list of datasources for the current case. -DataSourceSummaryDetailsPanel.getDataSources.error.title=Load Failure -DataSourceSummaryDetailsPanel.units.bytes=\ bytes -DataSourceSummaryDetailsPanel.units.gigabytes=\ GB -DataSourceSummaryDetailsPanel.units.kilobytes=\ kB -DataSourceSummaryDetailsPanel.units.megabytes=\ MB -DataSourceSummaryDetailsPanel.units.petabytes=\ PB -DataSourceSummaryDetailsPanel.units.terabytes=\ TB DataSourceSummaryDialog.closeButton.text=Close -DataSourceSummaryDetailsPanel.displayNameLabel.text=Display Name: -DataSourceSummaryDetailsPanel.originalNameLabel.text=Name: -DataSourceSummaryDetailsPanel.deviceIdLabel.text=Device ID: -DataSourceSummaryDetailsPanel.timeZoneLabel.text=Time Zone: -DataSourceSummaryDetailsPanel.imageTypeLabel.text=Image Type: -DataSourceSummaryDetailsPanel.sizeLabel.text=Size: -DataSourceSummaryDetailsPanel.sectorSizeLabel.text=Sector Size: -DataSourceSummaryDetailsPanel.md5HashLabel.text=MD5: -DataSourceSummaryDetailsPanel.sha1HashLabel.text=SHA1: -DataSourceSummaryDetailsPanel.sha256HashLabel.text=SHA256: -DataSourceSummaryDetailsPanel.filePathsLabel.text=File Paths: -DataSourceSummaryDetailsPanel.displayNameValue.text= -DataSourceSummaryDetailsPanel.originalNameValue.text= -DataSourceSummaryDetailsPanel.deviceIdValue.toolTipText= -DataSourceSummaryDetailsPanel.deviceIdValue.text= -DataSourceSummaryDetailsPanel.timeZoneValue.text= -DataSourceSummaryDetailsPanel.imageTypeValue.toolTipText= -DataSourceSummaryDetailsPanel.imageTypeValue.text= -DataSourceSummaryDetailsPanel.sizeValue.text= -DataSourceSummaryDetailsPanel.sectorSizeValue.text= -DataSourceSummaryDetailsPanel.md5HashValue.toolTipText= -DataSourceSummaryDetailsPanel.md5HashValue.text= -DataSourceSummaryDetailsPanel.sha1HashValue.text= -DataSourceSummaryDetailsPanel.sha256HashValue.text= -DataSourceSummaryDetailsPanel.filePathsTable.columnModel.title0= -DataSourceSummaryDetailsPanel.acquisitionDetailsTextArea.text= -DataSourceSummaryDetailsPanel.acquisitionDetailsLabel.text=Acquisition Details: -DataSourceSummaryDetailsPanel.unallocatedSizeLabel.text=Unallocated Space: -DataSourceSummaryDetailsPanel.unallocatedSizeValue.text= -DataSourceSummaryCountsPanel.byCategoryLabel.text=Files by Category -DataSourceSummaryCountsPanel.resultsByTypeLabel.text=Results by Type +ContainerPanel.displayNameLabel.text=Display Name: +ContainerPanel.originalNameLabel.text=Name: +ContainerPanel.deviceIdLabel.text=Device ID: +ContainerPanel.timeZoneLabel.text=Time Zone: +ContainerPanel.imageTypeLabel.text=Image Type: +ContainerPanel.sizeLabel.text=Size: +ContainerPanel.sectorSizeLabel.text=Sector Size: +ContainerPanel.md5HashLabel.text=MD5: +ContainerPanel.sha1HashLabel.text=SHA1: +ContainerPanel.sha256HashLabel.text=SHA256: +ContainerPanel.filePathsLabel.text=File Paths: +ContainerPanel.displayNameValue.text= +ContainerPanel.originalNameValue.text= +ContainerPanel.deviceIdValue.toolTipText= +ContainerPanel.deviceIdValue.text= +ContainerPanel.timeZoneValue.text= +ContainerPanel.imageTypeValue.toolTipText= +ContainerPanel.imageTypeValue.text= +ContainerPanel.sizeValue.text= +ContainerPanel.sectorSizeValue.text= +ContainerPanel.md5HashValue.toolTipText= +ContainerPanel.md5HashValue.text= +ContainerPanel.sha1HashValue.text= +ContainerPanel.sha256HashValue.text= +ContainerPanel.filePathsTable.columnModel.title0= +ContainerPanel.acquisitionDetailsTextArea.text= +ContainerPanel.acquisitionDetailsLabel.text=Acquisition Details: +ContainerPanel.unallocatedSizeLabel.text=Unallocated Space: +ContainerPanel.unallocatedSizeValue.text= DataSourceSummaryDialog.window.title=Data Sources Summary DataSourceSummaryNode.column.dataSourceName.header=Data Source Name DataSourceSummaryNode.column.files.header=Files @@ -67,42 +43,73 @@ DataSourceSummaryNode.column.tags.header=Tags DataSourceSummaryNode.column.type.header=Type DataSourceSummaryNode.viewDataSourceAction.text=Go to Data Source DataSourceSummaryTabbedPane_analysisTab_title=Analysis -DataSourceSummaryTabbedPane_countsTab_title=Counts DataSourceSummaryTabbedPane_detailsTab_title=Container DataSourceSummaryTabbedPane_ingestHistoryTab_title=Ingest History +DataSourceSummaryTabbedPane_pastCasesTab_title=Past Cases DataSourceSummaryTabbedPane_recentFileTab_title=Recent Files +DataSourceSummaryTabbedPane_typesTab_title=Types DataSourceSummaryTabbedPane_userActivityTab_title=User Activity -DataSourceSummaryUserActivityPanel.programsRunLabel.text=Recent Programs -DataSourceSummaryUserActivityPanel.recentAccountsLabel.text=Recent Accounts -DataSourceSummaryUserActivityPanel.topWebSearchLabel.text=Recent Web Searches -DataSourceSummaryUserActivityPanel.topDevicesAttachedLabel.text=Recent Devices Attached -DataSourceSummaryUserActivityPanel.recentDomainsLabel.text=Recent Domains -AnalysisPanel.hashsetHitsLabel.text=Hashset Hits -AnalysisPanel.keywordHitsLabel.text=Keyword Hits -AnalysisPanel.interestingItemLabel.text=Interesting Item Hits -DataSourceSummaryUserActivityPanel_noDataExists=No communication data exists -DataSourceSummaryUserActivityPanel_tab_title=User Activity -DataSourceSummaryUserActivityPanel_TopAccountTableModel_accountType_header=Account Type -DataSourceSummaryUserActivityPanel_TopAccountTableModel_lastAccess_header=Last Accessed -DataSourceSummaryUserActivityPanel_TopDeviceAttachedTableModel_dateAccessed_header=Last Accessed -DataSourceSummaryUserActivityPanel_TopDeviceAttachedTableModel_deviceId_header=Device Id -DataSourceSummaryUserActivityPanel_TopDeviceAttachedTableModel_makeModel_header=Make and Model -DataSourceSummaryUserActivityPanel_TopDomainsTableModel_domain_header=Domain -DataSourceSummaryUserActivityPanel_TopDomainsTableModel_lastAccess_header=Last Access -DataSourceSummaryUserActivityPanel_TopDomainsTableModel_url_header=URL -DataSourceSummaryUserActivityPanel_TopProgramsTableModel_count_header=Run Times -DataSourceSummaryUserActivityPanel_TopProgramsTableModel_folder_header=Folder -DataSourceSummaryUserActivityPanel_TopProgramsTableModel_lastrun_header=Last Run -DataSourceSummaryUserActivityPanel_TopProgramsTableModel_name_header=Program -DataSourceSummaryUserActivityPanel_TopWebSearchTableModel_dateAccessed_header=Date Accessed -DataSourceSummaryUserActivityPanel_TopWebSearchTableModel_searchString_header=Search String -DataSourceSummaryUserActivityPanel_TopWebSearchTableModel_translatedResult_header=Translated +PastCasesPanel_caseColumn_title=Case +PastCasesPanel_countColumn_title=Count +PastCasesPanel_onNoCrIngest_message=No results will be shown because the Central Repository module was not run. RecentFilePanel_col_header_domain=Domain RecentFilePanel_col_header_path=Path RecentFilePanel_col_header_sender=Sender +RecentFilePanel_emailParserModuleName=Email Parser RecentFilePanel_no_open_documents=No recently open documents found. +RecentFilesPanel_col_head_date=Date +SizeRepresentationUtil_units_bytes=\ bytes +SizeRepresentationUtil_units_gigabytes=\ GB +SizeRepresentationUtil_units_kilobytes=\ kB +SizeRepresentationUtil_units_megabytes=\ MB +SizeRepresentationUtil_units_petabytes=\ PB +SizeRepresentationUtil_units_terabytes=\ TB +TypesPanel_artifactsTypesPieChart_title=Artifact Types +TypesPanel_fileMimeTypesChart_audio_title=Audio +TypesPanel_fileMimeTypesChart_documents_title=Documents +TypesPanel_fileMimeTypesChart_executables_title=Executables +TypesPanel_fileMimeTypesChart_images_title=Images +TypesPanel_fileMimeTypesChart_notAnalyzed_title=Not Analyzed +TypesPanel_fileMimeTypesChart_other_title=Other +TypesPanel_fileMimeTypesChart_title=File Types +TypesPanel_fileMimeTypesChart_unknown_title=Unknown +TypesPanel_fileMimeTypesChart_videos_title=Videos +TypesPanel_filesByCategoryTable_allocatedRow_title=Allocated Files +TypesPanel_filesByCategoryTable_directoryRow_title=Directories +TypesPanel_filesByCategoryTable_slackRow_title=Slack Files +TypesPanel_filesByCategoryTable_unallocatedRow_title=Unallocated Files +TypesPanel_osLabel_title=OS +TypesPanel_sizeLabel_title=Size +TypesPanel_usageLabel_title=Usage +UserActivityPanel.programsRunLabel.text=Recent Programs +UserActivityPanel.recentAccountsLabel.text=Recent Account Types Used +UserActivityPanel.topWebSearchLabel.text=Recent Web Searches +UserActivityPanel.topDevicesAttachedLabel.text=Recent Devices Attached +UserActivityPanel.recentDomainsLabel.text=Recent Domains +AnalysisPanel.hashsetHitsLabel.text=Hashset Hits +AnalysisPanel.keywordHitsLabel.text=Keyword Hits +AnalysisPanel.interestingItemLabel.text=Interesting Item Hits RecentFilesPanel.openDocsLabel.text=Recently Opened Documents RecentFilesPanel.downloadLabel.text=Recent Downloads -RecentFilesPanel.attachmentLabel.text=Recent Attachements -RecentFilesPanel_col_head_date=Date +RecentFilesPanel.attachmentLabel.text=Recent Attachments +PastCasesPanel.notableFileLabel.text=Cases with Common Items That Were Tagged as Notable +PastCasesPanel.sameIdLabel.text=Past Cases with the Same Device IDs +DataSourceSummaryTabbedPane.noDataSourceLabel.text=No data source has been selected. +UserActivityPanel_noDataExists=No communication data exists +UserActivityPanel_tab_title=User Activity +UserActivityPanel_TopAccountTableModel_accountType_header=Account Type +UserActivityPanel_TopAccountTableModel_lastAccess_header=Last Accessed +UserActivityPanel_TopDeviceAttachedTableModel_dateAccessed_header=Last Accessed +UserActivityPanel_TopDeviceAttachedTableModel_deviceId_header=Device Id +UserActivityPanel_TopDeviceAttachedTableModel_makeModel_header=Make and Model +UserActivityPanel_TopDomainsTableModel_count_header=Visits +UserActivityPanel_TopDomainsTableModel_domain_header=Domain +UserActivityPanel_TopDomainsTableModel_lastAccess_header=Last Accessed +UserActivityPanel_TopProgramsTableModel_count_header=Run Times +UserActivityPanel_TopProgramsTableModel_folder_header=Folder +UserActivityPanel_TopProgramsTableModel_lastrun_header=Last Run +UserActivityPanel_TopProgramsTableModel_name_header=Program +UserActivityPanel_TopWebSearchTableModel_dateAccessed_header=Date Accessed +UserActivityPanel_TopWebSearchTableModel_searchString_header=Search String +UserActivityPanel_TopWebSearchTableModel_translatedResult_header=Translated ViewSummaryInformationAction.name.text=View Summary Information diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle_ja.properties b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle_ja.properties index 5b92c2c817..3e5e377b85 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle_ja.properties +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle_ja.properties @@ -13,44 +13,44 @@ DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.executables.row=\u5b9f\u8 DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.images.row=\u30a4\u30e1\u30fc\u30b8 DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.type.header=\u30d5\u30a1\u30a4\u30eb\u30bf\u30a4\u30d7 DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.videos.row=\u52d5\u753b -DataSourceSummaryDetailsPanel.getDataSources.error.text=\u73fe\u5728\u306e\u30b1\u30fc\u30b9\u306e\u30c7\u30fc\u30bf\u30bd\u30fc\u30b9\u30ea\u30b9\u30c8\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002 -DataSourceSummaryDetailsPanel.getDataSources.error.title=\u8aad\u307f\u8fbc\u307f\u5931\u6557 -DataSourceSummaryDetailsPanel.units.bytes=\ \u30d0\u30a4\u30c8 -DataSourceSummaryDetailsPanel.units.gigabytes=\ GB -DataSourceSummaryDetailsPanel.units.kilobytes=\ kB -DataSourceSummaryDetailsPanel.units.megabytes=\ MB -DataSourceSummaryDetailsPanel.units.petabytes=\ PB -DataSourceSummaryDetailsPanel.units.terabytes=\ TB +ContainerPanel.getDataSources.error.text=\u73fe\u5728\u306e\u30b1\u30fc\u30b9\u306e\u30c7\u30fc\u30bf\u30bd\u30fc\u30b9\u30ea\u30b9\u30c8\u3092\u53d6\u5f97\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002 +ContainerPanel.getDataSources.error.title=\u8aad\u307f\u8fbc\u307f\u5931\u6557 +ContainerPanel.units.bytes=\ \u30d0\u30a4\u30c8 +ContainerPanel.units.gigabytes=\ GB +ContainerPanel.units.kilobytes=\ kB +ContainerPanel.units.megabytes=\ MB +ContainerPanel.units.petabytes=\ PB +ContainerPanel.units.terabytes=\ TB DataSourceSummaryDialog.closeButton.text=\u9589\u3058\u308b -DataSourceSummaryDetailsPanel.displayNameLabel.text=\u8868\u793a\u540d: -DataSourceSummaryDetailsPanel.originalNameLabel.text=\u540d\u524d: -DataSourceSummaryDetailsPanel.deviceIdLabel.text=\u30c7\u30d0\u30a4\u30b9ID: -DataSourceSummaryDetailsPanel.timeZoneLabel.text=\u30bf\u30a4\u30e0\u30be\u30fc\u30f3: -DataSourceSummaryDetailsPanel.imageTypeLabel.text=\u30a4\u30e1\u30fc\u30b8\u30bf\u30a4\u30d7: -DataSourceSummaryDetailsPanel.sizeLabel.text=\u30b5\u30a4\u30ba: -DataSourceSummaryDetailsPanel.sectorSizeLabel.text=\u30bb\u30af\u30bf\u30fc\u30b5\u30a4\u30ba: -DataSourceSummaryDetailsPanel.md5HashLabel.text=MD5: -DataSourceSummaryDetailsPanel.sha1HashLabel.text=SHA1: -DataSourceSummaryDetailsPanel.sha256HashLabel.text=SHA256: -DataSourceSummaryDetailsPanel.filePathsLabel.text=\u30d5\u30a1\u30a4\u30eb\u30d1\u30b9: -DataSourceSummaryDetailsPanel.displayNameValue.text= -DataSourceSummaryDetailsPanel.originalNameValue.text= -DataSourceSummaryDetailsPanel.deviceIdValue.toolTipText= -DataSourceSummaryDetailsPanel.deviceIdValue.text= -DataSourceSummaryDetailsPanel.timeZoneValue.text= -DataSourceSummaryDetailsPanel.imageTypeValue.toolTipText= -DataSourceSummaryDetailsPanel.imageTypeValue.text= -DataSourceSummaryDetailsPanel.sizeValue.text= -DataSourceSummaryDetailsPanel.sectorSizeValue.text= -DataSourceSummaryDetailsPanel.md5HashValue.toolTipText= -DataSourceSummaryDetailsPanel.md5HashValue.text= -DataSourceSummaryDetailsPanel.sha1HashValue.text= -DataSourceSummaryDetailsPanel.sha256HashValue.text= -DataSourceSummaryDetailsPanel.filePathsTable.columnModel.title0= -DataSourceSummaryDetailsPanel.acquisitionDetailsTextArea.text= -DataSourceSummaryDetailsPanel.acquisitionDetailsLabel.text=\u53d6\u5f97\u306e\u8a73\u7d30: -DataSourceSummaryDetailsPanel.unallocatedSizeLabel.text=\u672a\u4f7f\u7528\u9818\u57df: -DataSourceSummaryDetailsPanel.unallocatedSizeValue.text= +ContainerPanel.displayNameLabel.text=\u8868\u793a\u540d: +ContainerPanel.originalNameLabel.text=\u540d\u524d: +ContainerPanel.deviceIdLabel.text=\u30c7\u30d0\u30a4\u30b9ID: +ContainerPanel.timeZoneLabel.text=\u30bf\u30a4\u30e0\u30be\u30fc\u30f3: +ContainerPanel.imageTypeLabel.text=\u30a4\u30e1\u30fc\u30b8\u30bf\u30a4\u30d7: +ContainerPanel.sizeLabel.text=\u30b5\u30a4\u30ba: +ContainerPanel.sectorSizeLabel.text=\u30bb\u30af\u30bf\u30fc\u30b5\u30a4\u30ba: +ContainerPanel.md5HashLabel.text=MD5: +ContainerPanel.sha1HashLabel.text=SHA1: +ContainerPanel.sha256HashLabel.text=SHA256: +ContainerPanel.filePathsLabel.text=\u30d5\u30a1\u30a4\u30eb\u30d1\u30b9: +ContainerPanel.displayNameValue.text= +ContainerPanel.originalNameValue.text= +ContainerPanel.deviceIdValue.toolTipText= +ContainerPanel.deviceIdValue.text= +ContainerPanel.timeZoneValue.text= +ContainerPanel.imageTypeValue.toolTipText= +ContainerPanel.imageTypeValue.text= +ContainerPanel.sizeValue.text= +ContainerPanel.sectorSizeValue.text= +ContainerPanel.md5HashValue.toolTipText= +ContainerPanel.md5HashValue.text= +ContainerPanel.sha1HashValue.text= +ContainerPanel.sha256HashValue.text= +ContainerPanel.filePathsTable.columnModel.title0= +ContainerPanel.acquisitionDetailsTextArea.text= +ContainerPanel.acquisitionDetailsLabel.text=\u53d6\u5f97\u306e\u8a73\u7d30: +ContainerPanel.unallocatedSizeLabel.text=\u672a\u4f7f\u7528\u9818\u57df: +ContainerPanel.unallocatedSizeValue.text= DataSourceSummaryCountsPanel.byMimeTypeLabel.text=MIME\u30bf\u30a4\u30d7\u5225\u30d5\u30a1\u30a4\u30eb DataSourceSummaryCountsPanel.byCategoryLabel.text=\u30ab\u30c6\u30b4\u30ea\u30fc\u5225\u30d5\u30a1\u30a4\u30eb DataSourceSummaryCountsPanel.jLabel1.text=\u30bf\u30a4\u30d7\u5225\u7d50\u679c diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryDetailsPanel.form b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/ContainerPanel.form similarity index 84% rename from Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryDetailsPanel.form rename to Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/ContainerPanel.form index 3060e67efb..b8dd1a2fa8 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryDetailsPanel.form +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/ContainerPanel.form @@ -40,7 +40,7 @@ - + @@ -52,7 +52,7 @@ - + @@ -64,7 +64,7 @@ - + @@ -76,7 +76,7 @@ - + @@ -88,7 +88,7 @@ - + @@ -100,7 +100,7 @@ - + @@ -112,10 +112,10 @@ - + - + @@ -152,7 +152,7 @@ - <ResourceString bundle="org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties" key="DataSourceSummaryDetailsPanel.filePathsTable.columnModel.title0" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> + <ResourceString bundle="org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties" key="ContainerPanel.filePathsTable.columnModel.title0" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> @@ -169,7 +169,7 @@ - + @@ -181,10 +181,10 @@ - + - + @@ -196,10 +196,10 @@ - + - + @@ -211,7 +211,7 @@ - + @@ -223,7 +223,7 @@ - + @@ -235,7 +235,7 @@ - + @@ -247,7 +247,7 @@ - + @@ -259,7 +259,7 @@ - + @@ -271,7 +271,7 @@ - + @@ -283,7 +283,7 @@ - + @@ -295,7 +295,7 @@ - + @@ -307,7 +307,7 @@ - + @@ -319,7 +319,7 @@ - + @@ -331,7 +331,7 @@ - + @@ -343,7 +343,7 @@ - + @@ -373,7 +373,7 @@ - + @@ -415,7 +415,7 @@ - + @@ -427,7 +427,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryDetailsPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/ContainerPanel.java similarity index 77% rename from Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryDetailsPanel.java rename to Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/ContainerPanel.java index c9df74d2b8..8f11e0dcd3 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryDetailsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/ContainerPanel.java @@ -18,13 +18,20 @@ */ package org.sleuthkit.autopsy.datasourcesummary.ui; -import java.text.DecimalFormat; +import java.beans.PropertyChangeEvent; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import java.util.logging.Level; import org.sleuthkit.autopsy.coreutils.Logger; import javax.swing.table.DefaultTableModel; -import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.datasourcesummary.datamodel.DataSourceDetailsSummary; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.ContainerSummary; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchResult.ResultType; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchWorker.DataFetchComponents; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DefaultUpdateGovernor; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.UpdateGovernor; import org.sleuthkit.datamodel.DataSource; import org.sleuthkit.datamodel.Image; import org.sleuthkit.datamodel.TskCoreException; @@ -32,34 +39,122 @@ import org.sleuthkit.datamodel.TskCoreException; /** * Panel to display additional details associated with a specific DataSource */ -class DataSourceSummaryDetailsPanel extends BaseDataSourceSummaryPanel { +class ContainerPanel extends BaseDataSourceSummaryPanel { + + /** + * Data payload for the Container panel. + */ + private static class ContainerPanelData { + + private final DataSource dataSource; + private final Long unallocatedFilesSize; + + /** + * Main constructor. + * + * @param dataSource The original datasource. + * @param unallocatedFilesSize The unallocated file size. + */ + ContainerPanelData(DataSource dataSource, Long unallocatedFilesSize) { + this.dataSource = dataSource; + this.unallocatedFilesSize = unallocatedFilesSize; + } + + /** + * @return The original datasource. + */ + DataSource getDataSource() { + return dataSource; + } + + /** + * @return The unallocated file size. + */ + Long getUnallocatedFilesSize() { + return unallocatedFilesSize; + } + } + + // set of case events for which to call update (if the name changes, that will impact data shown) + private static final Set CASE_EVENT_SET = new HashSet<>(Arrays.asList( + Case.Events.DATA_SOURCE_NAME_CHANGED + )); + + // governor for handling these updates + private static final UpdateGovernor CONTAINER_UPDATES = new DefaultUpdateGovernor() { + + @Override + public Set getCaseEventUpdates() { + return CASE_EVENT_SET; + } + + @Override + public boolean isRefreshRequiredForCaseEvent(PropertyChangeEvent evt) { + return true; + } + + }; //Because this panel was made using the gridbaglayout and netbean's Customize Layout tool it will be best to continue to modify it through that private static final long serialVersionUID = 1L; - private static final Integer SIZE_COVERSION_CONSTANT = 1000; - private static final DecimalFormat APPROXIMATE_SIZE_FORMAT = new DecimalFormat("#.##"); - private static final Logger logger = Logger.getLogger(DataSourceSummaryDetailsPanel.class.getName()); + private static final Logger logger = Logger.getLogger(ContainerPanel.class.getName()); + + private final List> dataFetchComponents; /** - * Creates new form DataSourceSummaryDetailsPanel + * Creates a new form ContainerPanel. */ - @Messages({"DataSourceSummaryDetailsPanel.getDataSources.error.text=Failed to get the list of datasources for the current case.", - "DataSourceSummaryDetailsPanel.getDataSources.error.title=Load Failure"}) - DataSourceSummaryDetailsPanel() { + ContainerPanel() { + this(new ContainerSummary()); + } + + /** + * Creates new form ContainerPanel. + */ + ContainerPanel(ContainerSummary containerSummary) { + super(containerSummary, CONTAINER_UPDATES); + + dataFetchComponents = Arrays.asList( + new DataFetchComponents<>( + (dataSource) -> { + return new ContainerPanelData( + dataSource, + containerSummary.getSizeOfUnallocatedFiles(dataSource) + ); + }, + (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); + } else { + if (result == null) { + logger.log(Level.WARNING, "No data fetch result was provided to the ContainerPanel."); + } else { + logger.log(Level.WARNING, "An exception occurred while attempting to fetch data for the ContainerPanel.", + result.getException()); + } + + updateDetailsPanelData(null, null); + } + } + ) + ); + initComponents(); setDataSource(null); } @Override protected void onNewDataSource(DataSource dataSource) { - if (dataSource == null || !Case.isCaseOpen()) { - updateDetailsPanelData(null, null, null, null); - } else { - updateDetailsPanelData(dataSource, - DataSourceDetailsSummary.getSizeOfUnallocatedFiles(dataSource), - DataSourceDetailsSummary.getOperatingSystems(dataSource), - DataSourceDetailsSummary.getDataSourceType(dataSource)); - } + fetchInformation(dataSource); + } + + @Override + protected void fetchInformation(DataSource dataSource) { + fetchInformation(dataFetchComponents, dataSource); } /** @@ -67,11 +162,10 @@ class DataSourceSummaryDetailsPanel extends BaseDataSourceSummaryPanel { * * @param selectedDataSource the DataSource to display details about. */ - private void updateDetailsPanelData(DataSource selectedDataSource, Long unallocatedFilesSize, String osDetails, String usage) { + private void updateDetailsPanelData(DataSource selectedDataSource, Long unallocatedFilesSize) { clearTableValues(); if (selectedDataSource != null) { - unallocatedSizeValue.setText(getSizeString(unallocatedFilesSize)); - + unallocatedSizeValue.setText(SizeRepresentationUtil.getSizeString(unallocatedFilesSize)); timeZoneValue.setText(selectedDataSource.getTimeZone()); displayNameValue.setText(selectedDataSource.getName()); originalNameValue.setText(selectedDataSource.getName()); @@ -100,8 +194,8 @@ class DataSourceSummaryDetailsPanel extends BaseDataSourceSummaryPanel { */ private void setFieldsForImage(Image selectedImage) { imageTypeValue.setText(selectedImage.getType().getName()); - sizeValue.setText(getSizeString(selectedImage.getSize())); - sectorSizeValue.setText(getSizeString(selectedImage.getSsize())); + sizeValue.setText(SizeRepresentationUtil.getSizeString(selectedImage.getSize())); + sectorSizeValue.setText(SizeRepresentationUtil.getSizeString(selectedImage.getSsize())); for (String path : selectedImage.getPaths()) { ((DefaultTableModel) filePathsTable.getModel()).addRow(new Object[]{path}); @@ -139,55 +233,6 @@ class DataSourceSummaryDetailsPanel extends BaseDataSourceSummaryPanel { } } - /** - * Get a long size in bytes as a string formated to be read by users. - * - * @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 - */ - @Messages({ - "DataSourceSummaryDetailsPanel.units.bytes= bytes", - "DataSourceSummaryDetailsPanel.units.kilobytes= kB", - "DataSourceSummaryDetailsPanel.units.megabytes= MB", - "DataSourceSummaryDetailsPanel.units.gigabytes= GB", - "DataSourceSummaryDetailsPanel.units.terabytes= TB", - "DataSourceSummaryDetailsPanel.units.petabytes= PB" - }) - private static String getSizeString(Long size) { - if (size == null) { - return ""; - } - double approximateSize = size; - if (approximateSize < SIZE_COVERSION_CONSTANT) { - return String.valueOf(size) + Bundle.DataSourceSummaryDetailsPanel_units_bytes(); - } - approximateSize /= SIZE_COVERSION_CONSTANT; - if (approximateSize < SIZE_COVERSION_CONSTANT) { - return APPROXIMATE_SIZE_FORMAT.format(approximateSize) + Bundle.DataSourceSummaryDetailsPanel_units_kilobytes() - + " (" + String.valueOf(size) + Bundle.DataSourceSummaryDetailsPanel_units_bytes() + ")"; - } - approximateSize /= SIZE_COVERSION_CONSTANT; - if (approximateSize < SIZE_COVERSION_CONSTANT) { - return APPROXIMATE_SIZE_FORMAT.format(approximateSize) + Bundle.DataSourceSummaryDetailsPanel_units_megabytes() - + " (" + String.valueOf(size) + Bundle.DataSourceSummaryDetailsPanel_units_bytes() + ")"; - } - approximateSize /= SIZE_COVERSION_CONSTANT; - if (approximateSize < SIZE_COVERSION_CONSTANT) { - return APPROXIMATE_SIZE_FORMAT.format(approximateSize) + Bundle.DataSourceSummaryDetailsPanel_units_gigabytes() - + " (" + String.valueOf(size) + Bundle.DataSourceSummaryDetailsPanel_units_bytes() + ")"; - } - approximateSize /= SIZE_COVERSION_CONSTANT; - if (approximateSize < SIZE_COVERSION_CONSTANT) { - return APPROXIMATE_SIZE_FORMAT.format(approximateSize) + Bundle.DataSourceSummaryDetailsPanel_units_terabytes() - + " (" + String.valueOf(size) + Bundle.DataSourceSummaryDetailsPanel_units_bytes() + ")"; - } - approximateSize /= SIZE_COVERSION_CONSTANT; - return APPROXIMATE_SIZE_FORMAT.format(approximateSize) + Bundle.DataSourceSummaryDetailsPanel_units_petabytes() - + " (" + String.valueOf(size) + Bundle.DataSourceSummaryDetailsPanel_units_bytes() + ")"; - } - /** * Update the visibility of all fields and their labels based on whether * they have contents. Empty fields have them and their contents hidden. @@ -287,7 +332,7 @@ class DataSourceSummaryDetailsPanel extends BaseDataSourceSummaryPanel { jPanel1.setLayout(new java.awt.GridBagLayout()); - org.openide.awt.Mnemonics.setLocalizedText(displayNameLabel, org.openide.util.NbBundle.getMessage(DataSourceSummaryDetailsPanel.class, "DataSourceSummaryDetailsPanel.displayNameLabel.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(displayNameLabel, org.openide.util.NbBundle.getMessage(ContainerPanel.class, "ContainerPanel.displayNameLabel.text")); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 0; @@ -296,7 +341,7 @@ class DataSourceSummaryDetailsPanel extends BaseDataSourceSummaryPanel { gridBagConstraints.insets = new java.awt.Insets(10, 10, 0, 4); jPanel1.add(displayNameLabel, gridBagConstraints); - org.openide.awt.Mnemonics.setLocalizedText(originalNameLabel, org.openide.util.NbBundle.getMessage(DataSourceSummaryDetailsPanel.class, "DataSourceSummaryDetailsPanel.originalNameLabel.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(originalNameLabel, org.openide.util.NbBundle.getMessage(ContainerPanel.class, "ContainerPanel.originalNameLabel.text")); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 1; @@ -305,7 +350,7 @@ class DataSourceSummaryDetailsPanel extends BaseDataSourceSummaryPanel { gridBagConstraints.insets = new java.awt.Insets(0, 10, 0, 4); jPanel1.add(originalNameLabel, gridBagConstraints); - org.openide.awt.Mnemonics.setLocalizedText(sha1HashValue, org.openide.util.NbBundle.getMessage(DataSourceSummaryDetailsPanel.class, "DataSourceSummaryDetailsPanel.sha1HashValue.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(sha1HashValue, org.openide.util.NbBundle.getMessage(ContainerPanel.class, "ContainerPanel.sha1HashValue.text")); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 1; gridBagConstraints.gridy = 12; @@ -316,7 +361,7 @@ class DataSourceSummaryDetailsPanel extends BaseDataSourceSummaryPanel { gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 10); jPanel1.add(sha1HashValue, gridBagConstraints); - org.openide.awt.Mnemonics.setLocalizedText(displayNameValue, org.openide.util.NbBundle.getMessage(DataSourceSummaryDetailsPanel.class, "DataSourceSummaryDetailsPanel.displayNameValue.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(displayNameValue, org.openide.util.NbBundle.getMessage(ContainerPanel.class, "ContainerPanel.displayNameValue.text")); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 1; gridBagConstraints.gridy = 0; @@ -327,7 +372,7 @@ class DataSourceSummaryDetailsPanel extends BaseDataSourceSummaryPanel { gridBagConstraints.insets = new java.awt.Insets(10, 0, 0, 10); jPanel1.add(displayNameValue, gridBagConstraints); - org.openide.awt.Mnemonics.setLocalizedText(sha256HashValue, org.openide.util.NbBundle.getMessage(DataSourceSummaryDetailsPanel.class, "DataSourceSummaryDetailsPanel.sha256HashValue.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(sha256HashValue, org.openide.util.NbBundle.getMessage(ContainerPanel.class, "ContainerPanel.sha256HashValue.text")); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 1; gridBagConstraints.gridy = 13; @@ -338,7 +383,7 @@ class DataSourceSummaryDetailsPanel extends BaseDataSourceSummaryPanel { gridBagConstraints.insets = new java.awt.Insets(0, 0, 6, 10); jPanel1.add(sha256HashValue, gridBagConstraints); - org.openide.awt.Mnemonics.setLocalizedText(originalNameValue, org.openide.util.NbBundle.getMessage(DataSourceSummaryDetailsPanel.class, "DataSourceSummaryDetailsPanel.originalNameValue.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(originalNameValue, org.openide.util.NbBundle.getMessage(ContainerPanel.class, "ContainerPanel.originalNameValue.text")); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 1; gridBagConstraints.gridy = 1; @@ -349,8 +394,8 @@ class DataSourceSummaryDetailsPanel extends BaseDataSourceSummaryPanel { gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 10); jPanel1.add(originalNameValue, gridBagConstraints); - org.openide.awt.Mnemonics.setLocalizedText(deviceIdValue, org.openide.util.NbBundle.getMessage(DataSourceSummaryDetailsPanel.class, "DataSourceSummaryDetailsPanel.deviceIdValue.text")); // NOI18N - deviceIdValue.setToolTipText(org.openide.util.NbBundle.getMessage(DataSourceSummaryDetailsPanel.class, "DataSourceSummaryDetailsPanel.deviceIdValue.toolTipText")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(deviceIdValue, org.openide.util.NbBundle.getMessage(ContainerPanel.class, "ContainerPanel.deviceIdValue.text")); // NOI18N + deviceIdValue.setToolTipText(org.openide.util.NbBundle.getMessage(ContainerPanel.class, "ContainerPanel.deviceIdValue.toolTipText")); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 1; gridBagConstraints.gridy = 2; @@ -382,7 +427,7 @@ class DataSourceSummaryDetailsPanel extends BaseDataSourceSummaryPanel { filePathsTable.setTableHeader(null); filePathsScrollPane.setViewportView(filePathsTable); if (filePathsTable.getColumnModel().getColumnCount() > 0) { - filePathsTable.getColumnModel().getColumn(0).setHeaderValue(org.openide.util.NbBundle.getMessage(DataSourceSummaryDetailsPanel.class, "DataSourceSummaryDetailsPanel.filePathsTable.columnModel.title0")); // NOI18N + filePathsTable.getColumnModel().getColumn(0).setHeaderValue(org.openide.util.NbBundle.getMessage(ContainerPanel.class, "ContainerPanel.filePathsTable.columnModel.title0")); // NOI18N } gridBagConstraints = new java.awt.GridBagConstraints(); @@ -396,7 +441,7 @@ class DataSourceSummaryDetailsPanel extends BaseDataSourceSummaryPanel { gridBagConstraints.insets = new java.awt.Insets(6, 0, 10, 10); jPanel1.add(filePathsScrollPane, gridBagConstraints); - org.openide.awt.Mnemonics.setLocalizedText(timeZoneValue, org.openide.util.NbBundle.getMessage(DataSourceSummaryDetailsPanel.class, "DataSourceSummaryDetailsPanel.timeZoneValue.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(timeZoneValue, org.openide.util.NbBundle.getMessage(ContainerPanel.class, "ContainerPanel.timeZoneValue.text")); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 1; gridBagConstraints.gridy = 5; @@ -407,8 +452,8 @@ class DataSourceSummaryDetailsPanel extends BaseDataSourceSummaryPanel { gridBagConstraints.insets = new java.awt.Insets(0, 0, 6, 10); jPanel1.add(timeZoneValue, gridBagConstraints); - org.openide.awt.Mnemonics.setLocalizedText(imageTypeValue, org.openide.util.NbBundle.getMessage(DataSourceSummaryDetailsPanel.class, "DataSourceSummaryDetailsPanel.imageTypeValue.text")); // NOI18N - imageTypeValue.setToolTipText(org.openide.util.NbBundle.getMessage(DataSourceSummaryDetailsPanel.class, "DataSourceSummaryDetailsPanel.imageTypeValue.toolTipText")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(imageTypeValue, org.openide.util.NbBundle.getMessage(ContainerPanel.class, "ContainerPanel.imageTypeValue.text")); // NOI18N + imageTypeValue.setToolTipText(org.openide.util.NbBundle.getMessage(ContainerPanel.class, "ContainerPanel.imageTypeValue.toolTipText")); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 1; gridBagConstraints.gridy = 7; @@ -419,8 +464,8 @@ class DataSourceSummaryDetailsPanel extends BaseDataSourceSummaryPanel { gridBagConstraints.insets = new java.awt.Insets(6, 0, 0, 10); jPanel1.add(imageTypeValue, gridBagConstraints); - org.openide.awt.Mnemonics.setLocalizedText(md5HashValue, org.openide.util.NbBundle.getMessage(DataSourceSummaryDetailsPanel.class, "DataSourceSummaryDetailsPanel.md5HashValue.text")); // NOI18N - md5HashValue.setToolTipText(org.openide.util.NbBundle.getMessage(DataSourceSummaryDetailsPanel.class, "DataSourceSummaryDetailsPanel.md5HashValue.toolTipText")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(md5HashValue, org.openide.util.NbBundle.getMessage(ContainerPanel.class, "ContainerPanel.md5HashValue.text")); // NOI18N + md5HashValue.setToolTipText(org.openide.util.NbBundle.getMessage(ContainerPanel.class, "ContainerPanel.md5HashValue.toolTipText")); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 1; gridBagConstraints.gridy = 11; @@ -431,7 +476,7 @@ class DataSourceSummaryDetailsPanel extends BaseDataSourceSummaryPanel { gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 10); jPanel1.add(md5HashValue, gridBagConstraints); - org.openide.awt.Mnemonics.setLocalizedText(sectorSizeValue, org.openide.util.NbBundle.getMessage(DataSourceSummaryDetailsPanel.class, "DataSourceSummaryDetailsPanel.sectorSizeValue.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(sectorSizeValue, org.openide.util.NbBundle.getMessage(ContainerPanel.class, "ContainerPanel.sectorSizeValue.text")); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 1; gridBagConstraints.gridy = 10; @@ -442,7 +487,7 @@ class DataSourceSummaryDetailsPanel extends BaseDataSourceSummaryPanel { gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 10); jPanel1.add(sectorSizeValue, gridBagConstraints); - org.openide.awt.Mnemonics.setLocalizedText(sizeValue, org.openide.util.NbBundle.getMessage(DataSourceSummaryDetailsPanel.class, "DataSourceSummaryDetailsPanel.sizeValue.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(sizeValue, org.openide.util.NbBundle.getMessage(ContainerPanel.class, "ContainerPanel.sizeValue.text")); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 1; gridBagConstraints.gridy = 8; @@ -453,7 +498,7 @@ class DataSourceSummaryDetailsPanel extends BaseDataSourceSummaryPanel { gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 10); jPanel1.add(sizeValue, gridBagConstraints); - org.openide.awt.Mnemonics.setLocalizedText(filePathsLabel, org.openide.util.NbBundle.getMessage(DataSourceSummaryDetailsPanel.class, "DataSourceSummaryDetailsPanel.filePathsLabel.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(filePathsLabel, org.openide.util.NbBundle.getMessage(ContainerPanel.class, "ContainerPanel.filePathsLabel.text")); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 14; @@ -463,7 +508,7 @@ class DataSourceSummaryDetailsPanel extends BaseDataSourceSummaryPanel { gridBagConstraints.insets = new java.awt.Insets(6, 10, 10, 4); jPanel1.add(filePathsLabel, gridBagConstraints); - org.openide.awt.Mnemonics.setLocalizedText(sha256HashLabel, org.openide.util.NbBundle.getMessage(DataSourceSummaryDetailsPanel.class, "DataSourceSummaryDetailsPanel.sha256HashLabel.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(sha256HashLabel, org.openide.util.NbBundle.getMessage(ContainerPanel.class, "ContainerPanel.sha256HashLabel.text")); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 13; @@ -472,7 +517,7 @@ class DataSourceSummaryDetailsPanel extends BaseDataSourceSummaryPanel { gridBagConstraints.insets = new java.awt.Insets(0, 10, 6, 4); jPanel1.add(sha256HashLabel, gridBagConstraints); - org.openide.awt.Mnemonics.setLocalizedText(sha1HashLabel, org.openide.util.NbBundle.getMessage(DataSourceSummaryDetailsPanel.class, "DataSourceSummaryDetailsPanel.sha1HashLabel.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(sha1HashLabel, org.openide.util.NbBundle.getMessage(ContainerPanel.class, "ContainerPanel.sha1HashLabel.text")); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 12; @@ -481,7 +526,7 @@ class DataSourceSummaryDetailsPanel extends BaseDataSourceSummaryPanel { gridBagConstraints.insets = new java.awt.Insets(0, 10, 0, 4); jPanel1.add(sha1HashLabel, gridBagConstraints); - org.openide.awt.Mnemonics.setLocalizedText(md5HashLabel, org.openide.util.NbBundle.getMessage(DataSourceSummaryDetailsPanel.class, "DataSourceSummaryDetailsPanel.md5HashLabel.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(md5HashLabel, org.openide.util.NbBundle.getMessage(ContainerPanel.class, "ContainerPanel.md5HashLabel.text")); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 11; @@ -490,7 +535,7 @@ class DataSourceSummaryDetailsPanel extends BaseDataSourceSummaryPanel { gridBagConstraints.insets = new java.awt.Insets(0, 10, 0, 4); jPanel1.add(md5HashLabel, gridBagConstraints); - org.openide.awt.Mnemonics.setLocalizedText(sectorSizeLabel, org.openide.util.NbBundle.getMessage(DataSourceSummaryDetailsPanel.class, "DataSourceSummaryDetailsPanel.sectorSizeLabel.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(sectorSizeLabel, org.openide.util.NbBundle.getMessage(ContainerPanel.class, "ContainerPanel.sectorSizeLabel.text")); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 10; @@ -499,7 +544,7 @@ class DataSourceSummaryDetailsPanel extends BaseDataSourceSummaryPanel { gridBagConstraints.insets = new java.awt.Insets(0, 10, 0, 4); jPanel1.add(sectorSizeLabel, gridBagConstraints); - org.openide.awt.Mnemonics.setLocalizedText(sizeLabel, org.openide.util.NbBundle.getMessage(DataSourceSummaryDetailsPanel.class, "DataSourceSummaryDetailsPanel.sizeLabel.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(sizeLabel, org.openide.util.NbBundle.getMessage(ContainerPanel.class, "ContainerPanel.sizeLabel.text")); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 8; @@ -508,7 +553,7 @@ class DataSourceSummaryDetailsPanel extends BaseDataSourceSummaryPanel { gridBagConstraints.insets = new java.awt.Insets(0, 10, 0, 4); jPanel1.add(sizeLabel, gridBagConstraints); - org.openide.awt.Mnemonics.setLocalizedText(imageTypeLabel, org.openide.util.NbBundle.getMessage(DataSourceSummaryDetailsPanel.class, "DataSourceSummaryDetailsPanel.imageTypeLabel.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(imageTypeLabel, org.openide.util.NbBundle.getMessage(ContainerPanel.class, "ContainerPanel.imageTypeLabel.text")); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 7; @@ -517,7 +562,7 @@ class DataSourceSummaryDetailsPanel extends BaseDataSourceSummaryPanel { gridBagConstraints.insets = new java.awt.Insets(6, 10, 0, 4); jPanel1.add(imageTypeLabel, gridBagConstraints); - org.openide.awt.Mnemonics.setLocalizedText(acquisitionDetailsLabel, org.openide.util.NbBundle.getMessage(DataSourceSummaryDetailsPanel.class, "DataSourceSummaryDetailsPanel.acquisitionDetailsLabel.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(acquisitionDetailsLabel, org.openide.util.NbBundle.getMessage(ContainerPanel.class, "ContainerPanel.acquisitionDetailsLabel.text")); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 6; @@ -527,7 +572,7 @@ class DataSourceSummaryDetailsPanel extends BaseDataSourceSummaryPanel { gridBagConstraints.insets = new java.awt.Insets(6, 10, 6, 4); jPanel1.add(acquisitionDetailsLabel, gridBagConstraints); - org.openide.awt.Mnemonics.setLocalizedText(timeZoneLabel, org.openide.util.NbBundle.getMessage(DataSourceSummaryDetailsPanel.class, "DataSourceSummaryDetailsPanel.timeZoneLabel.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(timeZoneLabel, org.openide.util.NbBundle.getMessage(ContainerPanel.class, "ContainerPanel.timeZoneLabel.text")); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 5; @@ -536,7 +581,7 @@ class DataSourceSummaryDetailsPanel extends BaseDataSourceSummaryPanel { gridBagConstraints.insets = new java.awt.Insets(0, 10, 6, 4); jPanel1.add(timeZoneLabel, gridBagConstraints); - org.openide.awt.Mnemonics.setLocalizedText(deviceIdLabel, org.openide.util.NbBundle.getMessage(DataSourceSummaryDetailsPanel.class, "DataSourceSummaryDetailsPanel.deviceIdLabel.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(deviceIdLabel, org.openide.util.NbBundle.getMessage(ContainerPanel.class, "ContainerPanel.deviceIdLabel.text")); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 2; @@ -549,7 +594,7 @@ class DataSourceSummaryDetailsPanel extends BaseDataSourceSummaryPanel { acquisitionDetailsTextArea.setBackground(javax.swing.UIManager.getDefaults().getColor("TextArea.disabledBackground")); acquisitionDetailsTextArea.setColumns(20); acquisitionDetailsTextArea.setRows(4); - acquisitionDetailsTextArea.setText(org.openide.util.NbBundle.getMessage(DataSourceSummaryDetailsPanel.class, "DataSourceSummaryDetailsPanel.acquisitionDetailsTextArea.text")); // NOI18N + acquisitionDetailsTextArea.setText(org.openide.util.NbBundle.getMessage(ContainerPanel.class, "ContainerPanel.acquisitionDetailsTextArea.text")); // NOI18N acquisitionDetailsTextArea.setBorder(null); acquisitionDetailsScrollPane.setViewportView(acquisitionDetailsTextArea); @@ -577,7 +622,7 @@ class DataSourceSummaryDetailsPanel extends BaseDataSourceSummaryPanel { gridBagConstraints.weighty = 0.1; jPanel1.add(filler2, gridBagConstraints); - org.openide.awt.Mnemonics.setLocalizedText(unallocatedSizeLabel, org.openide.util.NbBundle.getMessage(DataSourceSummaryDetailsPanel.class, "DataSourceSummaryDetailsPanel.unallocatedSizeLabel.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(unallocatedSizeLabel, org.openide.util.NbBundle.getMessage(ContainerPanel.class, "ContainerPanel.unallocatedSizeLabel.text")); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 9; @@ -586,7 +631,7 @@ class DataSourceSummaryDetailsPanel extends BaseDataSourceSummaryPanel { gridBagConstraints.insets = new java.awt.Insets(0, 10, 0, 4); jPanel1.add(unallocatedSizeLabel, gridBagConstraints); - org.openide.awt.Mnemonics.setLocalizedText(unallocatedSizeValue, org.openide.util.NbBundle.getMessage(DataSourceSummaryDetailsPanel.class, "DataSourceSummaryDetailsPanel.unallocatedSizeValue.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(unallocatedSizeValue, org.openide.util.NbBundle.getMessage(ContainerPanel.class, "ContainerPanel.unallocatedSizeValue.text")); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 1; gridBagConstraints.gridy = 9; diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryCountsPanel.form b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryCountsPanel.form deleted file mode 100644 index c9c1930787..0000000000 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryCountsPanel.form +++ /dev/null @@ -1,222 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryCountsPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryCountsPanel.java deleted file mode 100644 index 306a036941..0000000000 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryCountsPanel.java +++ /dev/null @@ -1,289 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2019 Basis Technology Corp. - * Contact: carrier sleuthkit org - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.sleuthkit.autopsy.datasourcesummary.ui; - -import org.sleuthkit.autopsy.datasourcesummary.uiutils.NonEditableTableModel; -import java.util.Map; -import javax.swing.JLabel; -import javax.swing.table.DefaultTableCellRenderer; -import org.openide.util.NbBundle.Messages; -import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.datasourcesummary.datamodel.DataSourceCountsSummary; - -import org.sleuthkit.datamodel.DataSource; - -/** - * Panel for displaying summary information on the known files present in the - * specified DataSource - */ -@Messages({ - "DataSourceSummaryCountsPanel.ArtifactCountsTableModel.type.header=Result Type", - "DataSourceSummaryCountsPanel.ArtifactCountsTableModel.count.header=Count", - "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.type.header=File Type", - "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.count.header=Count" -}) -class DataSourceSummaryCountsPanel extends BaseDataSourceSummaryPanel { - - private static final long serialVersionUID = 1L; - - // Result returned for a data model if no data found. - private static final Object[][] EMPTY_PAIRS = new Object[][]{}; - - // column headers for file by category table - private static final Object[] FILE_BY_CATEGORY_COLUMN_HEADERS = new Object[]{ - Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_type_header(), - Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_count_header() - }; - - // column headers for artifact counts table - private static final Object[] ARTIFACT_COUNTS_COLUMN_HEADERS = new Object[]{ - Bundle.DataSourceSummaryCountsPanel_ArtifactCountsTableModel_type_header(), - Bundle.DataSourceSummaryCountsPanel_ArtifactCountsTableModel_count_header() - }; - - private final DefaultTableCellRenderer rightAlignedRenderer = new DefaultTableCellRenderer(); - - private final FileTypePieChart fileTypePieChart = new FileTypePieChart(); - - /** - * Creates new form DataSourceSummaryCountsPanel - */ - DataSourceSummaryCountsPanel() { - rightAlignedRenderer.setHorizontalAlignment(JLabel.RIGHT); - initComponents(); - fileCountsByCategoryTable.getTableHeader().setReorderingAllowed(false); - artifactCountsTable.getTableHeader().setReorderingAllowed(false); - setDataSource(null); - } - - @Override - protected void onNewDataSource(DataSource dataSource) { - if (dataSource == null || !Case.isCaseOpen()) { - updateCountsTableData(EMPTY_PAIRS, EMPTY_PAIRS); - } else { - updateCountsTableData(getFileCategoryModel(dataSource), getArtifactCountsModel(dataSource)); - } - this.fileTypePieChart.setDataSource(dataSource); - } - - /** - * Specify the DataSource to display file information for. - * - * @param fileCategoryDataModel The file category data model. - * @param artifactDataModel The artifact type data model. - */ - private void updateCountsTableData(Object[][] fileCategoryDataModel, Object[][] artifactDataModel) { - fileCountsByCategoryTable.setModel(new NonEditableTableModel(fileCategoryDataModel, FILE_BY_CATEGORY_COLUMN_HEADERS)); - fileCountsByCategoryTable.getColumnModel().getColumn(1).setCellRenderer(rightAlignedRenderer); - fileCountsByCategoryTable.getColumnModel().getColumn(0).setPreferredWidth(130); - - artifactCountsTable.setModel(new NonEditableTableModel(artifactDataModel, ARTIFACT_COUNTS_COLUMN_HEADERS)); - artifactCountsTable.getColumnModel().getColumn(0).setPreferredWidth(230); - artifactCountsTable.getColumnModel().getColumn(1).setCellRenderer(rightAlignedRenderer); - - this.repaint(); - } - - /** - * Determines the JTable data model for datasource file categories. - * - * @param dataSource The DataSource. - * - * @return The model to be used with a JTable. - */ - @Messages({ - "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.all.row=All", - "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.allocated.row=Allocated", - "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.unallocated.row=Unallocated", - "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.slack.row=Slack", - "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.directory.row=Directory" - }) - private static Object[][] getFileCategoryModel(DataSource selectedDataSource) { - Long fileCount = zeroIfNull(DataSourceCountsSummary.getCountOfFiles(selectedDataSource)); - Long unallocatedFiles = zeroIfNull(DataSourceCountsSummary.getCountOfUnallocatedFiles(selectedDataSource)); - Long allocatedFiles = zeroIfNull(DataSourceCountsSummary.getCountOfAllocatedFiles(selectedDataSource)); - Long slackFiles = zeroIfNull(DataSourceCountsSummary.getCountOfSlackFiles(selectedDataSource)); - Long directories = zeroIfNull(DataSourceCountsSummary.getCountOfDirectories(selectedDataSource)); - - return new Object[][]{ - new Object[]{Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_all_row(), fileCount}, - new Object[]{Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_allocated_row(), allocatedFiles}, - new Object[]{Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_unallocated_row(), unallocatedFiles}, - new Object[]{Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_slack_row(), slackFiles}, - new Object[]{Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_directory_row(), directories} - }; - } - - /** - * Returns 0 if value is null. - * - * @param origValue The original value. - * - * @return The value or 0 if null. - */ - private static Long zeroIfNull(Long origValue) { - return origValue == null ? 0 : origValue; - } - - /** - * The counts of different artifact types found in a DataSource. - * - * @param selectedDataSource The DataSource. - * - * @return The JTable data model of counts of artifact types. - */ - private static Object[][] getArtifactCountsModel(DataSource selectedDataSource) { - Map artifactMapping = DataSourceCountsSummary.getCountsOfArtifactsByType(selectedDataSource); - if (artifactMapping == null) { - return EMPTY_PAIRS; - } - - return artifactMapping.entrySet().stream() - .filter((entrySet) -> entrySet != null && entrySet.getKey() != null) - .sorted((a, b) -> a.getKey().compareTo(b.getKey())) - .map((entrySet) -> new Object[]{entrySet.getKey(), entrySet.getValue()}) - .toArray(Object[][]::new); - } - - /** - * This method is called from within the constructor to initialize the form. - * WARNING: Do NOT modify this code. The content of this method is always - * regenerated by the Form Editor. - */ - @SuppressWarnings("unchecked") - // //GEN-BEGIN:initComponents - private void initComponents() { - - javax.swing.JScrollPane scrollParent = new javax.swing.JScrollPane(); - javax.swing.JPanel parentPanel = new javax.swing.JPanel(); - javax.swing.JScrollPane fileCountsByCategoryScrollPane = new javax.swing.JScrollPane(); - fileCountsByCategoryTable = new javax.swing.JTable(); - javax.swing.JLabel byCategoryLabel = new javax.swing.JLabel(); - javax.swing.JLabel resultsByTypeLabel = new javax.swing.JLabel(); - javax.swing.JScrollPane artifactCountsScrollPane = new javax.swing.JScrollPane(); - artifactCountsTable = new javax.swing.JTable(); - javax.swing.JPanel fileTypePiePanel = fileTypePieChart; - javax.swing.JPanel filesByCatParent = new javax.swing.JPanel(); - javax.swing.JPanel resultsByTypeParent = new javax.swing.JPanel(); - - parentPanel.setMinimumSize(new java.awt.Dimension(840, 320)); - - fileCountsByCategoryScrollPane.setViewportView(fileCountsByCategoryTable); - - org.openide.awt.Mnemonics.setLocalizedText(byCategoryLabel, org.openide.util.NbBundle.getMessage(DataSourceSummaryCountsPanel.class, "DataSourceSummaryCountsPanel.byCategoryLabel.text")); // NOI18N - - org.openide.awt.Mnemonics.setLocalizedText(resultsByTypeLabel, org.openide.util.NbBundle.getMessage(DataSourceSummaryCountsPanel.class, "DataSourceSummaryCountsPanel.resultsByTypeLabel.text")); // NOI18N - - artifactCountsTable.setAutoCreateRowSorter(true); - artifactCountsScrollPane.setViewportView(artifactCountsTable); - - fileTypePiePanel.setPreferredSize(new java.awt.Dimension(400, 300)); - - javax.swing.GroupLayout filesByCatParentLayout = new javax.swing.GroupLayout(filesByCatParent); - filesByCatParent.setLayout(filesByCatParentLayout); - filesByCatParentLayout.setHorizontalGroup( - filesByCatParentLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGap(0, 0, Short.MAX_VALUE) - ); - filesByCatParentLayout.setVerticalGroup( - filesByCatParentLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGap(0, 0, Short.MAX_VALUE) - ); - - javax.swing.GroupLayout resultsByTypeParentLayout = new javax.swing.GroupLayout(resultsByTypeParent); - resultsByTypeParent.setLayout(resultsByTypeParentLayout); - resultsByTypeParentLayout.setHorizontalGroup( - resultsByTypeParentLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGap(0, 0, Short.MAX_VALUE) - ); - resultsByTypeParentLayout.setVerticalGroup( - resultsByTypeParentLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGap(0, 0, Short.MAX_VALUE) - ); - - javax.swing.GroupLayout parentPanelLayout = new javax.swing.GroupLayout(parentPanel); - parentPanel.setLayout(parentPanelLayout); - parentPanelLayout.setHorizontalGroup( - parentPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(parentPanelLayout.createSequentialGroup() - .addContainerGap() - .addComponent(fileTypePiePanel, javax.swing.GroupLayout.PREFERRED_SIZE, 400, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(18, 18, 18) - .addGroup(parentPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(fileCountsByCategoryScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 140, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(byCategoryLabel) - .addComponent(filesByCatParent, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addGap(18, 18, 18) - .addGroup(parentPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(resultsByTypeLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 79, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGroup(parentPanelLayout.createSequentialGroup() - .addComponent(artifactCountsScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 244, javax.swing.GroupLayout.PREFERRED_SIZE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(resultsByTypeParent, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))) - .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) - ); - parentPanelLayout.setVerticalGroup( - parentPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(parentPanelLayout.createSequentialGroup() - .addContainerGap() - .addGroup(parentPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(parentPanelLayout.createSequentialGroup() - .addComponent(fileTypePiePanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) - .addGroup(parentPanelLayout.createSequentialGroup() - .addGroup(parentPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(byCategoryLabel) - .addComponent(resultsByTypeLabel)) - .addGroup(parentPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, parentPanelLayout.createSequentialGroup() - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(resultsByTypeParent, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(148, 148, 148)) - .addGroup(parentPanelLayout.createSequentialGroup() - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(parentPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(artifactCountsScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE) - .addGroup(parentPanelLayout.createSequentialGroup() - .addComponent(fileCountsByCategoryScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 107, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(31, 31, 31) - .addComponent(filesByCatParent, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(0, 0, Short.MAX_VALUE))) - .addContainerGap()))))) - ); - - scrollParent.setViewportView(parentPanel); - - javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); - this.setLayout(layout); - layout.setHorizontalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(scrollParent) - ); - layout.setVerticalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(scrollParent) - ); - }// //GEN-END:initComponents - - - // Variables declaration - do not modify//GEN-BEGIN:variables - private javax.swing.JTable artifactCountsTable; - private javax.swing.JTable fileCountsByCategoryTable; - // End of variables declaration//GEN-END:variables -} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryDialog.form b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryDialog.form index d4edf4b0ee..56b4314181 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryDialog.form +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryDialog.form @@ -67,7 +67,7 @@ - + @@ -79,7 +79,7 @@ - +
diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryDialog.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryDialog.java index 1bc033822d..5269bdff67 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryDialog.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryDialog.java @@ -20,11 +20,13 @@ package org.sleuthkit.autopsy.datasourcesummary.ui; import java.awt.Frame; import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; import java.util.EnumSet; import java.util.Map; import java.util.Observable; import java.util.Observer; import java.util.Set; +import javax.swing.WindowConstants; import javax.swing.event.ListSelectionEvent; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.datasourcesummary.datamodel.CaseDataSourcesSummary; @@ -43,6 +45,7 @@ final class DataSourceSummaryDialog extends javax.swing.JDialog implements Obser private static final Set INGEST_JOB_EVENTS_OF_INTEREST = EnumSet.of(IngestManager.IngestJobEvent.DATA_SOURCE_ANALYSIS_COMPLETED); private final DataSourceBrowser dataSourcesPanel; private final DataSourceSummaryTabbedPane dataSourceSummaryTabbedPane; + private final PropertyChangeListener ingestEventListener; /** * Creates new form DataSourceSummaryDialog for displaying a summary of the @@ -67,8 +70,8 @@ final class DataSourceSummaryDialog extends javax.swing.JDialog implements Obser this.repaint(); } }); - //add listener to refresh jobs with Started status when they complete - IngestManager.getInstance().addIngestJobEventListener(INGEST_JOB_EVENTS_OF_INTEREST, (PropertyChangeEvent evt) -> { + + ingestEventListener = (PropertyChangeEvent evt) -> { if (evt instanceof DataSourceAnalysisCompletedEvent) { DataSourceAnalysisCompletedEvent dsEvent = (DataSourceAnalysisCompletedEvent) evt; if (dsEvent.getResult() == Reason.ANALYSIS_COMPLETED) { @@ -77,10 +80,24 @@ final class DataSourceSummaryDialog extends javax.swing.JDialog implements Obser dataSourcesPanel.refresh(dsEvent.getDataSource().getId(), null); } } - }); + }; + + //add listener to refresh jobs with Started status when they complete + IngestManager.getInstance().addIngestJobEventListener(INGEST_JOB_EVENTS_OF_INTEREST, ingestEventListener); + + // verify that dialog will call dispose on close: + // https://docs.oracle.com/javase/tutorial/uiswing/components/frame.html#windowevents + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); this.pack(); } + @Override + public void dispose() { + IngestManager.getInstance().removeIngestJobEventListener(INGEST_JOB_EVENTS_OF_INTEREST, ingestEventListener); + this.dataSourceSummaryTabbedPane.close(); + super.dispose(); + } + /** * Make this dialog an observer of the DataSourcesPanel. */ @@ -104,7 +121,7 @@ final class DataSourceSummaryDialog extends javax.swing.JDialog implements Obser closeButton = new javax.swing.JButton(); dataSourceSummarySplitPane = new javax.swing.JSplitPane(); - javax.swing.JTabbedPane dataSourceTabbedPane = dataSourceSummaryTabbedPane; + javax.swing.JPanel dataSourceTabbedPane = dataSourceSummaryTabbedPane; org.openide.awt.Mnemonics.setLocalizedText(closeButton, org.openide.util.NbBundle.getMessage(DataSourceSummaryDialog.class, "DataSourceSummaryDialog.closeButton.text")); // NOI18N closeButton.addActionListener(new java.awt.event.ActionListener() { diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryTabbedPane.form b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryTabbedPane.form new file mode 100644 index 0000000000..7c01388a52 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryTabbedPane.form @@ -0,0 +1,61 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryTabbedPane.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryTabbedPane.java index 9696f4d4c4..55320f7dbb 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryTabbedPane.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryTabbedPane.java @@ -18,64 +18,142 @@ */ package org.sleuthkit.autopsy.datasourcesummary.ui; -import java.util.ArrayList; +import java.awt.CardLayout; +import java.awt.Component; import java.util.Arrays; import java.util.List; -import javax.swing.JTabbedPane; -import org.apache.commons.lang3.tuple.Pair; +import java.util.function.Consumer; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.casemodule.IngestJobInfoPanel; import org.sleuthkit.datamodel.DataSource; /** * A tabbed pane showing the summary of a data source including tabs of: - * DataSourceSummaryCountsPanel, DataSourceSummaryDetailsPanel, and - * IngestJobInfoPanel. + * DataSourceSummaryCountsPanel, ContainerPanel, and IngestJobInfoPanel. */ @Messages({ - "DataSourceSummaryTabbedPane_countsTab_title=Counts", + "DataSourceSummaryTabbedPane_typesTab_title=Types", "DataSourceSummaryTabbedPane_detailsTab_title=Container", "DataSourceSummaryTabbedPane_userActivityTab_title=User Activity", "DataSourceSummaryTabbedPane_ingestHistoryTab_title=Ingest History", "DataSourceSummaryTabbedPane_recentFileTab_title=Recent Files", + "DataSourceSummaryTabbedPane_pastCasesTab_title=Past Cases", "DataSourceSummaryTabbedPane_analysisTab_title=Analysis" }) -public class DataSourceSummaryTabbedPane extends JTabbedPane { +public class DataSourceSummaryTabbedPane extends javax.swing.JPanel { + + /** + * Records of tab information (i.e. title, component, function to call on + * new data source). + */ + private static class DataSourceTab { + + private final String tabTitle; + private final Component component; + private final Consumer onDataSource; + private final Runnable onClose; + + /** + * Main constructor. + * + * @param tabTitle The title of the tab. + * @param component The component to be displayed. + * @param onDataSource The function to be called on a new data source. + * @param onClose Called to cleanup resources when closing tabs. + */ + DataSourceTab(String tabTitle, Component component, Consumer onDataSource, Runnable onClose) { + this.tabTitle = tabTitle; + this.component = component; + this.onDataSource = onDataSource; + this.onClose = onClose; + } + + /** + * Main constructor. + * + * @param tabTitle The title of the tab. + * @param panel The component to be displayed in the tab. + */ + DataSourceTab(String tabTitle, BaseDataSourceSummaryPanel panel) { + this.tabTitle = tabTitle; + this.component = panel; + this.onDataSource = panel::setDataSource; + this.onClose = panel::close; + } + + /** + * @return The title for the tab. + */ + String getTabTitle() { + return tabTitle; + } + + /** + * @return The component to display in the tab. + */ + Component getComponent() { + return component; + } + + /** + * @return The function to be called on new data source. + */ + Consumer getOnDataSource() { + return onDataSource; + } + + /** + * @return The action for closing resources in the tab. + */ + public Runnable getOnClose() { + return onClose; + } + } private static final long serialVersionUID = 1L; - - // A pair of the tab name and the corresponding BaseDataSourceSummaryTabs to be displayed. - private final List> tabs = new ArrayList<>(Arrays.asList( - Pair.of(Bundle.DataSourceSummaryTabbedPane_countsTab_title(), new DataSourceSummaryCountsPanel()), - Pair.of(Bundle.DataSourceSummaryTabbedPane_userActivityTab_title(), new DataSourceSummaryUserActivityPanel()), - Pair.of(Bundle.DataSourceSummaryTabbedPane_recentFileTab_title(), new RecentFilesPanel()), - Pair.of(Bundle.DataSourceSummaryTabbedPane_analysisTab_title(), new AnalysisPanel()) - )); + // needs to match value provided for card layout in designed + private static final String TABBED_PANE = "tabbedPane"; + private static final String NO_DATASOURCE_PANE = "noDataSourcePane"; private final IngestJobInfoPanel ingestHistoryPanel = new IngestJobInfoPanel(); + private final List tabs = Arrays.asList( + new DataSourceTab(Bundle.DataSourceSummaryTabbedPane_typesTab_title(), new TypesPanel()), + new DataSourceTab(Bundle.DataSourceSummaryTabbedPane_userActivityTab_title(), new UserActivityPanel()), + new DataSourceTab(Bundle.DataSourceSummaryTabbedPane_analysisTab_title(), new AnalysisPanel()), + new DataSourceTab(Bundle.DataSourceSummaryTabbedPane_recentFileTab_title(), new RecentFilesPanel()), + new DataSourceTab(Bundle.DataSourceSummaryTabbedPane_pastCasesTab_title(), new PastCasesPanel()), + // do nothing on closing + new DataSourceTab(Bundle.DataSourceSummaryTabbedPane_ingestHistoryTab_title(), ingestHistoryPanel, ingestHistoryPanel::setDataSource, () -> { + }), + new DataSourceTab(Bundle.DataSourceSummaryTabbedPane_detailsTab_title(), new ContainerPanel()) + ); + private DataSource dataSource = null; + private CardLayout cardLayout; /** - * Constructs a tabbed pane showing the summary of a data source. + * Creates new form TabPane */ public DataSourceSummaryTabbedPane() { - initComponent(); + initComponents(); + postInit(); } - private void initComponent() { - for (Pair tab : tabs) { - addTab(tab.getKey(), tab.getValue()); + /** + * Method called right after initComponents during initialization. + */ + private void postInit() { + // get the card layout + cardLayout = (CardLayout) this.getLayout(); + + // set up the tabs + for (DataSourceTab tab : tabs) { + tabbedPane.addTab(tab.getTabTitle(), tab.getComponent()); } - // IngestJobInfoPanel is not specifically a data source summary panel - // and is called separately for that reason. - addTab(Bundle.DataSourceSummaryTabbedPane_ingestHistoryTab_title(), ingestHistoryPanel); - - // The Container tab should be last. - Pair tab = Pair.of(Bundle.DataSourceSummaryTabbedPane_detailsTab_title(), new DataSourceSummaryDetailsPanel()); - addTab(tab.getKey(), tab.getValue()); - tabs.add(tab); + // set this to no datasource initially + cardLayout.show(this, NO_DATASOURCE_PANE); } /** @@ -94,13 +172,52 @@ public class DataSourceSummaryTabbedPane extends JTabbedPane { */ public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; - - for (Pair tab : tabs) { - tab.getValue().setDataSource(dataSource); + if (this.dataSource == null) { + cardLayout.show(this, NO_DATASOURCE_PANE); + } else { + for (DataSourceTab tab : tabs) { + tab.getOnDataSource().accept(dataSource); + } + cardLayout.show(this, TABBED_PANE); } - - // IngestJobInfoPanel is not specifically a data source summary panel - // and is called separately for that reason. - ingestHistoryPanel.setDataSource(dataSource); } + + /** + * Handle close events on each tab. + */ + public void close() { + for (DataSourceTab tab : tabs) { + tab.getOnClose().run(); + } + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + tabbedPane = new javax.swing.JTabbedPane(); + javax.swing.JPanel noDataSourcePane = new javax.swing.JPanel(); + javax.swing.JLabel noDataSourceLabel = new javax.swing.JLabel(); + + setLayout(new java.awt.CardLayout()); + add(tabbedPane, "tabbedPane"); + + noDataSourcePane.setLayout(new java.awt.BorderLayout()); + + noDataSourceLabel.setHorizontalAlignment(javax.swing.SwingConstants.CENTER); + org.openide.awt.Mnemonics.setLocalizedText(noDataSourceLabel, org.openide.util.NbBundle.getMessage(DataSourceSummaryTabbedPane.class, "DataSourceSummaryTabbedPane.noDataSourceLabel.text")); // NOI18N + noDataSourcePane.add(noDataSourceLabel, java.awt.BorderLayout.CENTER); + + add(noDataSourcePane, "noDataSourcePane"); + }// //GEN-END:initComponents + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JTabbedPane tabbedPane; + // End of variables declaration//GEN-END:variables } diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/FileTypePieChart.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/FileTypePieChart.java deleted file mode 100644 index 691234cd48..0000000000 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/FileTypePieChart.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2020 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.awt.BorderLayout; -import java.awt.Font; -import javax.swing.JPanel; -import org.sleuthkit.datamodel.DataSource; - -import org.jfree.chart.ChartFactory; -import org.jfree.chart.ChartPanel; -import org.jfree.chart.JFreeChart; -import org.jfree.chart.labels.PieSectionLabelGenerator; -import org.jfree.chart.labels.StandardPieSectionLabelGenerator; -import org.jfree.chart.plot.PiePlot; -import org.jfree.data.general.DefaultPieDataset; - -import java.text.DecimalFormat; -import java.util.Arrays; -import java.util.Set; -import java.util.stream.Collectors; -import javax.swing.JLabel; -import org.openide.util.NbBundle.Messages; -import org.sleuthkit.autopsy.coreutils.FileTypeUtils; -import org.sleuthkit.autopsy.datasourcesummary.datamodel.DataSourceMimeTypeSummary; -import static org.sleuthkit.autopsy.coreutils.FileTypeUtils.FileTypeCategory; - -/** - * A Pie Chart that shows file mime types in a data source. - */ -class FileTypePieChart extends JPanel { - - private static final long serialVersionUID = 1L; - - private static final Font DEFAULT_FONT = new JLabel().getFont(); - private static final Font DEFAULT_HEADER_FONT = new Font(DEFAULT_FONT.getName(), DEFAULT_FONT.getStyle(), (int) (DEFAULT_FONT.getSize() * 1.5)); - - private final DefaultPieDataset dataset = new DefaultPieDataset(); - private DataSource dataSource; - - // used for determining mime types that fall in the 'other' category - private static final Set ALL_CATEGORY_MIME_TYPES = Arrays.asList( - FileTypeCategory.IMAGE, - FileTypeCategory.VIDEO, - FileTypeCategory.AUDIO, - FileTypeCategory.DOCUMENTS, - FileTypeCategory.EXECUTABLE) - .stream() - .flatMap((cat) -> cat.getMediaTypes().stream()) - .collect(Collectors.toSet()); - - /** - * Default constructor for the pie chart. - */ - @Messages({ - "DataSourceSummaryCountsPanel.byMimeTypeLabel.text=Files by MIME Type", - "DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.audio.row=Audio", - "DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.documents.row=Documents", - "DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.executables.row=Executables", - "DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.images.row=Images", - "DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.videos.row=Videos", - "DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_other_label=Other", - "DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_notAnalyzed_label=Not Analyzed" - }) - FileTypePieChart() { - // Create chart - JFreeChart chart = ChartFactory.createPieChart( - Bundle.DataSourceSummaryCountsPanel_byMimeTypeLabel_text(), - dataset, - true, - true, - false); - - chart.setBackgroundPaint(null); - chart.getLegend().setItemFont(DEFAULT_FONT); - chart.getTitle().setFont(DEFAULT_HEADER_FONT); - - PiePlot plot = ((PiePlot) chart.getPlot()); - - //Format Label - PieSectionLabelGenerator labelGenerator = new StandardPieSectionLabelGenerator( - "{0}: {1} ({2})", new DecimalFormat("0"), new DecimalFormat("0.0%")); - - plot.setLabelGenerator(labelGenerator); - plot.setLabelFont(DEFAULT_FONT); - - plot.setBackgroundPaint(null); - plot.setOutlinePaint(null); - - // Create Panel - ChartPanel panel = new ChartPanel(chart); - this.setLayout(new BorderLayout()); - this.add(panel, BorderLayout.CENTER); - } - - /** - * The datasource currently used as the model with this pie chart. - * - * @return The datasource currently being used as the model in this pie - * chart. - */ - DataSource getDataSource() { - return dataSource; - } - - /** - * Sets datasource to visualize in the pie chart. - * - * @param dataSource The datasource to use in this pie chart. - */ - void setDataSource(DataSource dataSource) { - this.dataSource = dataSource; - this.dataset.clear(); - - if (dataSource != null) { - addIfPresent(Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_images_row(), - this.dataSource, FileTypeCategory.IMAGE); - addIfPresent(Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_videos_row(), - this.dataSource, FileTypeCategory.VIDEO); - addIfPresent(Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_audio_row(), - this.dataSource, FileTypeCategory.AUDIO); - addIfPresent(Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_documents_row(), - this.dataSource, FileTypeCategory.DOCUMENTS); - addIfPresent(Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_executables_row(), - this.dataSource, FileTypeCategory.EXECUTABLE); - addIfPresent(Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_other_label(), - DataSourceMimeTypeSummary.getCountOfFilesNotInMimeTypes(this.dataSource, ALL_CATEGORY_MIME_TYPES)); - addIfPresent(Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_notAnalyzed_label(), - DataSourceMimeTypeSummary.getCountOfFilesWithNoMimeType(this.dataSource)); - } - } - - /** - * Adds count for file type category if there is a value. Uses fields - * 'dataSource' and 'dataset'. - * - * @param label The label for this pie slice. - * @param dataSource The data source. - * @param category The category for the pie slice. - */ - private void addIfPresent(String label, DataSource dataSource, FileTypeUtils.FileTypeCategory category) { - if (dataSource == null) { - return; - } - - Long count = getCount(dataSource, category); - addIfPresent(label, count); - } - - /** - * Adds count for a a label if the count is non-null and greater than 0. - * - * @param label The label. - * @param count The count. - */ - private void addIfPresent(String label, Long count) { - if (count != null && count > 0) { - this.dataset.setValue(label, count); - } - } - - /** - * Retrieves the counts of files of a particular mime type for a particular - * DataSource. - * - * @param dataSource The DataSource. - * @param category The mime type category. - * - * @return The count. - */ - private static Long getCount(DataSource dataSource, FileTypeUtils.FileTypeCategory category) { - return DataSourceMimeTypeSummary.getCountOfFilesForMimeTypes(dataSource, category.getMediaTypes()); - } -} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/PastCasesPanel.form b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/PastCasesPanel.form new file mode 100644 index 0000000000..7c84d44660 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/PastCasesPanel.form @@ -0,0 +1,219 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/PastCasesPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/PastCasesPanel.java new file mode 100644 index 0000000000..76d36f4785 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/PastCasesPanel.java @@ -0,0 +1,227 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.util.Arrays; +import java.util.List; +import java.util.function.Function; +import org.apache.commons.lang3.tuple.Pair; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.centralrepository.ingestmodule.CentralRepoIngestModuleFactory; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.PastCasesSummary; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.PastCasesSummary.PastCasesResult; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.CellModelTableCellRenderer.DefaultCellModel; +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.IngestRunningLabel; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.JTablePanel; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.JTablePanel.ColumnModel; +import org.sleuthkit.datamodel.DataSource; + +/** + * A tab shown in data source summary displaying information about a datasource + * and how it pertains to other cases. + */ +@Messages({ + "PastCasesPanel_caseColumn_title=Case", + "PastCasesPanel_countColumn_title=Count", + "PastCasesPanel_onNoCrIngest_message=No results will be shown because the Central Repository module was not run." +}) +public class PastCasesPanel extends BaseDataSourceSummaryPanel { + + private static final long serialVersionUID = 1L; + private static final String CR_FACTORY = CentralRepoIngestModuleFactory.class.getName(); + private static final String CR_NAME = CentralRepoIngestModuleFactory.getModuleName(); + + private static final ColumnModel> CASE_COL = new ColumnModel<>( + Bundle.PastCasesPanel_caseColumn_title(), + (pair) -> new DefaultCellModel(pair.getKey()), + 300 + ); + + private static final ColumnModel> COUNT_COL = new ColumnModel<>( + Bundle.PastCasesPanel_countColumn_title(), + (pair) -> new DefaultCellModel(String.valueOf(pair.getValue())), + 100 + ); + + private static final List>> DEFAULT_COLUMNS = Arrays.asList(CASE_COL, COUNT_COL); + + private final JTablePanel> notableFileTable = JTablePanel.getJTablePanel(DEFAULT_COLUMNS); + + private final JTablePanel> sameIdTable = JTablePanel.getJTablePanel(DEFAULT_COLUMNS); + + private final List> tables = Arrays.asList( + notableFileTable, + sameIdTable + ); + + private final List> dataFetchComponents; + + private final IngestRunningLabel ingestRunningLabel = new IngestRunningLabel(); + + public PastCasesPanel() { + this(new PastCasesSummary()); + } + + /** + * Creates new form PastCasesPanel + */ + public PastCasesPanel(PastCasesSummary pastCaseData) { + // set up data acquisition methods + dataFetchComponents = Arrays.asList( + new DataFetchWorker.DataFetchComponents<>( + (dataSource) -> pastCaseData.getPastCasesData(dataSource), + (result) -> handleResult(result)) + ); + + initComponents(); + } + + /** + * Handles displaying the result for each table by breaking apart subdata + * items into seperate results for each table. + * + * @param result The result. + */ + private void handleResult(DataFetchResult result) { + showResultWithModuleCheck(notableFileTable, getSubResult(result, (res) -> res.getTaggedNotable()), CR_FACTORY, CR_NAME); + showResultWithModuleCheck(sameIdTable, getSubResult(result, (res) -> res.getSameIdsResults()), CR_FACTORY, CR_NAME); + } + + /** + * Given an input data fetch result, creates an error result if the original + * is an error. Otherwise, uses the getSubResult function on the underlying + * data to create a new DataFetchResult. + * + * @param inputResult The input result. + * @param getSubComponent The means of getting the data given the original + * data. + * + * @return The new result with the error of the original or the processed + * data. + */ + private DataFetchResult getSubResult(DataFetchResult inputResult, Function getSubResult) { + if (inputResult == null) { + return null; + } else if (inputResult.getResultType() == ResultType.SUCCESS) { + O innerData = (inputResult.getData() == null) ? null : getSubResult.apply(inputResult.getData()); + return DataFetchResult.getSuccessResult(innerData); + } else { + return DataFetchResult.getErrorResult(inputResult.getException()); + } + } + + @Override + protected void fetchInformation(DataSource dataSource) { + fetchInformation(dataFetchComponents, dataSource); + } + + @Override + protected void onNewDataSource(DataSource dataSource) { + onNewDataSource(dataFetchComponents, tables, dataSource); + } + + @Override + public void close() { + ingestRunningLabel.unregister(); + super.close(); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + javax.swing.JScrollPane mainScrollPane = new javax.swing.JScrollPane(); + javax.swing.JPanel mainContentPanel = new javax.swing.JPanel(); + javax.swing.JPanel ingestRunningPanel = ingestRunningLabel; + javax.swing.JLabel notableFileLabel = new javax.swing.JLabel(); + javax.swing.Box.Filler filler1 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 2), new java.awt.Dimension(0, 2), new java.awt.Dimension(0, 2)); + javax.swing.JPanel notableFilePanel = notableFileTable; + javax.swing.Box.Filler filler2 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 20), new java.awt.Dimension(0, 20), new java.awt.Dimension(0, 20)); + javax.swing.JLabel sameIdLabel = new javax.swing.JLabel(); + javax.swing.Box.Filler filler3 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 2), new java.awt.Dimension(0, 2), new java.awt.Dimension(0, 2)); + javax.swing.JPanel sameIdPanel = sameIdTable; + javax.swing.Box.Filler filler5 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 32767)); + + mainContentPanel.setBorder(javax.swing.BorderFactory.createEmptyBorder(10, 10, 10, 10)); + mainContentPanel.setLayout(new javax.swing.BoxLayout(mainContentPanel, javax.swing.BoxLayout.PAGE_AXIS)); + + ingestRunningPanel.setAlignmentX(0.0F); + ingestRunningPanel.setMaximumSize(new java.awt.Dimension(32767, 25)); + ingestRunningPanel.setMinimumSize(new java.awt.Dimension(10, 25)); + ingestRunningPanel.setPreferredSize(new java.awt.Dimension(10, 25)); + mainContentPanel.add(ingestRunningPanel); + + org.openide.awt.Mnemonics.setLocalizedText(notableFileLabel, org.openide.util.NbBundle.getMessage(PastCasesPanel.class, "PastCasesPanel.notableFileLabel.text")); // NOI18N + mainContentPanel.add(notableFileLabel); + notableFileLabel.getAccessibleContext().setAccessibleName(org.openide.util.NbBundle.getMessage(PastCasesPanel.class, "PastCasesPanel.notableFileLabel.text")); // NOI18N + + filler1.setAlignmentX(0.0F); + mainContentPanel.add(filler1); + + notableFilePanel.setAlignmentX(0.0F); + notableFilePanel.setMaximumSize(new java.awt.Dimension(32767, 106)); + notableFilePanel.setMinimumSize(new java.awt.Dimension(100, 106)); + notableFilePanel.setPreferredSize(new java.awt.Dimension(100, 106)); + mainContentPanel.add(notableFilePanel); + + filler2.setAlignmentX(0.0F); + mainContentPanel.add(filler2); + + org.openide.awt.Mnemonics.setLocalizedText(sameIdLabel, org.openide.util.NbBundle.getMessage(PastCasesPanel.class, "PastCasesPanel.sameIdLabel.text")); // NOI18N + mainContentPanel.add(sameIdLabel); + + filler3.setAlignmentX(0.0F); + mainContentPanel.add(filler3); + + sameIdPanel.setAlignmentX(0.0F); + sameIdPanel.setMaximumSize(new java.awt.Dimension(32767, 106)); + sameIdPanel.setMinimumSize(new java.awt.Dimension(100, 106)); + sameIdPanel.setPreferredSize(new java.awt.Dimension(100, 106)); + mainContentPanel.add(sameIdPanel); + + filler5.setAlignmentX(0.0F); + mainContentPanel.add(filler5); + + mainScrollPane.setViewportView(mainContentPanel); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(mainScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 400, Short.MAX_VALUE) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(mainScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 300, Short.MAX_VALUE) + ); + }// //GEN-END:initComponents + + + // Variables declaration - do not modify//GEN-BEGIN:variables + // End of variables declaration//GEN-END:variables +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/RecentFilesPanel.form b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/RecentFilesPanel.form index 4d1cdccecd..150abf2ed0 100755 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/RecentFilesPanel.form +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/RecentFilesPanel.form @@ -31,6 +31,11 @@ + + + + + @@ -45,13 +50,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -63,7 +94,7 @@ - + @@ -75,7 +106,7 @@ - + @@ -93,7 +124,7 @@ - + @@ -109,7 +140,7 @@ - + @@ -125,7 +156,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/RecentFilesPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/RecentFilesPanel.java index 5910aba6e3..1b675032d0 100755 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/RecentFilesPanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/RecentFilesPanel.java @@ -21,16 +21,15 @@ package org.sleuthkit.autopsy.datasourcesummary.ui; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.stream.Collectors; import org.openide.util.NbBundle.Messages; -import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.IngestModuleCheckUtil; import org.sleuthkit.autopsy.datasourcesummary.datamodel.RecentFilesSummary; import org.sleuthkit.autopsy.datasourcesummary.datamodel.RecentFilesSummary.RecentAttachmentDetails; import org.sleuthkit.autopsy.datasourcesummary.datamodel.RecentFilesSummary.RecentDownloadDetails; import org.sleuthkit.autopsy.datasourcesummary.datamodel.RecentFilesSummary.RecentFileDetails; import org.sleuthkit.autopsy.datasourcesummary.uiutils.CellModelTableCellRenderer.DefaultCellModel; -import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchResult; import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchWorker; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.IngestRunningLabel; import org.sleuthkit.autopsy.datasourcesummary.uiutils.JTablePanel; import org.sleuthkit.autopsy.datasourcesummary.uiutils.JTablePanel.ColumnModel; import org.sleuthkit.autopsy.datasourcesummary.uiutils.ListTableModel; @@ -42,17 +41,22 @@ import org.sleuthkit.datamodel.DataSource; public final class RecentFilesPanel extends BaseDataSourceSummaryPanel { private static final long serialVersionUID = 1L; + private static final String EMAIL_PARSER_FACTORY = "org.sleuthkit.autopsy.thunderbirdparser.EmailParserModuleFactory"; + private static final String EMAIL_PARSER_MODULE_NAME = Bundle.RecentFilePanel_emailParserModuleName(); private final List> tablePanelList = new ArrayList<>(); private final List> dataFetchComponents = new ArrayList<>(); + private final IngestRunningLabel ingestRunningLabel = new IngestRunningLabel(); + private final RecentFilesSummary dataHandler; @Messages({ "RecentFilesPanel_col_head_date=Date", "RecentFilePanel_col_header_domain=Domain", "RecentFilePanel_col_header_path=Path", - "RecentFilePanel_col_header_sender=Sender" + "RecentFilePanel_col_header_sender=Sender", + "RecentFilePanel_emailParserModuleName=Email Parser" }) /** @@ -66,33 +70,27 @@ public final class RecentFilesPanel extends BaseDataSourceSummaryPanel { * Creates new form RecentFilesPanel */ public RecentFilesPanel(RecentFilesSummary dataHandler) { + super(dataHandler); this.dataHandler = dataHandler; initComponents(); initalizeTables(); } + @Override + protected void fetchInformation(DataSource dataSource) { + fetchInformation(dataFetchComponents, dataSource); + } + @Override protected void onNewDataSource(DataSource dataSource) { - // if no data source is present or the case is not open, - // set results for tables to null. - if (dataSource == null || !Case.isCaseOpen()) { - this.dataFetchComponents.forEach((item) -> item.getResultHandler() - .accept(DataFetchResult.getSuccessResult(null))); + onNewDataSource(dataFetchComponents, tablePanelList, dataSource); + } - } else { - // set tables to display loading screen - tablePanelList.forEach((table) -> table.showDefaultLoadingMessage()); - - // create swing workers to run for each table - List> workers = dataFetchComponents - .stream() - .map((components) -> new DataFetchWorker<>(components, dataSource)) - .collect(Collectors.toList()); - - // submit swing workers to run - submit(workers); - } + @Override + public void close() { + ingestRunningLabel.unregister(); + super.close(); } /** @@ -127,13 +125,17 @@ public final class RecentFilesPanel extends BaseDataSourceSummaryPanel { JTablePanel pane = (JTablePanel) openedDocPane; pane.setModel(tableModel); pane.setColumnModel(JTablePanel.getTableColumnModel(list)); + pane.setKeyFunction((recentFile) -> recentFile.getPath()); tablePanelList.add(pane); DataFetchWorker.DataFetchComponents> worker = new DataFetchWorker.DataFetchComponents<>( (dataSource) -> dataHandler.getRecentlyOpenedDocuments(dataSource, 10), - (result) -> pane.showDataFetchResult(result, JTablePanel.getDefaultErrorMessage(), - Bundle.RecentFilePanel_no_open_documents())); + (result) -> { + showResultWithModuleCheck(pane, result, + IngestModuleCheckUtil.RECENT_ACTIVITY_FACTORY, + IngestModuleCheckUtil.RECENT_ACTIVITY_MODULE_NAME); + }); dataFetchComponents.add(worker); } @@ -161,14 +163,18 @@ public final class RecentFilesPanel extends BaseDataSourceSummaryPanel { JTablePanel pane = (JTablePanel) downloadsPane; pane.setModel(tableModel); + pane.setKeyFunction((download) -> download.getPath()); pane.setColumnModel(JTablePanel.getTableColumnModel(list)); tablePanelList.add(pane); DataFetchWorker.DataFetchComponents> worker = new DataFetchWorker.DataFetchComponents<>( (dataSource) -> dataHandler.getRecentDownloads(dataSource, 10), - (result) -> pane.showDataFetchResult(result, JTablePanel.getDefaultErrorMessage(), - Bundle.RecentFilePanel_no_open_documents())); + (result) -> { + showResultWithModuleCheck(pane, result, + IngestModuleCheckUtil.RECENT_ACTIVITY_FACTORY, + IngestModuleCheckUtil.RECENT_ACTIVITY_MODULE_NAME); + }); dataFetchComponents.add(worker); } @@ -196,14 +202,15 @@ public final class RecentFilesPanel extends BaseDataSourceSummaryPanel { JTablePanel pane = (JTablePanel) attachmentsPane; pane.setModel(tableModel); + pane.setKeyFunction((attachment) -> attachment.getPath()); pane.setColumnModel(JTablePanel.getTableColumnModel(list)); tablePanelList.add(pane); DataFetchWorker.DataFetchComponents> worker = new DataFetchWorker.DataFetchComponents<>( (dataSource) -> dataHandler.getRecentAttachments(dataSource, 10), - (result) -> pane.showDataFetchResult(result, JTablePanel.getDefaultErrorMessage(), - Bundle.RecentFilePanel_no_open_documents())); + (result) -> showResultWithModuleCheck(pane, result, EMAIL_PARSER_FACTORY, EMAIL_PARSER_MODULE_NAME) + ); dataFetchComponents.add(worker); } @@ -220,6 +227,7 @@ public final class RecentFilesPanel extends BaseDataSourceSummaryPanel { javax.swing.JScrollPane scrollPane = new javax.swing.JScrollPane(); javax.swing.JPanel tablePanel = new javax.swing.JPanel(); + javax.swing.JPanel ingestRunningPanel = ingestRunningLabel; openedDocPane = new JTablePanel(); downloadsPane = new JTablePanel(); attachmentsPane = new JTablePanel(); @@ -229,61 +237,72 @@ public final class RecentFilesPanel extends BaseDataSourceSummaryPanel { setLayout(new java.awt.BorderLayout()); + tablePanel.setBorder(javax.swing.BorderFactory.createEmptyBorder(10, 10, 10, 10)); tablePanel.setMinimumSize(new java.awt.Dimension(400, 400)); tablePanel.setPreferredSize(new java.awt.Dimension(600, 400)); tablePanel.setLayout(new java.awt.GridBagLayout()); + + ingestRunningPanel.setAlignmentX(0.0F); + ingestRunningPanel.setMaximumSize(new java.awt.Dimension(32767, 25)); + ingestRunningPanel.setMinimumSize(new java.awt.Dimension(10, 25)); + ingestRunningPanel.setPreferredSize(new java.awt.Dimension(10, 25)); gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; - gridBagConstraints.gridy = 1; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + tablePanel.add(ingestRunningPanel, gridBagConstraints); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTH; gridBagConstraints.weightx = 1.0; gridBagConstraints.weighty = 1.0; - gridBagConstraints.insets = new java.awt.Insets(5, 5, 0, 5); + gridBagConstraints.insets = new java.awt.Insets(2, 0, 0, 0); tablePanel.add(openedDocPane, gridBagConstraints); gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; - gridBagConstraints.gridy = 3; + gridBagConstraints.gridy = 4; gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTH; gridBagConstraints.weightx = 1.0; gridBagConstraints.weighty = 1.0; - gridBagConstraints.insets = new java.awt.Insets(5, 5, 0, 5); + gridBagConstraints.insets = new java.awt.Insets(2, 0, 0, 0); tablePanel.add(downloadsPane, gridBagConstraints); gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; - gridBagConstraints.gridy = 5; + gridBagConstraints.gridy = 6; gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTH; gridBagConstraints.weightx = 1.0; gridBagConstraints.weighty = 1.0; - gridBagConstraints.insets = new java.awt.Insets(5, 5, 10, 5); + gridBagConstraints.insets = new java.awt.Insets(2, 0, 0, 0); tablePanel.add(attachmentsPane, gridBagConstraints); org.openide.awt.Mnemonics.setLocalizedText(openDocsLabel, org.openide.util.NbBundle.getMessage(RecentFilesPanel.class, "RecentFilesPanel.openDocsLabel.text")); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; - gridBagConstraints.gridy = 0; + gridBagConstraints.gridy = 1; gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTH; - gridBagConstraints.insets = new java.awt.Insets(10, 5, 0, 5); tablePanel.add(openDocsLabel, gridBagConstraints); org.openide.awt.Mnemonics.setLocalizedText(downloadLabel, org.openide.util.NbBundle.getMessage(RecentFilesPanel.class, "RecentFilesPanel.downloadLabel.text")); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; - gridBagConstraints.gridy = 2; + gridBagConstraints.gridy = 3; gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; - gridBagConstraints.insets = new java.awt.Insets(15, 5, 0, 5); + gridBagConstraints.insets = new java.awt.Insets(20, 0, 0, 0); tablePanel.add(downloadLabel, gridBagConstraints); org.openide.awt.Mnemonics.setLocalizedText(attachmentLabel, org.openide.util.NbBundle.getMessage(RecentFilesPanel.class, "RecentFilesPanel.attachmentLabel.text")); // NOI18N gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; - gridBagConstraints.gridy = 4; + gridBagConstraints.gridy = 5; gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTH; - gridBagConstraints.insets = new java.awt.Insets(15, 5, 0, 5); + gridBagConstraints.insets = new java.awt.Insets(20, 0, 0, 0); tablePanel.add(attachmentLabel, gridBagConstraints); scrollPane.setViewportView(tablePanel); @@ -297,5 +316,4 @@ public final class RecentFilesPanel extends BaseDataSourceSummaryPanel { private javax.swing.JPanel downloadsPane; private javax.swing.JPanel openedDocPane; // End of variables declaration//GEN-END:variables - } diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/SizeRepresentationUtil.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/SizeRepresentationUtil.java new file mode 100644 index 0000000000..34c622563d --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/SizeRepresentationUtil.java @@ -0,0 +1,103 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.DecimalFormat; +import java.util.Arrays; +import java.util.List; +import org.openide.util.NbBundle; + +/** + * This class provides utilities for representing storage size in most relevant + * units (i.e. bytes, megabytes, etc.). + */ +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() + ); + + /** + * Get a long size in bytes as a string formated to be read by users. + * + * @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. + */ + public static String getSizeString(Long size) { + return getSizeString(size, APPROXIMATE_SIZE_FORMAT, true); + } + + /** + * 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 showFullSize Optionally show the number of bytes in the + * datasource. + * + * @return Return a string formated with a user friendly version of the 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) { + 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; + } + } + + String fullSize = size + UNITS.get(0); + String closestUnitSize = format.format(approximateSize) + UNITS.get(unitsIndex); + + if (unitsIndex == 0) { + return fullSize; + } else if (showFullSize) { + return String.format("%s (%s)", closestUnitSize, fullSize); + } else { + return closestUnitSize; + } + } + + private SizeRepresentationUtil() { + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TypesPanel.form b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TypesPanel.form new file mode 100644 index 0000000000..efb1ca2aae --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TypesPanel.form @@ -0,0 +1,285 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TypesPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TypesPanel.java new file mode 100644 index 0000000000..448bf6bbea --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TypesPanel.java @@ -0,0 +1,590 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourcesummary.ui; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.sql.SQLException; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Arrays; +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 javax.swing.JLabel; +import org.apache.commons.lang3.StringUtils; +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.IngestModuleCheckUtil; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.MimeTypeSummary; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.SleuthkitCaseProvider.SleuthkitCaseProviderException; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.AbstractLoadableComponent; +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.IngestRunningLabel; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.LoadableComponent; +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; + +/** + * Panel for displaying summary information on the known files present in the + * specified DataSource. + */ +@Messages({ + "TypesPanel_artifactsTypesPieChart_title=Artifact Types", + "TypesPanel_filesByCategoryTable_allocatedRow_title=Allocated Files", + "TypesPanel_filesByCategoryTable_unallocatedRow_title=Unallocated Files", + "TypesPanel_filesByCategoryTable_slackRow_title=Slack Files", + "TypesPanel_filesByCategoryTable_directoryRow_title=Directories", + "TypesPanel_fileMimeTypesChart_title=File Types", + "TypesPanel_fileMimeTypesChart_audio_title=Audio", + "TypesPanel_fileMimeTypesChart_documents_title=Documents", + "TypesPanel_fileMimeTypesChart_executables_title=Executables", + "TypesPanel_fileMimeTypesChart_images_title=Images", + "TypesPanel_fileMimeTypesChart_videos_title=Videos", + "TypesPanel_fileMimeTypesChart_other_title=Other", + "TypesPanel_fileMimeTypesChart_unknown_title=Unknown", + "TypesPanel_fileMimeTypesChart_notAnalyzed_title=Not Analyzed", + "TypesPanel_usageLabel_title=Usage", + "TypesPanel_osLabel_title=OS", + "TypesPanel_sizeLabel_title=Size"}) +class TypesPanel extends BaseDataSourceSummaryPanel { + + /** + * A label that allows for displaying loading messages and can be used with + * a DataFetchResult. Text displays as ":". + */ + private static class LoadableLabel extends AbstractLoadableComponent { + + private static final long serialVersionUID = 1L; + + private final JLabel label = new JLabel(); + private final String key; + + /** + * Main constructor for the label. + * + * @param key The key to be displayed. + */ + LoadableLabel(String key) { + this.key = key; + setLayout(new BorderLayout()); + add(label, BorderLayout.CENTER); + this.showResults(null); + } + + private void setValue(String value) { + String formattedKey = StringUtils.isBlank(key) ? "" : key; + String formattedValue = StringUtils.isBlank(value) ? "" : value; + label.setText(String.format("%s: %s", formattedKey, formattedValue)); + } + + @Override + protected void setMessage(boolean visible, String message) { + setValue(message); + } + + @Override + protected void setResults(String data) { + setValue(data); + } + } + + /** + * Data for types pie chart. + */ + private static class TypesPieChartData { + + private final List pieSlices; + private final boolean usefulContent; + + /** + * Main constructor. + * + * @param pieSlices The pie slices. + * @param usefulContent True if this is useful content; false if there + * is 0 mime type information. + */ + public TypesPieChartData(List pieSlices, boolean usefulContent) { + this.pieSlices = pieSlices; + this.usefulContent = usefulContent; + } + + /** + * @return The pie chart data. + */ + public List getPieSlices() { + return pieSlices; + } + + /** + * @return Whether or not the data is usefulContent. + */ + public boolean isUsefulContent() { + return usefulContent; + } + } + + /** + * Information concerning a particular category in the file types pie chart. + */ + private static class TypesPieCategory { + + private final String label; + private final Set mimeTypes; + private final Color color; + + /** + * Main constructor. + * + * @param label The label for this slice. + * @param mimeTypes The mime types associated with this slice. + * @param color The color associated with this slice. + */ + TypesPieCategory(String label, Set mimeTypes, Color color) { + this.label = label; + this.mimeTypes = mimeTypes; + this.color = color; + } + + /** + * Constructor that accepts FileTypeCategory. + * + * @param label The label for this slice. + * @param mimeTypes The mime types associated with this slice. + * @param color The color associated with this slice. + */ + TypesPieCategory(String label, FileTypeCategory fileCategory, Color color) { + this(label, fileCategory.getMediaTypes(), color); + } + + /** + * @return The label for this category. + */ + String getLabel() { + return label; + } + + /** + * @return The mime types associated with this category. + */ + Set getMimeTypes() { + return mimeTypes; + } + + /** + * @return The color associated with this category. + */ + Color getColor() { + return color; + } + } + + 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 Color IMAGES_COLOR = new Color(156, 39, 176); + private static final Color VIDEOS_COLOR = Color.YELLOW; + private static final Color AUDIO_COLOR = Color.BLUE; + private static final Color DOCUMENTS_COLOR = Color.GREEN; + private static final Color EXECUTABLES_COLOR = new Color(0, 188, 212); + private static final Color UNKNOWN_COLOR = Color.ORANGE; + private static final Color OTHER_COLOR = new Color(78, 52, 46); + private static final Color NOT_ANALYZED_COLOR = Color.WHITE; + + // All file type categories. + private static final List FILE_MIME_TYPE_CATEGORIES = Arrays.asList( + new TypesPieCategory(Bundle.TypesPanel_fileMimeTypesChart_images_title(), FileTypeCategory.IMAGE.getMediaTypes(), IMAGES_COLOR), + new TypesPieCategory(Bundle.TypesPanel_fileMimeTypesChart_videos_title(), FileTypeCategory.VIDEO.getMediaTypes(), VIDEOS_COLOR), + new TypesPieCategory(Bundle.TypesPanel_fileMimeTypesChart_audio_title(), FileTypeCategory.AUDIO.getMediaTypes(), AUDIO_COLOR), + new TypesPieCategory(Bundle.TypesPanel_fileMimeTypesChart_documents_title(), FileTypeCategory.DOCUMENTS.getMediaTypes(), DOCUMENTS_COLOR), + new TypesPieCategory(Bundle.TypesPanel_fileMimeTypesChart_executables_title(), FileTypeCategory.EXECUTABLE.getMediaTypes(), EXECUTABLES_COLOR), + new TypesPieCategory(Bundle.TypesPanel_fileMimeTypesChart_unknown_title(), new HashSet<>(Arrays.asList("application/octet-stream")), UNKNOWN_COLOR) + ); + + 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()); + + private final PieChartPanel fileMimeTypesChart = new PieChartPanel(Bundle.TypesPanel_fileMimeTypesChart_title()); + + private final LoadableLabel allocatedLabel = new LoadableLabel(Bundle.TypesPanel_filesByCategoryTable_allocatedRow_title()); + private final LoadableLabel unallocatedLabel = new LoadableLabel(Bundle.TypesPanel_filesByCategoryTable_unallocatedRow_title()); + private final LoadableLabel slackLabel = new LoadableLabel(Bundle.TypesPanel_filesByCategoryTable_slackRow_title()); + private final LoadableLabel directoriesLabel = new LoadableLabel(Bundle.TypesPanel_filesByCategoryTable_directoryRow_title()); + + // all loadable components + private final List> loadables = Arrays.asList( + usageLabel, + osLabel, + sizeLabel, + fileMimeTypesChart, + allocatedLabel, + unallocatedLabel, + slackLabel, + directoriesLabel + ); + + private final IngestRunningLabel ingestRunningLabel = new IngestRunningLabel(); + + // all of the means for obtaining data for the gui components. + private final List> dataFetchComponents; + + /** + * Creates a new TypesPanel. + */ + public TypesPanel() { + this(new MimeTypeSummary(), new TypesSummary(), new ContainerSummary()); + } + + @Override + public void close() { + ingestRunningLabel.unregister(); + super.close(); + } + + /** + * Creates a new TypesPanel. + * + * @param mimeTypeData The service for mime types. + * @param typeData The service for file types data. + * @param containerData The service for container information. + */ + public TypesPanel( + MimeTypeSummary mimeTypeData, + TypesSummary typeData, + ContainerSummary containerData) { + + super(mimeTypeData, typeData, containerData); + + this.dataFetchComponents = Arrays.asList( + // usage label worker + new DataFetchWorker.DataFetchComponents<>( + containerData::getDataSourceType, + (result) -> { + showResultWithModuleCheck( + usageLabel, + result, + StringUtils::isNotBlank, + IngestModuleCheckUtil.RECENT_ACTIVITY_FACTORY, + IngestModuleCheckUtil.RECENT_ACTIVITY_MODULE_NAME); + }), + // os label worker + new DataFetchWorker.DataFetchComponents<>( + containerData::getOperatingSystems, + (result) -> { + showResultWithModuleCheck( + osLabel, + result, + StringUtils::isNotBlank, + IngestModuleCheckUtil.RECENT_ACTIVITY_FACTORY, + IngestModuleCheckUtil.RECENT_ACTIVITY_MODULE_NAME); + }), + // 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<>( + (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) + ); + + initComponents(); + } + + @Override + protected void fetchInformation(DataSource dataSource) { + fetchInformation(dataFetchComponents, dataSource); + } + + @Override + protected void onNewDataSource(DataSource dataSource) { + onNewDataSource(dataFetchComponents, loadables, dataSource); + } + + /** + * Gets all the data for the file type pie chart. + * + * @param mimeTypeData The means of acquiring data. + * @param dataSource The datasource. + * + * @return The pie chart items. + */ + private TypesPieChartData getMimeTypeCategoriesModel(MimeTypeSummary mimeTypeData, DataSource dataSource) + throws SQLException, SleuthkitCaseProviderException, TskCoreException { + + if (dataSource == null) { + return null; + } + + // for each category of file types, get the counts of files + List fileCategoryItems = new ArrayList<>(); + long categoryTotalCount = 0; + + for (TypesPieCategory cat : FILE_MIME_TYPE_CATEGORIES) { + long thisValue = getLongOrZero(mimeTypeData.getCountOfFilesForMimeTypes(dataSource, cat.getMimeTypes())); + categoryTotalCount += thisValue; + + fileCategoryItems.add(new PieChartItem( + cat.getLabel(), + thisValue, + cat.getColor())); + } + + // get a count of all files with no mime type + long noMimeTypeCount = getLongOrZero(mimeTypeData.getCountOfFilesWithNoMimeType(dataSource)); + + // get a count of all regular files + long allRegularFiles = getLongOrZero(mimeTypeData.getCountOfAllRegularFiles(dataSource)); + + // create entry for mime types in other category + long otherCount = allRegularFiles - (categoryTotalCount + noMimeTypeCount); + PieChartItem otherPieItem = new PieChartItem(Bundle.TypesPanel_fileMimeTypesChart_other_title(), + otherCount, OTHER_COLOR); + + // check at this point to see if these are all 0; if so, we don't have useful content. + boolean usefulContent = categoryTotalCount > 0 || otherCount > 0; + + // create entry for not analyzed mime types category + PieChartItem notAnalyzedItem = new PieChartItem(Bundle.TypesPanel_fileMimeTypesChart_notAnalyzed_title(), + noMimeTypeCount, NOT_ANALYZED_COLOR); + + // combine categories with 'other' and 'not analyzed' + List items = Stream.concat( + fileCategoryItems.stream(), + Stream.of(otherPieItem, notAnalyzedItem)) + // remove items that have no value + .filter(slice -> slice.getValue() > 0) + .collect(Collectors.toList()); + + return new TypesPieChartData(items, usefulContent); + } + + /** + * Handles properly showing data for the mime type categories pie chart + * accounting for whether there are any files with mime types specified and + * whether or not the current data source has been ingested with the file + * type ingest module. + * + * @param result The result to be shown. + */ + private void showMimeTypeCategories(DataFetchResult result) { + // if result is null check for ingest module and show empty results. + if (result == null) { + showPieResultWithModuleCheck(null); + return; + } + + // if error, show error + if (result.getResultType() == ResultType.ERROR) { + this.fileMimeTypesChart.showDataFetchResult(DataFetchResult.getErrorResult(result.getException())); + return; + } + + TypesPieChartData data = result.getData(); + if (data == null) { + // if no data, do an ingest module check with empty results + showPieResultWithModuleCheck(null); + } else if (!data.isUsefulContent()) { + // if no useful data, do an ingest module check and show data + showPieResultWithModuleCheck(data.getPieSlices()); + } else { + // otherwise, show the data + this.fileMimeTypesChart.showDataFetchResult(DataFetchResult.getSuccessResult(data.getPieSlices())); + } + } + + /** + * Shows a message in the fileMimeTypesChart about the data source not being + * ingested with the file type ingest module if the data source has not been + * ingested with that module. Also shows data if present. + * + * @param items The list of items to show. + */ + private void showPieResultWithModuleCheck(List items) { + boolean hasBeenIngested = false; + try { + hasBeenIngested = this.getIngestModuleCheckUtil().isModuleIngested(getDataSource(), FILE_TYPE_FACTORY); + } catch (TskCoreException | SleuthkitCaseProviderException ex) { + logger.log(Level.WARNING, "There was an error fetching whether or not the current data source has been ingested with the file type ingest module.", ex); + } + + if (hasBeenIngested) { + this.fileMimeTypesChart.showDataFetchResult(DataFetchResult.getSuccessResult(items)); + } else { + this.fileMimeTypesChart.showDataWithMessage(items, getDefaultNoIngestMessage(FILE_TYPE_MODULE_NAME)); + } + } + + /** + * Returns the long value or zero if longVal is null. + * + * @param longVal The long value. + * + * @return The long value or 0 if provided value is null. + */ + private static long getLongOrZero(Long longVal) { + return longVal == null ? 0 : longVal; + } + + /** + * Returns string value of long with comma separators. If null returns a + * string of '0'. + * + * @param longVal The long value. + * + * @return The string value of the long. + */ + private static String getStringOrZero(Long longVal) { + return longVal == null ? "0" : COMMA_FORMATTER.format(longVal); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + javax.swing.JScrollPane scrollParent = new javax.swing.JScrollPane(); + javax.swing.JPanel contentParent = new javax.swing.JPanel(); + javax.swing.JPanel ingestRunningPanel = ingestRunningLabel; + javax.swing.JPanel usagePanel = usageLabel; + javax.swing.JPanel osPanel = osLabel; + javax.swing.JPanel sizePanel = sizeLabel; + javax.swing.JPanel fileMimeTypesPanel = fileMimeTypesChart; + javax.swing.Box.Filler filler2 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 5), new java.awt.Dimension(0, 5), new java.awt.Dimension(32767, 5)); + javax.swing.JPanel allocatedPanel = allocatedLabel; + javax.swing.JPanel unallocatedPanel = unallocatedLabel; + javax.swing.JPanel slackPanel = slackLabel; + javax.swing.JPanel directoriesPanel = directoriesLabel; + javax.swing.Box.Filler filler3 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 32767)); + + setLayout(new java.awt.BorderLayout()); + + contentParent.setBorder(javax.swing.BorderFactory.createEmptyBorder(10, 10, 10, 10)); + contentParent.setMaximumSize(new java.awt.Dimension(32787, 32787)); + contentParent.setMinimumSize(new java.awt.Dimension(400, 490)); + contentParent.setLayout(new javax.swing.BoxLayout(contentParent, javax.swing.BoxLayout.PAGE_AXIS)); + + ingestRunningPanel.setAlignmentX(0.0F); + ingestRunningPanel.setMaximumSize(new java.awt.Dimension(32767, 25)); + ingestRunningPanel.setMinimumSize(new java.awt.Dimension(10, 25)); + ingestRunningPanel.setPreferredSize(new java.awt.Dimension(10, 25)); + contentParent.add(ingestRunningPanel); + + usagePanel.setAlignmentX(0.0F); + usagePanel.setMaximumSize(new java.awt.Dimension(32767, 20)); + usagePanel.setMinimumSize(new java.awt.Dimension(10, 20)); + usagePanel.setName(""); // NOI18N + usagePanel.setPreferredSize(new java.awt.Dimension(800, 20)); + contentParent.add(usagePanel); + + osPanel.setAlignmentX(0.0F); + osPanel.setMaximumSize(new java.awt.Dimension(32767, 20)); + osPanel.setMinimumSize(new java.awt.Dimension(10, 20)); + osPanel.setPreferredSize(new java.awt.Dimension(800, 20)); + contentParent.add(osPanel); + + sizePanel.setAlignmentX(0.0F); + sizePanel.setMaximumSize(new java.awt.Dimension(32767, 20)); + sizePanel.setMinimumSize(new java.awt.Dimension(10, 20)); + sizePanel.setPreferredSize(new java.awt.Dimension(800, 20)); + contentParent.add(sizePanel); + + fileMimeTypesPanel.setBorder(javax.swing.BorderFactory.createEmptyBorder(5, 5, 5, 5)); + fileMimeTypesPanel.setAlignmentX(0.0F); + fileMimeTypesPanel.setMaximumSize(new java.awt.Dimension(400, 300)); + fileMimeTypesPanel.setMinimumSize(new java.awt.Dimension(400, 300)); + fileMimeTypesPanel.setPreferredSize(new java.awt.Dimension(400, 300)); + contentParent.add(fileMimeTypesPanel); + contentParent.add(filler2); + + allocatedPanel.setAlignmentX(0.0F); + allocatedPanel.setMaximumSize(new java.awt.Dimension(32767, 16)); + allocatedPanel.setMinimumSize(new java.awt.Dimension(10, 16)); + allocatedPanel.setPreferredSize(new java.awt.Dimension(800, 16)); + contentParent.add(allocatedPanel); + + unallocatedPanel.setAlignmentX(0.0F); + unallocatedPanel.setMaximumSize(new java.awt.Dimension(32767, 16)); + unallocatedPanel.setMinimumSize(new java.awt.Dimension(10, 16)); + unallocatedPanel.setPreferredSize(new java.awt.Dimension(800, 16)); + contentParent.add(unallocatedPanel); + + slackPanel.setAlignmentX(0.0F); + slackPanel.setMaximumSize(new java.awt.Dimension(32767, 16)); + slackPanel.setMinimumSize(new java.awt.Dimension(10, 16)); + slackPanel.setPreferredSize(new java.awt.Dimension(800, 16)); + contentParent.add(slackPanel); + + directoriesPanel.setAlignmentX(0.0F); + directoriesPanel.setMaximumSize(new java.awt.Dimension(32767, 16)); + directoriesPanel.setMinimumSize(new java.awt.Dimension(10, 16)); + directoriesPanel.setPreferredSize(new java.awt.Dimension(800, 16)); + contentParent.add(directoriesPanel); + contentParent.add(filler3); + + scrollParent.setViewportView(contentParent); + + add(scrollParent, java.awt.BorderLayout.CENTER); + }// //GEN-END:initComponents + + + // Variables declaration - do not modify//GEN-BEGIN:variables + // End of variables declaration//GEN-END:variables +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryUserActivityPanel.form b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/UserActivityPanel.form similarity index 91% rename from Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryUserActivityPanel.form rename to Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/UserActivityPanel.form index f24955084b..28560a3ec7 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryUserActivityPanel.form +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/UserActivityPanel.form @@ -60,11 +60,32 @@
+ + + + + + + + + + + + + + + + + + + + + - + @@ -136,7 +157,7 @@ - + @@ -205,7 +226,7 @@ - + @@ -274,7 +295,7 @@ - + @@ -343,7 +364,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryUserActivityPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/UserActivityPanel.java similarity index 64% rename from Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryUserActivityPanel.java rename to Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/UserActivityPanel.java index b4011fe4d6..476d8c5e6f 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryUserActivityPanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/UserActivityPanel.java @@ -25,21 +25,19 @@ import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Locale; -import java.util.stream.Collectors; import org.apache.commons.lang.StringUtils; import org.openide.util.NbBundle.Messages; -import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.datasourcesummary.datamodel.DataSourceUserActivitySummary; -import org.sleuthkit.autopsy.datasourcesummary.datamodel.DataSourceTopProgramsSummary; -import org.sleuthkit.autopsy.datasourcesummary.datamodel.DataSourceUserActivitySummary.TopAccountResult; -import org.sleuthkit.autopsy.datasourcesummary.datamodel.DataSourceUserActivitySummary.TopDeviceAttachedResult; -import org.sleuthkit.autopsy.datasourcesummary.datamodel.DataSourceUserActivitySummary.TopWebSearchResult; -import org.sleuthkit.autopsy.datasourcesummary.datamodel.TopDomainsResult; -import org.sleuthkit.autopsy.datasourcesummary.datamodel.TopProgramsResult; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.IngestModuleCheckUtil; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.UserActivitySummary; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.TopProgramsSummary; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.UserActivitySummary.TopAccountResult; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.UserActivitySummary.TopDeviceAttachedResult; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.UserActivitySummary.TopWebSearchResult; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.TopProgramsSummary.TopProgramsResult; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.UserActivitySummary.TopDomainsResult; import org.sleuthkit.autopsy.datasourcesummary.uiutils.CellModelTableCellRenderer.DefaultCellModel; -import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchWorker; import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchWorker.DataFetchComponents; -import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchResult; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.IngestRunningLabel; import org.sleuthkit.autopsy.datasourcesummary.uiutils.JTablePanel; import org.sleuthkit.autopsy.datasourcesummary.uiutils.JTablePanel.ColumnModel; import org.sleuthkit.datamodel.DataSource; @@ -48,24 +46,24 @@ import org.sleuthkit.datamodel.DataSource; * A panel to display user activity. */ @Messages({ - "DataSourceSummaryUserActivityPanel_tab_title=User Activity", - "DataSourceSummaryUserActivityPanel_TopProgramsTableModel_name_header=Program", - "DataSourceSummaryUserActivityPanel_TopProgramsTableModel_folder_header=Folder", - "DataSourceSummaryUserActivityPanel_TopProgramsTableModel_count_header=Run Times", - "DataSourceSummaryUserActivityPanel_TopProgramsTableModel_lastrun_header=Last Run", - "DataSourceSummaryUserActivityPanel_TopDomainsTableModel_domain_header=Domain", - "DataSourceSummaryUserActivityPanel_TopDomainsTableModel_url_header=URL", - "DataSourceSummaryUserActivityPanel_TopDomainsTableModel_lastAccess_header=Last Access", - "DataSourceSummaryUserActivityPanel_noDataExists=No communication data exists", - "DataSourceSummaryUserActivityPanel_TopWebSearchTableModel_searchString_header=Search String", - "DataSourceSummaryUserActivityPanel_TopWebSearchTableModel_dateAccessed_header=Date Accessed", - "DataSourceSummaryUserActivityPanel_TopWebSearchTableModel_translatedResult_header=Translated", - "DataSourceSummaryUserActivityPanel_TopDeviceAttachedTableModel_deviceId_header=Device Id", - "DataSourceSummaryUserActivityPanel_TopDeviceAttachedTableModel_makeModel_header=Make and Model", - "DataSourceSummaryUserActivityPanel_TopDeviceAttachedTableModel_dateAccessed_header=Last Accessed", - "DataSourceSummaryUserActivityPanel_TopAccountTableModel_accountType_header=Account Type", - "DataSourceSummaryUserActivityPanel_TopAccountTableModel_lastAccess_header=Last Accessed",}) -public class DataSourceSummaryUserActivityPanel extends BaseDataSourceSummaryPanel { + "UserActivityPanel_tab_title=User Activity", + "UserActivityPanel_TopProgramsTableModel_name_header=Program", + "UserActivityPanel_TopProgramsTableModel_folder_header=Folder", + "UserActivityPanel_TopProgramsTableModel_count_header=Run Times", + "UserActivityPanel_TopProgramsTableModel_lastrun_header=Last Run", + "UserActivityPanel_TopDomainsTableModel_domain_header=Domain", + "UserActivityPanel_TopDomainsTableModel_count_header=Visits", + "UserActivityPanel_TopDomainsTableModel_lastAccess_header=Last Accessed", + "UserActivityPanel_TopWebSearchTableModel_searchString_header=Search String", + "UserActivityPanel_TopWebSearchTableModel_dateAccessed_header=Date Accessed", + "UserActivityPanel_TopWebSearchTableModel_translatedResult_header=Translated", + "UserActivityPanel_TopDeviceAttachedTableModel_deviceId_header=Device Id", + "UserActivityPanel_TopDeviceAttachedTableModel_makeModel_header=Make and Model", + "UserActivityPanel_TopDeviceAttachedTableModel_dateAccessed_header=Last Accessed", + "UserActivityPanel_TopAccountTableModel_accountType_header=Account Type", + "UserActivityPanel_TopAccountTableModel_lastAccess_header=Last Accessed", + "UserActivityPanel_noDataExists=No communication data exists"}) +public class UserActivityPanel extends BaseDataSourceSummaryPanel { private static final long serialVersionUID = 1L; private static final DateFormat DATETIME_FORMAT = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.getDefault()); @@ -74,6 +72,8 @@ public class DataSourceSummaryUserActivityPanel extends BaseDataSourceSummaryPan private static final int TOP_SEARCHES_COUNT = 10; private static final int TOP_ACCOUNTS_COUNT = 5; private static final int TOP_DEVICES_COUNT = 10; + private static final String ANDROID_FACTORY = "org.python.proxies.module$AndroidModuleFactory"; + private static final String ANDROID_MODULE_NAME = "Android Analyzer"; /** * Gets a string formatted date or returns empty string if the date is null. @@ -89,8 +89,8 @@ public class DataSourceSummaryUserActivityPanel extends BaseDataSourceSummaryPan // set up recent programs table private final JTablePanel topProgramsTable = JTablePanel.getJTablePanel(Arrays.asList( // program name column - new ColumnModel<>( - Bundle.DataSourceSummaryUserActivityPanel_TopProgramsTableModel_name_header(), + new ColumnModel( + Bundle.UserActivityPanel_TopProgramsTableModel_name_header(), (prog) -> { return new DefaultCellModel(prog.getProgramName()) .setTooltip(prog.getProgramPath()); @@ -98,7 +98,7 @@ public class DataSourceSummaryUserActivityPanel extends BaseDataSourceSummaryPan 250), // program folder column new ColumnModel<>( - Bundle.DataSourceSummaryUserActivityPanel_TopProgramsTableModel_folder_header(), + Bundle.UserActivityPanel_TopProgramsTableModel_folder_header(), (prog) -> { return new DefaultCellModel( getShortFolderName( @@ -109,7 +109,7 @@ public class DataSourceSummaryUserActivityPanel extends BaseDataSourceSummaryPan 150), // run count column new ColumnModel<>( - Bundle.DataSourceSummaryUserActivityPanel_TopProgramsTableModel_count_header(), + Bundle.UserActivityPanel_TopProgramsTableModel_count_header(), (prog) -> { String runTimes = prog.getRunTimes() == null ? "" : Long.toString(prog.getRunTimes()); return new DefaultCellModel(runTimes); @@ -117,69 +117,75 @@ public class DataSourceSummaryUserActivityPanel extends BaseDataSourceSummaryPan 80), // last run date column new ColumnModel<>( - Bundle.DataSourceSummaryUserActivityPanel_TopProgramsTableModel_lastrun_header(), + Bundle.UserActivityPanel_TopProgramsTableModel_lastrun_header(), (prog) -> new DefaultCellModel(getFormatted(prog.getLastRun())), 150) - )); + )) + .setKeyFunction((prog) -> prog.getProgramPath() + ":" + prog.getProgramName()); // set up recent domains table private final JTablePanel recentDomainsTable = JTablePanel.getJTablePanel(Arrays.asList( // domain column - new ColumnModel<>( - Bundle.DataSourceSummaryUserActivityPanel_TopDomainsTableModel_domain_header(), + new ColumnModel( + Bundle.UserActivityPanel_TopDomainsTableModel_domain_header(), (recentDomain) -> new DefaultCellModel(recentDomain.getDomain()), 250), - // url column + // count column new ColumnModel<>( - Bundle.DataSourceSummaryUserActivityPanel_TopDomainsTableModel_url_header(), - (recentDomain) -> new DefaultCellModel(recentDomain.getUrl()), - 250), + Bundle.UserActivityPanel_TopDomainsTableModel_count_header(), + (recentDomain) -> { + String visitTimes = recentDomain.getVisitTimes() == null ? "" : Long.toString(recentDomain.getVisitTimes()); + return new DefaultCellModel(visitTimes); + }, + 100), // last accessed column new ColumnModel<>( - Bundle.DataSourceSummaryUserActivityPanel_TopDomainsTableModel_lastAccess_header(), + Bundle.UserActivityPanel_TopDomainsTableModel_lastAccess_header(), (recentDomain) -> new DefaultCellModel(getFormatted(recentDomain.getLastVisit())), 150) - )); + )) + .setKeyFunction((domain) -> domain.getDomain()); // top web searches table private final JTablePanel topWebSearchesTable = JTablePanel.getJTablePanel(Arrays.asList( // search string column - new ColumnModel<>( - Bundle.DataSourceSummaryUserActivityPanel_TopWebSearchTableModel_searchString_header(), + new ColumnModel( + Bundle.UserActivityPanel_TopWebSearchTableModel_searchString_header(), (webSearch) -> new DefaultCellModel(webSearch.getSearchString()), 250 ), // last accessed new ColumnModel<>( - Bundle.DataSourceSummaryUserActivityPanel_TopWebSearchTableModel_dateAccessed_header(), + Bundle.UserActivityPanel_TopWebSearchTableModel_dateAccessed_header(), (webSearch) -> new DefaultCellModel(getFormatted(webSearch.getDateAccessed())), 150 ), // translated value new ColumnModel<>( - Bundle.DataSourceSummaryUserActivityPanel_TopWebSearchTableModel_translatedResult_header(), + Bundle.UserActivityPanel_TopWebSearchTableModel_translatedResult_header(), (webSearch) -> new DefaultCellModel(webSearch.getTranslatedResult()), 250 ) - )); + )) + .setKeyFunction((query) -> query.getSearchString()); // top devices attached table private final JTablePanel topDevicesAttachedTable = JTablePanel.getJTablePanel(Arrays.asList( // device id column - new ColumnModel<>( - Bundle.DataSourceSummaryUserActivityPanel_TopDeviceAttachedTableModel_deviceId_header(), + new ColumnModel( + Bundle.UserActivityPanel_TopDeviceAttachedTableModel_deviceId_header(), (device) -> new DefaultCellModel(device.getDeviceId()), 250 ), // last accessed new ColumnModel<>( - Bundle.DataSourceSummaryUserActivityPanel_TopDeviceAttachedTableModel_dateAccessed_header(), + Bundle.UserActivityPanel_TopDeviceAttachedTableModel_dateAccessed_header(), (device) -> new DefaultCellModel(getFormatted(device.getDateAccessed())), 150 ), // make and model new ColumnModel<>( - Bundle.DataSourceSummaryUserActivityPanel_TopDeviceAttachedTableModel_makeModel_header(), + Bundle.UserActivityPanel_TopDeviceAttachedTableModel_makeModel_header(), (device) -> { String make = StringUtils.isBlank(device.getDeviceMake()) ? "" : device.getDeviceMake().trim(); String model = StringUtils.isBlank(device.getDeviceModel()) ? "" : device.getDeviceModel().trim(); @@ -190,23 +196,25 @@ public class DataSourceSummaryUserActivityPanel extends BaseDataSourceSummaryPan }, 250 ) - )); + )) + .setKeyFunction((topDevice) -> topDevice.getDeviceId()); // top accounts table private final JTablePanel topAccountsTable = JTablePanel.getJTablePanel(Arrays.asList( // account type column - new ColumnModel<>( - Bundle.DataSourceSummaryUserActivityPanel_TopAccountTableModel_accountType_header(), + new ColumnModel( + Bundle.UserActivityPanel_TopAccountTableModel_accountType_header(), (account) -> new DefaultCellModel(account.getAccountType()), 250 ), // last accessed new ColumnModel<>( - Bundle.DataSourceSummaryUserActivityPanel_TopAccountTableModel_lastAccess_header(), + Bundle.UserActivityPanel_TopAccountTableModel_lastAccess_header(), (account) -> new DefaultCellModel(getFormatted(account.getLastAccess())), 150 ) - )); + )) + .setKeyFunction((topAccount) -> topAccount.getAccountType()); private final List> tables = Arrays.asList( topProgramsTable, @@ -216,56 +224,75 @@ public class DataSourceSummaryUserActivityPanel extends BaseDataSourceSummaryPan topAccountsTable ); + private final IngestRunningLabel ingestRunningLabel = new IngestRunningLabel(); + private final List> dataFetchComponents; - private final DataSourceTopProgramsSummary topProgramsData; + private final TopProgramsSummary topProgramsData; /** - * Creates a new DataSourceSummaryUserActivityPanel. + * Creates a new UserActivityPanel. */ - public DataSourceSummaryUserActivityPanel() { - this(new DataSourceTopProgramsSummary(), new DataSourceUserActivitySummary()); + public UserActivityPanel() { + this(new TopProgramsSummary(), new UserActivitySummary()); } /** - * Creates a new DataSourceSummaryUserActivityPanel. + * Creates a new UserActivityPanel. * * @param topProgramsData Class from which to obtain top programs data. * @param userActivityData Class from which to obtain remaining user * activity data. */ - public DataSourceSummaryUserActivityPanel( - DataSourceTopProgramsSummary topProgramsData, - DataSourceUserActivitySummary userActivityData) { + public UserActivityPanel( + TopProgramsSummary topProgramsData, + UserActivitySummary userActivityData) { + + super(topProgramsData, userActivityData); this.topProgramsData = topProgramsData; // set up data acquisition methods this.dataFetchComponents = Arrays.asList( // top programs query - new DataFetchComponents<>( + new DataFetchComponents>( (dataSource) -> topProgramsData.getTopPrograms(dataSource, TOP_PROGS_COUNT), - (result) -> topProgramsTable.showDataFetchResult(result, JTablePanel.getDefaultErrorMessage(), - Bundle.DataSourceSummaryUserActivityPanel_noDataExists())), + (result) -> { + showResultWithModuleCheck(topProgramsTable, result, + IngestModuleCheckUtil.RECENT_ACTIVITY_FACTORY, + IngestModuleCheckUtil.RECENT_ACTIVITY_MODULE_NAME); + }), // top domains query - new DataFetchComponents<>( + new DataFetchComponents>( (dataSource) -> userActivityData.getRecentDomains(dataSource, TOP_DOMAINS_COUNT), - (result) -> recentDomainsTable.showDataFetchResult(result, JTablePanel.getDefaultErrorMessage(), - Bundle.DataSourceSummaryUserActivityPanel_noDataExists())), + (result) -> { + showResultWithModuleCheck(recentDomainsTable, result, + IngestModuleCheckUtil.RECENT_ACTIVITY_FACTORY, + IngestModuleCheckUtil.RECENT_ACTIVITY_MODULE_NAME); + }), // top web searches query - new DataFetchComponents<>( + new DataFetchComponents>( (dataSource) -> userActivityData.getMostRecentWebSearches(dataSource, TOP_SEARCHES_COUNT), - (result) -> topWebSearchesTable.showDataFetchResult(result, JTablePanel.getDefaultErrorMessage(), - Bundle.DataSourceSummaryUserActivityPanel_noDataExists())), + (result) -> { + showResultWithModuleCheck(topWebSearchesTable, result, + IngestModuleCheckUtil.RECENT_ACTIVITY_FACTORY, + IngestModuleCheckUtil.RECENT_ACTIVITY_MODULE_NAME); + }), // top devices query - new DataFetchComponents<>( + new DataFetchComponents>( (dataSource) -> userActivityData.getRecentDevices(dataSource, TOP_DEVICES_COUNT), - (result) -> topDevicesAttachedTable.showDataFetchResult(result, JTablePanel.getDefaultErrorMessage(), - Bundle.DataSourceSummaryUserActivityPanel_noDataExists())), + (result) -> { + showResultWithModuleCheck(topDevicesAttachedTable, result, + IngestModuleCheckUtil.RECENT_ACTIVITY_FACTORY, + IngestModuleCheckUtil.RECENT_ACTIVITY_MODULE_NAME); + }), // top accounts query - new DataFetchComponents<>( + new DataFetchComponents>( (dataSource) -> userActivityData.getRecentAccounts(dataSource, TOP_ACCOUNTS_COUNT), - (result) -> topAccountsTable.showDataFetchResult(result, JTablePanel.getDefaultErrorMessage(), - Bundle.DataSourceSummaryUserActivityPanel_noDataExists())) + (result) -> { + showResultWithModuleCheck(topAccountsTable, result, + ANDROID_FACTORY, + ANDROID_MODULE_NAME); + }) ); initComponents(); @@ -283,27 +310,20 @@ public class DataSourceSummaryUserActivityPanel extends BaseDataSourceSummaryPan return this.topProgramsData.getShortFolderName(path, appName); } + @Override + protected void fetchInformation(DataSource dataSource) { + fetchInformation(dataFetchComponents, dataSource); + } + @Override protected void onNewDataSource(DataSource dataSource) { - // if no data source is present or the case is not open, - // set results for tables to null. - if (dataSource == null || !Case.isCaseOpen()) { - this.dataFetchComponents.forEach((item) -> item.getResultHandler() - .accept(DataFetchResult.getSuccessResult(null))); + onNewDataSource(dataFetchComponents, tables, dataSource); + } - } else { - // set tables to display loading screen - this.tables.forEach((table) -> table.showDefaultLoadingMessage()); - - // create swing workers to run for each table - List> workers = dataFetchComponents - .stream() - .map((components) -> new DataFetchWorker<>(components, dataSource)) - .collect(Collectors.toList()); - - // submit swing workers to run - submit(workers); - } + @Override + public void close() { + ingestRunningLabel.unregister(); + super.close(); } /** @@ -317,6 +337,7 @@ public class DataSourceSummaryUserActivityPanel extends BaseDataSourceSummaryPan javax.swing.JScrollPane contentScrollPane = new javax.swing.JScrollPane(); javax.swing.JPanel contentPanel = new javax.swing.JPanel(); + javax.swing.JPanel ingestRunningPanel = ingestRunningLabel; javax.swing.JLabel programsRunLabel = new javax.swing.JLabel(); javax.swing.Box.Filler filler1 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 2), new java.awt.Dimension(0, 2), new java.awt.Dimension(0, 2)); javax.swing.JPanel topProgramsTablePanel = topProgramsTable; @@ -347,8 +368,14 @@ public class DataSourceSummaryUserActivityPanel extends BaseDataSourceSummaryPan contentPanel.setMinimumSize(new java.awt.Dimension(10, 450)); contentPanel.setLayout(new javax.swing.BoxLayout(contentPanel, javax.swing.BoxLayout.PAGE_AXIS)); + ingestRunningPanel.setAlignmentX(0.0F); + ingestRunningPanel.setMaximumSize(new java.awt.Dimension(32767, 25)); + ingestRunningPanel.setMinimumSize(new java.awt.Dimension(10, 25)); + ingestRunningPanel.setPreferredSize(new java.awt.Dimension(10, 25)); + contentPanel.add(ingestRunningPanel); + programsRunLabel.setHorizontalAlignment(javax.swing.SwingConstants.LEFT); - org.openide.awt.Mnemonics.setLocalizedText(programsRunLabel, org.openide.util.NbBundle.getMessage(DataSourceSummaryUserActivityPanel.class, "DataSourceSummaryUserActivityPanel.programsRunLabel.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(programsRunLabel, org.openide.util.NbBundle.getMessage(UserActivityPanel.class, "UserActivityPanel.programsRunLabel.text")); // NOI18N programsRunLabel.setAlignmentX(Component.LEFT_ALIGNMENT); contentPanel.add(programsRunLabel); contentPanel.add(filler1); @@ -361,7 +388,7 @@ public class DataSourceSummaryUserActivityPanel extends BaseDataSourceSummaryPan contentPanel.add(filler3); recentDomainsLabel.setHorizontalAlignment(javax.swing.SwingConstants.LEFT); - org.openide.awt.Mnemonics.setLocalizedText(recentDomainsLabel, org.openide.util.NbBundle.getMessage(DataSourceSummaryUserActivityPanel.class, "DataSourceSummaryUserActivityPanel.recentDomainsLabel.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(recentDomainsLabel, org.openide.util.NbBundle.getMessage(UserActivityPanel.class, "UserActivityPanel.recentDomainsLabel.text")); // NOI18N contentPanel.add(recentDomainsLabel); contentPanel.add(filler2); @@ -373,7 +400,7 @@ public class DataSourceSummaryUserActivityPanel extends BaseDataSourceSummaryPan contentPanel.add(filler4); topWebSearchLabel.setHorizontalAlignment(javax.swing.SwingConstants.LEFT); - org.openide.awt.Mnemonics.setLocalizedText(topWebSearchLabel, org.openide.util.NbBundle.getMessage(DataSourceSummaryUserActivityPanel.class, "DataSourceSummaryUserActivityPanel.topWebSearchLabel.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(topWebSearchLabel, org.openide.util.NbBundle.getMessage(UserActivityPanel.class, "UserActivityPanel.topWebSearchLabel.text")); // NOI18N contentPanel.add(topWebSearchLabel); contentPanel.add(filler5); @@ -385,7 +412,7 @@ public class DataSourceSummaryUserActivityPanel extends BaseDataSourceSummaryPan contentPanel.add(filler6); topDevicesAttachedLabel.setHorizontalAlignment(javax.swing.SwingConstants.LEFT); - org.openide.awt.Mnemonics.setLocalizedText(topDevicesAttachedLabel, org.openide.util.NbBundle.getMessage(DataSourceSummaryUserActivityPanel.class, "DataSourceSummaryUserActivityPanel.topDevicesAttachedLabel.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(topDevicesAttachedLabel, org.openide.util.NbBundle.getMessage(UserActivityPanel.class, "UserActivityPanel.topDevicesAttachedLabel.text")); // NOI18N contentPanel.add(topDevicesAttachedLabel); contentPanel.add(filler7); @@ -397,7 +424,7 @@ public class DataSourceSummaryUserActivityPanel extends BaseDataSourceSummaryPan contentPanel.add(filler8); recentAccountsLabel.setHorizontalAlignment(javax.swing.SwingConstants.LEFT); - org.openide.awt.Mnemonics.setLocalizedText(recentAccountsLabel, org.openide.util.NbBundle.getMessage(DataSourceSummaryUserActivityPanel.class, "DataSourceSummaryUserActivityPanel.recentAccountsLabel.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(recentAccountsLabel, org.openide.util.NbBundle.getMessage(UserActivityPanel.class, "UserActivityPanel.recentAccountsLabel.text")); // NOI18N contentPanel.add(recentAccountsLabel); contentPanel.add(filler9); diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/AbstractLoadableComponent.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/AbstractLoadableComponent.java new file mode 100644 index 0000000000..74df7fd4d5 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/AbstractLoadableComponent.java @@ -0,0 +1,170 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourcesummary.uiutils; + +import java.util.Collection; +import java.util.logging.Level; +import javax.swing.JPanel; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coreutils.Logger; + +/** + * Abstract class for common methods of a loadable component. + */ +@NbBundle.Messages({ + "AbstractLoadableComponent_loadingMessage_defaultText=Loading results...", + "AbstractLoadableComponent_errorMessage_defaultText=There was an error loading results.", + "AbstractLoadableComponent_noDataExists_defaultText=No data exists.",}) +public abstract class AbstractLoadableComponent extends JPanel implements LoadableComponent { + + private static final long serialVersionUID = 1L; + + /** + * The default loading message. + */ + public static final String DEFAULT_LOADING_MESSAGE = Bundle.AbstractLoadableComponent_loadingMessage_defaultText(); + + /** + * The default error message. + */ + public static final String DEFAULT_ERROR_MESSAGE = Bundle.AbstractLoadableComponent_errorMessage_defaultText(); + + /** + * The default 'no results' message. + */ + public static final String DEFAULT_NO_RESULTS_MESSAGE = Bundle.AbstractLoadableComponent_noDataExists_defaultText(); + + private static final Logger logger = Logger.getLogger(AbstractLoadableComponent.class.getName()); + + /** + * @return The default error message. + */ + public static String getDefaultErrorMessage() { + return DEFAULT_ERROR_MESSAGE; + } + + /** + * @return The default message for no results. + */ + public static String getDefaultNoResultsMessage() { + return DEFAULT_NO_RESULTS_MESSAGE; + } + + /** + * Clears the results from the underlying JTable and shows the provided + * message. + * + * @param message The message to be shown. + */ + public synchronized void showMessage(String message) { + setResults(null); + setMessage(true, message); + repaint(); + } + + /** + * Shows a default loading message on the table. This will clear any results + * in the table. + */ + public void showDefaultLoadingMessage() { + showMessage(DEFAULT_LOADING_MESSAGE); + } + + /** + * Shows the list as rows of data in the table. If overlay message will be + * cleared if present. + * + * @param data The data to be shown where each item represents a row of + * data. + */ + public synchronized void showResults(T data) { + setMessage(false, null); + setResults(data); + repaint(); + } + + /** + * Shows the data in a DataFetchResult. If there was an error during the + * operation, the errorMessage will be displayed. If the operation completed + * successfully and no data is present, noResultsMessage will be shown. + * Otherwise, the data will be shown as rows in the table. + * + * @param result The DataFetchResult. + * @param errorMessage The error message to be shown in the event of an + * error. + * @param noResultsMessage The message to be shown if there are no results + * but the operation completed successfully. + */ + public void showDataFetchResult(DataFetchResult result, String errorMessage, String noResultsMessage) { + if (result == null) { + logger.log(Level.SEVERE, "Null data fetch result received."); + return; + } + + switch (result.getResultType()) { + case SUCCESS: + T data = result.getData(); + if (data == null || (data instanceof Collection && ((Collection) data).isEmpty())) { + showMessage(noResultsMessage); + } else { + showResults(data); + } + break; + case ERROR: + // if there is an error, log accordingly, set result list to + // empty and display error message + logger.log(Level.WARNING, "An exception was caused while results were loaded.", result.getException()); + showMessage(errorMessage); + break; + default: + // an unknown loading state was specified. log accordingly. + logger.log(Level.SEVERE, "No known loading state was found in result."); + break; + } + } + + /** + * Shows the data in a DataFetchResult. If there was an error during the + * operation, the DEFAULT_ERROR_MESSAGE will be displayed. If the operation + * completed successfully and no data is present, DEFAULT_NO_RESULTS_MESSAGE + * will be shown. Otherwise, the data will be shown as rows in the table. + * + * @param result The DataFetchResult. + */ + public void showDataFetchResult(DataFetchResult result) { + showDataFetchResult(result, DEFAULT_ERROR_MESSAGE, DEFAULT_NO_RESULTS_MESSAGE); + } + + /** + * Sets the message and visibility of the message. Repaint does not need to + * be handled in this method. + * + * @param visible The visibility of the message. + * @param message The message to be displayed if visible. + */ + protected abstract void setMessage(boolean visible, String message); + + /** + * Sets the data to be shown in the JTable. Repaint does not need to be + * handled in this method. + * + * @param data The list of data objects to be shown. + */ + protected abstract void setResults(T data); +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/BaseMessageOverlay.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/BaseMessageOverlay.java new file mode 100644 index 0000000000..8e90945eb7 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/BaseMessageOverlay.java @@ -0,0 +1,106 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourcesummary.uiutils; + +import java.awt.Graphics; +import javax.swing.JLabel; + +/** + * Base class for drawing a message overlay. Contains a paint method for + * painting a JLabel using a java.awt.Graphics object. + */ +public class BaseMessageOverlay { + + private final JLabel label; + private boolean visible = false; + + /** + * Main constructor for the Overlay. + */ + public BaseMessageOverlay() { + label = new JLabel(); + label.setHorizontalAlignment(JLabel.CENTER); + label.setVerticalAlignment(JLabel.CENTER); + label.setOpaque(false); + } + + /** + * @return Whether or not this message overlay should be visible. + */ + public boolean isVisible() { + return visible; + } + + /** + * Sets this layer visible when painted. In order to be shown in UI, this + * component needs to be repainted. + * + * @param visible Whether or not it is visible. + */ + public void setVisible(boolean visible) { + this.visible = visible; + } + + /** + * Sets the message to be displayed in the child jlabel. + * + * @param message The message to be displayed. + */ + public void setMessage(String message) { + label.setText(String.format("
%s
", + message == null ? "" : message)); + } + + /** + * Paints the jlabel at full width and height with the graphics object. + * + * @param g The graphics object. + * @param width The width. + * @param height The height. + */ + public void paintOverlay(Graphics g, int width, int height) { + paintOverlay(g, width, height, null); + } + + /** + * Paints the jlabel at full width and height with the graphics object. + * + * @param g The graphics object. + * @param parentWidth The width of the component. + * @param parentHeight The height of the component. + * @param labelMaxWidth The maximum width of the label drawn for the + * overlay. The label will be vertically and + * horizontally centered. + */ + public void paintOverlay(Graphics g, int parentWidth, int parentHeight, Integer labelMaxWidth) { + if (!visible) { + return; + } + + int labelWidth = (labelMaxWidth == null) ? parentWidth : Math.min(labelMaxWidth, parentWidth); + int leftPad = (parentWidth - labelWidth) / 2; + + // paint the jlabel if visible. + g.translate(leftPad, 0); + label.setBounds(0, 0, labelWidth, parentHeight); + g.translate(0, 0); + + label.paint(g); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/Bundle.properties-MERGED index 332a884f05..c06bc6850a 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/Bundle.properties-MERGED @@ -1,3 +1,5 @@ -JTablePanel_errorMessage_defaultText=There was an error loading results. -JTablePanel_loadingMessage_defaultText=Loading results... -JTablePanel_noDataExists_defaultText=No data exists. +AbstractLoadableComponent_errorMessage_defaultText=There was an error loading results. +AbstractLoadableComponent_loadingMessage_defaultText=Loading results... +AbstractLoadableComponent_noDataExists_defaultText=No data exists. +IngestRunningLabel_defaultMessage=Ingest is currently running. +PieChartPanel_noDataLabel=No Data diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DataFetchWorker.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DataFetchWorker.java index 89f11f544c..10ed19b87a 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DataFetchWorker.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DataFetchWorker.java @@ -20,7 +20,6 @@ package org.sleuthkit.autopsy.datasourcesummary.uiutils; import java.util.concurrent.ExecutionException; import java.util.function.Consumer; -import java.util.logging.Level; import javax.swing.SwingWorker; import org.sleuthkit.autopsy.coreutils.Logger; @@ -71,6 +70,7 @@ public class DataFetchWorker extends SwingWorker { } private static final Logger logger = Logger.getLogger(DataFetchWorker.class.getName()); + private static final int MAX_INNER_EXCEPTION_DEPTH = 100; private final A args; private final DataFetcher processor; @@ -128,14 +128,17 @@ public class DataFetchWorker extends SwingWorker { return; } catch (ExecutionException ex) { Throwable inner = ex.getCause(); - // if cancelled during operation, simply return - if (inner instanceof InterruptedException) { - return; + for (int i = 0; i < MAX_INNER_EXCEPTION_DEPTH; i++) { + if (inner == null) { + break; + } else if (inner instanceof InterruptedException) { + // if cancelled during operation, simply return + return; + } else { + inner = inner.getCause(); + } } - // otherwise, there is an error to log - logger.log(Level.WARNING, "There was an error while fetching results.", ex); - // and pass the result to the client resultHandler.accept(DataFetchResult.getErrorResult(inner)); return; diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DefaultArtifactUpdateGovernor.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DefaultArtifactUpdateGovernor.java new file mode 100644 index 0000000000..390317a955 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DefaultArtifactUpdateGovernor.java @@ -0,0 +1,60 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.Arrays; +import java.util.HashSet; +import java.util.Set; +import org.sleuthkit.autopsy.ingest.IngestManager; +import org.sleuthkit.autopsy.ingest.IngestManager.IngestJobEvent; +import org.sleuthkit.autopsy.ingest.ModuleDataEvent; + +/** + * An UpdateGovernor that provides a means of providing a set of artifact type + * id's that should trigger an update. + */ +public interface DefaultArtifactUpdateGovernor extends DefaultUpdateGovernor { + + Set INGEST_JOB_EVENTS = new HashSet<>( + Arrays.asList(IngestJobEvent.COMPLETED, IngestJobEvent.CANCELLED)); + + @Override + default boolean isRefreshRequired(ModuleDataEvent evt) { + if (evt == null || evt.getBlackboardArtifactType() == null) { + return false; + } + + return getArtifactTypeIdsForRefresh().contains(evt.getBlackboardArtifactType().getTypeID()); + } + + @Override + default boolean isRefreshRequired(IngestManager.IngestJobEvent evt) { + return (evt != null && INGEST_JOB_EVENTS.contains(evt)); + } + + @Override + default Set getIngestJobEventUpdates() { + return INGEST_JOB_EVENTS; + } + + /** + * @return The set of artifact type id's that should trigger an update. + */ + Set getArtifactTypeIdsForRefresh(); +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DefaultUpdateGovernor.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DefaultUpdateGovernor.java new file mode 100644 index 0000000000..f6ca7bc60f --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DefaultUpdateGovernor.java @@ -0,0 +1,70 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.beans.PropertyChangeEvent; +import java.util.Collections; +import java.util.Set; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.ingest.IngestManager; +import org.sleuthkit.autopsy.ingest.ModuleContentEvent; +import org.sleuthkit.autopsy.ingest.ModuleDataEvent; +import org.sleuthkit.datamodel.AbstractFile; + +/** + * The default UpdateGovernor where no updates will be triggered unless + * overridden. + */ +public interface DefaultUpdateGovernor extends UpdateGovernor { + + @Override + default boolean isRefreshRequiredForCaseEvent(PropertyChangeEvent evt) { + return false; + } + + @Override + default boolean isRefreshRequired(ModuleContentEvent evt) { + return false; + } + + @Override + default boolean isRefreshRequired(ModuleDataEvent evt) { + return false; + } + + @Override + default boolean isRefreshRequired(IngestManager.IngestJobEvent evt) { + return false; + } + + @Override + default Set getCaseEventUpdates() { + return Collections.emptySet(); + } + + @Override + default Set getIngestJobEventUpdates() { + return Collections.emptySet(); + } + + @Override + default boolean isRefreshRequired(AbstractFile evt) { + return false; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/EventUpdateHandler.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/EventUpdateHandler.java new file mode 100644 index 0000000000..7205821eb3 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/EventUpdateHandler.java @@ -0,0 +1,199 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.Set; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.guiutils.RefreshThrottler; +import org.sleuthkit.autopsy.ingest.IngestManager; +import org.sleuthkit.autopsy.ingest.IngestManager.IngestJobEvent; +import org.sleuthkit.autopsy.ingest.ModuleContentEvent; +import org.sleuthkit.autopsy.ingest.ModuleDataEvent; +import org.sleuthkit.datamodel.AbstractFile; + +/** + * Handles ingest and case events, and determines whether they should trigger an + * update. + */ +public class EventUpdateHandler { + + /** + * The refresh throttler that handles ingest events. + */ + private final RefreshThrottler refreshThrottler = new RefreshThrottler(new RefreshThrottler.Refresher() { + @Override + public void refresh() { + // delegate to EventUpdateHandler method. + EventUpdateHandler.this.onRefresh(); + } + + @Override + public boolean isRefreshRequired(PropertyChangeEvent evt) { + String eventType = evt.getPropertyName(); + if (Case.isCaseOpen()) { + if (IngestManager.IngestModuleEvent.DATA_ADDED.toString().equals(eventType) && evt.getOldValue() instanceof ModuleDataEvent) { + ModuleDataEvent dataEvent = (ModuleDataEvent) evt.getOldValue(); + return EventUpdateHandler.this.isRefreshRequired(dataEvent); + } else if (IngestManager.IngestModuleEvent.CONTENT_CHANGED.toString().equals(eventType) && evt.getOldValue() instanceof ModuleContentEvent) { + ModuleContentEvent contentEvent = (ModuleContentEvent) evt.getOldValue(); + return EventUpdateHandler.this.isRefreshRequired(contentEvent); + } else if (IngestManager.IngestModuleEvent.FILE_DONE.toString().equals(eventType) && evt.getNewValue() instanceof AbstractFile) { + AbstractFile analyzedFile = (AbstractFile) evt.getNewValue(); + return EventUpdateHandler.this.isRefreshRequired(analyzedFile); + } + } + return false; + } + }); + + /** + * Handler for case event updates. + */ + private final PropertyChangeListener caseEventsListener = (evt) -> { + if (isRefreshRequiredForCaseEvent(evt)) { + onRefresh(); + } + }; + + private final PropertyChangeListener ingestJobEventsListener = (evt) -> { + if (evt == null) { + return; + } + + String eventName = evt.getPropertyName(); + for (IngestJobEvent ingestEvt : IngestJobEvent.values()) { + if (ingestEvt.name().equals(eventName) && isRefreshRequired(ingestEvt)) { + onRefresh(); + } + } + }; + + private final UpdateGovernor governor; + private final Set caseEvents; + private final Set ingestEvents; + private final Runnable onUpdate; + + /** + * Constructor. + * + * @param onUpdate The function to call if an update should be required. + * @param governor The item used to determine if an update is required. If + * the governor requires an update, then onUpdate is + * triggered. + */ + public EventUpdateHandler(Runnable onUpdate, UpdateGovernor governor) { + if (onUpdate == null) { + throw new IllegalArgumentException("onUpdate parameter must be non-null."); + } + + this.onUpdate = onUpdate; + this.governor = governor; + this.caseEvents = governor.getCaseEventUpdates(); + this.ingestEvents = governor.getIngestJobEventUpdates(); + } + + /** + * Handles whether or not a ModuleDataEvent should trigger an update. + * + * @param evt The ModuleDataEvent. + * + * @return True if an update should occur. + */ + protected boolean isRefreshRequired(ModuleDataEvent evt) { + return governor.isRefreshRequired(evt); + } + + /** + * Handles whether or not a ModuleContentEvent should trigger an update. + * + * @param evt The ModuleContentEvent. + * + * @return True if an update should occur. + */ + protected boolean isRefreshRequired(ModuleContentEvent evt) { + return governor.isRefreshRequired(evt); + } + + /** + * Handles whether or not a newly added AbstractFile should trigger an + * update. + * + * @param evt The AbstractFile. + * + * @return True if an update should occur. + */ + protected boolean isRefreshRequired(AbstractFile evt) { + return governor.isRefreshRequired(evt); + } + + /** + * Handles whether or not a IngestJobEvent should trigger an update. + * + * @param evt The IngestJobEvent. + * + * @return True if an update should occur. + */ + protected boolean isRefreshRequired(IngestJobEvent evt) { + return governor.isRefreshRequired(evt); + } + + /** + * Handles whether or not a case event should trigger an update. + * + * @param evt The case event. + * + * @return True if an update should occur. + */ + protected boolean isRefreshRequiredForCaseEvent(PropertyChangeEvent evt) { + return governor.isRefreshRequiredForCaseEvent(evt); + } + + /** + * Method called that triggers refresh. + */ + protected void onRefresh() { + onUpdate.run(); + } + + /** + * Registers ingest and case event listeners. + */ + public void register() { + if (!caseEvents.isEmpty()) { + Case.addEventTypeSubscriber(caseEvents, caseEventsListener); + } + + IngestManager.getInstance().addIngestJobEventListener(ingestEvents, ingestJobEventsListener); + refreshThrottler.registerForIngestModuleEvents(); + } + + /** + * Unregisters ingest and case event listeners. + */ + public void unregister() { + if (!caseEvents.isEmpty()) { + Case.removeEventTypeSubscriber(caseEvents, caseEventsListener); + } + + IngestManager.getInstance().removeIngestJobEventListener(ingestEvents, ingestJobEventsListener); + refreshThrottler.unregisterEventListener(); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/IngestRunningLabel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/IngestRunningLabel.java new file mode 100644 index 0000000000..0be1e85396 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/IngestRunningLabel.java @@ -0,0 +1,167 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.awt.BorderLayout; +import java.beans.PropertyChangeListener; +import java.net.URL; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Set; +import javax.swing.ImageIcon; +import javax.swing.JLabel; +import javax.swing.JPanel; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.ingest.IngestManager; + +/** + * JLabel that shows ingest is running. + */ +@Messages({ + "IngestRunningLabel_defaultMessage=Ingest is currently running." +}) +public class IngestRunningLabel extends JPanel { + + private static final long serialVersionUID = 1L; + public static final String DEFAULT_MESSAGE = Bundle.IngestRunningLabel_defaultMessage(); + private static final URL DEFAULT_ICON = IngestRunningLabel.class.getResource("/org/sleuthkit/autopsy/modules/filetypeid/warning16.png"); + + private static final Set INGEST_JOB_EVENTS_OF_INTEREST = EnumSet.of( + IngestManager.IngestJobEvent.STARTED, + IngestManager.IngestJobEvent.CANCELLED, + IngestManager.IngestJobEvent.COMPLETED + ); + + private static Set activeLabels = new HashSet<>(); + private static PropertyChangeListener classListener = null; + private static Object lockObject = new Object(); + + /** + * Setup ingest event listener for the current label. + * + * @param label The label. + */ + private static void setupListener(IngestRunningLabel label) { + synchronized (lockObject) { + + // if listener is not initialized, initialize it. + if (classListener == null) { + classListener = (evt) -> { + if (evt == null) { + return; + } + + if (evt.getPropertyName().equals(IngestManager.IngestJobEvent.STARTED.toString())) { + // ingest started + notifyListeners(true); + + } else if (evt.getPropertyName().equals(IngestManager.IngestJobEvent.CANCELLED.toString()) + || evt.getPropertyName().equals(IngestManager.IngestJobEvent.COMPLETED.toString())) { + // ingest cancelled or finished + notifyListeners(false); + + } + }; + IngestManager.getInstance().addIngestJobEventListener(INGEST_JOB_EVENTS_OF_INTEREST, classListener); + } + + // add the item to the set + activeLabels.add(label); + } + } + + /** + * Notifies all listening instances of an update in ingest state. + * + * @param ingestIsRunning Whether or not ingest is running currently. + */ + private static void notifyListeners(boolean ingestIsRunning) { + synchronized (lockObject) { + for (IngestRunningLabel label : activeLabels) { + label.refreshState(ingestIsRunning); + } + } + } + + /** + * Removes a label from listening events. + * + * @param label The label to remove from listening events. + */ + private static void removeListener(IngestRunningLabel label) { + synchronized (lockObject) { + activeLabels.remove(label); + if (activeLabels.isEmpty() && classListener != null) { + IngestManager.getInstance().removeIngestJobEventListener(classListener); + classListener = null; + } + } + } + + /** + * Main constructor with default message and showing icon. + */ + public IngestRunningLabel() { + this(DEFAULT_MESSAGE, true); + } + + /** + * Constructor. + * + * @param message The message to be shown. + * @param showWarningIcon Whether or not to show warning icon. + */ + public IngestRunningLabel(String message, boolean showWarningIcon) { + JLabel jlabel = new JLabel(); + jlabel.setText(message); + + if (showWarningIcon) { + jlabel.setIcon(new ImageIcon(DEFAULT_ICON)); + } + + setLayout(new BorderLayout()); + add(jlabel, BorderLayout.NORTH); + + setupListener(this); + refreshState(); + } + + /** + * Refresh state of this label based on ingest status. + */ + protected final void refreshState() { + refreshState(IngestManager.getInstance().isIngestRunning()); + } + + /** + * Refresh state of this label based on ingest status. + * + * @param ingestIsRunning True if ingest is running. + */ + protected final void refreshState(boolean ingestIsRunning) { + setVisible(ingestIsRunning); + } + + /** + * Unregister this instance from listening for ingest status changes. + */ + public void unregister() { + removeListener(this); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/JTablePanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/JTablePanel.java index fe164db8df..550f334d7a 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/JTablePanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/JTablePanel.java @@ -23,31 +23,22 @@ import java.awt.Graphics; import java.util.Collections; import java.util.List; import java.util.function.Function; -import java.util.logging.Level; import java.util.stream.Collectors; import javax.swing.JComponent; -import javax.swing.JLabel; import javax.swing.JLayer; -import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.plaf.LayerUI; import javax.swing.table.DefaultTableColumnModel; import javax.swing.table.TableColumn; import javax.swing.table.TableColumnModel; -import org.openide.util.NbBundle.Messages; -import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.datasourcesummary.uiutils.CellModelTableCellRenderer.CellModel; /** * A table that displays a list of items and also can display messages for * loading, load error, and not loaded. */ -@Messages({ - "JTablePanel_loadingMessage_defaultText=Loading results...", - "JTablePanel_errorMessage_defaultText=There was an error loading results.", - "JTablePanel_noDataExists_defaultText=No data exists.",}) -public class JTablePanel extends JPanel { +public class JTablePanel extends AbstractLoadableComponent> { /** * JTables don't allow displaying messages. So this LayerUI is used to @@ -58,27 +49,7 @@ public class JTablePanel extends JPanel { private static class Overlay extends LayerUI { private static final long serialVersionUID = 1L; - - private final JLabel label; - private boolean visible; - - /** - * Main constructor for the Overlay. - */ - Overlay() { - label = new JLabel(); - label.setHorizontalAlignment(JLabel.CENTER); - label.setVerticalAlignment(JLabel.CENTER); - label.setOpaque(false); - - } - - /** - * @return Whether or not this message overlay should be visible. - */ - boolean isVisible() { - return visible; - } + private final BaseMessageOverlay overlayDelegate = new BaseMessageOverlay(); /** * Sets this layer visible when painted. In order to be shown in UI, @@ -87,7 +58,7 @@ public class JTablePanel extends JPanel { * @param visible Whether or not it is visible. */ void setVisible(boolean visible) { - this.visible = visible; + overlayDelegate.setVisible(visible); } /** @@ -96,25 +67,13 @@ public class JTablePanel extends JPanel { * @param message The message to be displayed. */ void setMessage(String message) { - label.setText(String.format("
%s
", - message == null ? "" : message)); + overlayDelegate.setMessage(message); } @Override public void paint(Graphics g, JComponent c) { - // Paint the underlying view. super.paint(g, c); - - if (!visible) { - return; - } - - int w = c.getWidth(); - int h = c.getHeight(); - - // paint the jlabel if visible. - label.setBounds(0, 0, w, h); - label.paint(g); + overlayDelegate.paintOverlay(g, c.getWidth(), c.getHeight()); } } @@ -179,12 +138,6 @@ public class JTablePanel extends JPanel { private static final long serialVersionUID = 1L; - private static final Logger logger = Logger.getLogger(JTablePanel.class.getName()); - - private static final String DEFAULT_LOADING_MESSAGE = Bundle.JTablePanel_loadingMessage_defaultText(); - private static final String DEFAULT_ERROR_MESSAGE = Bundle.JTablePanel_errorMessage_defaultText(); - private static final String DEFAULT_NO_RESULTS_MESSAGE = Bundle.JTablePanel_noDataExists_defaultText(); - private static final CellModelTableCellRenderer DEFAULT_CELL_RENDERER = new CellModelTableCellRenderer(); /** @@ -231,7 +184,7 @@ public class JTablePanel extends JPanel { .map((colModel) -> colModel.getCellRenderer()) .collect(Collectors.toList()); - return new DefaultListTableModel<>(columnRenderers); + return new DefaultListTableModel(columnRenderers); } /** @@ -248,24 +201,11 @@ public class JTablePanel extends JPanel { return resultTable.setColumnModel(getTableColumnModel(columns)); } - /** - * @return The default error message. - */ - public static String getDefaultErrorMessage() { - return DEFAULT_ERROR_MESSAGE; - } - - /** - * @return The default message for no results. - */ - public static String getDefaultNoResultsMessage() { - return DEFAULT_NO_RESULTS_MESSAGE; - } - private JScrollPane tableScrollPane; private Overlay overlayLayer; private ListTableModel tableModel; private JTable table; + private Function keyFunction = (rowItem) -> rowItem; /** * Panel constructor. @@ -285,18 +225,21 @@ public class JTablePanel extends JPanel { } /** - * Set the table model. This method must be called prior to calling + * Set the table model. This method must be called prior to calling * setResultList. * * @param tableModel + * + * @return As a utility, returns this. */ - public final void setModel(ListTableModel tableModel) { + public final JTablePanel setModel(ListTableModel tableModel) { if (tableModel == null) { throw new IllegalArgumentException("Null table model passed to setModel"); } this.tableModel = tableModel; table.setModel(tableModel); + return this; } /** @@ -319,124 +262,68 @@ public class JTablePanel extends JPanel { } /** - * Sets the data to be shown in the JTable. Repaint is not handled in this - * method and should be handled separately. - * - * @param data The list of data objects to be shown. + * @return The function for determining the key for a data row. This key is + * used to maintain current selection in the table despite changing + * rows. */ - private void setResultList(List data) { - - if(tableModel == null) { - throw new IllegalStateException("ListTableModel has not be initialized"); + public Function getKeyFunction() { + return keyFunction; + } + + /** + * Sets the function for determining the key for a data row. This key is + * used to maintain current selection in the table despite changing rows. + * + * @param keyFunction The function to determine the key of a row. + * + * @return As a utility, returns this. + */ + public JTablePanel setKeyFunction(Function keyFunction) { + if (keyFunction == null) { + throw new IllegalArgumentException("Key function must be non-null"); } + + this.keyFunction = keyFunction; + return this; + } + + @Override + protected synchronized void setResults(List data) { + // get previously selected value + int prevSelectedRow = this.table.getSelectedRow(); + List tableRows = this.tableModel.getDataRows(); + T prevValue = (tableRows != null && prevSelectedRow >= 0 && prevSelectedRow < tableRows.size()) + ? this.tableModel.getDataRows().get(prevSelectedRow) + : null; + Object prevKeyValue = (prevValue == null) ? null : this.keyFunction.apply(prevValue); + // set the list of data to be shown as either the data or an empty list // on null. List dataToSet = (data == null) ? Collections.emptyList() : data; - // since the data is being reset, scroll to the top. - tableScrollPane.getVerticalScrollBar().setValue(0); - // set the underlying table model's data. this.tableModel.setDataRows(dataToSet); + + // set the row to selected value if the value is found + if (prevKeyValue != null) { + for (int objIndex = 0; objIndex < dataToSet.size(); objIndex++) { + Object thisKey = this.keyFunction.apply(dataToSet.get(objIndex)); + if (prevKeyValue.equals(thisKey)) { + this.table.setRowSelectionInterval(objIndex, objIndex); + break; + } + } + } + } - /** - * Sets the message and visibility of the overlay. Repaint is not handled in - * this method and should be handled separately. - * - * @param visible The visibility of the overlay. - * @param message The message in the overlay. - */ - private void setOverlay(boolean visible, String message) { + @Override + protected void setMessage(boolean visible, String message) { this.overlayLayer.setVisible(visible); this.overlayLayer.setMessage(message); } - /** - * Clears the results from the underlying JTable and shows the provided - * message. - * - * @param message The message to be shown. - */ - public synchronized void showMessage(String message) { - setResultList(null); - setOverlay(true, message); - repaint(); - } - - /** - * Shows a default loading message on the table. This will clear any results - * in the table. - */ - public void showDefaultLoadingMessage() { - showMessage(DEFAULT_LOADING_MESSAGE); - } - - /** - * Shows the list as rows of data in the table. If overlay message will be - * cleared if present. - * - * @param data The data to be shown where each item represents a row of - * data. - */ - public synchronized void showResults(List data) { - setOverlay(false, null); - setResultList(data); - repaint(); - } - - /** - * Shows the data in a DataFetchResult. If there was an error during the - * operation, the errorMessage will be displayed. If the operation completed - * successfully and no data is present, noResultsMessage will be shown. - * Otherwise, the data will be shown as rows in the table. - * - * @param result The DataFetchResult. - * @param errorMessage The error message to be shown in the event of an - * error. - * @param noResultsMessage The message to be shown if there are no results - * but the operation completed successfully. - */ - public void showDataFetchResult(DataFetchResult> result, String errorMessage, String noResultsMessage) { - if (result == null) { - logger.log(Level.SEVERE, "Null data processor result received."); - return; - } - - switch (result.getResultType()) { - case SUCCESS: - if (result.getData() == null || result.getData().isEmpty()) { - showMessage(noResultsMessage); - } else { - showResults(result.getData()); - } - break; - case ERROR: - // if there is an error, log accordingly, set result list to - // empty and display error message - logger.log(Level.WARNING, "An exception was caused while results were loaded.", result.getException()); - showMessage(errorMessage); - break; - default: - // an unknown loading state was specified. log accordingly. - logger.log(Level.SEVERE, "No known loading state was found in result."); - break; - } - } - - /** - * Shows the data in a DataFetchResult. If there was an error during the - * operation, the DEFAULT_ERROR_MESSAGE will be displayed. If the operation - * completed successfully and no data is present, DEFAULT_NO_RESULTS_MESSAGE - * will be shown. Otherwise, the data will be shown as rows in the table. - * - * @param result The DataFetchResult. - */ - public void showDataFetchResult(DataFetchResult> result) { - showDataFetchResult(result, DEFAULT_ERROR_MESSAGE, DEFAULT_NO_RESULTS_MESSAGE); - } - /** * Initialize the gui components. */ diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/LoadableComponent.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/LoadableComponent.java new file mode 100644 index 0000000000..652cf3bcd1 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/LoadableComponent.java @@ -0,0 +1,73 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourcesummary.uiutils; + +/** + * Interface for a loadable component that can show messages, results, or a + * DataFetchResult. + */ +public interface LoadableComponent { + + /** + * Clears the results from the underlying JTable and shows the provided + * message. + * + * @param message The message to be shown. + */ + void showMessage(String message); + + /** + * Shows a default loading message on the table. This will clear any results + * in the table. + */ + void showDefaultLoadingMessage(); + + /** + * Shows the list as rows of data in the table. If overlay message will be + * cleared if present. + * + * @param data The data to be shown where each item represents a row of + * data. + */ + void showResults(T data); + + /** + * Shows the data in a DataFetchResult. If there was an error during the + * operation, the errorMessage will be displayed. If the operation completed + * successfully and no data is present, noResultsMessage will be shown. + * Otherwise, the data will be shown as rows in the table. + * + * @param result The DataFetchResult. + * @param errorMessage The error message to be shown in the event of an + * error. + * @param noResultsMessage The message to be shown if there are no results + * but the operation completed successfully. + */ + void showDataFetchResult(DataFetchResult result, String errorMessage, String noResultsMessage); + + /** + * Shows the data in a DataFetchResult. If there was an error during the + * operation, the DEFAULT_ERROR_MESSAGE will be displayed. If the operation + * completed successfully and no data is present, DEFAULT_NO_RESULTS_MESSAGE + * will be shown. Otherwise, the data will be shown as rows in the table. + * + * @param result The DataFetchResult. + */ + void showDataFetchResult(DataFetchResult result); +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/PieChartPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/PieChartPanel.java new file mode 100644 index 0000000000..971cb83367 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/PieChartPanel.java @@ -0,0 +1,253 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourcesummary.uiutils; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.text.DecimalFormat; +import java.util.List; +import javax.swing.JLabel; +import org.jfree.chart.ChartFactory; +import org.jfree.chart.ChartPanel; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.labels.PieSectionLabelGenerator; +import org.jfree.chart.labels.StandardPieSectionLabelGenerator; +import org.jfree.chart.panel.AbstractOverlay; +import org.jfree.chart.panel.Overlay; +import org.jfree.chart.plot.PiePlot; +import org.jfree.data.general.DefaultPieDataset; +import org.openide.util.NbBundle.Messages; + +/** + * A pie chart panel. + */ +@Messages({ + "PieChartPanel_noDataLabel=No Data" +}) +public class PieChartPanel extends AbstractLoadableComponent> { + + /** + * An individual pie chart slice in the pie chart. + */ + public static class PieChartItem { + + private final String label; + private final double value; + private final Color color; + + /** + * Main constructor. + * + * @param label The label for this pie slice. + * @param value The value for this item. + * @param color The color for the pie slice. Can be null for + * auto-determined. + */ + public PieChartItem(String label, double value, Color color) { + this.label = label; + this.value = value; + this.color = color; + } + + /** + * @return The label for this item. + */ + public String getLabel() { + return label; + } + + /** + * @return The value for this item. + */ + public double getValue() { + return value; + } + + /** + * @return The color for the pie slice or null for auto-determined. + */ + public Color getColor() { + return color; + } + } + + /** + * A JFreeChart message overlay that can show a message for the purposes of + * the LoadableComponent. + */ + private static class MessageOverlay extends AbstractOverlay implements Overlay { + + private static final long serialVersionUID = 1L; + private final BaseMessageOverlay overlay = new BaseMessageOverlay(); + + // multiply this value by the smaller dimension (height or width) of the component + // to determine width of text to be displayed. + private static final double MESSAGE_WIDTH_FACTOR = .6; + + /** + * Sets this layer visible when painted. In order to be shown in UI, + * this component needs to be repainted. + * + * @param visible Whether or not it is visible. + */ + void setVisible(boolean visible) { + overlay.setVisible(visible); + } + + /** + * Sets the message to be displayed in the child jlabel. + * + * @param message The message to be displayed. + */ + void setMessage(String message) { + overlay.setMessage(message); + } + + @Override + public void paintOverlay(Graphics2D gd, ChartPanel cp) { + int labelWidth = (int) (Math.min(cp.getWidth(), cp.getHeight()) * MESSAGE_WIDTH_FACTOR); + overlay.paintOverlay(gd, cp.getWidth(), cp.getHeight(), labelWidth); + } + + } + + private static final long serialVersionUID = 1L; + + private static final Font DEFAULT_FONT = new JLabel().getFont(); + + /** + * It appears that JFreeChart will show nothing if all values are zero. So + * this is a value close to zero but not to be displayed. + */ + private static final double NEAR_ZERO = Math.ulp(1d); + private static final Color NO_DATA_COLOR = Color.WHITE; + private static final double DEFAULT_CHART_PADDING = .1; + + private static final Font DEFAULT_HEADER_FONT = new Font(DEFAULT_FONT.getName(), DEFAULT_FONT.getStyle(), (int) (DEFAULT_FONT.getSize() * 1.5)); + private static final PieSectionLabelGenerator DEFAULT_LABEL_GENERATOR + = new StandardPieSectionLabelGenerator( + "{0}: {1} ({2})", new DecimalFormat("#,###"), new DecimalFormat("0.0%")); + + private final MessageOverlay overlay = new MessageOverlay(); + private final DefaultPieDataset dataset = new DefaultPieDataset(); + private final JFreeChart chart; + private final PiePlot plot; + + /** + * Main constructor. + */ + public PieChartPanel() { + this(null); + } + + /** + * Main constructor for the pie chart. + * + * @param title The title for this pie chart. + */ + public PieChartPanel(String title) { + // Create chart + this.chart = ChartFactory.createPieChart( + title, + dataset, + false, + false, + false); + + chart.setBackgroundPaint(null); + chart.getTitle().setFont(DEFAULT_HEADER_FONT); + + this.plot = ((PiePlot) chart.getPlot()); + plot.setInteriorGap(DEFAULT_CHART_PADDING); + plot.setLabelGenerator(DEFAULT_LABEL_GENERATOR); + plot.setLabelFont(DEFAULT_FONT); + plot.setBackgroundPaint(null); + plot.setOutlinePaint(null); + + // Create Panel + ChartPanel panel = new ChartPanel(chart); + panel.addOverlay(overlay); + panel.setPopupMenu(null); + + this.setLayout(new BorderLayout()); + this.add(panel, BorderLayout.CENTER); + } + + /** + * @return The title for this chart if one exists. + */ + public String getTitle() { + return (this.chart == null || this.chart.getTitle() == null) + ? null + : this.chart.getTitle().getText(); + } + + /** + * Sets the title for this pie chart. + * + * @param title The title. + * + * @return As a utility, returns this. + */ + public PieChartPanel setTitle(String title) { + this.chart.getTitle().setText(title); + return this; + } + + @Override + protected void setMessage(boolean visible, String message) { + this.overlay.setVisible(visible); + this.overlay.setMessage(message); + } + + @Override + protected void setResults(List data) { + this.dataset.clear(); + this.plot.clearSectionPaints(false); + + if (data != null && !data.isEmpty()) { + for (PieChartPanel.PieChartItem slice : data) { + this.dataset.setValue(slice.getLabel(), slice.getValue()); + if (slice.getColor() != null) { + this.plot.setSectionPaint(slice.getLabel(), slice.getColor()); + } + } + } else { + // show a no data label if no data. + // this in fact shows a very small number for the value + // that should be way below rounding error for formatters + this.dataset.setValue(Bundle.PieChartPanel_noDataLabel(), NEAR_ZERO); + this.plot.setSectionPaint(Bundle.PieChartPanel_noDataLabel(), NO_DATA_COLOR); + } + } + + /** + * Shows a message on top of data. + * + * @param data The data. + * @param message The message. + */ + public synchronized void showDataWithMessage(List data, String message) { + setResults(data); + setMessage(true, message); + repaint(); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/SwingWorkerSequentialExecutor.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/SwingWorkerSequentialExecutor.java index 54abdb0ad9..37c46df31c 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/SwingWorkerSequentialExecutor.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/SwingWorkerSequentialExecutor.java @@ -71,4 +71,22 @@ public class SwingWorkerSequentialExecutor { workers = Collections.emptyList(); futures = Collections.emptyList(); } + + /** + * Returns whether or not any of the workers provided are still running. + * + * @return Whether or not any of the submitted workers are still running. + */ + public synchronized boolean isRunning() { + // borrowed from this stack overflow answer: + // https://stackoverflow.com/a/33845730 + + for (Future future : futures) { + if (!future.isDone()) { + return true; + } + } + + return false; + } } diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/UpdateGovernor.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/UpdateGovernor.java new file mode 100644 index 0000000000..8fcbe2a6df --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/UpdateGovernor.java @@ -0,0 +1,90 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.beans.PropertyChangeEvent; +import java.util.Set; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.ingest.IngestManager.IngestJobEvent; +import org.sleuthkit.autopsy.ingest.ModuleContentEvent; +import org.sleuthkit.autopsy.ingest.ModuleDataEvent; +import org.sleuthkit.datamodel.AbstractFile; + +/** + * Interface for determiining when data should update based on autopsy (i.e. + * case/ingest) events. + */ +public interface UpdateGovernor { + + /** + * @return The set of Case Events for which data should be updated. + */ + Set getCaseEventUpdates(); + + /** + * @return The set of Ingest Job Events for which data should be updated. + */ + Set getIngestJobEventUpdates(); + + /** + * Given a module data event, whether or not an update should occur. + * + * @param evt The ModuleDataEvent that is occurring. + * + * @return Whether or not this event should trigger an update. + */ + boolean isRefreshRequired(ModuleDataEvent evt); + + /** + * Given a module content event, whether or not an update should occur. + * + * @param evt The ModuleContentEvent. + * + * @return Whether or not this event should trigger an update. + */ + boolean isRefreshRequired(ModuleContentEvent evt); + + /** + * Given an ingest job event, determines whether or not an update should + * occur. + * + * @param evt The event. + * + * @return Whether or not this event should trigger an update. + */ + boolean isRefreshRequired(IngestJobEvent evt); + + /** + * Given a case event, whether or not an update should occur. + * + * @param evt The event. + * + * @return Whether or not this event should trigger an update. + */ + boolean isRefreshRequiredForCaseEvent(PropertyChangeEvent evt); + + /** + * Whether or not a newly added AbstractFile should trigger an update. + * + * @param evt The AbstractFile. + * + * @return True if an update should occur. + */ + boolean isRefreshRequired(AbstractFile evt); +} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/Bundle.properties b/Core/src/org/sleuthkit/autopsy/discovery/Bundle.properties deleted file mode 100644 index 795018f1ac..0000000000 --- a/Core/src/org/sleuthkit/autopsy/discovery/Bundle.properties +++ /dev/null @@ -1,106 +0,0 @@ -FileSearchDialog.jLabel1.text=File Type -FileSearchDialog.dsCheckBox.text=Data source -FileSearchDialog.cancelButton.text=Cancel -FileSearchDialog.freqCheckBox.text=CR Frequency -FileSearchDialog.sizeCheckBox.text=Size -FileSearchDialog.kwCheckBox.text=Keyword -FileSearchDialog.addParentButton.text=Add -FileSearchDialog.deleteParentButton.text=Delete -FileSearchDialog.parentFullRadioButton.text=Full -FileSearchDialog.parentSubstringRadioButton.text=Substring -FileSearchDialog.jLabel2.text=(All will be used) -FileSearchDialog.jLabel3.text=Group by attribute: -FileSearchDialog.jLabel4.text=Order groups by: -FileSearchDialog.orderAttrRadioButton.text=Attribute -FileSearchDialog.orderSizeRadioButton.text=Group Size -FileSearchDialog.jLabel5.text=Order files by: -FileSearchDialog.parentCheckBox.text=Parent -FileSearchPanel.sortingPanel.border.title=Grouping -FileSearchPanel.addButton.text=Add -FileSearchPanel.substringRadioButton.text=Substring -FileSearchPanel.fullRadioButton.text=Full -FileSearchPanel.parentCheckbox.text=Parent Folder: -FileSearchPanel.keywordCheckbox.text=Keyword: -FileSearchPanel.crFrequencyCheckbox.text=Past Occurrences: -FileSearchPanel.dataSourceCheckbox.text=Data Source: -FileSearchPanel.sizeCheckbox.text=File Size: -FileSearchPanel.orderGroupsByLabel.text=Order Groups By: -FileSearchPanel.filtersScrollPane.border.title=Filters -FileSearchPanel.parentLabel.text=(All will be used) -FileSearchPanel.deleteButton.text=Delete -FileSearchPanel.orderByLabel.text=Order Within Groups By: -FileSearchPanel.groupByLabel.text=Group By: -FileSearchDialog.searchButton.text=Search -FileSearchDialog.hashCheckBox.text=Hash Set -FileSearchDialog.intCheckBox.text=Interesting Items -FileSearchDialog.tagsCheckBox.text=Tags -FileSearchDialog.objCheckBox.text=Objects -FileSearchDialog.exifCheckBox.text=Must contain EXIF data -FileSearchDialog.notableCheckBox.text=Must have been tagged as notable -FileSearchDialog.scoreCheckBox.text=Has score -FileSearchPanel.hashSetCheckbox.text=Hash Set: -FileSearchPanel.tagsCheckbox.text=Tag: -FileSearchPanel.interestingItemsCheckbox.text=Interesting Item: -FileSearchPanel.scoreCheckbox.text=Has Score: -FileSearchPanel.notableCheckbox.text=Must have been tagged as notable -FileSearchPanel.objectsCheckbox.text=Object Detected: -ResultsPanel.currentPageLabel.text=Page: - -ResultsPanel.pageControlsLabel.text=Pages: -ResultsPanel.gotoPageLabel.text=Go to Page: -ResultsPanel.pageSizeLabel.text=Page Size: -DiscoveryExtractAction.title.extractFiles.text=Extract File -FileSearchPanel.includeRadioButton.text=Include -FileSearchPanel.excludeRadioButton.text=Exclude -FileSearchPanel.knownFilesCheckbox.toolTipText= -FileSearchPanel.knownFilesCheckbox.text=Hide known files -GroupListPanel.groupKeyList.border.title=Groups -FileSearchPanel.stepThreeLabel.text=Step 3: Choose display settings -DocumentPanel.fileSizeLabel.toolTipText= -DocumentPanel.isDeletedLabel.toolTipText= -ImageThumbnailPanel.isDeletedLabel.toolTipText= -FileSearchPanel.userCreatedCheckbox.text=Possibly User Created -DiscoveryDialog.documentsButton.text=Documents -DiscoveryDialog.videosButton.text=Videos -DiscoveryDialog.imagesButton.text=Images -DiscoveryDialog.searchButton.text=Search -DetailsPanel.instancesList.border.title=Instances -SizeFilterPanel.sizeCheckbox.text=File Size: -DataSourceFilterPanel.dataSourceCheckbox.text=Data Source: -UserCreatedFilterPanel.userCreatedCheckbox.text=Possibly User Created -# To change this license header, choose License Headers in Project Properties. -# To change this template file, choose Tools | Templates -# and open the template in the editor. -HashSetFilterPanel.hashSetCheckbox.text=Hash Set: -InterestingItemFilterPanel.interestingItemsCheckbox.text=Interesting Item: -ParentFolderFilterPanel.parentCheckbox.text=Parent Folder: -ParentFolderFilterPanel.deleteButton.text=Delete -ParentFolderFilterPanel.excludeRadioButton.text=Exclude -ParentFolderFilterPanel.includeRadioButton.text=Include -ParentFolderFilterPanel.substringRadioButton.text=Substring -ParentFolderFilterPanel.fullRadioButton.text=Full -ParentFolderFilterPanel.parentLabel.text=(All will be used) -ParentFolderFilterPanel.addButton.text=Add -ParentFolderFilterPanel.parentCheckbox.text_1=Parent Folder: -ParentFolderFilterPanel.addButton.text_1=Add -ParentFolderFilterPanel.deleteButton.text_1=Delete -ParentFolderFilterPanel.excludeRadioButton.text_1=Exclude -ParentFolderFilterPanel.substringRadioButton.text_1=Substring -ParentFolderFilterPanel.includeRadioButton.text_1=Include -ParentFolderFilterPanel.fullRadioButton.text_1=Full -ParentFolderFilterPanel.parentLabel.text_1=(All will be used) -InterestingItemsFilterPanel.interestingItemsCheckbox.text=Interesting Item: -UserCreatedFilterPanel.userCreatedCheckbox.text_1=Possibly User Created -PastOccurrencesFilterPanel.pastOccurrencesCheckbox.text=Past Occurrences: -ObjectDetectedFilterPanel.text=Object Detected: -DiscoveryDialog.sortingPanel.border.title=Step 3: Choose display settings -DiscoveryDialog.groupByLabel.text=Group By: -DiscoveryDialog.orderByLabel.text=Order Within Groups By: -DiscoveryDialog.orderGroupsByLabel.text=Order Groups By: -ImageFilterPanel.imageFiltersSplitPane.toolTipText= -DocumentFilterPanel.documentsFiltersSplitPane.border.title=Step 2: Filter which documents to show -ImageFilterPanel.imageFiltersSplitPane.border.title=Step 2: Filter which images to show -VideoFilterPanel.videoFiltersSplitPane.border.title=Step 2: Filter which videos to show -DiscoveryDialog.step1Label.text=Step 1: Choose result type -ResultsSplitPaneDivider.hideButton.text= -ResultsSplitPaneDivider.showButton.text= -ResultsSplitPaneDivider.detailsLabel.text=Details Area diff --git a/Core/src/org/sleuthkit/autopsy/discovery/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/discovery/Bundle.properties-MERGED deleted file mode 100644 index a964a1826e..0000000000 --- a/Core/src/org/sleuthkit/autopsy/discovery/Bundle.properties-MERGED +++ /dev/null @@ -1,280 +0,0 @@ -CTL_OpenDiscoveryAction=Discovery -# {0} - dataSourceName -DataSourceModuleWrapper.exifModule.text=Exif Parser module was not run on data source: {0}\n -# {0} - dataSourceName -DataSourceModuleWrapper.fileTypeModule.text=File Type Identification module was not run on data source: {0}\n -# {0} - dataSourceName -DataSourceModuleWrapper.hashModule.text=Hash Lookup module was not run on data source: {0}\n -DiscoveryDialog.name.text=Discovery -DiscoveryTopComponent.cancelButton.text=Cancel Search -DiscoveryTopComponent.name=\ Discovery -DiscoveryTopComponent.newSearch.text=New Search -DiscoveryTopComponent.searchCancelled.text=Search has been cancelled. -# {0} - search -DiscoveryTopComponent.searchComplete.text=Results with {0} -# {0} - searchType -DiscoveryTopComponent.searchInProgress.text=Performing search for results of type {0}. Please wait. -DiscoveryUiUtility.bytes.text=bytes -DiscoveryUiUtility.gigaBytes.text=GB -DiscoveryUiUtility.kiloBytes.text=KB -DiscoveryUiUtility.megaBytes.text=MB -# {0} - fileSize -# {1} - units -DiscoveryUiUtility.sizeLabel.text=Size: {0} {1} -DiscoveryUiUtility.terraBytes.text=TB -# {0} - otherInstanceCount -DocumentPanel.nameLabel.more.text=\ and {0} more -DocumentPanel.noImageExtraction.text=0 of ? images -DocumentPanel.numberOfImages.noImages=No images -# {0} - numberOfImages -DocumentPanel.numberOfImages.text=1 of {0} images -DocumentWrapper.previewInitialValue=Preview not generated yet. -FileGroup.groupSortingAlgorithm.groupName.text=Group Name -FileGroup.groupSortingAlgorithm.groupSize.text=Group Size -# {0} - Data source name -# {1} - Data source ID -FileSearch.DataSourceGroupKey.datasourceAndID={0}(ID: {1}) -# {0} - Data source ID -FileSearch.DataSourceGroupKey.idOnly=Data source (ID: {0}) -FileSearch.documentSummary.noBytes=No bytes read for document, unable to display preview. -FileSearch.documentSummary.noPreview=No preview available. -FileSearch.FileTagGroupKey.noSets=None -# {0} - file name -FileSearch.genVideoThumb.progress.text=extracting temporary file {0} -FileSearch.GroupingAttributeType.datasource.displayName=Data Source -FileSearch.GroupingAttributeType.fileType.displayName=File Type -FileSearch.GroupingAttributeType.frequency.displayName=Past Occurrences -FileSearch.GroupingAttributeType.hash.displayName=Hash Set -FileSearch.GroupingAttributeType.interestingItem.displayName=Interesting Item -FileSearch.GroupingAttributeType.keywordList.displayName=Keyword -FileSearch.GroupingAttributeType.none.displayName=None -FileSearch.GroupingAttributeType.object.displayName=Object Detected -FileSearch.GroupingAttributeType.parent.displayName=Parent Folder -FileSearch.GroupingAttributeType.size.displayName=File Size -FileSearch.GroupingAttributeType.tag.displayName=Tag -FileSearch.HashHitsGroupKey.noHashHits=None -FileSearch.InterestingItemGroupKey.noSets=None -FileSearch.KeywordListGroupKey.noKeywords=None -FileSearch.NoGroupingGroupKey.allFiles=All Files -FileSearch.ObjectDetectedGroupKey.noSets=None -FileSearchData.FileSize.100kbto1mb=: 100KB-1MB -FileSearchData.FileSize.100mbto1gb=: 100MB-1GB -FileSearchData.FileSize.10PlusGb=: 10GB+ -FileSearchData.FileSize.16kbto100kb=: 16-100KB -FileSearchData.FileSize.1gbto5gb=: 1-5GB -FileSearchData.FileSize.1mbto50mb=: 1-50MB -FileSearchData.FileSize.200PlusMb=: 200MB+ -FileSearchData.FileSize.500kbto100mb=: 500KB-100MB -FileSearchData.FileSize.50mbto200mb=: 50-200MB -FileSearchData.FileSize.5gbto10gb=: 5-10GB -FileSearchData.FileSize.LARGE.displayName=Large -FileSearchData.FileSize.MEDIUM.displayName=Medium -FileSearchData.FileSize.SMALL.displayName=Small -FileSearchData.FileSize.upTo16kb=: 0-16KB -FileSearchData.FileSize.upTo500kb=: 0-500KB -FileSearchData.FileSize.XLARGE.displayName=XLarge -FileSearchData.FileSize.XSMALL.displayName=XSmall -FileSearchData.FileSize.XXLARGE.displayName=XXLarge -FileSearchData.FileType.Audio.displayName=Audio -FileSearchData.FileType.Documents.displayName=Documents -FileSearchData.FileType.Executables.displayName=Executables -FileSearchData.FileType.Image.displayName=Image -FileSearchData.FileType.Other.displayName=Other/Unknown -FileSearchData.FileType.Video.displayName=Video -FileSearchData.Frequency.common.displayName=Common (11 - 100) -FileSearchData.Frequency.known.displayName=Known (NSRL) -FileSearchData.Frequency.rare.displayName=Rare (2-10) -FileSearchData.Frequency.unique.displayName=Unique (1) -FileSearchData.Frequency.unknown.displayName=Unknown -FileSearchData.Frequency.verycommon.displayName=Very Common (100+) -FileSearchData.Score.interesting.displayName=Interesting -FileSearchData.Score.notable.displayName=Notable -FileSearchData.Score.unknown.displayName=Unknown -FileSearchDialog.jLabel1.text=File Type -FileSearchDialog.dsCheckBox.text=Data source -FileSearchDialog.cancelButton.text=Cancel -FileSearchDialog.freqCheckBox.text=CR Frequency -FileSearchDialog.sizeCheckBox.text=Size -FileSearchDialog.kwCheckBox.text=Keyword -FileSearchDialog.addParentButton.text=Add -FileSearchDialog.deleteParentButton.text=Delete -FileSearchDialog.parentFullRadioButton.text=Full -FileSearchDialog.parentSubstringRadioButton.text=Substring -FileSearchDialog.jLabel2.text=(All will be used) -FileSearchDialog.jLabel3.text=Group by attribute: -FileSearchDialog.jLabel4.text=Order groups by: -FileSearchDialog.orderAttrRadioButton.text=Attribute -FileSearchDialog.orderSizeRadioButton.text=Group Size -FileSearchDialog.jLabel5.text=Order files by: -FileSearchDialog.parentCheckBox.text=Parent -FileSearchFiltering.concatenateSetNamesForDisplay.comma=, -# {0} - Data source name -# {1} - Data source ID -FileSearchFiltering.DataSourceFilter.datasource={0}({1}) -# {0} - filters -FileSearchFiltering.DataSourceFilter.desc=Data source(s): {0} -FileSearchFiltering.DataSourceFilter.or=, -# {0} - filters -FileSearchFiltering.FileTypeFilter.desc=Type: {0} -FileSearchFiltering.FileTypeFilter.or=, -# {0} - filters -FileSearchFiltering.FrequencyFilter.desc=Past occurrences: {0} -FileSearchFiltering.FrequencyFilter.or=, -# {0} - filters -FileSearchFiltering.HashSetFilter.desc=Hash set hits in set(s): {0} -# {0} - filters -FileSearchFiltering.InterestingItemSetFilter.desc=Interesting item hits in set(s): {0} -# {0} - filters -FileSearchFiltering.KeywordListFilter.desc=Keywords in list(s): {0} -FileSearchFiltering.KnownFilter.desc=which are not known -# {0} - filters -FileSearchFiltering.ObjectDetectionFilter.desc=Objects detected in set(s): {0} -# {0} - filters -FileSearchFiltering.ParentFilter.desc=Paths matching: {0} -FileSearchFiltering.ParentFilter.exact=(exact match) -FileSearchFiltering.ParentFilter.excluded=(excluded) -FileSearchFiltering.ParentFilter.included=(included) -FileSearchFiltering.ParentFilter.or=, -FileSearchFiltering.ParentFilter.substring=(substring) -FileSearchFiltering.ParentSearchTerm.excludeString=\ (exclude) -FileSearchFiltering.ParentSearchTerm.fullString=\ (exact) -FileSearchFiltering.ParentSearchTerm.includeString=\ (include) -FileSearchFiltering.ParentSearchTerm.subString=\ (substring) -FileSearchFiltering.PreviouslyNotableFilter.desc=that were previously marked as notable -# {0} - filters -FileSearchFiltering.ScoreFilter.desc=Score(s) of : {0} -# {0} - filters -FileSearchFiltering.SizeFilter.desc=Size(s): {0} -FileSearchFiltering.SizeFilter.or=, -# {0} - tag names -FileSearchFiltering.TagsFilter.desc=Tagged {0} -FileSearchFiltering.TagsFilter.or=, -FileSearchFiltering.UserCreatedFilter.desc=that contain EXIF data -FileSearchPanel.sortingPanel.border.title=Grouping -FileSearchPanel.addButton.text=Add -FileSearchPanel.substringRadioButton.text=Substring -FileSearchPanel.fullRadioButton.text=Full -FileSearchPanel.parentCheckbox.text=Parent Folder: -FileSearchPanel.keywordCheckbox.text=Keyword: -FileSearchPanel.crFrequencyCheckbox.text=Past Occurrences: -FileSearchPanel.dataSourceCheckbox.text=Data Source: -FileSearchPanel.sizeCheckbox.text=File Size: -FileSearchPanel.orderGroupsByLabel.text=Order Groups By: -FileSearchPanel.filtersScrollPane.border.title=Filters -FileSearchPanel.parentLabel.text=(All will be used) -FileSearchPanel.deleteButton.text=Delete -FileSearchPanel.orderByLabel.text=Order Within Groups By: -FileSearchPanel.groupByLabel.text=Group By: -FileSearchDialog.searchButton.text=Search -FileSearchDialog.hashCheckBox.text=Hash Set -FileSearchDialog.intCheckBox.text=Interesting Items -FileSearchDialog.tagsCheckBox.text=Tags -FileSearchDialog.objCheckBox.text=Objects -FileSearchDialog.exifCheckBox.text=Must contain EXIF data -FileSearchDialog.notableCheckBox.text=Must have been tagged as notable -FileSearchDialog.scoreCheckBox.text=Has score -FileSearchPanel.hashSetCheckbox.text=Hash Set: -FileSearchPanel.tagsCheckbox.text=Tag: -FileSearchPanel.interestingItemsCheckbox.text=Interesting Item: -FileSearchPanel.scoreCheckbox.text=Has Score: -FileSearchPanel.notableCheckbox.text=Must have been tagged as notable -FileSearchPanel.objectsCheckbox.text=Object Detected: -FileSorter.SortingMethod.datasource.displayName=Data Source -FileSorter.SortingMethod.filename.displayName=File Name -FileSorter.SortingMethod.filesize.displayName=File Size -FileSorter.SortingMethod.filetype.displayName=File Type -FileSorter.SortingMethod.frequency.displayName=Central Repo Frequency -FileSorter.SortingMethod.fullPath.displayName=Full Path -FileSorter.SortingMethod.keywordlist.displayName=Keyword List Names -GroupsListPanel.noResults.message.text=No results were found for the selected filters.\n\nReminder:\n -The File Type Identification module must be run on each data source you want to find results in.\n -The Hash Lookup module must be run on each data source if you want to filter by past occurrence.\n -The Exif module must be run on each data source if you are filtering by User Created content. -GroupsListPanel.noResults.title.text=No results found -ImageThumbnailPanel.isDeleted.text=All instances of file are deleted. -# {0} - otherInstanceCount -ImageThumbnailPanel.nameLabel.more.text=\ and {0} more -OpenDiscoveryAction.resultsIncomplete.text=Discovery results may be incomplete -ResultFile.score.interestingResult.description=At least one instance of the file has an interesting result associated with it. -ResultFile.score.notableFile.description=At least one instance of the file was recognized as notable. -ResultFile.score.notableTaggedFile.description=At least one instance of the file is tagged with a notable tag. -ResultFile.score.taggedFile.description=At least one instance of the file has been tagged. -# {0} - currentPage -# {1} - totalPages -ResultsPanel.currentPage.displayValue=Page: {0} of {1} -ResultsPanel.currentPageLabel.text=Page: - -ResultsPanel.documentPreview.text=Document preview creation cancelled. -# {0} - selectedPage -# {1} - maxPage -ResultsPanel.invalidPageNumber.message=The selected page number {0} does not exist. Please select a value from 1 to {1}. -ResultsPanel.invalidPageNumber.title=Invalid Page Number -ResultsPanel.openInExternalViewer.name=Open in External Viewer -ResultsPanel.pageControlsLabel.text=Pages: -ResultsPanel.gotoPageLabel.text=Go to Page: -ResultsPanel.pageSizeLabel.text=Page Size: -DiscoveryExtractAction.title.extractFiles.text=Extract File -FileSearchPanel.includeRadioButton.text=Include -FileSearchPanel.excludeRadioButton.text=Exclude -FileSearchPanel.knownFilesCheckbox.toolTipText= -FileSearchPanel.knownFilesCheckbox.text=Hide known files -GroupListPanel.groupKeyList.border.title=Groups -FileSearchPanel.stepThreeLabel.text=Step 3: Choose display settings -DocumentPanel.fileSizeLabel.toolTipText= -DocumentPanel.isDeletedLabel.toolTipText= -ImageThumbnailPanel.isDeletedLabel.toolTipText= -FileSearchPanel.userCreatedCheckbox.text=Possibly User Created -DiscoveryDialog.documentsButton.text=Documents -DiscoveryDialog.videosButton.text=Videos -DiscoveryDialog.imagesButton.text=Images -DiscoveryDialog.searchButton.text=Search -DetailsPanel.instancesList.border.title=Instances -ResultsPanel.unableToCreate.text=Unable to create summary. -ResultsPanel.viewFileInDir.name=View File in Directory -SizeFilterPanel.sizeCheckbox.text=File Size: -DataSourceFilterPanel.dataSourceCheckbox.text=Data Source: -UserCreatedFilterPanel.userCreatedCheckbox.text=Possibly User Created -# To change this license header, choose License Headers in Project Properties. -# To change this template file, choose Tools | Templates -# and open the template in the editor. -HashSetFilterPanel.hashSetCheckbox.text=Hash Set: -InterestingItemFilterPanel.interestingItemsCheckbox.text=Interesting Item: -ParentFolderFilterPanel.parentCheckbox.text=Parent Folder: -ParentFolderFilterPanel.deleteButton.text=Delete -ParentFolderFilterPanel.excludeRadioButton.text=Exclude -ParentFolderFilterPanel.includeRadioButton.text=Include -ParentFolderFilterPanel.substringRadioButton.text=Substring -ParentFolderFilterPanel.fullRadioButton.text=Full -ParentFolderFilterPanel.parentLabel.text=(All will be used) -ParentFolderFilterPanel.addButton.text=Add -ParentFolderFilterPanel.parentCheckbox.text_1=Parent Folder: -ParentFolderFilterPanel.addButton.text_1=Add -ParentFolderFilterPanel.deleteButton.text_1=Delete -ParentFolderFilterPanel.excludeRadioButton.text_1=Exclude -ParentFolderFilterPanel.substringRadioButton.text_1=Substring -ParentFolderFilterPanel.includeRadioButton.text_1=Include -ParentFolderFilterPanel.fullRadioButton.text_1=Full -ParentFolderFilterPanel.parentLabel.text_1=(All will be used) -InterestingItemsFilterPanel.interestingItemsCheckbox.text=Interesting Item: -UserCreatedFilterPanel.userCreatedCheckbox.text_1=Possibly User Created -PastOccurrencesFilterPanel.pastOccurrencesCheckbox.text=Past Occurrences: -ObjectDetectedFilterPanel.text=Object Detected: -DiscoveryDialog.sortingPanel.border.title=Step 3: Choose display settings -DiscoveryDialog.groupByLabel.text=Group By: -DiscoveryDialog.orderByLabel.text=Order Within Groups By: -DiscoveryDialog.orderGroupsByLabel.text=Order Groups By: -ImageFilterPanel.imageFiltersSplitPane.toolTipText= -DocumentFilterPanel.documentsFiltersSplitPane.border.title=Step 2: Filter which documents to show -ImageFilterPanel.imageFiltersSplitPane.border.title=Step 2: Filter which images to show -VideoFilterPanel.videoFiltersSplitPane.border.title=Step 2: Filter which videos to show -DiscoveryDialog.step1Label.text=Step 1: Choose result type -ResultsSplitPaneDivider.hideButton.text= -ResultsSplitPaneDivider.showButton.text= -ResultsSplitPaneDivider.detailsLabel.text=Details Area -VideoThumbnailPanel.bytes.text=bytes -VideoThumbnailPanel.deleted.text=All instances of file are deleted. -VideoThumbnailPanel.gigaBytes.text=GB -VideoThumbnailPanel.kiloBytes.text=KB -VideoThumbnailPanel.megaBytes.text=MB -# {0} - otherInstanceCount -VideoThumbnailPanel.nameLabel.more.text=\ and {0} more -# {0} - fileSize -# {1} - units -VideoThumbnailPanel.sizeLabel.text=Size: {0} {1} -VideoThumbnailPanel.terraBytes.text=TB diff --git a/Core/src/org/sleuthkit/autopsy/discovery/DiscoveryUiUtils.java b/Core/src/org/sleuthkit/autopsy/discovery/DiscoveryUiUtils.java deleted file mode 100644 index 55392f54f2..0000000000 --- a/Core/src/org/sleuthkit/autopsy/discovery/DiscoveryUiUtils.java +++ /dev/null @@ -1,253 +0,0 @@ -/* - * Autopsy - * - * Copyright 2020 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.discovery; - -import java.awt.Component; -import java.awt.Dimension; -import java.awt.Point; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.logging.Level; -import javax.swing.ImageIcon; -import javax.swing.JComponent; -import javax.swing.JOptionPane; -import javax.swing.JScrollPane; -import javax.swing.JTextPane; -import org.openide.util.ImageUtilities; -import org.openide.util.NbBundle; -import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; -import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.datamodel.BlackboardArtifact; -import org.sleuthkit.datamodel.BlackboardAttribute; -import org.sleuthkit.datamodel.DataSource; -import org.sleuthkit.datamodel.IngestJobInfo; -import org.sleuthkit.datamodel.SleuthkitCase; -import org.sleuthkit.datamodel.TskCoreException; - -/** - * Utility class for the various user interface elements used by Discovery. - */ -final class DiscoveryUiUtils { - - private final static Logger logger = Logger.getLogger(DiscoveryUiUtils.class.getName()); - private static final int BYTE_UNIT_CONVERSION = 1000; - private static final int ICON_SIZE = 16; - private static final String RED_CIRCLE_ICON_PATH = "org/sleuthkit/autopsy/images/red-circle-exclamation.png"; - private static final String YELLOW_CIRCLE_ICON_PATH = "org/sleuthkit/autopsy/images/yellow-circle-yield.png"; - private static final String DELETE_ICON_PATH = "org/sleuthkit/autopsy/images/file-icon-deleted.png"; - private static final String UNSUPPORTED_DOC_PATH = "org/sleuthkit/autopsy/images/image-extraction-not-supported.png"; - private static final ImageIcon INTERESTING_SCORE_ICON = new ImageIcon(ImageUtilities.loadImage(YELLOW_CIRCLE_ICON_PATH, false)); - private static final ImageIcon NOTABLE_SCORE_ICON = new ImageIcon(ImageUtilities.loadImage(RED_CIRCLE_ICON_PATH, false)); - private static final ImageIcon DELETED_ICON = new ImageIcon(ImageUtilities.loadImage(DELETE_ICON_PATH, false)); - private static final ImageIcon UNSUPPORTED_DOCUMENT_THUMBNAIL = new ImageIcon(ImageUtilities.loadImage(UNSUPPORTED_DOC_PATH, false)); - - @NbBundle.Messages({"# {0} - fileSize", - "# {1} - units", - "DiscoveryUiUtility.sizeLabel.text=Size: {0} {1}", - "DiscoveryUiUtility.bytes.text=bytes", - "DiscoveryUiUtility.kiloBytes.text=KB", - "DiscoveryUiUtility.megaBytes.text=MB", - "DiscoveryUiUtility.gigaBytes.text=GB", - "DiscoveryUiUtility.terraBytes.text=TB"}) - /** - * Convert a size in bytes to a string with representing the size in the - * largest units which represent the value as being greater than or equal to - * one. Result will be rounded down to the nearest whole number of those - * units. - * - * @param bytes Size in bytes. - */ - static String getFileSizeString(long bytes) { - long size = bytes; - int unitsSwitchValue = 0; - while (size > BYTE_UNIT_CONVERSION && unitsSwitchValue < 4) { - size /= BYTE_UNIT_CONVERSION; - unitsSwitchValue++; - } - String units; - switch (unitsSwitchValue) { - case 1: - units = Bundle.DiscoveryUiUtility_kiloBytes_text(); - break; - case 2: - units = Bundle.DiscoveryUiUtility_megaBytes_text(); - break; - case 3: - units = Bundle.DiscoveryUiUtility_gigaBytes_text(); - break; - case 4: - units = Bundle.DiscoveryUiUtility_terraBytes_text(); - break; - default: - units = Bundle.DiscoveryUiUtility_bytes_text(); - break; - } - return Bundle.DiscoveryUiUtility_sizeLabel_text(size, units); - } - - /** - * Get the image to use when the document type does not support image - * extraction. - * - * @return An image that indicates we don't know if there are images. - */ - static ImageIcon getUnsupportedImageThumbnail() { - return UNSUPPORTED_DOCUMENT_THUMBNAIL; - } - - /** - * Get the names of the sets which exist in the case database for the - * specified artifact and attribute types. - * - * @param artifactType The artifact type to get the list of sets for. - * @param setNameAttribute The attribute type which contains the set names. - * - * @return A list of set names which exist in the case for the specified - * artifact and attribute types. - * - * @throws TskCoreException - */ - static List getSetNames(BlackboardArtifact.ARTIFACT_TYPE artifactType, BlackboardAttribute.ATTRIBUTE_TYPE setNameAttribute) throws TskCoreException { - List arts = Case.getCurrentCase().getSleuthkitCase().getBlackboardArtifacts(artifactType); - List setNames = new ArrayList<>(); - for (BlackboardArtifact art : arts) { - for (BlackboardAttribute attr : art.getAttributes()) { - if (attr.getAttributeType().getTypeID() == setNameAttribute.getTypeID()) { - String setName = attr.getValueString(); - if (!setNames.contains(setName)) { - setNames.add(setName); - } - } - } - } - Collections.sort(setNames); - return setNames; - } - - /** - * Helper method to see if point is on the icon. - * - * @param comp The component to check if the cursor is over the icon of - * @param point The point the cursor is at. - * - * @return True if the point is over the icon, false otherwise. - */ - static boolean isPointOnIcon(Component comp, Point point) { - return comp instanceof JComponent && point.x >= comp.getX() && point.x <= comp.getX() + ICON_SIZE && point.y >= comp.getY() && point.y <= comp.getY() + ICON_SIZE; - } - - /** - * Method to set the icon and tool tip text for a label to show deleted - * status. - * - * @param isDeleted True if the label should reflect deleted status, - * false otherwise. - * @param isDeletedLabel The label to set the icon and tooltip for. - */ - static void setDeletedIcon(boolean isDeleted, javax.swing.JLabel isDeletedLabel) { - if (isDeleted) { - isDeletedLabel.setIcon(DELETED_ICON); - isDeletedLabel.setToolTipText(Bundle.ImageThumbnailPanel_isDeleted_text()); - } else { - isDeletedLabel.setIcon(null); - isDeletedLabel.setToolTipText(null); - } - } - - /** - * Method to set the icon and tool tip text for a label to show the score. - * - * @param resultFile The result file which the label should reflect the - * score of. - * @param scoreLabel The label to set the icon and tooltip for. - */ - static void setScoreIcon(ResultFile resultFile, javax.swing.JLabel scoreLabel) { - switch (resultFile.getScore()) { - case NOTABLE_SCORE: - scoreLabel.setIcon(NOTABLE_SCORE_ICON); - break; - case INTERESTING_SCORE: - scoreLabel.setIcon(INTERESTING_SCORE_ICON); - break; - case NO_SCORE: // empty case - this is interpreted as an intentional fall-through - default: - scoreLabel.setIcon(null); - break; - } - scoreLabel.setToolTipText(resultFile.getScoreDescription()); - } - - /** - * Get the size of the icons used by the UI. - * - * @return - */ - static int getIconSize() { - return ICON_SIZE; - } - - /** - * Helper method to display an error message when the results of the - * Discovery Top component may be incomplete. - */ - static void displayErrorMessage(DiscoveryDialog dialog) { - //check if modules run and assemble message - try { - SleuthkitCase skCase = Case.getCurrentCaseThrows().getSleuthkitCase(); - Map dataSourceIngestModules = new HashMap<>(); - for (DataSource dataSource : skCase.getDataSources()) { - dataSourceIngestModules.put(dataSource.getId(), new DataSourceModulesWrapper(dataSource.getName())); - } - - for (IngestJobInfo jobInfo : skCase.getIngestJobs()) { - dataSourceIngestModules.get(jobInfo.getObjectId()).updateModulesRun(jobInfo); - } - String message = ""; - for (DataSourceModulesWrapper dsmodulesWrapper : dataSourceIngestModules.values()) { - message += dsmodulesWrapper.getMessage(); - } - if (!message.isEmpty()) { - JScrollPane messageScrollPane = new JScrollPane(); - JTextPane messageTextPane = new JTextPane(); - messageTextPane.setText(message); - messageTextPane.setVisible(true); - messageTextPane.setEditable(false); - messageTextPane.setCaretPosition(0); - messageScrollPane.setMaximumSize(new Dimension(600, 100)); - messageScrollPane.setPreferredSize(new Dimension(600, 100)); - messageScrollPane.setViewportView(messageTextPane); - JOptionPane.showMessageDialog(dialog, messageScrollPane, Bundle.OpenDiscoveryAction_resultsIncomplete_text(), JOptionPane.PLAIN_MESSAGE); - } - } catch (NoCurrentCaseException | TskCoreException ex) { - logger.log(Level.WARNING, "Exception while determining which modules have been run for Discovery", ex); - } - dialog.validateDialog(); - } - - /** - * Private constructor for DiscoveryUiUtils utility class. - */ - private DiscoveryUiUtils() { - //private constructor in a utility class intentionally left blank - } -} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/FileSearch.java b/Core/src/org/sleuthkit/autopsy/discovery/FileSearch.java deleted file mode 100644 index 0590351793..0000000000 --- a/Core/src/org/sleuthkit/autopsy/discovery/FileSearch.java +++ /dev/null @@ -1,2314 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2019-2020 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.discovery; - -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; -import com.google.common.io.Files; -import java.awt.Image; -import java.awt.image.BufferedImage; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.io.Reader; -import java.nio.file.Paths; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.logging.Level; -import javax.imageio.ImageIO; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.FilenameUtils; -import org.apache.commons.lang.StringUtils; -import org.imgscalr.Scalr; -import org.netbeans.api.progress.ProgressHandle; -import org.opencv.core.Mat; -import org.opencv.highgui.VideoCapture; -import org.openide.util.Lookup; -import org.openide.util.NbBundle; -import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; -import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; -import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoException; -import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoDbUtil; -import org.sleuthkit.autopsy.centralrepository.datamodel.InstanceTableCallback; -import org.sleuthkit.autopsy.corelibs.ScalrWrapper; -import org.sleuthkit.autopsy.coreutils.ImageUtils; -import org.sleuthkit.autopsy.coreutils.Logger; -import static org.sleuthkit.autopsy.coreutils.VideoUtils.getVideoFileInTempDir; -import org.sleuthkit.autopsy.datamodel.ContentUtils; -import org.sleuthkit.autopsy.discovery.FileSearchData.FileSize; -import org.sleuthkit.autopsy.discovery.FileSearchData.FileType; -import org.sleuthkit.autopsy.discovery.FileSearchData.Frequency; -import org.sleuthkit.datamodel.AbstractFile; -import org.sleuthkit.datamodel.BlackboardArtifact; -import org.sleuthkit.datamodel.BlackboardAttribute; -import org.sleuthkit.datamodel.CaseDbAccessManager; -import org.sleuthkit.datamodel.Content; -import org.sleuthkit.datamodel.ContentTag; -import org.sleuthkit.datamodel.SleuthkitCase; -import org.sleuthkit.datamodel.TskCoreException; -import org.sleuthkit.datamodel.TskData; -import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; -import org.sleuthkit.autopsy.textextractors.TextExtractor; -import org.sleuthkit.autopsy.textextractors.TextExtractorFactory; -import org.sleuthkit.autopsy.textsummarizer.TextSummarizer; -import org.sleuthkit.autopsy.textsummarizer.TextSummary; -import org.sleuthkit.autopsy.texttranslation.NoServiceProviderException; -import org.sleuthkit.autopsy.texttranslation.TextTranslationService; -import org.sleuthkit.autopsy.texttranslation.TranslationException; - -/** - * Main class to perform the file search. - */ -class FileSearch { - - private final static Logger logger = Logger.getLogger(FileSearch.class.getName()); - private static final int MAXIMUM_CACHE_SIZE = 10; - private static final String THUMBNAIL_FORMAT = "png"; //NON-NLS - private static final String VIDEO_THUMBNAIL_DIR = "video-thumbnails"; //NON-NLS - private static final Cache>> searchCache = CacheBuilder.newBuilder() - .maximumSize(MAXIMUM_CACHE_SIZE) - .build(); - private static final int PREVIEW_SIZE = 256; - private static volatile TextSummarizer summarizerToUse = null; - private static final BufferedImage VIDEO_DEFAULT_IMAGE = getDefaultVideoThumbnail(); - - /** - * Run the file search and returns the SearchResults object for debugging. - * Caching new results for access at later time. - * - * @param userName The name of the user performing the search. - * @param filters The filters to apply - * @param groupAttributeType The attribute to use for grouping - * @param groupSortingType The method to use to sort the groups - * @param fileSortingMethod The method to use to sort the files within the - * groups - * @param caseDb The case database - * @param centralRepoDb The central repository database. Can be null if - * not needed. - * - * @return The raw search results - * - * @throws FileSearchException - */ - static SearchResults runFileSearchDebug(String userName, - List filters, - AttributeType groupAttributeType, - FileGroup.GroupSortingAlgorithm groupSortingType, - FileSorter.SortingMethod fileSortingMethod, - SleuthkitCase caseDb, CentralRepository centralRepoDb) throws FileSearchException { - // Make a list of attributes that we want to add values for. This ensures the - // ResultFile objects will have all needed fields set when it's time to group - // and sort them. For example, if we're grouping by central repo frequency, we need - // to make sure we've loaded those values before grouping. - List attributesNeededForGroupingOrSorting = new ArrayList<>(); - attributesNeededForGroupingOrSorting.add(groupAttributeType); - attributesNeededForGroupingOrSorting.addAll(fileSortingMethod.getRequiredAttributes()); - - // Run the queries for each filter - List resultFiles = FileSearchFiltering.runQueries(filters, caseDb, centralRepoDb); - - // Add the data to resultFiles for any attributes needed for sorting and grouping - addAttributes(attributesNeededForGroupingOrSorting, resultFiles, caseDb, centralRepoDb); - - // Collect everything in the search results - SearchResults searchResults = new SearchResults(groupSortingType, groupAttributeType, fileSortingMethod); - searchResults.add(resultFiles); - - // Sort and group the results - searchResults.sortGroupsAndFiles(); - Map> resultHashMap = searchResults.toLinkedHashMap(); - SearchKey searchKey = new SearchKey(userName, filters, groupAttributeType, groupSortingType, fileSortingMethod); - synchronized (searchCache) { - searchCache.put(searchKey, resultHashMap); - } - return searchResults; - } - - /** - * Run the file search to get the group keys and sizes. Clears cache of - * search results, caching new results for access at later time. - * - * @param userName The name of the user performing the search. - * @param filters The filters to apply - * @param groupAttributeType The attribute to use for grouping - * @param groupSortingType The method to use to sort the groups - * @param fileSortingMethod The method to use to sort the files within the - * groups - * @param caseDb The case database - * @param centralRepoDb The central repository database. Can be null if - * not needed. - * - * @return A LinkedHashMap grouped and sorted according to the parameters - * - * @throws FileSearchException - */ - static Map getGroupSizes(String userName, - List filters, - AttributeType groupAttributeType, - FileGroup.GroupSortingAlgorithm groupSortingType, - FileSorter.SortingMethod fileSortingMethod, - SleuthkitCase caseDb, CentralRepository centralRepoDb) throws FileSearchException { - Map> searchResults = runFileSearch(userName, filters, - groupAttributeType, groupSortingType, fileSortingMethod, caseDb, centralRepoDb); - LinkedHashMap groupSizes = new LinkedHashMap<>(); - for (GroupKey groupKey : searchResults.keySet()) { - groupSizes.put(groupKey, searchResults.get(groupKey).size()); - } - return groupSizes; - } - - /** - * Get the files from the specified group from the cache, if the the group - * was not cached perform a search caching the groups. - * - * @param userName The name of the user performing the search. - * @param filters The filters to apply - * @param groupAttributeType The attribute to use for grouping - * @param groupSortingType The method to use to sort the groups - * @param fileSortingMethod The method to use to sort the files within the - * groups - * @param groupKey The key which uniquely identifies the group to - * get entries from - * @param startingEntry The first entry to return - * @param numberOfEntries The number of entries to return - * @param caseDb The case database - * @param centralRepoDb The central repository database. Can be null if - * not needed. - * - * @return A LinkedHashMap grouped and sorted according to the parameters - * - * @throws FileSearchException - */ - static List getFilesInGroup(String userName, - List filters, - AttributeType groupAttributeType, - FileGroup.GroupSortingAlgorithm groupSortingType, - FileSorter.SortingMethod fileSortingMethod, - GroupKey groupKey, - int startingEntry, - int numberOfEntries, - SleuthkitCase caseDb, CentralRepository centralRepoDb) throws FileSearchException { - //the group should be in the cache at this point - List filesInGroup = null; - SearchKey searchKey = new SearchKey(userName, filters, groupAttributeType, groupSortingType, fileSortingMethod); - Map> resultsMap; - synchronized (searchCache) { - resultsMap = searchCache.getIfPresent(searchKey); - } - if (resultsMap != null) { - filesInGroup = resultsMap.get(groupKey); - } - List page = new ArrayList<>(); - if (filesInGroup == null) { - logger.log(Level.INFO, "Group {0} was not cached, performing search to cache all groups again", groupKey); - runFileSearch(userName, filters, groupAttributeType, groupSortingType, fileSortingMethod, caseDb, centralRepoDb); - synchronized (searchCache) { - resultsMap = searchCache.getIfPresent(searchKey.getKeyString()); - } - if (resultsMap != null) { - filesInGroup = resultsMap.get(groupKey); - } - if (filesInGroup == null) { - logger.log(Level.WARNING, "Group {0} did not exist in cache or new search results", groupKey); - return page; //group does not exist - } - } - // Check that there is data after the starting point - if (filesInGroup.size() < startingEntry) { - logger.log(Level.WARNING, "Group only contains {0} files, starting entry of {1} is too large.", new Object[]{filesInGroup.size(), startingEntry}); - return page; - } - // Add files to the page - for (int i = startingEntry; (i < startingEntry + numberOfEntries) - && (i < filesInGroup.size()); i++) { - page.add(filesInGroup.get(i)); - } - return page; - } - - /** - * Get a summary for the specified AbstractFile. If no TextSummarizers exist - * get the beginning of the file. - * - * @param file The AbstractFile to summarize. - * - * @return The summary or beginning of the specified file as a String. - */ - @NbBundle.Messages({"FileSearch.documentSummary.noPreview=No preview available.", - "FileSearch.documentSummary.noBytes=No bytes read for document, unable to display preview."}) - static TextSummary summarize(AbstractFile file) { - TextSummary summary = null; - TextSummarizer localSummarizer = summarizerToUse; - if (localSummarizer == null) { - synchronized (searchCache) { - if (localSummarizer == null) { - localSummarizer = getLocalSummarizer(); - } - } - } - if (localSummarizer != null) { - try { - //a summary of length 40 seems to fit without vertical scroll bars - summary = localSummarizer.summarize(file, 40); - } catch (IOException ex) { - return new TextSummary(Bundle.FileSearch_documentSummary_noPreview(), null, 0); - } - } - if (summary == null || StringUtils.isBlank(summary.getSummaryText())) { - //summary text was empty grab the beginning of the file - summary = getDefaultSummary(file); - } - return summary; - } - - private static TextSummary getDefaultSummary(AbstractFile file) { - Image image = null; - int countOfImages = 0; - try { - Content largestChild = null; - for (Content child : file.getChildren()) { - if (child instanceof AbstractFile && ImageUtils.isImageThumbnailSupported((AbstractFile) child)) { - countOfImages++; - if (largestChild == null || child.getSize() > largestChild.getSize()) { - largestChild = child; - } - } - } - if (largestChild != null) { - image = ImageUtils.getThumbnail(largestChild, ImageUtils.ICON_SIZE_LARGE); - } - } catch (TskCoreException ex) { - logger.log(Level.WARNING, "Error getting children for file: " + file.getId(), ex); - } - image = image == null ? image : image.getScaledInstance(ImageUtils.ICON_SIZE_MEDIUM, ImageUtils.ICON_SIZE_MEDIUM, - Image.SCALE_SMOOTH); - String summaryText = null; - if (file.getMd5Hash() != null) { - try { - summaryText = getSavedSummary(Paths.get(Case.getCurrentCaseThrows().getCacheDirectory(), "summaries", file.getMd5Hash() + "-default-" + PREVIEW_SIZE + "-translated.txt").toString()); - } catch (NoCurrentCaseException ex) { - logger.log(Level.WARNING, "Unable to retrieve saved summary. No case is open.", ex); - } - } - if (StringUtils.isBlank(summaryText)) { - String firstLines = getFirstLines(file); - String translatedFirstLines = getTranslatedVersion(firstLines); - if (!StringUtils.isBlank(translatedFirstLines)) { - summaryText = translatedFirstLines; - if (file.getMd5Hash() != null) { - try { - saveSummary(summaryText, Paths.get(Case.getCurrentCaseThrows().getCacheDirectory(), "summaries", file.getMd5Hash() + "-default-" + PREVIEW_SIZE + "-translated.txt").toString()); - } catch (NoCurrentCaseException ex) { - logger.log(Level.WARNING, "Unable to save translated summary. No case is open.", ex); - } - } - } else { - summaryText = firstLines; - } - } - return new TextSummary(summaryText, image, countOfImages); - } - - /** - * Provide an English version of the specified String if it is not English, - * translation is enabled, and it can be translated. - * - * @param documentString The String to provide an English version of. - * - * @return The English version of the provided String, or null if no - * translation occurred. - */ - private static String getTranslatedVersion(String documentString) { - try { - TextTranslationService translatorInstance = TextTranslationService.getInstance(); - if (translatorInstance.hasProvider()) { - String translatedResult = translatorInstance.translate(documentString); - if (translatedResult.isEmpty() == false) { - return translatedResult; - } - } - } catch (NoServiceProviderException | TranslationException ex) { - logger.log(Level.INFO, "Error translating string for summary", ex); - } - return null; - } - - /** - * Find and load a saved summary from the case folder for the specified - * file. - * - * @param summarySavePath The full path for the saved summary file. - * - * @return The summary found given the specified path, null if no summary - * was found. - */ - private static String getSavedSummary(String summarySavePath) { - if (summarySavePath == null) { - return null; - } - File savedFile = new File(summarySavePath); - if (savedFile.exists()) { - try (BufferedReader bReader = new BufferedReader(new FileReader(savedFile))) { - // pass the path to the file as a parameter - StringBuilder sBuilder = new StringBuilder(); - String sCurrentLine = bReader.readLine(); - while (sCurrentLine != null) { - sBuilder.append(sCurrentLine).append('\n'); - sCurrentLine = bReader.readLine(); - } - return sBuilder.toString(); - } catch (IOException ingored) { - //summary file may not exist or may be incomplete in which case return null so a summary can be generated - return null; //no saved summary was able to be found - } - } else { - try { //if the file didn't exist make sure the parent directories exist before we move on to creating a summary - Files.createParentDirs(savedFile); - } catch (IOException ex) { - logger.log(Level.WARNING, "Unable to create summaries directory in case folder for file at: " + summarySavePath, ex); - } - return null; //no saved summary was able to be found - } - - } - - /** - * Save a summary at the specified location. - * - * @param summary The text of the summary being saved. - * @param summarySavePath The full path for the saved summary file. - */ - private static void saveSummary(String summary, String summarySavePath) { - if (summarySavePath == null) { - return; //can't save a summary if we don't have a path - } - try (FileWriter myWriter = new FileWriter(summarySavePath)) { - myWriter.write(summary); - } catch (IOException ex) { - logger.log(Level.WARNING, "Unable to save summary at: " + summarySavePath, ex); - } - } - - /** - * Get the beginning of text from the specified AbstractFile. - * - * @param file The AbstractFile to get text from. - * - * @return The beginning of text from the specified AbstractFile. - */ - private static String getFirstLines(AbstractFile file) { - TextExtractor extractor; - try { - extractor = TextExtractorFactory.getExtractor(file, null); - } catch (TextExtractorFactory.NoTextExtractorFound ignored) { - //no extractor found, use Strings Extractor - extractor = TextExtractorFactory.getStringsExtractor(file, null); - } - - try (Reader reader = extractor.getReader()) { - char[] cbuf = new char[PREVIEW_SIZE]; - reader.read(cbuf, 0, PREVIEW_SIZE); - return new String(cbuf); - } catch (IOException ex) { - return Bundle.FileSearch_documentSummary_noBytes(); - } catch (TextExtractor.InitReaderException ex) { - return Bundle.FileSearch_documentSummary_noPreview(); - } - } - - /** - * Get the first TextSummarizer found by a lookup of TextSummarizers. - * - * @return The first TextSummarizer found by a lookup of TextSummarizers. - * - * @throws IOException - */ - private static TextSummarizer getLocalSummarizer() { - Collection summarizers - = Lookup.getDefault().lookupAll(TextSummarizer.class - ); - if (!summarizers.isEmpty()) { - summarizerToUse = summarizers.iterator().next(); - return summarizerToUse; - } - return null; - } - - /** - * Run the file search. Caching new results for access at later time. - * - * @param userName The name of the user performing the search. - * @param filters The filters to apply - * @param groupAttributeType The attribute to use for grouping - * @param groupSortingType The method to use to sort the groups - * @param fileSortingMethod The method to use to sort the files within the - * groups - * @param caseDb The case database - * @param centralRepoDb The central repository database. Can be null if - * not needed. - * - * @return A LinkedHashMap grouped and sorted according to the parameters - * - * @throws FileSearchException - */ - private static Map> runFileSearch(String userName, - List filters, - AttributeType groupAttributeType, - FileGroup.GroupSortingAlgorithm groupSortingType, - FileSorter.SortingMethod fileSortingMethod, - SleuthkitCase caseDb, CentralRepository centralRepoDb) throws FileSearchException { - - // Make a list of attributes that we want to add values for. This ensures the - // ResultFile objects will have all needed fields set when it's time to group - // and sort them. For example, if we're grouping by central repo frequency, we need - // to make sure we've loaded those values before grouping. - List attributesNeededForGroupingOrSorting = new ArrayList<>(); - attributesNeededForGroupingOrSorting.add(groupAttributeType); - attributesNeededForGroupingOrSorting.addAll(fileSortingMethod.getRequiredAttributes()); - - // Run the queries for each filter - List resultFiles = FileSearchFiltering.runQueries(filters, caseDb, centralRepoDb); - - // Add the data to resultFiles for any attributes needed for sorting and grouping - addAttributes(attributesNeededForGroupingOrSorting, resultFiles, caseDb, centralRepoDb); - - // Collect everything in the search results - SearchResults searchResults = new SearchResults(groupSortingType, groupAttributeType, fileSortingMethod); - searchResults.add(resultFiles); - Map> resultHashMap = searchResults.toLinkedHashMap(); - SearchKey searchKey = new SearchKey(userName, filters, groupAttributeType, groupSortingType, fileSortingMethod); - synchronized (searchCache) { - searchCache.put(searchKey, resultHashMap); - } - // Return a version of the results in general Java objects - return resultHashMap; - } - - /** - * Add any attributes corresponding to the attribute list to the given - * result files. For example, specifying the KeywordListAttribute will - * populate the list of keyword set names in the ResultFile objects. - * - * @param attrs The attributes to add to the list of result files - * @param resultFiles The result files - * @param caseDb The case database - * @param centralRepoDb The central repository database. Can be null if not - * needed. - * - * @throws FileSearchException - */ - private static void addAttributes(List attrs, List resultFiles, SleuthkitCase caseDb, CentralRepository centralRepoDb) - throws FileSearchException { - for (AttributeType attr : attrs) { - attr.addAttributeToResultFiles(resultFiles, caseDb, centralRepoDb); - } - } - - /** - * Computes the CR frequency of all the given hashes and updates the list of - * files. - * - * @param hashesToLookUp Hashes to find the frequency of - * @param currentFiles List of files to update with frequencies - */ - private static void computeFrequency(Set hashesToLookUp, List currentFiles, CentralRepository centralRepoDb) { - - if (hashesToLookUp.isEmpty()) { - return; - } - - String hashes = String.join("','", hashesToLookUp); - hashes = "'" + hashes + "'"; - try { - CorrelationAttributeInstance.Type attributeType = centralRepoDb.getCorrelationTypeById(CorrelationAttributeInstance.FILES_TYPE_ID); - String tableName = CentralRepoDbUtil.correlationTypeToInstanceTableName(attributeType); - - String selectClause = " value, COUNT(value) FROM " - + "(SELECT DISTINCT case_id, value FROM " + tableName - + " WHERE value IN (" - + hashes - + ")) AS foo GROUP BY value"; - - FrequencyCallback callback = new FrequencyCallback(currentFiles); - centralRepoDb.processSelectClause(selectClause, callback); - - } catch (CentralRepoException ex) { - logger.log(Level.WARNING, "Error getting frequency counts from Central Repository", ex); // NON-NLS - } - - } - - private static String createSetNameClause(List files, - int artifactTypeID, int setNameAttrID) throws FileSearchException { - - // Concatenate the object IDs in the list of files - String objIdList = ""; // NON-NLS - for (ResultFile file : files) { - if (!objIdList.isEmpty()) { - objIdList += ","; // NON-NLS - } - objIdList += "\'" + file.getFirstInstance().getId() + "\'"; // NON-NLS - } - - // Get pairs of (object ID, set name) for all files in the list of files that have - // the given artifact type. - return "blackboard_artifacts.obj_id AS object_id, blackboard_attributes.value_text AS set_name " - + "FROM blackboard_artifacts " - + "INNER JOIN blackboard_attributes ON blackboard_artifacts.artifact_id=blackboard_attributes.artifact_id " - + "WHERE blackboard_attributes.artifact_type_id=\'" + artifactTypeID + "\' " - + "AND blackboard_attributes.attribute_type_id=\'" + setNameAttrID + "\' " - + "AND blackboard_artifacts.obj_id IN (" + objIdList + ") "; // NON-NLS - } - - /** - * Get the default image to display when a thumbnail is not available. - * - * @return The default video thumbnail. - */ - private static BufferedImage getDefaultVideoThumbnail() { - try { - return ImageIO.read(ImageUtils.class.getResourceAsStream("/org/sleuthkit/autopsy/images/failedToCreateVideoThumb.png"));//NON-NLS - } catch (IOException ex) { - logger.log(Level.SEVERE, "Failed to load 'failed to create video' placeholder.", ex); //NON-NLS - } - return null; - } - - /** - * Get the video thumbnails for a file which exists in a - * VideoThumbnailsWrapper and update the VideoThumbnailsWrapper to include - * them. - * - * @param thumbnailWrapper the object which contains the file to generate - * thumbnails for. - * - */ - @NbBundle.Messages({"# {0} - file name", - "FileSearch.genVideoThumb.progress.text=extracting temporary file {0}"}) - static void getVideoThumbnails(VideoThumbnailsWrapper thumbnailWrapper) { - AbstractFile file = thumbnailWrapper.getResultFile().getFirstInstance(); - String cacheDirectory; - try { - cacheDirectory = Case.getCurrentCaseThrows().getCacheDirectory(); - } catch (NoCurrentCaseException ex) { - cacheDirectory = null; - logger.log(Level.WARNING, "Unable to get cache directory, video thumbnails will not be saved", ex); - } - if (cacheDirectory == null || file.getMd5Hash() == null || !Paths.get(cacheDirectory, VIDEO_THUMBNAIL_DIR, file.getMd5Hash()).toFile().exists()) { - java.io.File tempFile; - try { - tempFile = getVideoFileInTempDir(file); - } catch (NoCurrentCaseException ex) { - logger.log(Level.WARNING, "Exception while getting open case.", ex); //NON-NLS - int[] framePositions = new int[]{ - 0, - 0, - 0, - 0}; - thumbnailWrapper.setThumbnails(createDefaultThumbnailList(VIDEO_DEFAULT_IMAGE), framePositions); - return; - } - if (tempFile.exists() == false || tempFile.length() < file.getSize()) { - ProgressHandle progress = ProgressHandle.createHandle(Bundle.FileSearch_genVideoThumb_progress_text(file.getName())); - progress.start(100); - try { - Files.createParentDirs(tempFile); - if (Thread.interrupted()) { - int[] framePositions = new int[]{ - 0, - 0, - 0, - 0}; - thumbnailWrapper.setThumbnails(createDefaultThumbnailList(VIDEO_DEFAULT_IMAGE), framePositions); - return; - } - ContentUtils.writeToFile(file, tempFile, progress, null, true); - } catch (IOException ex) { - logger.log(Level.WARNING, "Error extracting temporary file for " + file.getParentPath() + "/" + file.getName(), ex); //NON-NLS - } finally { - progress.finish(); - } - } - VideoCapture videoFile = new VideoCapture(); // will contain the video - BufferedImage bufferedImage = null; - - try { - if (!videoFile.open(tempFile.toString())) { - logger.log(Level.WARNING, "Error opening {0} for preview generation.", file.getParentPath() + "/" + file.getName()); //NON-NLS - int[] framePositions = new int[]{ - 0, - 0, - 0, - 0}; - thumbnailWrapper.setThumbnails(createDefaultThumbnailList(VIDEO_DEFAULT_IMAGE), framePositions); - return; - } - double fps = videoFile.get(5); // gets frame per second - double totalFrames = videoFile.get(7); // gets total frames - if (fps <= 0 || totalFrames <= 0) { - logger.log(Level.WARNING, "Error getting fps or total frames for {0}", file.getParentPath() + "/" + file.getName()); //NON-NLS - int[] framePositions = new int[]{ - 0, - 0, - 0, - 0}; - thumbnailWrapper.setThumbnails(createDefaultThumbnailList(VIDEO_DEFAULT_IMAGE), framePositions); - return; - } - if (Thread.interrupted()) { - int[] framePositions = new int[]{ - 0, - 0, - 0, - 0}; - thumbnailWrapper.setThumbnails(createDefaultThumbnailList(VIDEO_DEFAULT_IMAGE), framePositions); - return; - } - - double duration = 1000 * (totalFrames / fps); //total milliseconds - - int[] framePositions = new int[]{ - (int) (duration * .01), - (int) (duration * .25), - (int) (duration * .5), - (int) (duration * .75),}; - - Mat imageMatrix = new Mat(); - List videoThumbnails = new ArrayList<>(); - if (cacheDirectory == null || file.getMd5Hash() == null) { - cacheDirectory = null; - } else { - try { - FileUtils.forceMkdir(Paths.get(cacheDirectory, VIDEO_THUMBNAIL_DIR, file.getMd5Hash()).toFile()); - } catch (IOException ex) { - cacheDirectory = null; - logger.log(Level.WARNING, "Unable to make video thumbnails directory, thumbnails will not be saved", ex); - } - } - for (int i = 0; i < framePositions.length; i++) { - if (!videoFile.set(0, framePositions[i])) { - logger.log(Level.WARNING, "Error seeking to " + framePositions[i] + "ms in {0}", file.getParentPath() + "/" + file.getName()); //NON-NLS - // If we can't set the time, continue to the next frame position and try again. - - videoThumbnails.add(VIDEO_DEFAULT_IMAGE); - if (cacheDirectory != null) { - try { - ImageIO.write(VIDEO_DEFAULT_IMAGE, THUMBNAIL_FORMAT, - Paths.get(cacheDirectory, VIDEO_THUMBNAIL_DIR, file.getMd5Hash(), i + "-" + framePositions[i] + "." + THUMBNAIL_FORMAT).toFile()); //NON-NLS) - } catch (IOException ex) { - logger.log(Level.WARNING, "Unable to save default video thumbnail for " + file.getMd5Hash() + " at frame position " + framePositions[i], ex); - } - } - continue; - } - // Read the frame into the image/matrix. - if (!videoFile.read(imageMatrix)) { - logger.log(Level.WARNING, "Error reading frame at " + framePositions[i] + "ms from {0}", file.getParentPath() + "/" + file.getName()); //NON-NLS - // If the image is bad for some reason, continue to the next frame position and try again. - videoThumbnails.add(VIDEO_DEFAULT_IMAGE); - if (cacheDirectory != null) { - try { - ImageIO.write(VIDEO_DEFAULT_IMAGE, THUMBNAIL_FORMAT, - Paths.get(cacheDirectory, VIDEO_THUMBNAIL_DIR, file.getMd5Hash(), i + "-" + framePositions[i] + "." + THUMBNAIL_FORMAT).toFile()); //NON-NLS) - } catch (IOException ex) { - logger.log(Level.WARNING, "Unable to save default video thumbnail for " + file.getMd5Hash() + " at frame position " + framePositions[i], ex); - } - } - - continue; - } - // If the image is empty, return since no buffered image can be created. - if (imageMatrix.empty()) { - videoThumbnails.add(VIDEO_DEFAULT_IMAGE); - if (cacheDirectory != null) { - try { - ImageIO.write(VIDEO_DEFAULT_IMAGE, THUMBNAIL_FORMAT, - Paths.get(cacheDirectory, VIDEO_THUMBNAIL_DIR, file.getMd5Hash(), i + "-" + framePositions[i] + "." + THUMBNAIL_FORMAT).toFile()); //NON-NLS) - } catch (IOException ex) { - logger.log(Level.WARNING, "Unable to save default video thumbnail for " + file.getMd5Hash() + " at frame position " + framePositions[i], ex); - } - } - continue; - } - - int matrixColumns = imageMatrix.cols(); - int matrixRows = imageMatrix.rows(); - - // Convert the matrix that contains the frame to a buffered image. - if (bufferedImage == null) { - bufferedImage = new BufferedImage(matrixColumns, matrixRows, BufferedImage.TYPE_3BYTE_BGR); - } - - byte[] data = new byte[matrixRows * matrixColumns * (int) (imageMatrix.elemSize())]; - imageMatrix.get(0, 0, data); //copy the image to data - - if (imageMatrix.channels() == 3) { - for (int k = 0; k < data.length; k += 3) { - byte temp = data[k]; - data[k] = data[k + 2]; - data[k + 2] = temp; - } - } - - bufferedImage.getRaster().setDataElements(0, 0, matrixColumns, matrixRows, data); - if (Thread.interrupted()) { - thumbnailWrapper.setThumbnails(videoThumbnails, framePositions); - try { - FileUtils.forceDelete(Paths.get(cacheDirectory, VIDEO_THUMBNAIL_DIR, file.getMd5Hash()).toFile()); - } catch (IOException ex) { - logger.log(Level.WARNING, "Unable to delete directory for cancelled video thumbnail process", ex); - } - return; - } - BufferedImage thumbnail = ScalrWrapper.resize(bufferedImage, Scalr.Method.SPEED, Scalr.Mode.FIT_TO_HEIGHT, ImageUtils.ICON_SIZE_LARGE, ImageUtils.ICON_SIZE_MEDIUM, Scalr.OP_ANTIALIAS); - //We are height limited here so it can be wider than it can be tall.Scalr maintains the aspect ratio. - videoThumbnails.add(thumbnail); - if (cacheDirectory != null) { - try { - ImageIO.write(thumbnail, THUMBNAIL_FORMAT, - Paths.get(cacheDirectory, VIDEO_THUMBNAIL_DIR, file.getMd5Hash(), i + "-" + framePositions[i] + "." + THUMBNAIL_FORMAT).toFile()); //NON-NLS) - } catch (IOException ex) { - logger.log(Level.WARNING, "Unable to save video thumbnail for " + file.getMd5Hash() + " at frame position " + framePositions[i], ex); - } - } - } - thumbnailWrapper.setThumbnails(videoThumbnails, framePositions); - } finally { - videoFile.release(); // close the file} - } - } else { - loadSavedThumbnails(cacheDirectory, thumbnailWrapper, VIDEO_DEFAULT_IMAGE); - } - } - - /** - * Load the thumbnails that exist in the cache directory for the specified - * video file. - * - * @param cacheDirectory The directory which exists for the video - * thumbnails. - * @param thumbnailWrapper The VideoThumbnailWrapper object which contains - * information about the file and the thumbnails - * associated with it. - */ - private static void loadSavedThumbnails(String cacheDirectory, VideoThumbnailsWrapper thumbnailWrapper, BufferedImage failedVideoThumbImage) { - int[] framePositions = new int[4]; - List videoThumbnails = new ArrayList<>(); - int thumbnailNumber = 0; - String md5 = thumbnailWrapper.getResultFile().getFirstInstance().getMd5Hash(); - for (String fileName : Paths.get(cacheDirectory, VIDEO_THUMBNAIL_DIR, md5).toFile().list()) { - try { - videoThumbnails.add(ImageIO.read(Paths.get(cacheDirectory, VIDEO_THUMBNAIL_DIR, md5, fileName).toFile())); - } catch (IOException ex) { - videoThumbnails.add(failedVideoThumbImage); - logger.log(Level.WARNING, "Unable to read saved video thumbnail " + fileName + " for " + md5, ex); - } - int framePos = Integer.valueOf(FilenameUtils.getBaseName(fileName).substring(2)); - framePositions[thumbnailNumber] = framePos; - thumbnailNumber++; - } - thumbnailWrapper.setThumbnails(videoThumbnails, framePositions); - } - - /** - * Private helper method for creating video thumbnails, for use when no - * thumbnails are created. - * - * @return List containing the default thumbnail. - */ - private static List createDefaultThumbnailList(BufferedImage failedVideoThumbImage) { - List videoThumbnails = new ArrayList<>(); - videoThumbnails.add(failedVideoThumbImage); - videoThumbnails.add(failedVideoThumbImage); - videoThumbnails.add(failedVideoThumbImage); - videoThumbnails.add(failedVideoThumbImage); - return videoThumbnails; - } - - private FileSearch() { - // Class should not be instantiated - } - - /** - * Base class for the grouping attributes. - */ - abstract static class AttributeType { - - /** - * For a given file, return the key for the group it belongs to for this - * attribute type. - * - * @param file the result file to be grouped - * - * @return the key for the group this file goes in - */ - abstract GroupKey getGroupKey(ResultFile file); - - /** - * Add any extra data to the ResultFile object from this attribute. - * - * @param files The list of files to enhance - * @param caseDb The case database - * @param centralRepoDb The central repository database. Can be null if - * not needed. - * - * @throws FileSearchException - */ - void addAttributeToResultFiles(List files, SleuthkitCase caseDb, CentralRepository centralRepoDb) throws FileSearchException { - // Default is to do nothing - } - } - - /** - * The key used for grouping for each attribute type. - */ - abstract static class GroupKey implements Comparable { - - /** - * Get the string version of the group key for display. Each display - * name should correspond to a unique GroupKey object. - * - * @return The display name for this key - */ - abstract String getDisplayName(); - - /** - * Subclasses must implement equals(). - * - * @param otherKey - * - * @return true if the keys are equal, false otherwise - */ - @Override - abstract public boolean equals(Object otherKey); - - /** - * Subclasses must implement hashCode(). - * - * @return the hash code - */ - @Override - abstract public int hashCode(); - - /** - * It should not happen with the current setup, but we need to cover the - * case where two different GroupKey subclasses are compared against - * each other. Use a lexicographic comparison on the class names. - * - * @param otherGroupKey The other group key - * - * @return result of alphabetical comparison on the class name - */ - int compareClassNames(GroupKey otherGroupKey) { - return this.getClass().getName().compareTo(otherGroupKey.getClass().getName()); - } - - @Override - public String toString() { - return getDisplayName(); - } - } - - /** - * Attribute for grouping/sorting by file size - */ - static class FileSizeAttribute extends AttributeType { - - @Override - GroupKey getGroupKey(ResultFile file) { - return new FileSizeGroupKey(file); - } - } - - /** - * Key representing a file size group - */ - private static class FileSizeGroupKey extends GroupKey { - - private final FileSize fileSize; - - FileSizeGroupKey(ResultFile file) { - if (file.getFileType() == FileType.VIDEO) { - fileSize = FileSize.fromVideoSize(file.getFirstInstance().getSize()); - } else { - fileSize = FileSize.fromImageSize(file.getFirstInstance().getSize()); - } - } - - @Override - String getDisplayName() { - return getFileSize().toString(); - } - - @Override - public int compareTo(GroupKey otherGroupKey) { - if (otherGroupKey instanceof FileSizeGroupKey) { - FileSizeGroupKey otherFileSizeGroupKey = (FileSizeGroupKey) otherGroupKey; - return Integer.compare(getFileSize().getRanking(), otherFileSizeGroupKey.getFileSize().getRanking()); - } else { - return compareClassNames(otherGroupKey); - } - } - - @Override - public boolean equals(Object otherKey) { - if (otherKey == this) { - return true; - } - - if (!(otherKey instanceof FileSizeGroupKey)) { - return false; - } - - FileSizeGroupKey otherFileSizeGroupKey = (FileSizeGroupKey) otherKey; - return getFileSize().equals(otherFileSizeGroupKey.getFileSize()); - } - - @Override - public int hashCode() { - return Objects.hash(getFileSize().getRanking()); - } - - /** - * @return the fileSize - */ - FileSize getFileSize() { - return fileSize; - } - } - - /** - * Attribute for grouping/sorting by parent path - */ - static class ParentPathAttribute extends AttributeType { - - @Override - GroupKey getGroupKey(ResultFile file) { - return new ParentPathGroupKey(file); - } - } - - /** - * Key representing a parent path group - */ - private static class ParentPathGroupKey extends GroupKey { - - private String parentPath; - private Long parentID; - - ParentPathGroupKey(ResultFile file) { - Content parent; - try { - parent = file.getFirstInstance().getParent(); - } catch (TskCoreException ignored) { - parent = null; - } - //Find the directory this file is in if it is an embedded file - while (parent != null && parent instanceof AbstractFile && ((AbstractFile) parent).isFile()) { - try { - parent = parent.getParent(); - } catch (TskCoreException ignored) { - parent = null; - } - } - setParentPathAndID(parent, file); - } - - /** - * Helper method to set the parent path and parent ID. - * - * @param parent The parent content object. - * @param file The ResultFile object. - */ - private void setParentPathAndID(Content parent, ResultFile file) { - if (parent != null) { - try { - parentPath = parent.getUniquePath(); - parentID = parent.getId(); - } catch (TskCoreException ignored) { - //catch block left blank purposefully next if statement will handle case when exception takes place as well as when parent is null - } - - } - if (parentPath == null) { - if (file.getFirstInstance().getParentPath() != null) { - parentPath = file.getFirstInstance().getParentPath(); - } else { - parentPath = ""; // NON-NLS - } - parentID = -1L; - } - } - - @Override - String getDisplayName() { - return getParentPath(); - } - - @Override - public int compareTo(GroupKey otherGroupKey) { - if (otherGroupKey instanceof ParentPathGroupKey) { - ParentPathGroupKey otherParentPathGroupKey = (ParentPathGroupKey) otherGroupKey; - int comparisonResult = getParentPath().compareTo(otherParentPathGroupKey.getParentPath()); - if (comparisonResult == 0) { - comparisonResult = getParentID().compareTo(otherParentPathGroupKey.getParentID()); - } - return comparisonResult; - } else { - return compareClassNames(otherGroupKey); - } - } - - @Override - public boolean equals(Object otherKey) { - if (otherKey == this) { - return true; - } - - if (!(otherKey instanceof ParentPathGroupKey)) { - return false; - } - - ParentPathGroupKey otherParentPathGroupKey = (ParentPathGroupKey) otherKey; - return getParentPath().equals(otherParentPathGroupKey.getParentPath()) && getParentID().equals(otherParentPathGroupKey.getParentID()); - } - - @Override - public int hashCode() { - int hashCode = 11; - hashCode = 61 * hashCode + Objects.hash(getParentPath()); - hashCode = 61 * hashCode + Objects.hash(getParentID()); - return hashCode; - } - - /** - * @return the parentPath - */ - String getParentPath() { - return parentPath; - } - - /** - * @return the parentID - */ - Long getParentID() { - return parentID; - } - } - - /** - * Attribute for grouping/sorting by data source - */ - static class DataSourceAttribute extends AttributeType { - - @Override - GroupKey getGroupKey(ResultFile file) { - return new DataSourceGroupKey(file); - } - } - - /** - * Key representing a data source group - */ - private static class DataSourceGroupKey extends GroupKey { - - private final long dataSourceID; - private String displayName; - - @NbBundle.Messages({ - "# {0} - Data source name", - "# {1} - Data source ID", - "FileSearch.DataSourceGroupKey.datasourceAndID={0}(ID: {1})", - "# {0} - Data source ID", - "FileSearch.DataSourceGroupKey.idOnly=Data source (ID: {0})"}) - DataSourceGroupKey(ResultFile file) { - dataSourceID = file.getFirstInstance().getDataSourceObjectId(); - - try { - // The data source should be cached so this won't actually be a database query. - Content ds = file.getFirstInstance().getDataSource(); - displayName = Bundle.FileSearch_DataSourceGroupKey_datasourceAndID(ds.getName(), ds.getId()); - } catch (TskCoreException ex) { - logger.log(Level.WARNING, "Error looking up data source with ID " + dataSourceID, ex); // NON-NLS - displayName = Bundle.FileSearch_DataSourceGroupKey_idOnly(dataSourceID); - } - } - - @Override - String getDisplayName() { - return displayName; - } - - @Override - public int compareTo(GroupKey otherGroupKey) { - if (otherGroupKey instanceof DataSourceGroupKey) { - DataSourceGroupKey otherDataSourceGroupKey = (DataSourceGroupKey) otherGroupKey; - return Long.compare(getDataSourceID(), otherDataSourceGroupKey.getDataSourceID()); - } else { - return compareClassNames(otherGroupKey); - } - } - - @Override - public boolean equals(Object otherKey) { - if (otherKey == this) { - return true; - } - - if (!(otherKey instanceof DataSourceGroupKey)) { - return false; - } - - DataSourceGroupKey otherDataSourceGroupKey = (DataSourceGroupKey) otherKey; - return getDataSourceID() == otherDataSourceGroupKey.getDataSourceID(); - } - - @Override - public int hashCode() { - return Objects.hash(getDataSourceID()); - } - - /** - * @return the dataSourceID - */ - long getDataSourceID() { - return dataSourceID; - } - } - - /** - * Attribute for grouping/sorting by file type - */ - static class FileTypeAttribute extends AttributeType { - - @Override - GroupKey getGroupKey(ResultFile file) { - return new FileTypeGroupKey(file); - } - } - - /** - * Key representing a file type group - */ - private static class FileTypeGroupKey extends GroupKey { - - private final FileType fileType; - - FileTypeGroupKey(ResultFile file) { - fileType = file.getFileType(); - } - - @Override - String getDisplayName() { - return getFileType().toString(); - } - - @Override - public int compareTo(GroupKey otherGroupKey) { - if (otherGroupKey instanceof FileTypeGroupKey) { - FileTypeGroupKey otherFileTypeGroupKey = (FileTypeGroupKey) otherGroupKey; - return Integer.compare(getFileType().getRanking(), otherFileTypeGroupKey.getFileType().getRanking()); - } else { - return compareClassNames(otherGroupKey); - } - } - - @Override - public boolean equals(Object otherKey) { - if (otherKey == this) { - return true; - } - - if (!(otherKey instanceof FileTypeGroupKey)) { - return false; - } - - FileTypeGroupKey otherFileTypeGroupKey = (FileTypeGroupKey) otherKey; - return getFileType().equals(otherFileTypeGroupKey.getFileType()); - } - - @Override - public int hashCode() { - return Objects.hash(getFileType().getRanking()); - } - - /** - * @return the fileType - */ - FileType getFileType() { - return fileType; - } - } - - /** - * Attribute for grouping/sorting by keyword lists - */ - static class KeywordListAttribute extends AttributeType { - - @Override - GroupKey getGroupKey(ResultFile file) { - return new KeywordListGroupKey(file); - } - - @Override - void addAttributeToResultFiles(List files, SleuthkitCase caseDb, - CentralRepository centralRepoDb) throws FileSearchException { - - // Get pairs of (object ID, keyword list name) for all files in the list of files that have - // keyword list hits. - String selectQuery = createSetNameClause(files, BlackboardArtifact.ARTIFACT_TYPE.TSK_KEYWORD_HIT.getTypeID(), - BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID()); - - SetKeywordListNamesCallback callback = new SetKeywordListNamesCallback(files); - try { - caseDb.getCaseDbAccessManager().select(selectQuery, callback); - } catch (TskCoreException ex) { - throw new FileSearchException("Error looking up keyword list attributes", ex); // NON-NLS - } - } - - /** - * Callback to process the results of the CaseDbAccessManager select - * query. Will add the keyword list names to the list of ResultFile - * objects. - */ - private static class SetKeywordListNamesCallback implements CaseDbAccessManager.CaseDbAccessQueryCallback { - - List resultFiles; - - /** - * Create the callback. - * - * @param resultFiles List of files to add keyword list names to - */ - SetKeywordListNamesCallback(List resultFiles) { - this.resultFiles = resultFiles; - } - - @Override - public void process(ResultSet rs) { - try { - // Create a temporary map of object ID to ResultFile - Map tempMap = new HashMap<>(); - for (ResultFile file : resultFiles) { - tempMap.put(file.getFirstInstance().getId(), file); - } - - while (rs.next()) { - try { - Long objId = rs.getLong("object_id"); // NON-NLS - String keywordListName = rs.getString("set_name"); // NON-NLS - - tempMap.get(objId).addKeywordListName(keywordListName); - - } catch (SQLException ex) { - logger.log(Level.SEVERE, "Unable to get object_id or set_name from result set", ex); // NON-NLS - } - } - } catch (SQLException ex) { - logger.log(Level.SEVERE, "Failed to get keyword list names", ex); // NON-NLS - } - } - } - } - - /** - * Key representing a keyword list group - */ - private static class KeywordListGroupKey extends GroupKey { - - private final List keywordListNames; - private final String keywordListNamesString; - - @NbBundle.Messages({ - "FileSearch.KeywordListGroupKey.noKeywords=None"}) - KeywordListGroupKey(ResultFile file) { - keywordListNames = file.getKeywordListNames(); - - if (keywordListNames.isEmpty()) { - keywordListNamesString = Bundle.FileSearch_KeywordListGroupKey_noKeywords(); - } else { - keywordListNamesString = String.join(",", keywordListNames); // NON-NLS - } - } - - @Override - String getDisplayName() { - return getKeywordListNamesString(); - } - - @Override - public int compareTo(GroupKey otherGroupKey) { - if (otherGroupKey instanceof KeywordListGroupKey) { - KeywordListGroupKey otherKeywordListNamesGroupKey = (KeywordListGroupKey) otherGroupKey; - - // Put the empty list at the end - if (getKeywordListNames().isEmpty()) { - if (otherKeywordListNamesGroupKey.getKeywordListNames().isEmpty()) { - return 0; - } else { - return 1; - } - } else if (otherKeywordListNamesGroupKey.getKeywordListNames().isEmpty()) { - return -1; - } - - return getKeywordListNamesString().compareTo(otherKeywordListNamesGroupKey.getKeywordListNamesString()); - } else { - return compareClassNames(otherGroupKey); - } - } - - @Override - public boolean equals(Object otherKey) { - if (otherKey == this) { - return true; - } - - if (!(otherKey instanceof KeywordListGroupKey)) { - return false; - } - - KeywordListGroupKey otherKeywordListGroupKey = (KeywordListGroupKey) otherKey; - return getKeywordListNamesString().equals(otherKeywordListGroupKey.getKeywordListNamesString()); - } - - @Override - public int hashCode() { - return Objects.hash(getKeywordListNamesString()); - } - - /** - * @return the keywordListNames - */ - List getKeywordListNames() { - return Collections.unmodifiableList(keywordListNames); - } - - /** - * @return the keywordListNamesString - */ - String getKeywordListNamesString() { - return keywordListNamesString; - } - } - - /** - * Attribute for grouping/sorting by frequency in the central repository - */ - static class FrequencyAttribute extends AttributeType { - - static final int BATCH_SIZE = 50; // Number of hashes to look up at one time - - @Override - GroupKey getGroupKey(ResultFile file) { - return new FrequencyGroupKey(file); - } - - @Override - void addAttributeToResultFiles(List files, SleuthkitCase caseDb, - CentralRepository centralRepoDb) throws FileSearchException { - if (centralRepoDb == null) { - for (ResultFile file : files) { - if (file.getFrequency() == Frequency.UNKNOWN && file.getFirstInstance().getKnown() == TskData.FileKnown.KNOWN) { - file.setFrequency(Frequency.KNOWN); - } - } - } else { - processResultFilesForCR(files, centralRepoDb); - } - } - - /** - * Private helper method for adding Frequency attribute when CR is - * enabled. - * - * @param files The list of ResultFiles to caluclate frequency - * for. - * @param centralRepoDb The central repository currently in use. - */ - private void processResultFilesForCR(List files, - CentralRepository centralRepoDb) { - List currentFiles = new ArrayList<>(); - Set hashesToLookUp = new HashSet<>(); - for (ResultFile file : files) { - if (file.getFirstInstance().getKnown() == TskData.FileKnown.KNOWN) { - file.setFrequency(Frequency.KNOWN); - } - if (file.getFrequency() == Frequency.UNKNOWN - && file.getFirstInstance().getMd5Hash() != null - && !file.getFirstInstance().getMd5Hash().isEmpty()) { - hashesToLookUp.add(file.getFirstInstance().getMd5Hash()); - currentFiles.add(file); - } - if (hashesToLookUp.size() >= BATCH_SIZE) { - computeFrequency(hashesToLookUp, currentFiles, centralRepoDb); - - hashesToLookUp.clear(); - currentFiles.clear(); - } - } - computeFrequency(hashesToLookUp, currentFiles, centralRepoDb); - } - } - - /** - * Callback to use with findInterCaseValuesByCount which generates a list of - * values for common property search - */ - private static class FrequencyCallback implements InstanceTableCallback { - - private final List files; - - private FrequencyCallback(List files) { - this.files = new ArrayList<>(files); - } - - @Override - public void process(ResultSet resultSet) { - try { - - while (resultSet.next()) { - String hash = resultSet.getString(1); - int count = resultSet.getInt(2); - for (Iterator iterator = files.iterator(); iterator.hasNext();) { - ResultFile file = iterator.next(); - if (file.getFirstInstance().getMd5Hash().equalsIgnoreCase(hash)) { - file.setFrequency(Frequency.fromCount(count)); - iterator.remove(); - } - } - } - - // The files left had no matching entries in the CR, so mark them as unique - for (ResultFile file : files) { - file.setFrequency(Frequency.UNIQUE); - } - } catch (SQLException ex) { - logger.log(Level.WARNING, "Error getting frequency counts from Central Repository", ex); // NON-NLS - } - } - } - - /** - * Key representing a central repository frequency group - */ - private static class FrequencyGroupKey extends GroupKey { - - private final Frequency frequency; - - FrequencyGroupKey(ResultFile file) { - frequency = file.getFrequency(); - } - - @Override - String getDisplayName() { - return getFrequency().toString(); - } - - @Override - public int compareTo(GroupKey otherGroupKey) { - if (otherGroupKey instanceof FrequencyGroupKey) { - FrequencyGroupKey otherFrequencyGroupKey = (FrequencyGroupKey) otherGroupKey; - return Integer.compare(getFrequency().getRanking(), otherFrequencyGroupKey.getFrequency().getRanking()); - } else { - return compareClassNames(otherGroupKey); - } - } - - @Override - public boolean equals(Object otherKey) { - if (otherKey == this) { - return true; - } - - if (!(otherKey instanceof FrequencyGroupKey)) { - return false; - } - - FrequencyGroupKey otherFrequencyGroupKey = (FrequencyGroupKey) otherKey; - return getFrequency().equals(otherFrequencyGroupKey.getFrequency()); - } - - @Override - public int hashCode() { - return Objects.hash(getFrequency().getRanking()); - } - - /** - * @return the frequency - */ - Frequency getFrequency() { - return frequency; - } - } - - /** - * Attribute for grouping/sorting by hash set lists - */ - static class HashHitsAttribute extends AttributeType { - - @Override - GroupKey getGroupKey(ResultFile file) { - return new HashHitsGroupKey(file); - } - - @Override - void addAttributeToResultFiles(List files, SleuthkitCase caseDb, - CentralRepository centralRepoDb) throws FileSearchException { - - // Get pairs of (object ID, hash set name) for all files in the list of files that have - // hash set hits. - String selectQuery = createSetNameClause(files, BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT.getTypeID(), - BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID()); - - HashSetNamesCallback callback = new HashSetNamesCallback(files); - try { - caseDb.getCaseDbAccessManager().select(selectQuery, callback); - } catch (TskCoreException ex) { - throw new FileSearchException("Error looking up hash set attributes", ex); // NON-NLS - } - } - - /** - * Callback to process the results of the CaseDbAccessManager select - * query. Will add the hash set names to the list of ResultFile objects. - */ - private static class HashSetNamesCallback implements CaseDbAccessManager.CaseDbAccessQueryCallback { - - List resultFiles; - - /** - * Create the callback. - * - * @param resultFiles List of files to add hash set names to - */ - HashSetNamesCallback(List resultFiles) { - this.resultFiles = resultFiles; - } - - @Override - public void process(ResultSet rs) { - try { - // Create a temporary map of object ID to ResultFile - Map tempMap = new HashMap<>(); - for (ResultFile file : resultFiles) { - tempMap.put(file.getFirstInstance().getId(), file); - } - - while (rs.next()) { - try { - Long objId = rs.getLong("object_id"); // NON-NLS - String hashSetName = rs.getString("set_name"); // NON-NLS - - tempMap.get(objId).addHashSetName(hashSetName); - - } catch (SQLException ex) { - logger.log(Level.SEVERE, "Unable to get object_id or set_name from result set", ex); // NON-NLS - } - } - } catch (SQLException ex) { - logger.log(Level.SEVERE, "Failed to get hash set names", ex); // NON-NLS - } - } - } - } - - /** - * Key representing a hash hits group - */ - private static class HashHitsGroupKey extends GroupKey { - - private final List hashSetNames; - private final String hashSetNamesString; - - @NbBundle.Messages({ - "FileSearch.HashHitsGroupKey.noHashHits=None"}) - HashHitsGroupKey(ResultFile file) { - hashSetNames = file.getHashSetNames(); - - if (hashSetNames.isEmpty()) { - hashSetNamesString = Bundle.FileSearch_HashHitsGroupKey_noHashHits(); - } else { - hashSetNamesString = String.join(",", hashSetNames); // NON-NLS - } - } - - @Override - String getDisplayName() { - return getHashSetNamesString(); - } - - @Override - public int compareTo(GroupKey otherGroupKey) { - if (otherGroupKey instanceof HashHitsGroupKey) { - HashHitsGroupKey otherHashHitsGroupKey = (HashHitsGroupKey) otherGroupKey; - - // Put the empty list at the end - if (getHashSetNames().isEmpty()) { - if (otherHashHitsGroupKey.getHashSetNames().isEmpty()) { - return 0; - } else { - return 1; - } - } else if (otherHashHitsGroupKey.getHashSetNames().isEmpty()) { - return -1; - } - - return getHashSetNamesString().compareTo(otherHashHitsGroupKey.getHashSetNamesString()); - } else { - return compareClassNames(otherGroupKey); - } - } - - @Override - public boolean equals(Object otherKey) { - if (otherKey == this) { - return true; - } - - if (!(otherKey instanceof HashHitsGroupKey)) { - return false; - } - - HashHitsGroupKey otherHashHitsGroupKey = (HashHitsGroupKey) otherKey; - return getHashSetNamesString().equals(otherHashHitsGroupKey.getHashSetNamesString()); - } - - @Override - public int hashCode() { - return Objects.hash(getHashSetNamesString()); - } - - /** - * @return the hashSetNames - */ - List getHashSetNames() { - return Collections.unmodifiableList(hashSetNames); - } - - /** - * @return the hashSetNamesString - */ - String getHashSetNamesString() { - return hashSetNamesString; - } - } - - /** - * Attribute for grouping/sorting by interesting item set lists - */ - static class InterestingItemAttribute extends AttributeType { - - @Override - GroupKey getGroupKey(ResultFile file) { - return new InterestingItemGroupKey(file); - } - - @Override - void addAttributeToResultFiles(List files, SleuthkitCase caseDb, - CentralRepository centralRepoDb) throws FileSearchException { - - // Get pairs of (object ID, interesting item set name) for all files in the list of files that have - // interesting file set hits. - String selectQuery = createSetNameClause(files, BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT.getTypeID(), - BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID()); - - InterestingFileSetNamesCallback callback = new InterestingFileSetNamesCallback(files); - try { - caseDb.getCaseDbAccessManager().select(selectQuery, callback); - } catch (TskCoreException ex) { - throw new FileSearchException("Error looking up interesting file set attributes", ex); // NON-NLS - } - } - - /** - * Callback to process the results of the CaseDbAccessManager select - * query. Will add the interesting file set names to the list of - * ResultFile objects. - */ - private static class InterestingFileSetNamesCallback implements CaseDbAccessManager.CaseDbAccessQueryCallback { - - List resultFiles; - - /** - * Create the callback. - * - * @param resultFiles List of files to add interesting file set - * names to - */ - InterestingFileSetNamesCallback(List resultFiles) { - this.resultFiles = resultFiles; - } - - @Override - public void process(ResultSet rs) { - try { - // Create a temporary map of object ID to ResultFile - Map tempMap = new HashMap<>(); - for (ResultFile file : resultFiles) { - tempMap.put(file.getFirstInstance().getId(), file); - } - - while (rs.next()) { - try { - Long objId = rs.getLong("object_id"); // NON-NLS - String setName = rs.getString("set_name"); // NON-NLS - - tempMap.get(objId).addInterestingSetName(setName); - - } catch (SQLException ex) { - logger.log(Level.SEVERE, "Unable to get object_id or set_name from result set", ex); // NON-NLS - } - } - } catch (SQLException ex) { - logger.log(Level.SEVERE, "Failed to get interesting file set names", ex); // NON-NLS - } - } - } - } - - /** - * Key representing a interesting item set group - */ - private static class InterestingItemGroupKey extends GroupKey { - - private final List interestingItemSetNames; - private final String interestingItemSetNamesString; - - @NbBundle.Messages({ - "FileSearch.InterestingItemGroupKey.noSets=None"}) - InterestingItemGroupKey(ResultFile file) { - interestingItemSetNames = file.getInterestingSetNames(); - - if (interestingItemSetNames.isEmpty()) { - interestingItemSetNamesString = Bundle.FileSearch_InterestingItemGroupKey_noSets(); - } else { - interestingItemSetNamesString = String.join(",", interestingItemSetNames); // NON-NLS - } - } - - @Override - String getDisplayName() { - return getInterestingItemSetNamesString(); - } - - @Override - public int compareTo(GroupKey otherGroupKey) { - if (otherGroupKey instanceof InterestingItemGroupKey) { - InterestingItemGroupKey otherInterestingItemGroupKey = (InterestingItemGroupKey) otherGroupKey; - - // Put the empty list at the end - if (this.getInterestingItemSetNames().isEmpty()) { - if (otherInterestingItemGroupKey.getInterestingItemSetNames().isEmpty()) { - return 0; - } else { - return 1; - } - } else if (otherInterestingItemGroupKey.getInterestingItemSetNames().isEmpty()) { - return -1; - } - - return getInterestingItemSetNamesString().compareTo(otherInterestingItemGroupKey.getInterestingItemSetNamesString()); - } else { - return compareClassNames(otherGroupKey); - } - } - - @Override - public boolean equals(Object otherKey) { - if (otherKey == this) { - return true; - } - - if (!(otherKey instanceof InterestingItemGroupKey)) { - return false; - } - - InterestingItemGroupKey otherInterestingItemGroupKey = (InterestingItemGroupKey) otherKey; - return getInterestingItemSetNamesString().equals(otherInterestingItemGroupKey.getInterestingItemSetNamesString()); - } - - @Override - public int hashCode() { - return Objects.hash(getInterestingItemSetNamesString()); - } - - /** - * @return the interestingItemSetNames - */ - List getInterestingItemSetNames() { - return Collections.unmodifiableList(interestingItemSetNames); - } - - /** - * @return the interestingItemSetNamesString - */ - String getInterestingItemSetNamesString() { - return interestingItemSetNamesString; - } - } - - /** - * Attribute for grouping/sorting by objects detected - */ - static class ObjectDetectedAttribute extends AttributeType { - - @Override - GroupKey getGroupKey(ResultFile file) { - return new ObjectDetectedGroupKey(file); - } - - @Override - void addAttributeToResultFiles(List files, SleuthkitCase caseDb, - CentralRepository centralRepoDb) throws FileSearchException { - - // Get pairs of (object ID, object type name) for all files in the list of files that have - // objects detected - String selectQuery = createSetNameClause(files, BlackboardArtifact.ARTIFACT_TYPE.TSK_OBJECT_DETECTED.getTypeID(), - BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DESCRIPTION.getTypeID()); - - ObjectDetectedNamesCallback callback = new ObjectDetectedNamesCallback(files); - try { - caseDb.getCaseDbAccessManager().select(selectQuery, callback); - } catch (TskCoreException ex) { - throw new FileSearchException("Error looking up object detected attributes", ex); // NON-NLS - } - } - - /** - * Callback to process the results of the CaseDbAccessManager select - * query. Will add the object type names to the list of ResultFile - * objects. - */ - private static class ObjectDetectedNamesCallback implements CaseDbAccessManager.CaseDbAccessQueryCallback { - - List resultFiles; - - /** - * Create the callback. - * - * @param resultFiles List of files to add object detected names to - */ - ObjectDetectedNamesCallback(List resultFiles) { - this.resultFiles = resultFiles; - } - - @Override - public void process(ResultSet rs) { - try { - // Create a temporary map of object ID to ResultFile - Map tempMap = new HashMap<>(); - for (ResultFile file : resultFiles) { - tempMap.put(file.getFirstInstance().getId(), file); - } - - while (rs.next()) { - try { - Long objId = rs.getLong("object_id"); // NON-NLS - String setName = rs.getString("set_name"); // NON-NLS - - tempMap.get(objId).addObjectDetectedName(setName); - - } catch (SQLException ex) { - logger.log(Level.SEVERE, "Unable to get object_id or set_name from result set", ex); // NON-NLS - } - } - } catch (SQLException ex) { - logger.log(Level.SEVERE, "Failed to get object detected names", ex); // NON-NLS - } - } - } - } - - /** - * Key representing an object detected group - */ - private static class ObjectDetectedGroupKey extends GroupKey { - - private final List objectDetectedNames; - private final String objectDetectedNamesString; - - @NbBundle.Messages({ - "FileSearch.ObjectDetectedGroupKey.noSets=None"}) - ObjectDetectedGroupKey(ResultFile file) { - objectDetectedNames = file.getObjectDetectedNames(); - - if (objectDetectedNames.isEmpty()) { - objectDetectedNamesString = Bundle.FileSearch_ObjectDetectedGroupKey_noSets(); - } else { - objectDetectedNamesString = String.join(",", objectDetectedNames); // NON-NLS - } - } - - @Override - String getDisplayName() { - return getObjectDetectedNamesString(); - } - - @Override - public int compareTo(GroupKey otherGroupKey) { - if (otherGroupKey instanceof ObjectDetectedGroupKey) { - ObjectDetectedGroupKey otherObjectDetectedGroupKey = (ObjectDetectedGroupKey) otherGroupKey; - - // Put the empty list at the end - if (this.getObjectDetectedNames().isEmpty()) { - if (otherObjectDetectedGroupKey.getObjectDetectedNames().isEmpty()) { - return 0; - } else { - return 1; - } - } else if (otherObjectDetectedGroupKey.getObjectDetectedNames().isEmpty()) { - return -1; - } - - return getObjectDetectedNamesString().compareTo(otherObjectDetectedGroupKey.getObjectDetectedNamesString()); - } else { - return compareClassNames(otherGroupKey); - } - } - - @Override - public boolean equals(Object otherKey) { - if (otherKey == this) { - return true; - } - - if (!(otherKey instanceof ObjectDetectedGroupKey)) { - return false; - } - - ObjectDetectedGroupKey otherObjectDetectedGroupKey = (ObjectDetectedGroupKey) otherKey; - return getObjectDetectedNamesString().equals(otherObjectDetectedGroupKey.getObjectDetectedNamesString()); - } - - @Override - public int hashCode() { - return Objects.hash(getObjectDetectedNamesString()); - } - - /** - * @return the objectDetectedNames - */ - List getObjectDetectedNames() { - return Collections.unmodifiableList(objectDetectedNames); - } - - /** - * @return the objectDetectedNamesString - */ - String getObjectDetectedNamesString() { - return objectDetectedNamesString; - } - } - - /** - * Attribute for grouping/sorting by tag name - */ - static class FileTagAttribute extends AttributeType { - - @Override - GroupKey getGroupKey(ResultFile file) { - return new FileTagGroupKey(file); - } - - @Override - void addAttributeToResultFiles(List files, SleuthkitCase caseDb, - CentralRepository centralRepoDb) throws FileSearchException { - - try { - for (ResultFile resultFile : files) { - List contentTags = caseDb.getContentTagsByContent(resultFile.getFirstInstance()); - - for (ContentTag tag : contentTags) { - resultFile.addTagName(tag.getName().getDisplayName()); - } - } - } catch (TskCoreException ex) { - throw new FileSearchException("Error looking up file tag attributes", ex); // NON-NLS - } - } - } - - /** - * Represents a key for a specific search for a specific user. - */ - private static class SearchKey implements Comparable { - - private final String keyString; - - /** - * Construct a new SearchKey with all information that defines a search. - * - * @param userName The name of the user performing the search. - * @param filters The FileFilters being used for the search. - * @param groupAttributeType The AttributeType to group by. - * @param groupSortingType The algorithm to sort the groups by. - * @param fileSortingMethod The method to sort the files by. - */ - SearchKey(String userName, List filters, - AttributeType groupAttributeType, - FileGroup.GroupSortingAlgorithm groupSortingType, - FileSorter.SortingMethod fileSortingMethod) { - StringBuilder searchStringBuilder = new StringBuilder(); - searchStringBuilder.append(userName); - for (FileSearchFiltering.FileFilter filter : filters) { - searchStringBuilder.append(filter.toString()); - } - searchStringBuilder.append(groupAttributeType).append(groupSortingType).append(fileSortingMethod); - keyString = searchStringBuilder.toString(); - } - - @Override - public int compareTo(SearchKey otherSearchKey) { - return getKeyString().compareTo(otherSearchKey.getKeyString()); - } - - @Override - public boolean equals(Object otherKey) { - if (otherKey == this) { - return true; - } - - if (!(otherKey instanceof SearchKey)) { - return false; - } - - SearchKey otherSearchKey = (SearchKey) otherKey; - return getKeyString().equals(otherSearchKey.getKeyString()); - } - - @Override - public int hashCode() { - int hash = 5; - hash = 79 * hash + Objects.hashCode(getKeyString()); - return hash; - } - - /** - * @return the keyString - */ - String getKeyString() { - return keyString; - } - } - - /** - * Key representing a file tag group - */ - private static class FileTagGroupKey extends GroupKey { - - private final List tagNames; - private final String tagNamesString; - - @NbBundle.Messages({ - "FileSearch.FileTagGroupKey.noSets=None"}) - FileTagGroupKey(ResultFile file) { - tagNames = file.getTagNames(); - - if (tagNames.isEmpty()) { - tagNamesString = Bundle.FileSearch_FileTagGroupKey_noSets(); - } else { - tagNamesString = String.join(",", tagNames); // NON-NLS - } - } - - @Override - String getDisplayName() { - return getTagNamesString(); - } - - @Override - public int compareTo(GroupKey otherGroupKey) { - if (otherGroupKey instanceof FileTagGroupKey) { - FileTagGroupKey otherFileTagGroupKey = (FileTagGroupKey) otherGroupKey; - - // Put the empty list at the end - if (getTagNames().isEmpty()) { - if (otherFileTagGroupKey.getTagNames().isEmpty()) { - return 0; - } else { - return 1; - } - } else if (otherFileTagGroupKey.getTagNames().isEmpty()) { - return -1; - } - - return getTagNamesString().compareTo(otherFileTagGroupKey.getTagNamesString()); - } else { - return compareClassNames(otherGroupKey); - } - } - - @Override - public boolean equals(Object otherKey) { - if (otherKey == this) { - return true; - } - - if (!(otherKey instanceof FileTagGroupKey)) { - return false; - } - - FileTagGroupKey otherFileTagGroupKey = (FileTagGroupKey) otherKey; - return getTagNamesString().equals(otherFileTagGroupKey.getTagNamesString()); - } - - @Override - public int hashCode() { - return Objects.hash(getTagNamesString()); - } - - /** - * @return the tagNames - */ - List getTagNames() { - return Collections.unmodifiableList(tagNames); - } - - /** - * @return the tagNamesString - */ - String getTagNamesString() { - return tagNamesString; - } - } - - /** - * Default attribute used to make one group - */ - static class NoGroupingAttribute extends AttributeType { - - @Override - GroupKey getGroupKey(ResultFile file) { - return new NoGroupingGroupKey(); - } - } - - /** - * Dummy key for when there is no grouping. All files will have the same - * key. - */ - private static class NoGroupingGroupKey extends GroupKey { - - NoGroupingGroupKey() { - // Nothing to save - all files will get the same GroupKey - } - - @NbBundle.Messages({ - "FileSearch.NoGroupingGroupKey.allFiles=All Files"}) - @Override - String getDisplayName() { - return Bundle.FileSearch_NoGroupingGroupKey_allFiles(); - } - - @Override - public int compareTo(GroupKey otherGroupKey) { - // As long as the other key is the same type, they are equal - if (otherGroupKey instanceof NoGroupingGroupKey) { - return 0; - } else { - return compareClassNames(otherGroupKey); - } - } - - @Override - public boolean equals(Object otherKey) { - if (otherKey == this) { - return true; - } - // As long as the other key is the same type, they are equal - return otherKey instanceof NoGroupingGroupKey; - } - - @Override - public int hashCode() { - return 0; - } - } - - /** - * Enum for the attribute types that can be used for grouping. - */ - @NbBundle.Messages({ - "FileSearch.GroupingAttributeType.fileType.displayName=File Type", - "FileSearch.GroupingAttributeType.frequency.displayName=Past Occurrences", - "FileSearch.GroupingAttributeType.keywordList.displayName=Keyword", - "FileSearch.GroupingAttributeType.size.displayName=File Size", - "FileSearch.GroupingAttributeType.datasource.displayName=Data Source", - "FileSearch.GroupingAttributeType.parent.displayName=Parent Folder", - "FileSearch.GroupingAttributeType.hash.displayName=Hash Set", - "FileSearch.GroupingAttributeType.interestingItem.displayName=Interesting Item", - "FileSearch.GroupingAttributeType.tag.displayName=Tag", - "FileSearch.GroupingAttributeType.object.displayName=Object Detected", - "FileSearch.GroupingAttributeType.none.displayName=None"}) - enum GroupingAttributeType { - FILE_SIZE(new FileSizeAttribute(), Bundle.FileSearch_GroupingAttributeType_size_displayName()), - FREQUENCY(new FrequencyAttribute(), Bundle.FileSearch_GroupingAttributeType_frequency_displayName()), - KEYWORD_LIST_NAME(new KeywordListAttribute(), Bundle.FileSearch_GroupingAttributeType_keywordList_displayName()), - DATA_SOURCE(new DataSourceAttribute(), Bundle.FileSearch_GroupingAttributeType_datasource_displayName()), - PARENT_PATH(new ParentPathAttribute(), Bundle.FileSearch_GroupingAttributeType_parent_displayName()), - HASH_LIST_NAME(new HashHitsAttribute(), Bundle.FileSearch_GroupingAttributeType_hash_displayName()), - INTERESTING_ITEM_SET(new InterestingItemAttribute(), Bundle.FileSearch_GroupingAttributeType_interestingItem_displayName()), - FILE_TAG(new FileTagAttribute(), Bundle.FileSearch_GroupingAttributeType_tag_displayName()), - OBJECT_DETECTED(new ObjectDetectedAttribute(), Bundle.FileSearch_GroupingAttributeType_object_displayName()), - NO_GROUPING(new NoGroupingAttribute(), Bundle.FileSearch_GroupingAttributeType_none_displayName()); - - private final AttributeType attributeType; - private final String displayName; - - GroupingAttributeType(AttributeType attributeType, String displayName) { - this.attributeType = attributeType; - this.displayName = displayName; - } - - @Override - public String toString() { - return displayName; - } - - AttributeType getAttributeType() { - return attributeType; - } - - /** - * Get the list of enums that are valid for grouping images. - * - * @return enums that can be used to group images - */ - static List getOptionsForGrouping() { - return Arrays.asList(FILE_SIZE, FREQUENCY, PARENT_PATH, OBJECT_DETECTED, HASH_LIST_NAME, INTERESTING_ITEM_SET); - } - } -} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/FileSearchData.java b/Core/src/org/sleuthkit/autopsy/discovery/FileSearchData.java deleted file mode 100644 index 81bb2ca641..0000000000 --- a/Core/src/org/sleuthkit/autopsy/discovery/FileSearchData.java +++ /dev/null @@ -1,444 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2019 Basis Technology Corp. - * Contact: carrier sleuthkit org - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.sleuthkit.autopsy.discovery; - -import com.google.common.collect.ImmutableSet; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import org.openide.util.NbBundle; -import org.sleuthkit.autopsy.coreutils.FileTypeUtils; - -/** - * Utility enums for FileSearch. - */ -final class FileSearchData { - - private final static long BYTES_PER_MB = 1000000; - - /** - * Enum representing how often the file occurs in the Central Repository. - */ - @NbBundle.Messages({ - "FileSearchData.Frequency.unique.displayName=Unique (1)", - "FileSearchData.Frequency.rare.displayName=Rare (2-10)", - "FileSearchData.Frequency.common.displayName=Common (11 - 100)", - "FileSearchData.Frequency.verycommon.displayName=Very Common (100+)", - "FileSearchData.Frequency.known.displayName=Known (NSRL)", - "FileSearchData.Frequency.unknown.displayName=Unknown",}) - enum Frequency { - UNIQUE(0, 1, Bundle.FileSearchData_Frequency_unique_displayName()), - RARE(1, 10, Bundle.FileSearchData_Frequency_rare_displayName()), - COMMON(2, 100, Bundle.FileSearchData_Frequency_common_displayName()), - VERY_COMMON(3, 0, Bundle.FileSearchData_Frequency_verycommon_displayName()), - KNOWN(4, 0, Bundle.FileSearchData_Frequency_known_displayName()), - UNKNOWN(5, 0, Bundle.FileSearchData_Frequency_unknown_displayName()); - - private final int ranking; - private final String displayName; - private final int maxOccur; - - Frequency(int ranking, int maxOccur, String displayName) { - this.ranking = ranking; - this.maxOccur = maxOccur; - this.displayName = displayName; - } - - /** - * Get the rank for sorting. - * - * @return the rank (lower should be displayed first) - */ - int getRanking() { - return ranking; - } - - /** - * Get the enum matching the given occurrence count. - * - * @param count Number of times a file is in the Central Repository. - * - * @return the corresponding enum - */ - static Frequency fromCount(long count) { - if (count <= UNIQUE.getMaxOccur()) { - return UNIQUE; - } else if (count <= RARE.getMaxOccur()) { - return RARE; - } else if (count <= COMMON.getMaxOccur()) { - return COMMON; - } - return VERY_COMMON; - } - - /** - * Get the list of enums that are valid for filtering when a CR is - * enabled. - * - * @return enums that can be used to filter with a CR. - */ - static List getOptionsForFilteringWithCr() { - return Arrays.asList(UNIQUE, RARE, COMMON, VERY_COMMON, KNOWN); - } - - /** - * Get the list of enums that are valid for filtering when no CR is - * enabled. - * - * @return enums that can be used to filter without a CR. - */ - static List getOptionsForFilteringWithoutCr() { - return Arrays.asList(KNOWN, UNKNOWN); - } - - @Override - public String toString() { - return displayName; - } - - /** - * @return the maxOccur - */ - int getMaxOccur() { - return maxOccur; - } - } - - /** - * Enum representing the file size - */ - @NbBundle.Messages({ - "FileSearchData.FileSize.XXLARGE.displayName=XXLarge", - "FileSearchData.FileSize.XLARGE.displayName=XLarge", - "FileSearchData.FileSize.LARGE.displayName=Large", - "FileSearchData.FileSize.MEDIUM.displayName=Medium", - "FileSearchData.FileSize.SMALL.displayName=Small", - "FileSearchData.FileSize.XSMALL.displayName=XSmall", - "FileSearchData.FileSize.10PlusGb=: 10GB+", - "FileSearchData.FileSize.5gbto10gb=: 5-10GB", - "FileSearchData.FileSize.1gbto5gb=: 1-5GB", - "FileSearchData.FileSize.100mbto1gb=: 100MB-1GB", - "FileSearchData.FileSize.200PlusMb=: 200MB+", - "FileSearchData.FileSize.50mbto200mb=: 50-200MB", - "FileSearchData.FileSize.500kbto100mb=: 500KB-100MB", - "FileSearchData.FileSize.1mbto50mb=: 1-50MB", - "FileSearchData.FileSize.100kbto1mb=: 100KB-1MB", - "FileSearchData.FileSize.16kbto100kb=: 16-100KB", - "FileSearchData.FileSize.upTo500kb=: 0-500KB", - "FileSearchData.FileSize.upTo16kb=: 0-16KB",}) - enum FileSize { - XXLARGE_VIDEO(0, 10000 * BYTES_PER_MB, -1, Bundle.FileSearchData_FileSize_XXLARGE_displayName(), Bundle.FileSearchData_FileSize_10PlusGb()), - XLARGE_VIDEO(1, 5000 * BYTES_PER_MB, 10000 * BYTES_PER_MB, Bundle.FileSearchData_FileSize_XLARGE_displayName(), Bundle.FileSearchData_FileSize_5gbto10gb()), - LARGE_VIDEO(2, 1000 * BYTES_PER_MB, 5000 * BYTES_PER_MB, Bundle.FileSearchData_FileSize_LARGE_displayName(), Bundle.FileSearchData_FileSize_1gbto5gb()), - MEDIUM_VIDEO(3, 100 * BYTES_PER_MB, 1000 * BYTES_PER_MB, Bundle.FileSearchData_FileSize_MEDIUM_displayName(), Bundle.FileSearchData_FileSize_100mbto1gb()), - SMALL_VIDEO(4, 500000, 100 * BYTES_PER_MB, Bundle.FileSearchData_FileSize_SMALL_displayName(), Bundle.FileSearchData_FileSize_500kbto100mb()), - XSMALL_VIDEO(5, 0, 500000, Bundle.FileSearchData_FileSize_XSMALL_displayName(), Bundle.FileSearchData_FileSize_upTo500kb()), - XXLARGE_IMAGE(6, 200 * BYTES_PER_MB, -1, Bundle.FileSearchData_FileSize_XXLARGE_displayName(), Bundle.FileSearchData_FileSize_200PlusMb()), - XLARGE_IMAGE(7, 50 * BYTES_PER_MB, 200 * BYTES_PER_MB, Bundle.FileSearchData_FileSize_XLARGE_displayName(), Bundle.FileSearchData_FileSize_50mbto200mb()), - LARGE_IMAGE(8, 1 * BYTES_PER_MB, 50 * BYTES_PER_MB, Bundle.FileSearchData_FileSize_LARGE_displayName(), Bundle.FileSearchData_FileSize_1mbto50mb()), - MEDIUM_IMAGE(9, 100000, 1 * BYTES_PER_MB, Bundle.FileSearchData_FileSize_MEDIUM_displayName(), Bundle.FileSearchData_FileSize_100kbto1mb()), - SMALL_IMAGE(10, 16000, 100000, Bundle.FileSearchData_FileSize_SMALL_displayName(), Bundle.FileSearchData_FileSize_16kbto100kb()), - XSMALL_IMAGE(11, 0, 16000, Bundle.FileSearchData_FileSize_XSMALL_displayName(), Bundle.FileSearchData_FileSize_upTo16kb()); - - private final int ranking; // Must be unique for each value - private final long minBytes; // Note that the size must be strictly greater than this to match - private final long maxBytes; - private final String sizeGroup; - private final String displaySize; - final static long NO_MAXIMUM = -1; - - FileSize(int ranking, long minB, long maxB, String displayName, String displaySize) { - this.ranking = ranking; - this.minBytes = minB; - if (maxB >= 0) { - this.maxBytes = maxB; - } else { - this.maxBytes = NO_MAXIMUM; - } - this.sizeGroup = displayName; - this.displaySize = displaySize; - } - - /** - * Get the enum corresponding to the given file size for image files. - * The file size must be strictly greater than minBytes. - * - * @param size the file size - * - * @return the enum whose range contains the file size - */ - static FileSize fromImageSize(long size) { - if (size > XXLARGE_IMAGE.getMinBytes()) { - return XXLARGE_IMAGE; - } else if (size > XLARGE_IMAGE.getMinBytes()) { - return XLARGE_IMAGE; - } else if (size > LARGE_IMAGE.getMinBytes()) { - return LARGE_IMAGE; - } else if (size > MEDIUM_IMAGE.getMinBytes()) { - return MEDIUM_IMAGE; - } else if (size > SMALL_IMAGE.getMinBytes()) { - return SMALL_IMAGE; - } else { - return XSMALL_IMAGE; - } - } - - /** - * Get the enum corresponding to the given file size for video files. - * The file size must be strictly greater than minBytes. - * - * @param size the file size - * - * @return the enum whose range contains the file size - */ - static FileSize fromVideoSize(long size) { - if (size > XXLARGE_VIDEO.getMinBytes()) { - return XXLARGE_VIDEO; - } else if (size > XLARGE_VIDEO.getMinBytes()) { - return XLARGE_VIDEO; - } else if (size > LARGE_VIDEO.getMinBytes()) { - return LARGE_VIDEO; - } else if (size > MEDIUM_VIDEO.getMinBytes()) { - return MEDIUM_VIDEO; - } else if (size > SMALL_VIDEO.getMinBytes()) { - return SMALL_VIDEO; - } else { - return XSMALL_VIDEO; - } - } - - /** - * Get the upper limit of the range. - * - * @return the maximum file size that will fit in this range. - */ - long getMaxBytes() { - return maxBytes; - } - - /** - * Get the lower limit of the range. - * - * @return the maximum file size that is not part of this range - */ - long getMinBytes() { - return minBytes; - } - - /** - * Get the rank for sorting. - * - * @return the rank (lower should be displayed first) - */ - int getRanking() { - return ranking; - } - - @Override - public String toString() { - return sizeGroup + displaySize; - } - - String getSizeGroup(){ - return sizeGroup; - } - - /** - * Get the list of enums that are valid for most file sizes. - * - * @return Enums that can be used to filter most file including images - * by size. - */ - static List getDefaultSizeOptions() { - return Arrays.asList(XXLARGE_IMAGE, XLARGE_IMAGE, LARGE_IMAGE, MEDIUM_IMAGE, SMALL_IMAGE, XSMALL_IMAGE); - } - - /** - * Get the list of enums that are valid for video sizes. - * - * @return enums that can be used to filter videos by size. - */ - static List getOptionsForVideos() { - return Arrays.asList(XXLARGE_VIDEO, XLARGE_VIDEO, LARGE_VIDEO, MEDIUM_VIDEO, SMALL_VIDEO, XSMALL_VIDEO); - } - } - - //Discovery uses a different list of document mime types than FileTypeUtils.FileTypeCategory.DOCUMENTS - private static final ImmutableSet DOCUMENT_MIME_TYPES - = new ImmutableSet.Builder() - .add("text/html", //NON-NLS - "text/csv", //NON-NLS - "application/rtf", //NON-NLS - "application/pdf", //NON-NLS - "application/xhtml+xml", //NON-NLS - "application/x-msoffice", //NON-NLS - "application/msword", //NON-NLS - "application/msword2", //NON-NLS - "application/vnd.wordperfect", //NON-NLS - "application/vnd.openxmlformats-officedocument.wordprocessingml.document", //NON-NLS - "application/vnd.ms-powerpoint", //NON-NLS - "application/vnd.openxmlformats-officedocument.presentationml.presentation", //NON-NLS - "application/vnd.ms-excel", //NON-NLS - "application/vnd.ms-excel.sheet.4", //NON-NLS - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", //NON-NLS - "application/vnd.oasis.opendocument.presentation", //NON-NLS - "application/vnd.oasis.opendocument.spreadsheet", //NON-NLS - "application/vnd.oasis.opendocument.text" //NON-NLS - ).build(); - - private static final ImmutableSet IMAGE_UNSUPPORTED_DOC_TYPES - = new ImmutableSet.Builder() - .add("application/pdf", //NON-NLS - "application/xhtml+xml").build(); //NON-NLS - - static Collection getDocTypesWithoutImageExtraction() { - return Collections.unmodifiableCollection(IMAGE_UNSUPPORTED_DOC_TYPES); - } - - /** - * Enum representing the file type. We don't simply use - * FileTypeUtils.FileTypeCategory because: - Some file types categories - * overlap - It is convenient to have the "OTHER" option for files that - * don't match the given types - */ - @NbBundle.Messages({ - "FileSearchData.FileType.Audio.displayName=Audio", - "FileSearchData.FileType.Video.displayName=Video", - "FileSearchData.FileType.Image.displayName=Image", - "FileSearchData.FileType.Documents.displayName=Documents", - "FileSearchData.FileType.Executables.displayName=Executables", - "FileSearchData.FileType.Other.displayName=Other/Unknown"}) - enum FileType { - - IMAGE(0, Bundle.FileSearchData_FileType_Image_displayName(), FileTypeUtils.FileTypeCategory.IMAGE.getMediaTypes()), - AUDIO(1, Bundle.FileSearchData_FileType_Audio_displayName(), FileTypeUtils.FileTypeCategory.AUDIO.getMediaTypes()), - VIDEO(2, Bundle.FileSearchData_FileType_Video_displayName(), FileTypeUtils.FileTypeCategory.VIDEO.getMediaTypes()), - EXECUTABLE(3, Bundle.FileSearchData_FileType_Executables_displayName(), FileTypeUtils.FileTypeCategory.EXECUTABLE.getMediaTypes()), - DOCUMENTS(4, Bundle.FileSearchData_FileType_Documents_displayName(), DOCUMENT_MIME_TYPES), - OTHER(5, Bundle.FileSearchData_FileType_Other_displayName(), new ArrayList<>()); - - private final int ranking; // For ordering in the UI - private final String displayName; - private final Collection mediaTypes; - - FileType(int value, String displayName, Collection mediaTypes) { - this.ranking = value; - this.displayName = displayName; - this.mediaTypes = mediaTypes; - } - - /** - * Get the MIME types matching this category. - * - * @return Collection of MIME type strings - */ - Collection getMediaTypes() { - return Collections.unmodifiableCollection(mediaTypes); - } - - @Override - public String toString() { - return displayName; - } - - /** - * Get the rank for sorting. - * - * @return the rank (lower should be displayed first) - */ - int getRanking() { - return ranking; - } - - /** - * Get the enum matching the given MIME type. - * - * @param mimeType The MIME type for the file - * - * @return the corresponding enum (will be OTHER if no types matched) - */ - static FileType fromMIMEtype(String mimeType) { - for (FileType type : FileType.values()) { - if (type.getMediaTypes().contains(mimeType)) { - return type; - } - } - return OTHER; - } - - /** - * Get the list of enums that are valid for filtering. - * - * @return enums that can be used to filter - */ - static List getOptionsForFiltering() { - return Arrays.asList(IMAGE, VIDEO); - } - } - - /** - * Enum representing the score of the file. - */ - @NbBundle.Messages({ - "FileSearchData.Score.notable.displayName=Notable", - "FileSearchData.Score.interesting.displayName=Interesting", - "FileSearchData.Score.unknown.displayName=Unknown",}) - enum Score { - NOTABLE(0, Bundle.FileSearchData_Score_notable_displayName()), - INTERESTING(1, Bundle.FileSearchData_Score_interesting_displayName()), - UNKNOWN(2, Bundle.FileSearchData_Score_unknown_displayName()); - - private final int ranking; - private final String displayName; - - Score(int ranking, String displayName) { - this.ranking = ranking; - this.displayName = displayName; - } - - /** - * Get the rank for sorting. - * - * @return the rank (lower should be displayed first) - */ - int getRanking() { - return ranking; - } - - /** - * Get the list of enums that are valid for filtering. - * - * @return enums that can be used to filter - */ - static List getOptionsForFiltering() { - return Arrays.asList(NOTABLE, INTERESTING); - } - - @Override - public String toString() { - return displayName; - } - } - - private FileSearchData() { - // Class should not be instantiated - } -} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/FileSorter.java b/Core/src/org/sleuthkit/autopsy/discovery/FileSorter.java deleted file mode 100644 index fc74dd5713..0000000000 --- a/Core/src/org/sleuthkit/autopsy/discovery/FileSorter.java +++ /dev/null @@ -1,292 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2019 Basis Technology Corp. - * Contact: carrier sleuthkit org - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.sleuthkit.autopsy.discovery; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import org.openide.util.NbBundle; -import org.sleuthkit.datamodel.TskCoreException; - -/** - * Class used to sort ResultFiles using the supplied method. - */ -class FileSorter implements Comparator { - - private final List> comparators = new ArrayList<>(); - - /** - * Set up the sorter using the supplied sorting method. The sorting is - * defined by a list of ResultFile comparators. These comparators will be - * run in order until one returns a non-zero result. - * - * @param method The method that should be used to sort the files - */ - FileSorter(SortingMethod method) { - - // Set up the primary comparators that should applied to the files - switch (method) { - case BY_DATA_SOURCE: - comparators.add(getDataSourceComparator()); - break; - case BY_FILE_SIZE: - comparators.add(getFileSizeComparator()); - break; - case BY_FILE_TYPE: - comparators.add(getFileTypeComparator()); - comparators.add(getMIMETypeComparator()); - break; - case BY_FREQUENCY: - comparators.add(getFrequencyComparator()); - break; - case BY_KEYWORD_LIST_NAMES: - comparators.add(getKeywordListNameComparator()); - break; - case BY_FULL_PATH: - comparators.add(getParentPathComparator()); - break; - case BY_FILE_NAME: - comparators.add(getFileNameComparator()); - break; - default: - // The default comparator will be added afterward - break; - } - - // Add the default comparator to the end. This will ensure a consistent sort - // order regardless of the order the files were added to the list. - comparators.add(getDefaultComparator()); - } - - @Override - public int compare(ResultFile file1, ResultFile file2) { - - int result = 0; - for (Comparator comp : comparators) { - result = comp.compare(file1, file2); - if (result != 0) { - return result; - } - } - - // The files are the same - return result; - } - - /** - * Compare files using data source ID. Will order smallest to largest. - * - * @return -1 if file1 has the lower data source ID, 0 if equal, 1 otherwise - */ - private static Comparator getDataSourceComparator() { - return (ResultFile file1, ResultFile file2) -> Long.compare(file1.getFirstInstance().getDataSourceObjectId(), file2.getFirstInstance().getDataSourceObjectId()); - } - - /** - * Compare files using their FileType enum. Orders based on the ranking in - * the FileType enum. - * - * @return -1 if file1 has the lower FileType value, 0 if equal, 1 otherwise - */ - private static Comparator getFileTypeComparator() { - return (ResultFile file1, ResultFile file2) -> Integer.compare(file1.getFileType().getRanking(), file2.getFileType().getRanking()); - } - - /** - * Compare files using a concatenated version of keyword list names. - * Alphabetical by the list names with files with no keyword list hits going - * last. - * - * @return -1 if file1 has the earliest combined keyword list name, 0 if - * equal, 1 otherwise - */ - private static Comparator getKeywordListNameComparator() { - return (ResultFile file1, ResultFile file2) -> { - // Put empty lists at the bottom - if (file1.getKeywordListNames().isEmpty()) { - if (file2.getKeywordListNames().isEmpty()) { - return 0; - } - return 1; - } else if (file2.getKeywordListNames().isEmpty()) { - return -1; - } - - String list1 = String.join(",", file1.getKeywordListNames()); - String list2 = String.join(",", file2.getKeywordListNames()); - return compareStrings(list1, list2); - }; - } - - /** - * Compare files based on parent path. Order alphabetically. - * - * @return -1 if file1's path comes first alphabetically, 0 if equal, 1 - * otherwise - */ - private static Comparator getParentPathComparator() { - - return new Comparator() { - @Override - public int compare(ResultFile file1, ResultFile file2) { - String file1ParentPath; - try { - file1ParentPath = file1.getFirstInstance().getParent().getUniquePath(); - } catch (TskCoreException ingored) { - file1ParentPath = file1.getFirstInstance().getParentPath(); - } - String file2ParentPath; - try { - file2ParentPath = file2.getFirstInstance().getParent().getUniquePath(); - } catch (TskCoreException ingored) { - file2ParentPath = file2.getFirstInstance().getParentPath(); - } - return compareStrings(file1ParentPath.toLowerCase(), file2ParentPath.toLowerCase()); - } - }; - } - - /** - * Compare files based on number of occurrences in the central repository. - * Order from most rare to least rare Frequency enum. - * - * @return -1 if file1's rarity is lower than file2, 0 if equal, 1 otherwise - */ - private static Comparator getFrequencyComparator() { - return (ResultFile file1, ResultFile file2) -> Integer.compare(file1.getFrequency().getRanking(), file2.getFrequency().getRanking()); - } - - /** - * Compare files based on MIME type. Order is alphabetical. - * - * @return -1 if file1's MIME type comes before file2's, 0 if equal, 1 - * otherwise - */ - private static Comparator getMIMETypeComparator() { - return (ResultFile file1, ResultFile file2) -> compareStrings(file1.getFirstInstance().getMIMEType(), file2.getFirstInstance().getMIMEType()); - } - - /** - * Compare files based on size. Order large to small. - * - * @return -1 if file1 is larger than file2, 0 if equal, 1 otherwise - */ - private static Comparator getFileSizeComparator() { - return (ResultFile file1, ResultFile file2) -> -1 * Long.compare(file1.getFirstInstance().getSize(), file2.getFirstInstance().getSize()) // Sort large to small - ; - } - - /** - * Compare files based on file name. Order alphabetically. - * - * @return -1 if file1 comes before file2, 0 if equal, 1 otherwise - */ - private static Comparator getFileNameComparator() { - return (ResultFile file1, ResultFile file2) -> compareStrings(file1.getFirstInstance().getName().toLowerCase(), file2.getFirstInstance().getName().toLowerCase()); - } - - /** - * A final default comparison between two ResultFile objects. Currently this - * is on file name and then object ID. It can be changed but should always - * include something like the object ID to ensure a consistent sorting when - * the rest of the compared fields are the same. - * - * @return -1 if file1 comes before file2, 0 if equal, 1 otherwise - */ - private static Comparator getDefaultComparator() { - return (ResultFile file1, ResultFile file2) -> { - // Compare file names and then object ID (to ensure a consistent sort) - int result = getFileNameComparator().compare(file1, file2); - if (result == 0) { - return Long.compare(file1.getFirstInstance().getId(), file2.getFirstInstance().getId()); - } - return result; - }; - } - - /** - * Compare two strings alphabetically. Nulls are allowed. - * - * @param s1 - * @param s2 - * - * @return -1 if s1 comes before s2, 0 if equal, 1 otherwise - */ - private static int compareStrings(String s1, String s2) { - String string1 = s1 == null ? "" : s1; - String string2 = s2 == null ? "" : s2; - return string1.compareTo(string2); - } - - /** - * Enum for selecting the primary method for sorting result files. - */ - @NbBundle.Messages({ - "FileSorter.SortingMethod.datasource.displayName=Data Source", - "FileSorter.SortingMethod.filename.displayName=File Name", - "FileSorter.SortingMethod.filesize.displayName=File Size", - "FileSorter.SortingMethod.filetype.displayName=File Type", - "FileSorter.SortingMethod.frequency.displayName=Central Repo Frequency", - "FileSorter.SortingMethod.keywordlist.displayName=Keyword List Names", - "FileSorter.SortingMethod.fullPath.displayName=Full Path"}) - enum SortingMethod { - BY_FILE_NAME(new ArrayList<>(), - Bundle.FileSorter_SortingMethod_filename_displayName()), // Sort alphabetically by file name - BY_DATA_SOURCE(new ArrayList<>(), - Bundle.FileSorter_SortingMethod_datasource_displayName()), // Sort in increasing order of data source ID - BY_FILE_SIZE(new ArrayList<>(), - Bundle.FileSorter_SortingMethod_filesize_displayName()), // Sort in decreasing order of size - BY_FILE_TYPE(Arrays.asList(new FileSearch.FileTypeAttribute()), - Bundle.FileSorter_SortingMethod_filetype_displayName()), // Sort in order of file type (defined in FileType enum), with secondary sort on MIME type - BY_FREQUENCY(Arrays.asList(new FileSearch.FrequencyAttribute()), - Bundle.FileSorter_SortingMethod_frequency_displayName()), // Sort by decreasing rarity in the central repository - BY_KEYWORD_LIST_NAMES(Arrays.asList(new FileSearch.KeywordListAttribute()), - Bundle.FileSorter_SortingMethod_keywordlist_displayName()), // Sort alphabetically by list of keyword list names found - BY_FULL_PATH(new ArrayList<>(), - Bundle.FileSorter_SortingMethod_fullPath_displayName()); // Sort alphabetically by path - - private final String displayName; - private final List requiredAttributes; - - SortingMethod(List attributes, String displayName) { - this.requiredAttributes = attributes; - this.displayName = displayName; - } - - @Override - public String toString() { - return displayName; - } - - List getRequiredAttributes() { - return Collections.unmodifiableList(requiredAttributes); - } - - /** - * Get the list of enums that are valid for ordering images. - * - * @return enums that can be used to ordering images. - */ - static List getOptionsForOrdering() { - return Arrays.asList(BY_FILE_SIZE, BY_FULL_PATH, BY_FILE_NAME, BY_DATA_SOURCE); - } - } -} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/search/AbstractFilter.java b/Core/src/org/sleuthkit/autopsy/discovery/search/AbstractFilter.java new file mode 100644 index 0000000000..bb2e258f2e --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/AbstractFilter.java @@ -0,0 +1,74 @@ +/* + * Autopsy + * + * Copyright 2020 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.discovery.search; + +import java.util.ArrayList; +import java.util.List; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; +import org.sleuthkit.datamodel.SleuthkitCase; + +/** + * Base class for the filters. + */ +public abstract class AbstractFilter { + + /** + * Returns part of a query on the table that can be AND-ed with other pieces + * + * @return the SQL query or an empty string if there is no SQL query for + * this filter. + */ + public abstract String getWhereClause(); + + /** + * Indicates whether this filter needs to use the secondary, non-SQL method + * applyAlternateFilter(). + * + * @return false by default + */ + public boolean useAlternateFilter() { + return false; + } + + /** + * Run a secondary filter that does not operate on table. + * + * @param currentResults The current list of matching results; empty if no + * filters have yet been run. + * @param caseDb The case database + * @param centralRepoDb The central repo database. Can be null if the + * filter does not require it. + * + * @return The list of results that match this filter (and any that came + * before it) + * + * @throws DiscoveryException + */ + public List applyAlternateFilter(List currentResults, SleuthkitCase caseDb, + CentralRepository centralRepoDb) throws DiscoveryException { + return new ArrayList<>(); + } + + /** + * Get a description of the selected filter. + * + * @return A description of the filter + */ + public abstract String getDesc(); +} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/search/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/discovery/search/Bundle.properties-MERGED new file mode 100644 index 0000000000..037868e838 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/Bundle.properties-MERGED @@ -0,0 +1,132 @@ +DiscoveryAttributes.GroupingAttributeType.datasource.displayName=Data Source +DiscoveryAttributes.GroupingAttributeType.fileType.displayName=File Type +DiscoveryAttributes.GroupingAttributeType.firstDate.displayName=First Activity Date +DiscoveryAttributes.GroupingAttributeType.frequency.displayName=Past Occurrences +DiscoveryAttributes.GroupingAttributeType.hash.displayName=Hash Set +DiscoveryAttributes.GroupingAttributeType.interestingItem.displayName=Interesting Item +DiscoveryAttributes.GroupingAttributeType.keywordList.displayName=Keyword +DiscoveryAttributes.GroupingAttributeType.mostRecentDate.displayName=Most Recent Activity Date +DiscoveryAttributes.GroupingAttributeType.none.displayName=None +DiscoveryAttributes.GroupingAttributeType.numberOfVisits.displayName=Number of Visits +DiscoveryAttributes.GroupingAttributeType.object.displayName=Object Detected +DiscoveryAttributes.GroupingAttributeType.parent.displayName=Parent Folder +DiscoveryAttributes.GroupingAttributeType.size.displayName=File Size +DiscoveryAttributes.GroupingAttributeType.tag.displayName=Tag +# {0} - Data source name +# {1} - Data source ID +DiscoveryKeyUtils.DataSourceGroupKey.datasourceAndID={0}(ID: {1}) +# {0} - Data source ID +DiscoveryKeyUtils.DataSourceGroupKey.idOnly=Data source (ID: {0}) +DiscoveryKeyUtils.FileTagGroupKey.noSets=None +DiscoveryKeyUtils.FirstActivityDateGroupKey.noDate=No Date Available +DiscoveryKeyUtils.HashHitsGroupKey.noHashHits=None +DiscoveryKeyUtils.InterestingItemGroupKey.noSets=None +DiscoveryKeyUtils.KeywordListGroupKey.noKeywords=None +DiscoveryKeyUtils.MostRecentActivityDateGroupKey.noDate=No Date Available +DiscoveryKeyUtils.NoGroupingGroupKey.allFiles=All Files +# {0} - totalVisits +DiscoveryKeyUtils.NumberOfVisitsGroupKey.displayName={0} visits +DiscoveryKeyUtils.NumberOfVisitsGroupKey.noVisits=No visits +DiscoveryKeyUtils.ObjectDetectedGroupKey.noSets=None +FileGroup.groupSortingAlgorithm.groupName.text=Group Name +FileGroup.groupSortingAlgorithm.groupSize.text=Group Size +FileSearch.documentSummary.noBytes=No bytes read for document, unable to display preview. +FileSearch.documentSummary.noPreview=No preview available. +FileSearchFiltering.concatenateSetNamesForDisplay.comma=, +# {0} - filters +FileSearchFiltering.HashSetFilter.desc=Hash set hits in set(s): {0} +FileSearchFiltering.KnownFilter.desc=which are not known +FileSearchFiltering.PreviouslyNotableFilter.desc=that were previously marked as notable +# {0} - tag names +FileSearchFiltering.TagsFilter.desc=Tagged {0} +FileSearchFiltering.TagsFilter.or=, +FileSearchFiltering.UserCreatedFilter.desc=that contain EXIF data +FileSorter.SortingMethod.datasource.displayName=Data Source +FileSorter.SortingMethod.domain.displayName=Domain +FileSorter.SortingMethod.filename.displayName=File Name +FileSorter.SortingMethod.filesize.displayName=File Size +FileSorter.SortingMethod.filetype.displayName=File Type +FileSorter.SortingMethod.frequency.displayName=Central Repo Frequency +FileSorter.SortingMethod.fullPath.displayName=Full Path +FileSorter.SortingMethod.keywordlist.displayName=Keyword List Names +ResultFile.score.interestingResult.description=At least one instance of the file has an interesting result associated with it. +ResultFile.score.notableFile.description=At least one instance of the file was recognized as notable. +ResultFile.score.notableTaggedFile.description=At least one instance of the file is tagged with a notable tag. +ResultFile.score.taggedFile.description=At least one instance of the file has been tagged. +SearchData.AttributeType.Domain.displayName=Domain +SearchData.FileSize.100kbto1mb=: 100KB-1MB +SearchData.FileSize.100mbto1gb=: 100MB-1GB +SearchData.FileSize.10PlusGb=: 10GB+ +SearchData.FileSize.16kbto100kb=: 16-100KB +SearchData.FileSize.1gbto5gb=: 1-5GB +SearchData.FileSize.1mbto50mb=: 1-50MB +SearchData.FileSize.200PlusMb=: 200MB+ +SearchData.FileSize.500kbto100mb=: 500KB-100MB +SearchData.FileSize.50mbto200mb=: 50-200MB +SearchData.FileSize.5gbto10gb=: 5-10GB +SearchData.FileSize.LARGE.displayName=Large +SearchData.FileSize.MEDIUM.displayName=Medium +SearchData.FileSize.SMALL.displayName=Small +SearchData.FileSize.upTo16kb=: 0-16KB +SearchData.FileSize.upTo500kb=: 0-500KB +SearchData.FileSize.XLARGE.displayName=XLarge +SearchData.FileSize.XSMALL.displayName=XSmall +SearchData.FileSize.XXLARGE.displayName=XXLarge +SearchData.FileType.Audio.displayName=Audio +SearchData.FileType.Documents.displayName=Document +SearchData.FileType.Executables.displayName=Executable +SearchData.FileType.Image.displayName=Image +SearchData.FileType.Other.displayName=Other/Unknown +SearchData.FileType.Video.displayName=Video +SearchData.Frequency.common.displayName=Common (11 - 100) +SearchData.Frequency.known.displayName=Known (NSRL) +SearchData.Frequency.rare.displayName=Rare (2-10) +SearchData.Frequency.unique.displayName=Unique (1) +SearchData.Frequency.unknown.displayName=Unknown +SearchData.Frequency.verycommon.displayName=Very Common (100+) +SearchData.Score.interesting.displayName=Interesting +SearchData.Score.notable.displayName=Notable +SearchData.Score.unknown.displayName=Unknown +# {0} - artifactTypes +SearchFiltering.artifactTypeFilter.desc=Result type(s): {0} +SearchFiltering.artifactTypeFilter.or=, +# {0} - Data source name +# {1} - Data source ID +SearchFiltering.DataSourceFilter.datasource={0}({1}) +# {0} - filters +SearchFiltering.DataSourceFilter.desc=Data source(s): {0} +SearchFiltering.DataSourceFilter.or=, +# {0} - startDate +SearchFiltering.dateRangeFilter.after=after: {0} +SearchFiltering.dateRangeFilter.and=\ and +# {0} - endDate +SearchFiltering.dateRangeFilter.before=before: {0} +SearchFiltering.dateRangeFilter.lable=Activity date +# {0} - filters +SearchFiltering.FileTypeFilter.desc=Type: {0} +SearchFiltering.FileTypeFilter.or=, +# {0} - filters +SearchFiltering.FrequencyFilter.desc=Past occurrences: {0} +SearchFiltering.FrequencyFilter.or=, +# {0} - filters +SearchFiltering.InterestingItemSetFilter.desc=Interesting item hits in set(s): {0} +# {0} - filters +SearchFiltering.KeywordListFilter.desc=Keywords in list(s): {0} +# {0} - filters +SearchFiltering.ObjectDetectionFilter.desc=Objects detected in set(s): {0} +# {0} - filters +SearchFiltering.ParentFilter.desc=Paths matching: {0} +SearchFiltering.ParentFilter.exact=(exact match) +SearchFiltering.ParentFilter.excluded=(excluded) +SearchFiltering.ParentFilter.included=(included) +SearchFiltering.ParentFilter.or=, +SearchFiltering.ParentFilter.substring=(substring) +SearchFiltering.ParentSearchTerm.excludeString=\ (exclude) +SearchFiltering.ParentSearchTerm.fullString=\ (exact) +SearchFiltering.ParentSearchTerm.includeString=\ (include) +SearchFiltering.ParentSearchTerm.subString=\ (substring) +# {0} - filters +SearchFiltering.ScoreFilter.desc=Score(s) of : {0} +# {0} - filters +SearchFiltering.SizeFilter.desc=Size(s): {0} +SearchFiltering.SizeFilter.or=, diff --git a/Core/src/org/sleuthkit/autopsy/discovery/search/DiscoveryAttributes.java b/Core/src/org/sleuthkit/autopsy/discovery/search/DiscoveryAttributes.java new file mode 100644 index 0000000000..0153fe8e65 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/DiscoveryAttributes.java @@ -0,0 +1,892 @@ +/* + * Autopsy + * + * Copyright 2020 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.discovery.search; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoDbUtil; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoException; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeNormalizationException; +import org.sleuthkit.autopsy.centralrepository.datamodel.InstanceTableCallback; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.CaseDbAccessManager; +import org.sleuthkit.datamodel.ContentTag; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.TskData; +import java.util.StringJoiner; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeNormalizer; + +/** + * Class which contains the search attributes which can be specified for + * Discovery. + */ +public class DiscoveryAttributes { + + private final static Logger logger = Logger.getLogger(DiscoveryAttributes.class.getName()); + + /** + * Base class for the grouping attributes. + */ + public abstract static class AttributeType { + + /** + * For a given Result, return the key for the group it belongs to for + * this attribute type. + * + * @param result The result to be grouped. + * + * @return The key for the group this result goes in. + */ + public abstract DiscoveryKeyUtils.GroupKey getGroupKey(Result result); + + /** + * Add any extra data to the ResultFile object from this attribute. + * + * @param results The list of results to enhance. + * @param caseDb The case database. + * @param centralRepoDb The central repository database. Can be null if + * not needed. + * + * @throws DiscoveryException + */ + public void addAttributeToResults(List results, SleuthkitCase caseDb, CentralRepository centralRepoDb) throws DiscoveryException { + // Default is to do nothing + } + } + + /** + * Attribute for grouping/sorting by file size. + */ + public static class FileSizeAttribute extends AttributeType { + + @Override + public DiscoveryKeyUtils.GroupKey getGroupKey(Result result) { + return new DiscoveryKeyUtils.FileSizeGroupKey(result); + } + } + + /** + * Attribute for grouping/sorting by parent path. + */ + public static class ParentPathAttribute extends AttributeType { + + @Override + public DiscoveryKeyUtils.GroupKey getGroupKey(Result file) { + return new DiscoveryKeyUtils.ParentPathGroupKey((ResultFile) file); + } + } + + /** + * Default attribute used to make one group. + */ + static class NoGroupingAttribute extends AttributeType { + + @Override + public DiscoveryKeyUtils.GroupKey getGroupKey(Result result) { + return new DiscoveryKeyUtils.NoGroupingGroupKey(); + } + } + + /** + * Attribute for grouping/sorting by data source. + */ + static class DataSourceAttribute extends AttributeType { + + @Override + public DiscoveryKeyUtils.GroupKey getGroupKey(Result result) { + return new DiscoveryKeyUtils.DataSourceGroupKey(result); + } + } + + /** + * Attribute for grouping/sorting by file type. + */ + static class FileTypeAttribute extends AttributeType { + + @Override + public DiscoveryKeyUtils.GroupKey getGroupKey(Result file) { + return new DiscoveryKeyUtils.FileTypeGroupKey(file); + } + } + + /** + * Attribute for grouping/sorting by keyword lists. + */ + static class KeywordListAttribute extends AttributeType { + + @Override + public DiscoveryKeyUtils.GroupKey getGroupKey(Result file) { + return new DiscoveryKeyUtils.KeywordListGroupKey((ResultFile) file); + } + + @Override + public void addAttributeToResults(List results, SleuthkitCase caseDb, + CentralRepository centralRepoDb) throws DiscoveryException { + + // Get pairs of (object ID, keyword list name) for all files in the list of files that have + // keyword list hits. + String selectQuery = createSetNameClause(results, BlackboardArtifact.ARTIFACT_TYPE.TSK_KEYWORD_HIT.getTypeID(), + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID()); + + SetKeywordListNamesCallback callback = new SetKeywordListNamesCallback(results); + try { + caseDb.getCaseDbAccessManager().select(selectQuery, callback); + } catch (TskCoreException ex) { + throw new DiscoveryException("Error looking up keyword list attributes", ex); // NON-NLS + } + } + + /** + * Callback to process the results of the CaseDbAccessManager select + * query. Will add the keyword list names to the list of ResultFile + * objects. + */ + private static class SetKeywordListNamesCallback implements CaseDbAccessManager.CaseDbAccessQueryCallback { + + List resultFiles; + + /** + * Create the callback. + * + * @param resultFiles List of files to add keyword list names to. + */ + SetKeywordListNamesCallback(List resultFiles) { + this.resultFiles = resultFiles; + } + + @Override + public void process(ResultSet rs) { + try { + // Create a temporary map of object ID to ResultFile + Map tempMap = new HashMap<>(); + for (Result result : resultFiles) { + if (result.getType() == SearchData.Type.DOMAIN) { + break; + } + ResultFile file = (ResultFile) result; + tempMap.put(file.getFirstInstance().getId(), file); + } + + while (rs.next()) { + try { + Long objId = rs.getLong("object_id"); // NON-NLS + String keywordListName = rs.getString("set_name"); // NON-NLS + + tempMap.get(objId).addKeywordListName(keywordListName); + + } catch (SQLException ex) { + logger.log(Level.SEVERE, "Unable to get object_id or set_name from result set", ex); // NON-NLS + } + } + } catch (SQLException ex) { + logger.log(Level.SEVERE, "Failed to get keyword list names", ex); // NON-NLS + } + } + } + } + + /** + * Attribute for grouping/sorting by frequency in the central repository. + */ + static class FrequencyAttribute extends AttributeType { + + static final int BATCH_SIZE = 50; // Number of hashes to look up at one time + + static final int DOMAIN_BATCH_SIZE = 500; // Number of domains to look up at one time + + @Override + public DiscoveryKeyUtils.GroupKey getGroupKey(Result file) { + return new DiscoveryKeyUtils.FrequencyGroupKey(file); + } + + @Override + public void addAttributeToResults(List results, SleuthkitCase caseDb, + CentralRepository centralRepoDb) throws DiscoveryException { + if (centralRepoDb == null) { + for (Result result : results) { + if (result.getFrequency() == SearchData.Frequency.UNKNOWN && result.getKnown() == TskData.FileKnown.KNOWN) { + result.setFrequency(SearchData.Frequency.KNOWN); + } + } + } else { + processResultFilesForCR(results, centralRepoDb); + } + } + + /** + * Private helper method for adding Frequency attribute when CR is + * enabled. + * + * @param files The list of ResultFiles to caluclate frequency + * for. + * @param centralRepoDb The central repository currently in use. + */ + private void processResultFilesForCR(List results, + CentralRepository centralRepoDb) throws DiscoveryException { + List currentFiles = new ArrayList<>(); + Set hashesToLookUp = new HashSet<>(); + List domainsToQuery = new ArrayList<>(); + for (Result result : results) { + // If frequency was already calculated, skip... + if (result.getFrequency() == SearchData.Frequency.UNKNOWN) { + if (result.getKnown() == TskData.FileKnown.KNOWN) { + result.setFrequency(SearchData.Frequency.KNOWN); + } + + if (result.getType() != SearchData.Type.DOMAIN) { + ResultFile file = (ResultFile) result; + if (file.getFirstInstance().getMd5Hash() != null + && !file.getFirstInstance().getMd5Hash().isEmpty()) { + hashesToLookUp.add(file.getFirstInstance().getMd5Hash()); + currentFiles.add(file); + } + + if (hashesToLookUp.size() >= BATCH_SIZE) { + computeFrequency(hashesToLookUp, currentFiles, centralRepoDb); + + hashesToLookUp.clear(); + currentFiles.clear(); + } + } else { + domainsToQuery.add((ResultDomain) result); + if (domainsToQuery.size() == DOMAIN_BATCH_SIZE) { + queryDomainFrequency(domainsToQuery, centralRepoDb); + + domainsToQuery.clear(); + } + } + } + } + + queryDomainFrequency(domainsToQuery, centralRepoDb); + computeFrequency(hashesToLookUp, currentFiles, centralRepoDb); + } + } + + /** + * Query to get the frequency of a domain. + * + * @param domainsToQuery List of domains to check the frequency of. + * @param centralRepository The central repository to query. + * + * @throws DiscoveryException + */ + private static void queryDomainFrequency(List domainsToQuery, CentralRepository centralRepository) throws DiscoveryException { + if (domainsToQuery.isEmpty()) { + return; + } + try { + final Map> resultDomainTable = new HashMap<>(); + final StringJoiner joiner = new StringJoiner(", "); + + final CorrelationAttributeInstance.Type attributeType = centralRepository.getCorrelationTypeById(CorrelationAttributeInstance.DOMAIN_TYPE_ID); + for (ResultDomain domainInstance : domainsToQuery) { + try { + final String domainValue = domainInstance.getDomain(); + final String normalizedDomain = CorrelationAttributeNormalizer.normalize(attributeType, domainValue); + final List bucket = resultDomainTable.getOrDefault(normalizedDomain, new ArrayList<>()); + bucket.add(domainInstance); + resultDomainTable.put(normalizedDomain, bucket); + joiner.add("'" + normalizedDomain + "'"); + } catch (CorrelationAttributeNormalizationException ex) { + logger.log(Level.INFO, String.format("Domain [%s] failed normalization, skipping...", domainInstance.getDomain())); + } + } + + final String tableName = CentralRepoDbUtil.correlationTypeToInstanceTableName(attributeType); + final String domainFrequencyQuery = " value AS domain_name, COUNT(*) AS frequency " + + "FROM " + tableName + " " + + "WHERE value IN (" + joiner + ") " + + "GROUP BY value"; + + final DomainFrequencyCallback frequencyCallback = new DomainFrequencyCallback(resultDomainTable); + centralRepository.processSelectClause(domainFrequencyQuery, frequencyCallback); + + if (frequencyCallback.getCause() != null) { + throw frequencyCallback.getCause(); + } + } catch (CentralRepoException | SQLException ex) { + throw new DiscoveryException("Fatal exception encountered querying the CR.", ex); + } + } + + /** + * Callback to get the frequency of domain. + */ + private static class DomainFrequencyCallback implements InstanceTableCallback { + + private final Map> domainLookup; + private SQLException sqlCause; + + /** + * Construct a new DomainFrequencyCallback. + * + * @param domainLookup The map to get domain from. + */ + private DomainFrequencyCallback(Map> domainLookup) { + this.domainLookup = domainLookup; + } + + @Override + public void process(ResultSet resultSet) { + try { + while (resultSet.next()) { + String domain = resultSet.getString("domain_name"); + Long frequency = resultSet.getLong("frequency"); + + List domainInstances = domainLookup.get(domain); + for (ResultDomain domainInstance : domainInstances) { + domainInstance.setFrequency(SearchData.Frequency.fromCount(frequency)); + } + } + } catch (SQLException ex) { + this.sqlCause = ex; + } + } + + /** + * Get the SQL exception if one occurred during this callback. + * + * @return + */ + SQLException getCause() { + return this.sqlCause; + } + } + + /** + * Callback to use with findInterCaseValuesByCount which generates a list of + * values for common property search + */ + private static class FrequencyCallback implements InstanceTableCallback { + + private final List files; + + /** + * Construct a new FrequencyCallback. + * + * @param files List of files to add hash set names to. + */ + private FrequencyCallback(List files) { + this.files = new ArrayList<>(files); + } + + @Override + public void process(ResultSet resultSet) { + try { + + while (resultSet.next()) { + String hash = resultSet.getString(1); + int count = resultSet.getInt(2); + for (Iterator iterator = files.iterator(); iterator.hasNext();) { + ResultFile file = iterator.next(); + if (file.getFirstInstance().getMd5Hash().equalsIgnoreCase(hash)) { + file.setFrequency(SearchData.Frequency.fromCount(count)); + iterator.remove(); + } + } + } + + // The files left had no matching entries in the CR, so mark them as unique + for (ResultFile file : files) { + file.setFrequency(SearchData.Frequency.UNIQUE); + } + } catch (SQLException ex) { + logger.log(Level.WARNING, "Error getting frequency counts from Central Repository", ex); // NON-NLS + } + } + } + + /** + * Attribute for grouping/sorting by hash set lists. + */ + static class HashHitsAttribute extends AttributeType { + + @Override + public DiscoveryKeyUtils.GroupKey getGroupKey(Result result) { + if (result.getType() == SearchData.Type.DOMAIN) { + return null; + } + return new DiscoveryKeyUtils.HashHitsGroupKey((ResultFile) result); + } + + @Override + public void addAttributeToResults(List results, SleuthkitCase caseDb, + CentralRepository centralRepoDb) throws DiscoveryException { + + // Get pairs of (object ID, hash set name) for all files in the list of files that have + // hash set hits. + String selectQuery = createSetNameClause(results, BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT.getTypeID(), + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID()); + + HashSetNamesCallback callback = new HashSetNamesCallback(results); + try { + caseDb.getCaseDbAccessManager().select(selectQuery, callback); + } catch (TskCoreException ex) { + throw new DiscoveryException("Error looking up hash set attributes", ex); // NON-NLS + } + } + + /** + * Callback to process the results of the CaseDbAccessManager select + * query. Will add the hash set names to the list of ResultFile objects. + */ + private static class HashSetNamesCallback implements CaseDbAccessManager.CaseDbAccessQueryCallback { + + List results; + + /** + * Create the callback. + * + * @param resultFiles List of files to add hash set names to. + */ + HashSetNamesCallback(List results) { + this.results = results; + } + + @Override + public void process(ResultSet rs) { + try { + // Create a temporary map of object ID to ResultFile + Map tempMap = new HashMap<>(); + for (Result result : results) { + if (result.getType() == SearchData.Type.DOMAIN) { + return; + } + ResultFile file = (ResultFile) result; + tempMap.put(file.getFirstInstance().getId(), file); + } + + while (rs.next()) { + try { + Long objId = rs.getLong("object_id"); // NON-NLS + String hashSetName = rs.getString("set_name"); // NON-NLS + + tempMap.get(objId).addHashSetName(hashSetName); + + } catch (SQLException ex) { + logger.log(Level.SEVERE, "Unable to get object_id or set_name from result set", ex); // NON-NLS + } + } + } catch (SQLException ex) { + logger.log(Level.SEVERE, "Failed to get hash set names", ex); // NON-NLS + } + } + } + } + + /** + * Attribute for grouping/sorting by interesting item set lists. + */ + static class InterestingItemAttribute extends AttributeType { + + @Override + public DiscoveryKeyUtils.GroupKey getGroupKey(Result file) { + return new DiscoveryKeyUtils.InterestingItemGroupKey((ResultFile) file); + } + + @Override + public void addAttributeToResults(List results, SleuthkitCase caseDb, + CentralRepository centralRepoDb) throws DiscoveryException { + + // Get pairs of (object ID, interesting item set name) for all files in the list of files that have + // interesting file set hits. + String selectQuery = createSetNameClause(results, BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT.getTypeID(), + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID()); + + InterestingFileSetNamesCallback callback = new InterestingFileSetNamesCallback(results); + try { + caseDb.getCaseDbAccessManager().select(selectQuery, callback); + } catch (TskCoreException ex) { + throw new DiscoveryException("Error looking up interesting file set attributes", ex); // NON-NLS + } + } + + /** + * Callback to process the results of the CaseDbAccessManager select + * query. Will add the interesting file set names to the list of + * ResultFile objects. + */ + private static class InterestingFileSetNamesCallback implements CaseDbAccessManager.CaseDbAccessQueryCallback { + + List results; + + /** + * Create the callback. + * + * @param resultFiles List of files to add interesting file set + * names to. + */ + InterestingFileSetNamesCallback(List results) { + this.results = results; + } + + @Override + public void process(ResultSet rs) { + try { + // Create a temporary map of object ID to ResultFile + Map tempMap = new HashMap<>(); + for (Result result : results) { + if (result.getType() == SearchData.Type.DOMAIN) { + return; + } + ResultFile file = (ResultFile) result; + tempMap.put(file.getFirstInstance().getId(), file); + } + + while (rs.next()) { + try { + Long objId = rs.getLong("object_id"); // NON-NLS + String setName = rs.getString("set_name"); // NON-NLS + + tempMap.get(objId).addInterestingSetName(setName); + + } catch (SQLException ex) { + logger.log(Level.SEVERE, "Unable to get object_id or set_name from result set", ex); // NON-NLS + } + } + } catch (SQLException ex) { + logger.log(Level.SEVERE, "Failed to get interesting file set names", ex); // NON-NLS + } + } + } + } + + /** + * Attribute for grouping/sorting by date of most recent activity. + */ + static class MostRecentActivityDateAttribute extends AttributeType { + + @Override + public DiscoveryKeyUtils.GroupKey getGroupKey(Result result) { + return new DiscoveryKeyUtils.MostRecentActivityDateGroupKey(result); + } + + } + + /** + * Attribute for grouping/sorting by date of first activity. + */ + static class FirstActivityDateAttribute extends AttributeType { + + @Override + public DiscoveryKeyUtils.GroupKey getGroupKey(Result result) { + return new DiscoveryKeyUtils.FirstActivityDateGroupKey(result); + } + + } + + /** + * Attribute for grouping/sorting by number of visits. + */ + static class NumberOfVisitsAttribute extends AttributeType { + + @Override + public DiscoveryKeyUtils.GroupKey getGroupKey(Result result) { + return new DiscoveryKeyUtils.NumberOfVisitsGroupKey(result); + } + } + + /** + * Attribute for grouping/sorting by objects detected. + */ + static class ObjectDetectedAttribute extends AttributeType { + + @Override + public DiscoveryKeyUtils.GroupKey getGroupKey(Result file) { + return new DiscoveryKeyUtils.ObjectDetectedGroupKey((ResultFile) file); + } + + @Override + public void addAttributeToResults(List results, SleuthkitCase caseDb, + CentralRepository centralRepoDb) throws DiscoveryException { + + // Get pairs of (object ID, object type name) for all files in the list of files that have + // objects detected + String selectQuery = createSetNameClause(results, BlackboardArtifact.ARTIFACT_TYPE.TSK_OBJECT_DETECTED.getTypeID(), + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DESCRIPTION.getTypeID()); + + ObjectDetectedNamesCallback callback = new ObjectDetectedNamesCallback(results); + try { + caseDb.getCaseDbAccessManager().select(selectQuery, callback); + } catch (TskCoreException ex) { + throw new DiscoveryException("Error looking up object detected attributes", ex); // NON-NLS + } + } + + /** + * Callback to process the results of the CaseDbAccessManager select + * query. Will add the object type names to the list of ResultFile + * objects. + */ + private static class ObjectDetectedNamesCallback implements CaseDbAccessManager.CaseDbAccessQueryCallback { + + List results; + + /** + * Create the callback. + * + * @param resultFiles List of files to add object detected names to. + */ + ObjectDetectedNamesCallback(List results) { + this.results = results; + } + + @Override + public void process(ResultSet rs) { + try { + // Create a temporary map of object ID to ResultFile + Map tempMap = new HashMap<>(); + for (Result result : results) { + if (result.getType() == SearchData.Type.DOMAIN) { + return; + } + ResultFile file = (ResultFile) result; + tempMap.put(file.getFirstInstance().getId(), file); + } + + while (rs.next()) { + try { + Long objId = rs.getLong("object_id"); // NON-NLS + String setName = rs.getString("set_name"); // NON-NLS + + tempMap.get(objId).addObjectDetectedName(setName); + + } catch (SQLException ex) { + logger.log(Level.SEVERE, "Unable to get object_id or set_name from result set", ex); // NON-NLS + } + } + } catch (SQLException ex) { + logger.log(Level.SEVERE, "Failed to get object detected names", ex); // NON-NLS + } + } + } + } + + /** + * Attribute for grouping/sorting by tag name. + */ + static class FileTagAttribute extends AttributeType { + + @Override + public DiscoveryKeyUtils.GroupKey getGroupKey(Result file) { + return new DiscoveryKeyUtils.FileTagGroupKey((ResultFile) file); + } + + @Override + public void addAttributeToResults(List results, SleuthkitCase caseDb, + CentralRepository centralRepoDb) throws DiscoveryException { + + try { + for (Result result : results) { + if (result.getType() == SearchData.Type.DOMAIN) { + return; + } + ResultFile file = (ResultFile) result; + List contentTags = caseDb.getContentTagsByContent(file.getFirstInstance()); + + for (ContentTag tag : contentTags) { + result.addTagName(tag.getName().getDisplayName()); + } + } + } catch (TskCoreException ex) { + throw new DiscoveryException("Error looking up file tag attributes", ex); // NON-NLS + } + } + } + + /** + * Enum for the attribute types that can be used for grouping. + */ + @NbBundle.Messages({ + "DiscoveryAttributes.GroupingAttributeType.fileType.displayName=File Type", + "DiscoveryAttributes.GroupingAttributeType.frequency.displayName=Past Occurrences", + "DiscoveryAttributes.GroupingAttributeType.keywordList.displayName=Keyword", + "DiscoveryAttributes.GroupingAttributeType.size.displayName=File Size", + "DiscoveryAttributes.GroupingAttributeType.datasource.displayName=Data Source", + "DiscoveryAttributes.GroupingAttributeType.parent.displayName=Parent Folder", + "DiscoveryAttributes.GroupingAttributeType.hash.displayName=Hash Set", + "DiscoveryAttributes.GroupingAttributeType.interestingItem.displayName=Interesting Item", + "DiscoveryAttributes.GroupingAttributeType.tag.displayName=Tag", + "DiscoveryAttributes.GroupingAttributeType.object.displayName=Object Detected", + "DiscoveryAttributes.GroupingAttributeType.mostRecentDate.displayName=Most Recent Activity Date", + "DiscoveryAttributes.GroupingAttributeType.firstDate.displayName=First Activity Date", + "DiscoveryAttributes.GroupingAttributeType.numberOfVisits.displayName=Number of Visits", + "DiscoveryAttributes.GroupingAttributeType.none.displayName=None"}) + public enum GroupingAttributeType { + FILE_SIZE(new FileSizeAttribute(), Bundle.DiscoveryAttributes_GroupingAttributeType_size_displayName()), + FREQUENCY(new FrequencyAttribute(), Bundle.DiscoveryAttributes_GroupingAttributeType_frequency_displayName()), + KEYWORD_LIST_NAME(new KeywordListAttribute(), Bundle.DiscoveryAttributes_GroupingAttributeType_keywordList_displayName()), + DATA_SOURCE(new DataSourceAttribute(), Bundle.DiscoveryAttributes_GroupingAttributeType_datasource_displayName()), + PARENT_PATH(new ParentPathAttribute(), Bundle.DiscoveryAttributes_GroupingAttributeType_parent_displayName()), + HASH_LIST_NAME(new HashHitsAttribute(), Bundle.DiscoveryAttributes_GroupingAttributeType_hash_displayName()), + INTERESTING_ITEM_SET(new InterestingItemAttribute(), Bundle.DiscoveryAttributes_GroupingAttributeType_interestingItem_displayName()), + FILE_TAG(new FileTagAttribute(), Bundle.DiscoveryAttributes_GroupingAttributeType_tag_displayName()), + OBJECT_DETECTED(new ObjectDetectedAttribute(), Bundle.DiscoveryAttributes_GroupingAttributeType_object_displayName()), + MOST_RECENT_DATE(new MostRecentActivityDateAttribute(), Bundle.DiscoveryAttributes_GroupingAttributeType_mostRecentDate_displayName()), + FIRST_DATE(new FirstActivityDateAttribute(), Bundle.DiscoveryAttributes_GroupingAttributeType_firstDate_displayName()), + NUMBER_OF_VISITS(new NumberOfVisitsAttribute(), Bundle.DiscoveryAttributes_GroupingAttributeType_numberOfVisits_displayName()), + NO_GROUPING(new NoGroupingAttribute(), Bundle.DiscoveryAttributes_GroupingAttributeType_none_displayName()); + + private final AttributeType attributeType; + private final String displayName; + + /** + * Construct a new GroupingAttributeType enum value. + * + * @param attributeType The type of attribute this enum value was + * constructed for. + * @param displayName The display name for this grouping attribute + * type. + */ + GroupingAttributeType(AttributeType attributeType, String displayName) { + this.attributeType = attributeType; + this.displayName = displayName; + } + + @Override + public String toString() { + return displayName; + } + + /** + * Get the type of attribute this enum value was constructed for. + * + * @return The type of attribute this enum value was constructed for. + */ + public AttributeType getAttributeType() { + return attributeType; + } + + /** + * Get the list of enums that are valid for grouping files. + * + * @return Enums that can be used to group files. + */ + public static List getOptionsForGroupingForFiles() { + return Arrays.asList(FILE_SIZE, FREQUENCY, PARENT_PATH, OBJECT_DETECTED, HASH_LIST_NAME, INTERESTING_ITEM_SET); + } + + /** + * Get the list of enums that are valid for grouping files. + * + * @return Enums that can be used to group files. + */ + public static List getOptionsForGroupingForDomains() { + return Arrays.asList(FREQUENCY, MOST_RECENT_DATE, FIRST_DATE, NUMBER_OF_VISITS); + } + } + + /** + * Computes the CR frequency of all the given hashes and updates the list of + * files. + * + * @param hashesToLookUp Hashes to find the frequency of. + * @param currentFiles List of files to update with frequencies. + * @param centralRepoDb The central repository being used. + */ + private static void computeFrequency(Set hashesToLookUp, List currentFiles, CentralRepository centralRepoDb) { + + if (hashesToLookUp.isEmpty()) { + return; + } + + String hashes = String.join("','", hashesToLookUp); + hashes = "'" + hashes + "'"; + try { + CorrelationAttributeInstance.Type attributeType = centralRepoDb.getCorrelationTypeById(CorrelationAttributeInstance.FILES_TYPE_ID); + String tableName = CentralRepoDbUtil.correlationTypeToInstanceTableName(attributeType); + + String selectClause = " value, COUNT(value) FROM " + + "(SELECT DISTINCT case_id, value FROM " + tableName + + " WHERE value IN (" + + hashes + + ")) AS foo GROUP BY value"; + + FrequencyCallback callback = new FrequencyCallback(currentFiles); + centralRepoDb.processSelectClause(selectClause, callback); + + } catch (CentralRepoException ex) { + logger.log(Level.WARNING, "Error getting frequency counts from Central Repository", ex); // NON-NLS + } + + } + + /** + * Private helper method to create a set name clause to be used in queries. + * + * @param results The list of results to create the set name clause + * for. + * @param artifactTypeID The Blackboard Artifact type ID for the artifact + * type. + * @param setNameAttrID The set name attribute id. + * + * @return The String to use as a set name clause in queries. + * + * @throws DiscoveryException + */ + private static String createSetNameClause(List results, + int artifactTypeID, int setNameAttrID) throws DiscoveryException { + + // Concatenate the object IDs in the list of files + String objIdList = ""; // NON-NLS + for (Result result : results) { + if (result.getType() == SearchData.Type.DOMAIN) { + break; + } + ResultFile file = (ResultFile) result; + if (!objIdList.isEmpty()) { + objIdList += ","; // NON-NLS + } + objIdList += "\'" + file.getFirstInstance().getId() + "\'"; // NON-NLS + } + + // Get pairs of (object ID, set name) for all files in the list of files that have + // the given artifact type. + return "blackboard_artifacts.obj_id AS object_id, blackboard_attributes.value_text AS set_name " + + "FROM blackboard_artifacts " + + "INNER JOIN blackboard_attributes ON blackboard_artifacts.artifact_id=blackboard_attributes.artifact_id " + + "WHERE blackboard_attributes.artifact_type_id=\'" + artifactTypeID + "\' " + + "AND blackboard_attributes.attribute_type_id=\'" + setNameAttrID + "\' " + + "AND blackboard_artifacts.obj_id IN (" + objIdList + + ") "; // NON-NLS + } + + /** + * Private constructor for DiscoveryAttributes class. + */ + private DiscoveryAttributes() { + // Class should not be instantiated + } +} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/DiscoveryEventUtils.java b/Core/src/org/sleuthkit/autopsy/discovery/search/DiscoveryEventUtils.java similarity index 57% rename from Core/src/org/sleuthkit/autopsy/discovery/DiscoveryEventUtils.java rename to Core/src/org/sleuthkit/autopsy/discovery/search/DiscoveryEventUtils.java index ea5fca6676..e224c02d51 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/DiscoveryEventUtils.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/DiscoveryEventUtils.java @@ -16,20 +16,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.search; import com.google.common.eventbus.EventBus; import java.util.Collections; import java.util.List; import java.util.Map; -import org.sleuthkit.autopsy.discovery.FileSearch.GroupKey; -import org.sleuthkit.autopsy.discovery.FileSearchData.FileType; +import org.sleuthkit.autopsy.discovery.search.DiscoveryKeyUtils.GroupKey; +import org.sleuthkit.autopsy.discovery.search.SearchData.Type; import org.sleuthkit.datamodel.AbstractFile; /** * Class to handle event bus and events for discovery tool. */ -final class DiscoveryEventUtils { +public final class DiscoveryEventUtils { private final static EventBus discoveryEventBus = new EventBus(); @@ -38,7 +38,7 @@ final class DiscoveryEventUtils { * * @return The discovery event bus. */ - static EventBus getDiscoveryEventBus() { + public static EventBus getDiscoveryEventBus() { return discoveryEventBus; } @@ -52,60 +52,74 @@ final class DiscoveryEventUtils { /** * Event to signal the start of a search being performed. */ - static final class SearchStartedEvent { + public static final class SearchStartedEvent { - private final FileType fileType; + private final Type type; /** - * Construct a new SearchStartedEvent + * Construct a new SearchStartedEvent. * - * @param type The type of file the search event is for. + * @param type The type of result the search event is for. */ - SearchStartedEvent(FileType type) { - this.fileType = type; + public SearchStartedEvent(Type type) { + this.type = type; } /** - * Get the type of file the search is being performed for. + * Get the type of result the search is being performed for. * - * @return The type of files being searched for. + * @return The type of results being searched for. */ - FileType getType() { - return fileType; + public Type getType() { + return type; } + } /** * Event to signal that the Instances list should have selection cleared. */ - static final class ClearInstanceSelectionEvent { + public static final class ClearInstanceSelectionEvent { /** * Construct a new ClearInstanceSelectionEvent. */ - ClearInstanceSelectionEvent() { + public ClearInstanceSelectionEvent() { //no arg constructor } } + + /** + * Event to signal that any background tasks currently running should + * be cancelled. + */ + public static final class CancelBackgroundTasksEvent { + + public CancelBackgroundTasksEvent() { + //no-arg constructor + } + } /** * Event to signal that the Instances list should be populated. */ - static final class PopulateInstancesListEvent { + public static final class PopulateInstancesListEvent { private final List instances; /** * Construct a new PopulateInstancesListEvent. */ - PopulateInstancesListEvent(List files) { + public PopulateInstancesListEvent(List files) { instances = files; } /** - * @return the instances + * Get the list of AbstractFiles for the instances list. + * + * @return The list of AbstractFiles for the instances list. */ - List getInstances() { + public List getInstances() { return Collections.unmodifiableList(instances); } } @@ -113,33 +127,33 @@ final class DiscoveryEventUtils { /** * Event to signal the completion of a search being performed. */ - static final class SearchCompleteEvent { + public static final class SearchCompleteEvent { private final Map groupMap; - private final List searchFilters; - private final FileSearch.AttributeType groupingAttribute; - private final FileGroup.GroupSortingAlgorithm groupSort; - private final FileSorter.SortingMethod fileSortMethod; + private final List searchFilters; + private final DiscoveryAttributes.AttributeType groupingAttribute; + private final Group.GroupSortingAlgorithm groupSort; + private final ResultsSorter.SortingMethod sortMethod; /** * Construct a new SearchCompleteEvent, * * @param groupMap The map of groups which were found by the * search. - * @param searchFilters The search filters which were used by the + * @param searchfilters The search filters which were used by the * search. * @param groupingAttribute The grouping attribute used by the search. * @param groupSort The sorting algorithm used for groups. - * @param fileSortMethod The sorting method used for files. + * @param sortMethod The sorting method used for results. */ - SearchCompleteEvent(Map groupMap, List searchfilters, - FileSearch.AttributeType groupingAttribute, FileGroup.GroupSortingAlgorithm groupSort, - FileSorter.SortingMethod fileSortMethod) { + public SearchCompleteEvent(Map groupMap, List searchfilters, + DiscoveryAttributes.AttributeType groupingAttribute, Group.GroupSortingAlgorithm groupSort, + ResultsSorter.SortingMethod sortMethod) { this.groupMap = groupMap; this.searchFilters = searchfilters; this.groupingAttribute = groupingAttribute; this.groupSort = groupSort; - this.fileSortMethod = fileSortMethod; + this.sortMethod = sortMethod; } /** @@ -147,16 +161,16 @@ final class DiscoveryEventUtils { * * @return The map of groups which were found by the search. */ - Map getGroupMap() { + public Map getGroupMap() { return Collections.unmodifiableMap(groupMap); } /** - * Get the file filters used by the search. + * Get the filters used by the search. * * @return The search filters which were used by the search. */ - List getFilters() { + public List getFilters() { return Collections.unmodifiableList(searchFilters); } @@ -165,7 +179,7 @@ final class DiscoveryEventUtils { * * @return The grouping attribute used by the search. */ - FileSearch.AttributeType getGroupingAttr() { + public DiscoveryAttributes.AttributeType getGroupingAttr() { return groupingAttribute; } @@ -174,17 +188,17 @@ final class DiscoveryEventUtils { * * @return The sorting algorithm used for groups. */ - FileGroup.GroupSortingAlgorithm getGroupSort() { + public Group.GroupSortingAlgorithm getGroupSort() { return groupSort; } /** - * Get the sorting method used for files. + * Get the sorting method used for results. * - * @return The sorting method used for files. + * @return The sorting method used for results. */ - FileSorter.SortingMethod getFileSort() { - return fileSortMethod; + public ResultsSorter.SortingMethod getResultSort() { + return sortMethod; } } @@ -193,31 +207,31 @@ final class DiscoveryEventUtils { * Event to signal the completion of page retrieval and include the page * contents. */ - static final class PageRetrievedEvent { + public static final class PageRetrievedEvent { - private final List results; + private final List results; private final int page; - private final FileType resultType; + private final Type resultType; /** * Construct a new PageRetrievedEvent. * - * @param resultType The type of files which exist in the page. + * @param resultType The type of results which exist in the page. * @param page The number of the page which was retrieved. - * @param results The list of files in the page retrieved. + * @param results The list of results in the page retrieved. */ - PageRetrievedEvent(FileType resultType, int page, List results) { + public PageRetrievedEvent(Type resultType, int page, List results) { this.results = results; this.page = page; this.resultType = resultType; } /** - * Get the list of files in the page retrieved. + * Get the list of results in the page retrieved. * - * @return The list of files in the page retrieved. + * @return The list of results in the page retrieved. */ - List getSearchResults() { + public List getSearchResults() { return Collections.unmodifiableList(results); } @@ -226,16 +240,16 @@ final class DiscoveryEventUtils { * * @return The number of the page which was retrieved. */ - int getPageNumber() { + public int getPageNumber() { return page; } /** - * Get the type of files which exist in the page. + * Get the type of results which exist in the page. * - * @return The type of files which exist in the page. + * @return The type of results which exist in the page. */ - FileType getType() { + public Type getType() { return resultType; } } @@ -243,25 +257,25 @@ final class DiscoveryEventUtils { /** * Event to signal that there were no results for the search. */ - static final class NoResultsEvent { + public static final class NoResultsEvent { /** * Construct a new NoResultsEvent. */ - NoResultsEvent() { + public NoResultsEvent() { //no arg constructor } } /** - * Event to signal that a search has been cancelled + * Event to signal that a search has been cancelled. */ - static final class SearchCancelledEvent { + public static final class SearchCancelledEvent { /** * Construct a new SearchCancelledEvent. */ - SearchCancelledEvent() { + public SearchCancelledEvent() { //no arg constructor } @@ -270,15 +284,15 @@ final class DiscoveryEventUtils { /** * Event to signal that a group has been selected. */ - static final class GroupSelectedEvent { + public static final class GroupSelectedEvent { - private final FileType resultType; + private final Type resultType; private final GroupKey groupKey; private final int groupSize; - private final List searchfilters; - private final FileSearch.AttributeType groupingAttribute; - private final FileGroup.GroupSortingAlgorithm groupSort; - private final FileSorter.SortingMethod fileSortMethod; + private final List searchfilters; + private final DiscoveryAttributes.AttributeType groupingAttribute; + private final Group.GroupSortingAlgorithm groupSort; + private final ResultsSorter.SortingMethod sortMethod; /** * Construct a new GroupSelectedEvent. @@ -287,31 +301,32 @@ final class DiscoveryEventUtils { * search. * @param groupingAttribute The grouping attribute used by the search. * @param groupSort The sorting algorithm used for groups. - * @param fileSortMethod The sorting method used for files. + * @param sortMethod The sorting method used for results. * @param groupKey The key associated with the group which was * selected. - * @param groupSize The number of files in the group which was + * @param groupSize The number of results in the group which was * selected. - * @param resultType The type of files which exist in the group. + * @param resultType The type of results which exist in the + * group. */ - GroupSelectedEvent(List searchfilters, - FileSearch.AttributeType groupingAttribute, FileGroup.GroupSortingAlgorithm groupSort, - FileSorter.SortingMethod fileSortMethod, GroupKey groupKey, int groupSize, FileType resultType) { + public GroupSelectedEvent(List searchfilters, + DiscoveryAttributes.AttributeType groupingAttribute, Group.GroupSortingAlgorithm groupSort, + ResultsSorter.SortingMethod sortMethod, GroupKey groupKey, int groupSize, Type resultType) { this.searchfilters = searchfilters; this.groupingAttribute = groupingAttribute; this.groupSort = groupSort; - this.fileSortMethod = fileSortMethod; + this.sortMethod = sortMethod; this.groupKey = groupKey; this.groupSize = groupSize; this.resultType = resultType; } /** - * Get the type of files which exist in the group. + * Get the type of results which exist in the group. * - * @return The type of files which exist in the group. + * @return The type of results which exist in the group. */ - FileType getResultType() { + public Type getResultType() { return resultType; } @@ -322,16 +337,16 @@ final class DiscoveryEventUtils { * @return The group key which is used to uniquely identify the group * selected. */ - GroupKey getGroupKey() { + public GroupKey getGroupKey() { return groupKey; } /** - * Get the number of files in the group which was selected. + * Get the number of results in the group which was selected. * - * @return The number of files in the group which was selected. + * @return The number of results in the group which was selected. */ - int getGroupSize() { + public int getGroupSize() { return groupSize; } @@ -340,25 +355,25 @@ final class DiscoveryEventUtils { * * @return The sorting algorithm used for groups. */ - FileGroup.GroupSortingAlgorithm getGroupSort() { + public Group.GroupSortingAlgorithm getGroupSort() { return groupSort; } /** - * Get the sorting method used for files in the group. + * Get the sorting method used for results in the group. * - * @return The sorting method used for files. + * @return The sorting method used for results. */ - FileSorter.SortingMethod getFileSort() { - return fileSortMethod; + public ResultsSorter.SortingMethod getResultSort() { + return sortMethod; } /** - * Get the file filters which were used by the search + * Get the result filters which were used by the search. * * @return The search filters which were used by the search. */ - List getFilters() { + public List getFilters() { return Collections.unmodifiableList(searchfilters); } @@ -367,7 +382,7 @@ final class DiscoveryEventUtils { * * @return The grouping attribute used by the search. */ - FileSearch.AttributeType getGroupingAttr() { + public DiscoveryAttributes.AttributeType getGroupingAttr() { return groupingAttribute; } @@ -376,7 +391,7 @@ final class DiscoveryEventUtils { /** * Event to signal that the visibility of the Details area should change. */ - static class DetailsVisibleEvent { + public static class DetailsVisibleEvent { private final boolean showDetailsArea; @@ -386,7 +401,7 @@ final class DiscoveryEventUtils { * @param isVisible True if the details area should be visible, false * otherwise. */ - DetailsVisibleEvent(boolean isVisible) { + public DetailsVisibleEvent(boolean isVisible) { showDetailsArea = isVisible; } @@ -395,7 +410,7 @@ final class DiscoveryEventUtils { * * @return True if the details area should be visible, false otherwise. */ - boolean isShowDetailsArea() { + public boolean isShowDetailsArea() { return showDetailsArea; } } diff --git a/Core/src/org/sleuthkit/autopsy/discovery/FileSearchException.java b/Core/src/org/sleuthkit/autopsy/discovery/search/DiscoveryException.java similarity index 82% rename from Core/src/org/sleuthkit/autopsy/discovery/FileSearchException.java rename to Core/src/org/sleuthkit/autopsy/discovery/search/DiscoveryException.java index df22e26488..13a4f87fa0 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/FileSearchException.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/DiscoveryException.java @@ -16,12 +16,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.search; /** - * Exception type used for FileSearch. + * Exception type used for Discovery search. */ -final public class FileSearchException extends Exception { +final public class DiscoveryException extends Exception { private static final long serialVersionUID = 1L; @@ -30,7 +30,7 @@ final public class FileSearchException extends Exception { * * @param message The message to associate with this exception. */ - FileSearchException(String message) { + DiscoveryException(String message) { super(message); } @@ -40,7 +40,7 @@ final public class FileSearchException extends Exception { * @param message The message to associate with this exception. * @param cause The Throwable cause of the exception. */ - FileSearchException(String message, Throwable cause) { + DiscoveryException(String message, Throwable cause) { super(message, cause); } } diff --git a/Core/src/org/sleuthkit/autopsy/discovery/search/DiscoveryKeyUtils.java b/Core/src/org/sleuthkit/autopsy/discovery/search/DiscoveryKeyUtils.java new file mode 100644 index 0000000000..9fe4fbf946 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/DiscoveryKeyUtils.java @@ -0,0 +1,1390 @@ +/* + * Autopsy + * + * Copyright 2020 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.discovery.search; + +import java.text.SimpleDateFormat; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Utility class for constructing keys for groups and searches. + */ +public class DiscoveryKeyUtils { + + private final static Logger logger = Logger.getLogger(DiscoveryKeyUtils.class.getName()); + + /** + * Represents a key for a specific search for a specific user. + */ + static class SearchKey implements Comparable { + + private final String keyString; + private final Group.GroupSortingAlgorithm groupSortingType; + private final DiscoveryAttributes.AttributeType groupAttributeType; + private final ResultsSorter.SortingMethod sortingMethod; + private final List filters; + private final SleuthkitCase sleuthkitCase; + private final CentralRepository centralRepository; + + /** + * Construct a new SearchKey with all information that defines a search. + * + * @param userName The name of the user performing the search. + * @param filters The Filters being used for the search. + * @param groupAttributeType The AttributeType to group by. + * @param groupSortingType The algorithm to sort the groups by. + * @param sortingMethod The method to sort the results by. + * @param sleuthkitCase The SleuthkitCase being searched. + * @param centralRepository The Central Repository being searched. + */ + SearchKey(String userName, List filters, + DiscoveryAttributes.AttributeType groupAttributeType, + Group.GroupSortingAlgorithm groupSortingType, + ResultsSorter.SortingMethod sortingMethod, + SleuthkitCase sleuthkitCase, CentralRepository centralRepository) { + this.groupAttributeType = groupAttributeType; + this.groupSortingType = groupSortingType; + this.sortingMethod = sortingMethod; + this.filters = filters; + + StringBuilder searchStringBuilder = new StringBuilder(); + searchStringBuilder.append(userName); + for (AbstractFilter filter : filters) { + searchStringBuilder.append(filter.toString()); + } + searchStringBuilder.append(groupAttributeType).append(groupSortingType).append(sortingMethod); + keyString = searchStringBuilder.toString(); + this.sleuthkitCase = sleuthkitCase; + this.centralRepository = centralRepository; + } + + /** + * Construct a SearchKey without a SleuthkitCase or CentralRepositry + * instance. + * + * @param userName The name of the user performing the search. + * @param filters The Filters being used for the search. + * @param groupAttributeType The AttributeType to group by. + * @param groupSortingType The algorithm to sort the groups by. + * @param sortingMethod The method to sort the results by. + */ + SearchKey(String userName, List filters, + DiscoveryAttributes.AttributeType groupAttributeType, + Group.GroupSortingAlgorithm groupSortingType, + ResultsSorter.SortingMethod sortingMethod) { + this(userName, filters, groupAttributeType, groupSortingType, + sortingMethod, null, null); + } + + @Override + public int compareTo(SearchKey otherSearchKey) { + return getKeyString().compareTo(otherSearchKey.getKeyString()); + } + + @Override + public boolean equals(Object otherKey) { + if (otherKey == this) { + return true; + } + + if (!(otherKey instanceof SearchKey)) { + return false; + } + + SearchKey otherSearchKey = (SearchKey) otherKey; + if (this.sleuthkitCase != otherSearchKey.getSleuthkitCase() + || this.centralRepository != otherSearchKey.getCentralRepository()) { + return false; + } + + return getKeyString().equals(otherSearchKey.getKeyString()); + } + + @Override + public int hashCode() { + int hash = 5; + hash = 79 * hash + Objects.hashCode(getKeyString()); + return hash; + } + + /** + * Get the String representation of this key. + * + * @return The String representation of this key. + */ + String getKeyString() { + return keyString; + } + + /** + * Get the list of filters associated with this key. + * + * @return The list of filters associated with this key. + */ + List getFilters() { + return Collections.unmodifiableList(this.filters); + } + + /** + * Get the group sorting type for this key. + * + * @return The group sorting type for this key. + */ + Group.GroupSortingAlgorithm getGroupSortingType() { + return groupSortingType; + } + + /** + * Get the grouping attribute for this key. + * + * @return The grouping attribute for this key. + */ + DiscoveryAttributes.AttributeType getGroupAttributeType() { + return groupAttributeType; + } + + /** + * Get the SortingMethod for this key. + * + * @return The SortingMethod for this key. + */ + ResultsSorter.SortingMethod getFileSortingMethod() { + return sortingMethod; + } + + /** + * Get the case database for this key. + * + * @return The case database for this key. + */ + SleuthkitCase getSleuthkitCase() { + return this.sleuthkitCase; + } + + /** + * Get the central repository for this key. + * + * @return The central repository for this key. + */ + CentralRepository getCentralRepository() { + return this.centralRepository; + } + } + + /** + * The key used for grouping for each attribute type. + */ + public abstract static class GroupKey implements Comparable { + + /** + * Get the string version of the group key for display. Each display + * name should correspond to a unique GroupKey object. + * + * @return The display name for this key + */ + abstract String getDisplayName(); + + /** + * Subclasses must implement equals(). + * + * @param otherKey The GroupKey to compare to this key. + * + * @return true if the keys are equal, false otherwise + */ + @Override + abstract public boolean equals(Object otherKey); + + /** + * Subclasses must implement hashCode(). + * + * @return The hash code for the GroupKey. + */ + @Override + abstract public int hashCode(); + + /** + * It should not happen with the current setup, but we need to cover the + * case where two different GroupKey subclasses are compared against + * each other. Use a lexicographic comparison on the class names. + * + * @param otherGroupKey The other group key. + * + * @return Result of alphabetical comparison on the class name. + */ + int compareClassNames(GroupKey otherGroupKey) { + return this.getClass().getName().compareTo(otherGroupKey.getClass().getName()); + } + + @Override + public String toString() { + return getDisplayName(); + } + } + + /** + * Key representing a file size group. + */ + static class FileSizeGroupKey extends GroupKey { + + private final SearchData.FileSize fileSize; + + /** + * Construct a new FileSizeGroupKey. + * + * @param file The file to create the group key for. + */ + FileSizeGroupKey(Result file) { + ResultFile resultFile = (ResultFile) file; + if (resultFile.getFileType() == SearchData.Type.VIDEO) { + fileSize = SearchData.FileSize.fromVideoSize(resultFile.getFirstInstance().getSize()); + } else { + fileSize = SearchData.FileSize.fromImageSize(resultFile.getFirstInstance().getSize()); + } + } + + @Override + String getDisplayName() { + return getFileSize().toString(); + } + + @Override + public int compareTo(GroupKey otherGroupKey) { + if (otherGroupKey instanceof FileSizeGroupKey) { + FileSizeGroupKey otherFileSizeGroupKey = (FileSizeGroupKey) otherGroupKey; + return Integer.compare(getFileSize().getRanking(), otherFileSizeGroupKey.getFileSize().getRanking()); + } else { + return compareClassNames(otherGroupKey); + } + } + + @Override + public boolean equals(Object otherKey) { + if (otherKey == this) { + return true; + } + + if (!(otherKey instanceof FileSizeGroupKey)) { + return false; + } + + FileSizeGroupKey otherFileSizeGroupKey = (FileSizeGroupKey) otherKey; + return getFileSize().equals(otherFileSizeGroupKey.getFileSize()); + } + + @Override + public int hashCode() { + return Objects.hash(getFileSize().getRanking()); + } + + /** + * The size of the file. + * + * @return The size of the file. + */ + SearchData.FileSize getFileSize() { + return fileSize; + } + } + + /** + * Key representing a file type group. + */ + static class FileTypeGroupKey extends GroupKey { + + private final SearchData.Type fileType; + + /** + * Construct a new FileTypeGroupKey. + * + * @param file The file to create the group key for. + */ + FileTypeGroupKey(Result file) { + fileType = ((ResultFile) file).getFileType(); + } + + @Override + String getDisplayName() { + return getFileType().toString(); + } + + @Override + public int compareTo(GroupKey otherGroupKey) { + if (otherGroupKey instanceof FileTypeGroupKey) { + FileTypeGroupKey otherFileTypeGroupKey = (FileTypeGroupKey) otherGroupKey; + return Integer.compare(getFileType().getRanking(), otherFileTypeGroupKey.getFileType().getRanking()); + } else { + return compareClassNames(otherGroupKey); + } + } + + @Override + public boolean equals(Object otherKey) { + if (otherKey == this) { + return true; + } + + if (!(otherKey instanceof FileTypeGroupKey)) { + return false; + } + + FileTypeGroupKey otherFileTypeGroupKey = (FileTypeGroupKey) otherKey; + return getFileType().equals(otherFileTypeGroupKey.getFileType()); + } + + @Override + public int hashCode() { + return Objects.hash(getFileType().getRanking()); + } + + /** + * Get the type of file the group exists for. + * + * @return The type of file the group exists for. + */ + SearchData.Type getFileType() { + return fileType; + } + } + + /** + * Key representing a keyword list group. + */ + static class KeywordListGroupKey extends GroupKey { + + private final List keywordListNames; + private final String keywordListNamesString; + + /** + * Construct a new KeywordListGroupKey. + * + * @param file The file to create the group key for. + */ + @NbBundle.Messages({ + "DiscoveryKeyUtils.KeywordListGroupKey.noKeywords=None"}) + KeywordListGroupKey(ResultFile file) { + keywordListNames = file.getKeywordListNames(); + if (keywordListNames.isEmpty()) { + keywordListNamesString = Bundle.DiscoveryKeyUtils_KeywordListGroupKey_noKeywords(); + } else { + keywordListNamesString = String.join(",", keywordListNames); // NON-NLS + } + } + + @Override + String getDisplayName() { + return getKeywordListNamesString(); + } + + @Override + public int compareTo(GroupKey otherGroupKey) { + if (otherGroupKey instanceof KeywordListGroupKey) { + KeywordListGroupKey otherKeywordListNamesGroupKey = (KeywordListGroupKey) otherGroupKey; + + // Put the empty list at the end + if (getKeywordListNames().isEmpty()) { + if (otherKeywordListNamesGroupKey.getKeywordListNames().isEmpty()) { + return 0; + } else { + return 1; + } + } else if (otherKeywordListNamesGroupKey.getKeywordListNames().isEmpty()) { + return -1; + } + + return getKeywordListNamesString().compareTo(otherKeywordListNamesGroupKey.getKeywordListNamesString()); + } else { + return compareClassNames(otherGroupKey); + } + } + + @Override + public boolean equals(Object otherKey) { + if (otherKey == this) { + return true; + } + + if (!(otherKey instanceof KeywordListGroupKey)) { + return false; + } + + KeywordListGroupKey otherKeywordListGroupKey = (KeywordListGroupKey) otherKey; + return getKeywordListNamesString().equals(otherKeywordListGroupKey.getKeywordListNamesString()); + } + + @Override + public int hashCode() { + return Objects.hash(getKeywordListNamesString()); + } + + /** + * Get the list of keywords this group is for. + * + * @return The list of keywords this group is for. + */ + List getKeywordListNames() { + return Collections.unmodifiableList(keywordListNames); + } + + /** + * Get the string which represents the keyword names represented by this + * group key. + * + * @return The string which represents the keyword names represented by + * this group key. + */ + String getKeywordListNamesString() { + return keywordListNamesString; + } + } + + /** + * Key representing a file tag group. + */ + static class FileTagGroupKey extends GroupKey { + + private final List tagNames; + private final String tagNamesString; + + /** + * Construct a new FileTagGroupKey. + * + * @param file The file to create the group key for. + */ + @NbBundle.Messages({ + "DiscoveryKeyUtils.FileTagGroupKey.noSets=None"}) + FileTagGroupKey(ResultFile file) { + tagNames = file.getTagNames(); + + if (tagNames.isEmpty()) { + tagNamesString = Bundle.DiscoveryKeyUtils_FileTagGroupKey_noSets(); + } else { + tagNamesString = String.join(",", tagNames); // NON-NLS + } + } + + @Override + String getDisplayName() { + return getTagNamesString(); + } + + @Override + public int compareTo(GroupKey otherGroupKey) { + if (otherGroupKey instanceof FileTagGroupKey) { + FileTagGroupKey otherFileTagGroupKey = (FileTagGroupKey) otherGroupKey; + + // Put the empty list at the end + if (getTagNames().isEmpty()) { + if (otherFileTagGroupKey.getTagNames().isEmpty()) { + return 0; + } else { + return 1; + } + } else if (otherFileTagGroupKey.getTagNames().isEmpty()) { + return -1; + } + + return getTagNamesString().compareTo(otherFileTagGroupKey.getTagNamesString()); + } else { + return compareClassNames(otherGroupKey); + } + } + + @Override + public boolean equals(Object otherKey) { + if (otherKey == this) { + return true; + } + if (!(otherKey instanceof FileTagGroupKey)) { + return false; + } + FileTagGroupKey otherFileTagGroupKey = (FileTagGroupKey) otherKey; + return getTagNamesString().equals(otherFileTagGroupKey.getTagNamesString()); + } + + @Override + public int hashCode() { + return Objects.hash(getTagNamesString()); + } + + /** + * Get the list of tag names which are represented by this group. + * + * @return The list of tag names which are represented by this group. + */ + List getTagNames() { + return Collections.unmodifiableList(tagNames); + } + + /** + * Get the String representation of the tags which are represented by + * this group. + * + * @return The String representation of the tags which are represented + * by this group. + */ + String getTagNamesString() { + return tagNamesString; + } + } + + /** + * Key representing a parent path group. + */ + static class ParentPathGroupKey extends GroupKey { + + private String parentPath; + private Long parentID; + + /** + * Construct a new ParentPathGroupKey. + * + * @param file The file to create the group key for. + */ + ParentPathGroupKey(ResultFile file) { + Content parent; + try { + parent = file.getFirstInstance().getParent(); + } catch (TskCoreException ignored) { + parent = null; + } + //Find the directory this file is in if it is an embedded file + while (parent != null && parent instanceof AbstractFile && ((AbstractFile) parent).isFile()) { + try { + parent = parent.getParent(); + } catch (TskCoreException ignored) { + parent = null; + } + } + setParentPathAndID(parent, file); + } + + /** + * Helper method to set the parent path and parent ID. + * + * @param parent The parent content object. + * @param file The ResultFile object. + */ + private void setParentPathAndID(Content parent, ResultFile file) { + if (parent != null) { + try { + parentPath = parent.getUniquePath(); + parentID = parent.getId(); + } catch (TskCoreException ignored) { + //catch block left blank purposefully next if statement will handle case when exception takes place as well as when parent is null + } + + } + if (parentPath == null) { + if (file.getFirstInstance().getParentPath() != null) { + parentPath = file.getFirstInstance().getParentPath(); + } else { + parentPath = ""; // NON-NLS + } + parentID = -1L; + } + } + + @Override + String getDisplayName() { + return getParentPath(); + } + + @Override + public int compareTo(GroupKey otherGroupKey) { + if (otherGroupKey instanceof ParentPathGroupKey) { + ParentPathGroupKey otherParentPathGroupKey = (ParentPathGroupKey) otherGroupKey; + int comparisonResult = getParentPath().compareTo(otherParentPathGroupKey.getParentPath()); + if (comparisonResult == 0) { + comparisonResult = getParentID().compareTo(otherParentPathGroupKey.getParentID()); + } + return comparisonResult; + } else { + return compareClassNames(otherGroupKey); + } + } + + @Override + public boolean equals(Object otherKey) { + if (otherKey == this) { + return true; + } + + if (!(otherKey instanceof ParentPathGroupKey)) { + return false; + } + + ParentPathGroupKey otherParentPathGroupKey = (ParentPathGroupKey) otherKey; + return getParentPath().equals(otherParentPathGroupKey.getParentPath()) && getParentID().equals(otherParentPathGroupKey.getParentID()); + } + + @Override + public int hashCode() { + int hashCode = 11; + hashCode = 61 * hashCode + Objects.hash(getParentPath()); + hashCode = 61 * hashCode + Objects.hash(getParentID()); + return hashCode; + } + + /** + * Get the parent path this group is for. + * + * @return The parent path this group is for as a String. + */ + String getParentPath() { + return parentPath; + } + + /** + * Get the object ID of the parent object. + * + * @return The object ID of the parent object. + */ + Long getParentID() { + return parentID; + } + } + + /** + * Key representing a data source group. + */ + static class DataSourceGroupKey extends GroupKey { + + private final long dataSourceID; + private String displayName; + + /** + * Construct a new DataSourceGroupKey. + * + * @param result The Result to create the group key for. + */ + @NbBundle.Messages({ + "# {0} - Data source name", + "# {1} - Data source ID", + "DiscoveryKeyUtils.DataSourceGroupKey.datasourceAndID={0}(ID: {1})", + "# {0} - Data source ID", + "DiscoveryKeyUtils.DataSourceGroupKey.idOnly=Data source (ID: {0})"}) + DataSourceGroupKey(Result result) { + //get the id first so that it can be used when logging if necessary + dataSourceID = result.getDataSourceObjectId(); + try { + // The data source should be cached so this won't actually be a database query. + Content ds = result.getDataSource(); + displayName = Bundle.DiscoveryKeyUtils_DataSourceGroupKey_datasourceAndID(ds.getName(), ds.getId()); + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "Error looking up data source with ID " + dataSourceID, ex); // NON-NLS + displayName = Bundle.DiscoveryKeyUtils_DataSourceGroupKey_idOnly(dataSourceID); + } + } + + @Override + String getDisplayName() { + return displayName; + } + + @Override + public int compareTo(GroupKey otherGroupKey) { + if (otherGroupKey instanceof DataSourceGroupKey) { + DataSourceGroupKey otherDataSourceGroupKey = (DataSourceGroupKey) otherGroupKey; + return Long.compare(getDataSourceID(), otherDataSourceGroupKey.getDataSourceID()); + } else { + return compareClassNames(otherGroupKey); + } + } + + @Override + public boolean equals(Object otherKey) { + if (otherKey == this) { + return true; + } + + if (!(otherKey instanceof DataSourceGroupKey)) { + return false; + } + + DataSourceGroupKey otherDataSourceGroupKey = (DataSourceGroupKey) otherKey; + return getDataSourceID() == otherDataSourceGroupKey.getDataSourceID(); + } + + @Override + public int hashCode() { + return Objects.hash(getDataSourceID()); + } + + /** + * Get the object ID of the data source. + * + * @return The object ID of the data source. + */ + long getDataSourceID() { + return dataSourceID; + } + } + + /** + * Dummy key for when there is no grouping. All files will have the same + * key. + */ + static class NoGroupingGroupKey extends GroupKey { + + /** + * Constructor for dummy group which puts all files together. + */ + NoGroupingGroupKey() { + // Nothing to save - all files will get the same GroupKey + } + + @NbBundle.Messages({ + "DiscoveryKeyUtils.NoGroupingGroupKey.allFiles=All Files"}) + @Override + String getDisplayName() { + return Bundle.DiscoveryKeyUtils_NoGroupingGroupKey_allFiles(); + } + + @Override + public int compareTo(GroupKey otherGroupKey) { + // As long as the other key is the same type, they are equal + if (otherGroupKey instanceof NoGroupingGroupKey) { + return 0; + } else { + return compareClassNames(otherGroupKey); + } + } + + @Override + public boolean equals(Object otherKey) { + if (otherKey == this) { + return true; + } + // As long as the other key is the same type, they are equal + return otherKey instanceof NoGroupingGroupKey; + } + + @Override + public int hashCode() { + return 0; + } + } + + /** + * Key representing a central repository frequency group. + */ + static class FrequencyGroupKey extends GroupKey { + + private final SearchData.Frequency frequency; + + /** + * Construct a new FrequencyGroupKey. + * + * @param result The Result to create the group key for. + */ + FrequencyGroupKey(Result result) { + frequency = result.getFrequency(); + } + + @Override + String getDisplayName() { + return getFrequency().toString(); + } + + @Override + public int compareTo(GroupKey otherGroupKey) { + if (otherGroupKey instanceof FrequencyGroupKey) { + FrequencyGroupKey otherFrequencyGroupKey = (FrequencyGroupKey) otherGroupKey; + return Integer.compare(getFrequency().getRanking(), otherFrequencyGroupKey.getFrequency().getRanking()); + } else { + return compareClassNames(otherGroupKey); + } + } + + @Override + public boolean equals(Object otherKey) { + if (otherKey == this) { + return true; + } + + if (!(otherKey instanceof FrequencyGroupKey)) { + return false; + } + + FrequencyGroupKey otherFrequencyGroupKey = (FrequencyGroupKey) otherKey; + return getFrequency().equals(otherFrequencyGroupKey.getFrequency()); + } + + @Override + public int hashCode() { + return Objects.hash(getFrequency().getRanking()); + } + + /** + * Get the frequency which the group is for. + * + * @return The frequency which the group is for. + */ + SearchData.Frequency getFrequency() { + return frequency; + } + } + + /** + * Key representing a hash hits group. + */ + static class HashHitsGroupKey extends GroupKey { + + private final List hashSetNames; + private final String hashSetNamesString; + + /** + * Construct a new HashHitsGroupKey. + * + * @param file The file to create the group key for. + */ + @NbBundle.Messages({ + "DiscoveryKeyUtils.HashHitsGroupKey.noHashHits=None"}) + HashHitsGroupKey(ResultFile file) { + hashSetNames = file.getHashSetNames(); + + if (hashSetNames.isEmpty()) { + hashSetNamesString = Bundle.DiscoveryKeyUtils_HashHitsGroupKey_noHashHits(); + } else { + hashSetNamesString = String.join(",", hashSetNames); // NON-NLS + } + } + + @Override + String getDisplayName() { + return getHashSetNamesString(); + } + + @Override + public int compareTo(GroupKey otherGroupKey) { + if (otherGroupKey instanceof HashHitsGroupKey) { + HashHitsGroupKey otherHashHitsGroupKey = (HashHitsGroupKey) otherGroupKey; + + // Put the empty list at the end + if (getHashSetNames().isEmpty()) { + if (otherHashHitsGroupKey.getHashSetNames().isEmpty()) { + return 0; + } else { + return 1; + } + } else if (otherHashHitsGroupKey.getHashSetNames().isEmpty()) { + return -1; + } + + return getHashSetNamesString().compareTo(otherHashHitsGroupKey.getHashSetNamesString()); + } else { + return compareClassNames(otherGroupKey); + } + } + + @Override + public boolean equals(Object otherKey) { + if (otherKey == this) { + return true; + } + + if (!(otherKey instanceof HashHitsGroupKey)) { + return false; + } + + HashHitsGroupKey otherHashHitsGroupKey = (HashHitsGroupKey) otherKey; + return getHashSetNamesString().equals(otherHashHitsGroupKey.getHashSetNamesString()); + } + + @Override + public int hashCode() { + return Objects.hash(getHashSetNamesString()); + } + + /** + * Get the list of hash set names the group is for. + * + * @return The list of hash set names the group is for. + */ + List getHashSetNames() { + return Collections.unmodifiableList(hashSetNames); + } + + /** + * Get the String representation of the list of hash set names. + * + * @return The String representation of the list of hash set names. + */ + String getHashSetNamesString() { + return hashSetNamesString; + } + } + + /** + * Key representing a interesting item set group. + */ + static class InterestingItemGroupKey extends GroupKey { + + private final List interestingItemSetNames; + private final String interestingItemSetNamesString; + + /** + * Construct a new InterestingItemGroupKey. + * + * @param file The file to create the group key for. + */ + @NbBundle.Messages({ + "DiscoveryKeyUtils.InterestingItemGroupKey.noSets=None"}) + InterestingItemGroupKey(ResultFile file) { + interestingItemSetNames = file.getInterestingSetNames(); + + if (interestingItemSetNames.isEmpty()) { + interestingItemSetNamesString = Bundle.DiscoveryKeyUtils_InterestingItemGroupKey_noSets(); + } else { + interestingItemSetNamesString = String.join(",", interestingItemSetNames); // NON-NLS + } + } + + @Override + String getDisplayName() { + return getInterestingItemSetNamesString(); + } + + @Override + public int compareTo(GroupKey otherGroupKey) { + if (otherGroupKey instanceof InterestingItemGroupKey) { + InterestingItemGroupKey otherInterestingItemGroupKey = (InterestingItemGroupKey) otherGroupKey; + + // Put the empty list at the end + if (this.getInterestingItemSetNames().isEmpty()) { + if (otherInterestingItemGroupKey.getInterestingItemSetNames().isEmpty()) { + return 0; + } else { + return 1; + } + } else if (otherInterestingItemGroupKey.getInterestingItemSetNames().isEmpty()) { + return -1; + } + + return getInterestingItemSetNamesString().compareTo(otherInterestingItemGroupKey.getInterestingItemSetNamesString()); + } else { + return compareClassNames(otherGroupKey); + } + } + + @Override + public boolean equals(Object otherKey) { + if (otherKey == this) { + return true; + } + + if (!(otherKey instanceof InterestingItemGroupKey)) { + return false; + } + + InterestingItemGroupKey otherInterestingItemGroupKey = (InterestingItemGroupKey) otherKey; + return getInterestingItemSetNamesString().equals(otherInterestingItemGroupKey.getInterestingItemSetNamesString()); + } + + @Override + public int hashCode() { + return Objects.hash(getInterestingItemSetNamesString()); + } + + /** + * Get the list of interesting item set names the group is for. + * + * @return The list of interesting item set names the group is for. + */ + List getInterestingItemSetNames() { + return Collections.unmodifiableList(interestingItemSetNames); + } + + /** + * Get the String representation of the interesting item set names the + * group is for. + * + * @return The String representation of the interesting item set names + * the group is for. + */ + String getInterestingItemSetNamesString() { + return interestingItemSetNamesString; + } + } + + /** + * Key representing a date of most recent activity. + */ + static class MostRecentActivityDateGroupKey extends GroupKey { + + private final Long epochDate; + private final String dateNameString; + + /** + * Construct a new MostRecentActivityDateGroupKey. + * + * @param result The Result to create the group key for. + */ + @NbBundle.Messages({ + "DiscoveryKeyUtils.MostRecentActivityDateGroupKey.noDate=No Date Available"}) + MostRecentActivityDateGroupKey(Result result) { + if (result instanceof ResultDomain) { + epochDate = ((ResultDomain) result).getActivityEnd(); + dateNameString = new SimpleDateFormat("yyyy/MM/dd", Locale.getDefault()).format(new Date(TimeUnit.SECONDS.toMillis(epochDate))); + } else { + epochDate = Long.MAX_VALUE; + dateNameString = Bundle.DiscoveryKeyUtils_MostRecentActivityDateGroupKey_noDate(); + } + } + + @Override + String getDisplayName() { + return getDateNameString(); + } + + @Override + public boolean equals(Object otherKey) { + if (otherKey == this) { + return true; + } + + if (!(otherKey instanceof MostRecentActivityDateGroupKey)) { + return false; + } + + MostRecentActivityDateGroupKey dateGroupKey = (MostRecentActivityDateGroupKey) otherKey; + return getDateNameString().equals(dateGroupKey.getDateNameString()); + } + + @Override + public int hashCode() { + return Objects.hash(getDateNameString()); + } + + @Override + public int compareTo(GroupKey otherGroupKey) { + if (otherGroupKey instanceof MostRecentActivityDateGroupKey) { + MostRecentActivityDateGroupKey otherDateGroupKey = (MostRecentActivityDateGroupKey) otherGroupKey; + + // Put the empty list at the end + if (this.getEpochDate().equals(Long.MAX_VALUE)) { + if (otherDateGroupKey.getEpochDate().equals(Long.MAX_VALUE)) { + return 0; + } else { + return 1; + } + } else if (otherDateGroupKey.getEpochDate().equals(Long.MAX_VALUE)) { + return -1; + } + + return getDateNameString().compareTo(otherDateGroupKey.getDateNameString()); + } else { + return compareClassNames(otherGroupKey); + } + } + + /** + * Get the date this group is for as a Long. + * + * @return The date. + */ + Long getEpochDate() { + return epochDate; + } + + /** + * Get the name which identifies this group. + * + * @return The dateNameString. + */ + String getDateNameString() { + return dateNameString; + } + } + + /** + * Key representing a date of first activity. + */ + static class FirstActivityDateGroupKey extends GroupKey { + + private final Long epochDate; + private final String dateNameString; + + /** + * Construct a new FirstActivityDateGroupKey. + * + * @param result The Result to create the group key for. + */ + @NbBundle.Messages({ + "DiscoveryKeyUtils.FirstActivityDateGroupKey.noDate=No Date Available"}) + FirstActivityDateGroupKey(Result result) { + if (result instanceof ResultDomain) { + epochDate = ((ResultDomain) result).getActivityStart(); + dateNameString = new SimpleDateFormat("yyyy/MM/dd", Locale.getDefault()).format(new Date(TimeUnit.SECONDS.toMillis(epochDate))); + } else { + epochDate = Long.MAX_VALUE; + dateNameString = Bundle.DiscoveryKeyUtils_FirstActivityDateGroupKey_noDate(); + } + } + + @Override + String getDisplayName() { + return getDateNameString(); + } + + @Override + public boolean equals(Object otherKey) { + if (otherKey == this) { + return true; + } + + if (!(otherKey instanceof FirstActivityDateGroupKey)) { + return false; + } + + FirstActivityDateGroupKey dateGroupKey = (FirstActivityDateGroupKey) otherKey; + return getDateNameString().equals(dateGroupKey.getDateNameString()); + } + + @Override + public int hashCode() { + return Objects.hash(getDateNameString()); + } + + @Override + public int compareTo(GroupKey otherGroupKey) { + if (otherGroupKey instanceof FirstActivityDateGroupKey) { + FirstActivityDateGroupKey otherDateGroupKey = (FirstActivityDateGroupKey) otherGroupKey; + + // Put the empty list at the end + if (this.getEpochDate().equals(Long.MAX_VALUE)) { + if (otherDateGroupKey.getEpochDate().equals(Long.MAX_VALUE)) { + return 0; + } else { + return 1; + } + } else if (otherDateGroupKey.getEpochDate().equals(Long.MAX_VALUE)) { + return -1; + } + + return getDateNameString().compareTo(otherDateGroupKey.getDateNameString()); + } else { + return compareClassNames(otherGroupKey); + } + } + + /** + * Get the date this group is for as a Long. + * + * @return The date. + */ + Long getEpochDate() { + return epochDate; + } + + /** + * Get the name which identifies this group. + * + * @return The dateNameString. + */ + String getDateNameString() { + return dateNameString; + } + } + + /** + * Key representing the number of visits. + */ + static class NumberOfVisitsGroupKey extends GroupKey { + + private final String displayName; + private final Long visits; + + /** + * Construct a new NumberOfVisitsGroupKey. + * + * @param result The Result to create the group key for. + */ + @NbBundle.Messages({ + "# {0} - totalVisits", + "DiscoveryKeyUtils.NumberOfVisitsGroupKey.displayName={0} visits", + "DiscoveryKeyUtils.NumberOfVisitsGroupKey.noVisits=No visits"}) + NumberOfVisitsGroupKey(Result result) { + if (result instanceof ResultDomain) { + Long totalVisits = ((ResultDomain) result).getTotalVisits(); + if (totalVisits == null) { + totalVisits = 0L; + } + visits = totalVisits; + displayName = Bundle.DiscoveryKeyUtils_NumberOfVisitsGroupKey_displayName(Long.toString(visits)); + } else { + displayName = Bundle.DiscoveryKeyUtils_NumberOfVisitsGroupKey_noVisits(); + visits = -1L; + } + } + + @Override + String getDisplayName() { + return displayName; + } + + @Override + public int hashCode() { + return Objects.hash(displayName); + } + + /** + * Get the number of visits this group is for. + * + * @return The number of visits this group is for. + */ + Long getVisits() { + return visits; + } + + @Override + public boolean equals(Object otherKey) { + if (otherKey == this) { + return true; + } + + if (!(otherKey instanceof NumberOfVisitsGroupKey)) { + return false; + } + + NumberOfVisitsGroupKey visitsKey = (NumberOfVisitsGroupKey) otherKey; + return visits.equals(visitsKey.getVisits()); + } + + @Override + public int compareTo(GroupKey otherGroupKey) { + if (otherGroupKey instanceof NumberOfVisitsGroupKey) { + NumberOfVisitsGroupKey visitsKey = (NumberOfVisitsGroupKey) otherGroupKey; + return Long.compare(getVisits(), visitsKey.getVisits()); + } else { + return compareClassNames(otherGroupKey); + } + } + } + + /** + * Key representing an object detected group. + */ + static class ObjectDetectedGroupKey extends GroupKey { + + private final List objectDetectedNames; + private final String objectDetectedNamesString; + + /** + * Construct a new ObjectDetectedGroupKey. + * + * @param file The file to create the group key for. + */ + @NbBundle.Messages({ + "DiscoveryKeyUtils.ObjectDetectedGroupKey.noSets=None"}) + ObjectDetectedGroupKey(ResultFile file) { + objectDetectedNames = file.getObjectDetectedNames(); + if (objectDetectedNames.isEmpty()) { + objectDetectedNamesString = Bundle.DiscoveryKeyUtils_ObjectDetectedGroupKey_noSets(); + } else { + objectDetectedNamesString = String.join(",", objectDetectedNames); // NON-NLS + } + } + + @Override + String getDisplayName() { + return getObjectDetectedNamesString(); + } + + @Override + public int compareTo(GroupKey otherGroupKey) { + if (otherGroupKey instanceof ObjectDetectedGroupKey) { + ObjectDetectedGroupKey otherObjectDetectedGroupKey = (ObjectDetectedGroupKey) otherGroupKey; + + // Put the empty list at the end + if (this.getObjectDetectedNames().isEmpty()) { + if (otherObjectDetectedGroupKey.getObjectDetectedNames().isEmpty()) { + return 0; + } else { + return 1; + } + } else if (otherObjectDetectedGroupKey.getObjectDetectedNames().isEmpty()) { + return -1; + } + + return getObjectDetectedNamesString().compareTo(otherObjectDetectedGroupKey.getObjectDetectedNamesString()); + } else { + return compareClassNames(otherGroupKey); + } + } + + @Override + public boolean equals(Object otherKey) { + if (otherKey == this) { + return true; + } + + if (!(otherKey instanceof ObjectDetectedGroupKey)) { + return false; + } + + ObjectDetectedGroupKey otherObjectDetectedGroupKey = (ObjectDetectedGroupKey) otherKey; + return getObjectDetectedNamesString().equals(otherObjectDetectedGroupKey.getObjectDetectedNamesString()); + } + + @Override + public int hashCode() { + return Objects.hash(getObjectDetectedNamesString()); + } + + /** + * Get the list of object detected names for this group. + * + * @return The list of object detected names for this group. + */ + List getObjectDetectedNames() { + return Collections.unmodifiableList(objectDetectedNames); + } + + /** + * Get the String representation of the object detected names for this + * group. + * + * @return The String representation of the object detected names for + * this group. + */ + String getObjectDetectedNamesString() { + return objectDetectedNamesString; + } + } + + /** + * Private constructor for GroupKeyUtils utility class. + */ + private DiscoveryKeyUtils() { + //private constructor in a utility class intentionally left blank + } +} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearch.java b/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearch.java new file mode 100644 index 0000000000..c616ba12a8 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearch.java @@ -0,0 +1,155 @@ +/* + * Autopsy + * + * Copyright 2020 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.discovery.search; + +import java.awt.Image; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; +import org.sleuthkit.autopsy.discovery.search.DiscoveryKeyUtils.GroupKey; +import org.sleuthkit.datamodel.SleuthkitCase; + +/** + * Main class to perform the domain search. + */ +public class DomainSearch { + + private final DomainSearchCache searchCache; + private final DomainSearchThumbnailCache thumbnailCache; + + /** + * Construct a new DomainSearch object. + */ + public DomainSearch() { + this(new DomainSearchCache(), new DomainSearchThumbnailCache()); + } + + /** + * Construct a new DomainSearch object with an existing DomainSearchCache + * and DomainSearchThumbnailCache. + * + * @param cache The DomainSearchCache to use for this DomainSearch. + * @param thumbnailCache The DomainSearchThumnailCache to use for this + * DomainSearch. + */ + DomainSearch(DomainSearchCache cache, DomainSearchThumbnailCache thumbnailCache) { + this.searchCache = cache; + this.thumbnailCache = thumbnailCache; + } + + /** + * Run the domain search to get the group keys and sizes. Clears cache of + * search results, caching new results for access at later time. + * + * @param userName The name of the user performing the search. + * @param filters The filters to apply. + * @param groupAttributeType The attribute to use for grouping. + * @param groupSortingType The method to use to sort the groups. + * @param domainSortingMethod The method to use to sort the domains within + * the groups. + * @param caseDb The case database. + * @param centralRepoDb The central repository database. Can be null + * if not needed. + * + * @return A LinkedHashMap grouped and sorted according to the parameters. + * + * @throws DiscoveryException + */ + public Map getGroupSizes(String userName, + List filters, + DiscoveryAttributes.AttributeType groupAttributeType, + Group.GroupSortingAlgorithm groupSortingType, + ResultsSorter.SortingMethod domainSortingMethod, + SleuthkitCase caseDb, CentralRepository centralRepoDb) throws DiscoveryException { + + 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()) { + groupSizes.put(groupKey, searchResults.get(groupKey).size()); + } + + return groupSizes; + } + + /** + * Get the domains from the specified group from the cache, if the the group + * was not cached perform a search caching the groups. + * + * @param userName The name of the user performing the search. + * @param filters The filters to apply. + * @param groupAttributeType The attribute to use for grouping. + * @param groupSortingType The method to use to sort the groups. + * @param domainSortingMethod The method to use to sort the Domains within + * the groups. + * @param groupKey The key which uniquely identifies the group to + * get entries from. + * @param startingEntry The first entry to return. + * @param numberOfEntries The number of entries to return. + * @param caseDb The case database. + * @param centralRepoDb The central repository database. Can be null + * if not needed. + * + * @return A LinkedHashMap grouped and sorted according to the parameters. + * + * @throws DiscoveryException + */ + public List getDomainsInGroup(String userName, + List filters, + DiscoveryAttributes.AttributeType groupAttributeType, + Group.GroupSortingAlgorithm groupSortingType, + ResultsSorter.SortingMethod domainSortingMethod, + GroupKey groupKey, int startingEntry, int numberOfEntries, + SleuthkitCase caseDb, CentralRepository centralRepoDb) throws DiscoveryException { + + final Map> searchResults = searchCache.get( + 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++) { + page.add(domainsInGroup.get(i)); + } + + return page; + } + + /** + * Get a thumbnail representation of a domain name. See + * DomainSearchThumbnailRequest for more details. + * + * @param thumbnailRequest Thumbnail request for domain. + * + * @return An Image instance or null if no thumbnail is available. + * + * @throws DiscoveryException If there is an error with Discovery related + * processing. + */ + public Image getThumbnail(DomainSearchThumbnailRequest thumbnailRequest) throws DiscoveryException { + return thumbnailCache.get(thumbnailRequest); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchArtifactsCache.java b/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchArtifactsCache.java new file mode 100755 index 0000000000..41cfcc7e9b --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchArtifactsCache.java @@ -0,0 +1,56 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.discovery.search; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.LoadingCache; +import java.util.List; +import java.util.concurrent.ExecutionException; +import org.sleuthkit.datamodel.BlackboardArtifact; + +/** + * Caches artifact requests. + */ +public class DomainSearchArtifactsCache { + + private static final int MAXIMUM_CACHE_SIZE = 500; + private static final LoadingCache> cache + = CacheBuilder.newBuilder() + .maximumSize(MAXIMUM_CACHE_SIZE) + .build(new DomainSearchArtifactsLoader()); + + /** + * Get artifact instances that match the requested criteria. If the request + * is new, the results will be automatically loaded. + * + * @param request Artifact request, specifies type, Case, and domain name. + * + * @return A list of matching artifacts. + * + * @throws DiscoveryException Any error that occurs during the loading + * process. + */ + public List get(DomainSearchArtifactsRequest request) throws DiscoveryException { + try { + return cache.get(request); + } catch (ExecutionException ex) { + throw new DiscoveryException("Error fetching artifacts from cache", ex); + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchArtifactsLoader.java b/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchArtifactsLoader.java new file mode 100755 index 0000000000..49bdce0c2d --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchArtifactsLoader.java @@ -0,0 +1,64 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.discovery.search; + +import com.google.common.cache.CacheLoader; +import java.util.List; +import java.util.ArrayList; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE; +import org.sleuthkit.datamodel.BlackboardAttribute.Type; +import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.BlackboardArtifact; + +/** + * Loads artifacts for the given request. Searches TSK_DOMAIN and TSK_URL + * attributes for the requested domain name. TSK_DOMAIN is exact match (ignoring + * case). TSK_URL is sub-string match (ignoring case). + */ +public class DomainSearchArtifactsLoader extends CacheLoader> { + + private static final Type TSK_DOMAIN = new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DOMAIN); + private static final Type TSK_URL = new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_URL); + + @Override + public List load(DomainSearchArtifactsRequest artifactsRequest) throws TskCoreException, InterruptedException { + final SleuthkitCase caseDb = artifactsRequest.getSleuthkitCase(); + final String normalizedDomain = artifactsRequest.getDomain().toLowerCase(); + final List artifacts = caseDb.getBlackboardArtifacts(artifactsRequest.getArtifactType()); + final List matchingDomainArtifacts = new ArrayList<>(); + + for (BlackboardArtifact artifact : artifacts) { + if(Thread.currentThread().isInterrupted()) { + throw new InterruptedException(); + } + final BlackboardAttribute tskDomain = artifact.getAttribute(TSK_DOMAIN); + final BlackboardAttribute tskUrl = artifact.getAttribute(TSK_URL); + + if (tskDomain != null && tskDomain.getValueString().equalsIgnoreCase(normalizedDomain)) { + matchingDomainArtifacts.add(artifact); + } else if (tskUrl != null && tskUrl.getValueString().toLowerCase().contains(normalizedDomain)) { + matchingDomainArtifacts.add(artifact); + } + } + + return matchingDomainArtifacts; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchArtifactsRequest.java b/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchArtifactsRequest.java new file mode 100755 index 0000000000..7391858981 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchArtifactsRequest.java @@ -0,0 +1,93 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.discovery.search; + +import java.util.Objects; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE; + +/** + * Requests artifacts of a specific type and domain from a given Case. + */ +public class DomainSearchArtifactsRequest { + + private final SleuthkitCase sleuthkitCase; + private final String domain; + private final ARTIFACT_TYPE artifactType; + + /** + * Construct a new DomainSearchArtifactsRequest object. + * + * @param sleuthkitCase The case database for the search. + * @param domain The domain that artifacts are being requested for. + * @param artifactType The type of artifact being requested. + */ + public DomainSearchArtifactsRequest(SleuthkitCase sleuthkitCase, + String domain, ARTIFACT_TYPE artifactType) { + this.sleuthkitCase = sleuthkitCase; + this.domain = domain; + this.artifactType = artifactType; + } + + /** + * Get the case database for the search. + * + * @return The case database for the search. + */ + public SleuthkitCase getSleuthkitCase() { + return sleuthkitCase; + } + + /** + * Get the domain that artifacts are being requested for. + * + * @return The domain that artifacts are being requested for. + */ + public String getDomain() { + return domain; + } + + /** + * Get the type of artifact being requested. + * + * @return The type of artifact being requested. + */ + public ARTIFACT_TYPE getArtifactType() { + return artifactType; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof DomainSearchArtifactsRequest)) { + return false; + } + DomainSearchArtifactsRequest otherRequest = (DomainSearchArtifactsRequest) other; + return this.sleuthkitCase == otherRequest.getSleuthkitCase() + && this.domain.equals(otherRequest.getDomain()) + && this.artifactType == otherRequest.getArtifactType(); + } + + @Override + public int hashCode() { + return 79 * 5 + Objects.hash(this.domain, this.artifactType); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchCache.java b/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchCache.java new file mode 100755 index 0000000000..306a66b287 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchCache.java @@ -0,0 +1,77 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.discovery.search; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.LoadingCache; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; +import org.sleuthkit.autopsy.discovery.search.DiscoveryKeyUtils.GroupKey; +import org.sleuthkit.autopsy.discovery.search.DiscoveryKeyUtils.SearchKey; +import org.sleuthkit.datamodel.SleuthkitCase; + +/** + * Caches results for domain searches initiated by the user in the Discovery + * panel. Uses a Guava Cache as a backing data structure. See + * DomainSearchCacheLoader for database querying in the event of a cache miss. + */ +class DomainSearchCache { + + private static final int MAXIMUM_CACHE_SIZE = 10; + private static final LoadingCache>> cache + = CacheBuilder.newBuilder() + .maximumSize(MAXIMUM_CACHE_SIZE) + .build(new DomainSearchCacheLoader()); + + /** + * Get domain search results matching the given parameters. If no results + * are found, the cache will automatically load them. + * + * + * @param userName The name of the user performing the search. + * @param filters The filters to apply. + * @param groupAttributeType The attribute to use for grouping. + * @param groupSortingType The method to use to sort the groups. + * @param fileSortingMethod The method to use to sort the domains within + * the groups. + * @param caseDb The case database. + * @param centralRepoDb The central repository database. Can be null if + * not needed. + * + * @return Domain search results matching the given parameters. + * + * @throws DiscoveryException + */ + Map> get(String userName, + List filters, + DiscoveryAttributes.AttributeType groupAttributeType, + Group.GroupSortingAlgorithm groupSortingType, + ResultsSorter.SortingMethod domainSortingMethod, + SleuthkitCase caseDb, CentralRepository centralRepoDb) throws DiscoveryException { + try { + final SearchKey searchKey = new SearchKey(userName, filters, groupAttributeType, + groupSortingType, domainSortingMethod, caseDb, centralRepoDb); + return cache.get(searchKey); + } catch (ExecutionException ex) { + throw new DiscoveryException("Error fetching results from cache", ex.getCause()); + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchCacheLoader.java b/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchCacheLoader.java new file mode 100755 index 0000000000..11b34f5fd4 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchCacheLoader.java @@ -0,0 +1,365 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.discovery.search; + +import com.google.common.cache.CacheLoader; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.StringJoiner; +import org.apache.commons.lang3.tuple.Pair; +import org.sleuthkit.autopsy.discovery.search.DiscoveryAttributes.AttributeType; +import org.sleuthkit.autopsy.discovery.search.DiscoveryAttributes.DataSourceAttribute; +import org.sleuthkit.autopsy.discovery.search.DiscoveryKeyUtils.GroupKey; +import org.sleuthkit.autopsy.discovery.search.DiscoveryKeyUtils.SearchKey; +import org.sleuthkit.autopsy.discovery.search.SearchFiltering.ArtifactDateRangeFilter; +import org.sleuthkit.autopsy.discovery.search.SearchFiltering.ArtifactTypeFilter; +import org.sleuthkit.autopsy.discovery.search.SearchFiltering.DataSourceFilter; +import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_DOWNLOAD; +import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_HISTORY; +import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN; +import org.sleuthkit.datamodel.CaseDbAccessManager; +import org.sleuthkit.datamodel.CaseDbAccessManager.CaseDbAccessQueryCallback; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Loads domain search results for cache misses. This loader is a Guava cache + * loader, which will be used in tandem with the DomainSearchCache, which is + * backed by a Guava LoadingCache. + */ +class DomainSearchCacheLoader extends CacheLoader>> { + + @Override + public Map> load(SearchKey key) throws DiscoveryException, SQLException, TskCoreException, InterruptedException { + List domainResults = getResultDomainsFromDatabase(key); + // Grouping by CR Frequency, for example, will require further processing + // in order to make the correct decision. The attribute types that require + // more information implement their logic by overriding `addAttributeToResults`. + List searchAttributes = new ArrayList<>(); + searchAttributes.add(key.getGroupAttributeType()); + searchAttributes.addAll(key.getFileSortingMethod().getRequiredAttributes()); + for (AttributeType attr : searchAttributes) { + if (Thread.currentThread().isInterrupted()) { + throw new InterruptedException(); + } + attr.addAttributeToResults(domainResults, + key.getSleuthkitCase(), key.getCentralRepository()); + } + // Apply secondary in memory filters + for (AbstractFilter filter : key.getFilters()) { + if (Thread.currentThread().isInterrupted()) { + throw new InterruptedException(); + } + if (filter.useAlternateFilter()) { + domainResults = filter.applyAlternateFilter(domainResults, key.getSleuthkitCase(), key.getCentralRepository()); + } + } + // Sort the ResultDomains by the requested criteria. + final SearchResults searchResults = new SearchResults( + key.getGroupSortingType(), + key.getGroupAttributeType(), + key.getFileSortingMethod()); + searchResults.add(domainResults); + return searchResults.toLinkedHashMap(); + } + + /** + * Queries for domain names from the case database. + * + * @param key The SearchKey passed to the cache. + * + * @return A list of results corresponding to the domains found in the case + * database. + */ + List getResultDomainsFromDatabase(SearchKey key) throws TskCoreException, SQLException, DiscoveryException, InterruptedException { + + // Filters chosen in the UI are aggregated into SQL statements to be used in + // the queries that follow. + final Pair filterClauses = createWhereAndHavingClause(key.getFilters()); + final String whereClause = filterClauses.getLeft(); + final String havingClause = filterClauses.getRight(); + + // You may think of each row of this result as a TSK_DOMAIN attribute, where the parent + // artifact type is within the (optional) filter and the parent artifact + // had a date time attribute that was within the (optional) filter. With this + // table in hand, we can simply group by domain and apply aggregate functions + // to get, for example, # of downloads, # of visits in last 60, etc. + final String domainsTable + = "SELECT LOWER(MAX(value_text)) AS domain," + + " MAX(value_int64) AS date," + + " artifact_id AS parent_artifact_id," + + " MAX(artifact_type_id) AS parent_artifact_type_id " + + "FROM blackboard_attributes " + + "WHERE " + whereClause + " " + + "GROUP BY artifact_id " + + "HAVING " + havingClause; + + // Needed to populate the visitsInLast60 data. + final Instant currentTime = Instant.now(); + final Instant sixtyDaysAgo = currentTime.minus(60, ChronoUnit.DAYS); + + // Check the group attribute, if by data source then the GROUP BY clause + // should group by data source id before grouping by domain. + final AttributeType groupAttribute = key.getGroupAttributeType(); + final String groupByClause = (groupAttribute instanceof DataSourceAttribute) + ? "data_source_obj_id, domain" : "domain"; + + final Optional dataSourceFilter = key.getFilters().stream() + .filter(filter -> filter instanceof DataSourceFilter) + .findFirst(); + + String dataSourceWhereClause = null; + if (dataSourceFilter.isPresent()) { + dataSourceWhereClause = dataSourceFilter.get().getWhereClause(); + } + + // This query just processes the domains table, performing additional + // groupings and applying aggregate functions to calculate discovery data. + final String domainsQuery + = /* + * SELECT + */ " domain," + + " MIN(date) AS activity_start," + + " MAX(date) AS activity_end," + + " SUM(CASE " + + " WHEN artifact_type_id = " + TSK_WEB_DOWNLOAD.getTypeID() + " THEN 1 " + + " ELSE 0 " + + " END) AS fileDownloads," + + " SUM(CASE " + + " WHEN artifact_type_id = " + TSK_WEB_HISTORY.getTypeID() + " THEN 1 " + + " ELSE 0 " + + " END) AS totalVisits," + + " SUM(CASE " + + " WHEN artifact_type_id = " + TSK_WEB_HISTORY.getTypeID() + " AND" + + " date BETWEEN " + sixtyDaysAgo.getEpochSecond() + " AND " + currentTime.getEpochSecond() + " THEN 1 " + + " ELSE 0 " + + " END) AS last60," + + " MAX(data_source_obj_id) AS dataSource " + + "FROM blackboard_artifacts" + + " JOIN (" + domainsTable + ") AS domains_table" + + " ON artifact_id = parent_artifact_id " + + // Add the data source where clause here if present. + ((dataSourceWhereClause != null) ? "WHERE " + dataSourceWhereClause + " " : "") + + "GROUP BY " + groupByClause; + + final SleuthkitCase caseDb = key.getSleuthkitCase(); + final CaseDbAccessManager dbManager = caseDb.getCaseDbAccessManager(); + + final DomainCallback domainCallback = new DomainCallback(caseDb); + dbManager.select(domainsQuery, domainCallback); + + if (domainCallback.getSQLException() != null) { + throw domainCallback.getSQLException(); + } + + if (domainCallback.getTskCoreException() != null) { + throw domainCallback.getTskCoreException(); + } + + if (domainCallback.getInterruptedException() != null) { + throw domainCallback.getInterruptedException(); + } + + return domainCallback.getResultDomains(); + } + + /** + * A utility method to transform filters into the necessary SQL statements + * for the domainsTable query. The complexity of that query requires this + * transformation process to be conditional. The date time filter is a good + * example of the type of conditional handling that follows in the method + * below. If no dateTime filter is supplied, then in order for the query to + * be correct, an additional clause needs to be added in. + * + * @param filters The list of filters to apply create the where clause from. + * + * @return The whereClause and havingClause as a pair. These methods are one + * to stress that these clauses are tightly coupled. + */ + Pair createWhereAndHavingClause(List filters) { + final StringJoiner whereClause = new StringJoiner(" OR "); + final StringJoiner havingClause = new StringJoiner(" AND "); + + String artifactTypeFilter = null; + boolean hasDateTimeFilter = false; + + for (AbstractFilter filter : filters) { + if (filter instanceof ArtifactTypeFilter) { + artifactTypeFilter = filter.getWhereClause(); + } else if (!(filter instanceof DataSourceFilter) && !filter.useAlternateFilter()) { + if (filter instanceof ArtifactDateRangeFilter) { + hasDateTimeFilter = true; + } + + whereClause.add("(" + filter.getWhereClause() + ")"); + havingClause.add("SUM(CASE WHEN " + filter.getWhereClause() + " THEN 1 ELSE 0 END) > 0"); + } + } + + if (!hasDateTimeFilter) { + whereClause.add(ArtifactDateRangeFilter.createAttributeTypeClause()); + } + + String domainAttributeFilter = "attribute_type_id = " + TSK_DOMAIN.getTypeID() + + " AND value_text <> ''"; + + whereClause.add("(" + domainAttributeFilter + ")"); + havingClause.add("SUM(CASE WHEN " + domainAttributeFilter + " THEN 1 ELSE 0 END) > 0"); + + return Pair.of( + whereClause.toString() + ((artifactTypeFilter != null) ? " AND (" + artifactTypeFilter + ")" : ""), + havingClause.toString() + ); + } + + /** + * Callback to handle the result set of the domain query. This callback is + * responsible for mapping result set rows into ResultDomain objects for + * display. + */ + private class DomainCallback implements CaseDbAccessQueryCallback { + + private final List resultDomains; + private final SleuthkitCase skc; + private SQLException sqlCause; + private TskCoreException coreCause; + private InterruptedException interruptedException; + + private final Set bannedDomains = new HashSet() { + { + add("localhost"); + add("127.0.0.1"); + } + }; + + /** + * Construct a new DomainCallback object. + * + * @param skc The case database for the query being performed. + */ + private DomainCallback(SleuthkitCase skc) { + this.resultDomains = new ArrayList<>(); + this.skc = skc; + } + + @Override + public void process(ResultSet resultSet) { + try { + resultSet.setFetchSize(500); + + while (resultSet.next()) { + if (Thread.currentThread().isInterrupted()) { + throw new InterruptedException(); + } + + String domain = resultSet.getString("domain"); + + if (bannedDomains.contains(domain)) { + // Skip banned domains + // Domain names are lowercased in the SQL query + continue; + } + + Long activityStart = resultSet.getLong("activity_start"); + if (resultSet.wasNull()) { + activityStart = null; + } + Long activityEnd = resultSet.getLong("activity_end"); + if (resultSet.wasNull()) { + activityEnd = null; + } + Long filesDownloaded = resultSet.getLong("fileDownloads"); + if (resultSet.wasNull()) { + filesDownloaded = null; + } + Long totalVisits = resultSet.getLong("totalVisits"); + if (resultSet.wasNull()) { + totalVisits = null; + } + + Long visitsInLast60 = resultSet.getLong("last60"); + if (resultSet.wasNull()) { + visitsInLast60 = null; + } + Long dataSourceID = resultSet.getLong("dataSource"); + + Content dataSource = skc.getContentById(dataSourceID); + + resultDomains.add(new ResultDomain(domain, activityStart, + activityEnd, totalVisits, visitsInLast60, filesDownloaded, dataSource)); + } + } catch (SQLException ex) { + this.sqlCause = ex; + } catch (TskCoreException ex) { + this.coreCause = ex; + } catch (InterruptedException ex) { + this.interruptedException = ex; + } + } + + /** + * Get the list of Result object for the domains which were in the + * search results. + * + * @return The list of Result object for the domains which were in the + * search results. + */ + private List getResultDomains() { + return Collections.unmodifiableList(this.resultDomains); + } + + /** + * Get the SQLEception in an exception occurred. + * + * @return The SQLEception in an exception occurred. + */ + private SQLException getSQLException() { + return this.sqlCause; + } + + /** + * Get the TskCoreException if a SQL exception occurred. + * + * @return The TskCoreException if a tsk core exception occurred. + */ + private TskCoreException getTskCoreException() { + return this.coreCause; + } + + /** + * Get the interrupted exception if the processing thread was + * interrupted. + * + * @return The interrupted exception or null if none was thrown. + */ + private InterruptedException getInterruptedException() { + return this.interruptedException; + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchThumbnailCache.java b/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchThumbnailCache.java new file mode 100755 index 0000000000..4662c45b7b --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchThumbnailCache.java @@ -0,0 +1,56 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.discovery.search; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.LoadingCache; +import java.awt.Image; +import java.util.concurrent.ExecutionException; + +/** + * Caches thumbnail requests. + */ +public class DomainSearchThumbnailCache { + + private static final int MAXIMUM_CACHE_SIZE = 500; + private static final LoadingCache cache + = CacheBuilder.newBuilder() + .maximumSize(MAXIMUM_CACHE_SIZE) + .build(new DomainSearchThumbnailLoader()); + + /** + * Get a thumbnail for the requested domain. If the request is new, the + * thumbnail will be automatically loaded. + * + * @param request Requested domain to thumbnail. + * + * @return The thumbnail Image instance, or null if no thumbnail is + * available. + * + * @throws DiscoveryException If any error occurs during thumbnail + * generation. + */ + public Image get(DomainSearchThumbnailRequest request) throws DiscoveryException { + try { + return cache.get(request); + } catch (ExecutionException ex) { + throw new DiscoveryException("Error fetching artifacts from cache", ex); + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchThumbnailLoader.java b/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchThumbnailLoader.java new file mode 100755 index 0000000000..309ef798e0 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchThumbnailLoader.java @@ -0,0 +1,175 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.discovery.search; + +import com.google.common.cache.CacheLoader; +import java.awt.Image; +import java.util.List; +import java.util.ArrayList; +import java.util.Collections; +import org.sleuthkit.autopsy.coreutils.ImageUtils; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.AbstractFile; +import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_CACHE; +import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_DOWNLOAD; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.DataSource; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; + +import org.openide.util.ImageUtilities; + +/** + * Loads a thumbnail for the given request. Thumbnail candidates are JPEG files + * that are either TSK_WEB_DOWNLOAD or TSK_WEB_CACHE artifacts. JPEG files are + * sorted by most recent if sourced from TSK_WEB_DOWNLOADs. JPEG files are + * sorted by size if sourced from TSK_WEB_CACHE artifacts. Artifacts are first + * loaded from the DomainSearchArtifactsCache and then further analyzed. + */ +public class DomainSearchThumbnailLoader extends CacheLoader { + + private static final String UNSUPPORTED_IMAGE = "org/sleuthkit/autopsy/images/image-extraction-not-supported.png"; + private static final String JPG_EXTENSION = "jpg"; + private static final String JPG_MIME_TYPE = "image/jpeg"; + private final DomainSearchArtifactsCache artifactsCache; + + /** + * Construct a new DomainSearchThumbnailLoader. + */ + public DomainSearchThumbnailLoader() { + this(new DomainSearchArtifactsCache()); + } + + /** + * Construct a new DomainSearchThumbnailLoader with an existing + * DomainSearchArtifactsCache. + * + * @param artifactsCache The DomainSearchArtifactsCache to use for this + * DomainSearchThumnailLoader. + */ + DomainSearchThumbnailLoader(DomainSearchArtifactsCache artifactsCache) { + this.artifactsCache = artifactsCache; + } + + @Override + public Image load(DomainSearchThumbnailRequest thumbnailRequest) throws TskCoreException, DiscoveryException, InterruptedException { + final SleuthkitCase caseDb = thumbnailRequest.getSleuthkitCase(); + final DomainSearchArtifactsRequest webDownloadsRequest = new DomainSearchArtifactsRequest( + caseDb, thumbnailRequest.getDomain(), TSK_WEB_DOWNLOAD); + final List webDownloads = artifactsCache.get(webDownloadsRequest); + final List webDownloadPictures = getJpegsFromWebDownload(caseDb, webDownloads); + Collections.sort(webDownloadPictures, (file1, file2) -> Long.compare(file1.getCrtime(), file2.getCrtime())); + for (int i = webDownloadPictures.size() - 1; i >= 0; i--) { + if(Thread.currentThread().isInterrupted()) { + throw new InterruptedException(); + } + // Get the most recent image, according to creation time. + final AbstractFile mostRecent = webDownloadPictures.get(i); + + final Image candidateThumbnail = ImageUtils.getThumbnail(mostRecent, thumbnailRequest.getIconSize()); + if (candidateThumbnail != ImageUtils.getDefaultThumbnail()) { + return candidateThumbnail; + } + } + final DomainSearchArtifactsRequest webCacheRequest = new DomainSearchArtifactsRequest( + caseDb, thumbnailRequest.getDomain(), TSK_WEB_CACHE); + final List webCacheArtifacts = artifactsCache.get(webCacheRequest); + final List webCachePictures = getJpegsFromWebCache(caseDb, webCacheArtifacts); + Collections.sort(webCachePictures, (file1, file2) -> Long.compare(file1.getSize(), file2.getSize())); + for (int i = webCachePictures.size() - 1; i >= 0; i--) { + if(Thread.currentThread().isInterrupted()) { + throw new InterruptedException(); + } + // Get the largest image, according to file size. + final AbstractFile largest = webCachePictures.get(i); + final Image candidateThumbnail = ImageUtils.getThumbnail(largest, thumbnailRequest.getIconSize()); + if (candidateThumbnail != ImageUtils.getDefaultThumbnail()) { + return candidateThumbnail; + } + } + return ImageUtilities.loadImage(UNSUPPORTED_IMAGE, false); + } + + /** + * Finds all JPEG source files from TSK_WEB_DOWNLOAD instances. + * + * @param caseDb The case database being searched. + * @param artifacts The list of artifacts to get jpegs from. + * + * @return The list of AbstractFiles representing jpegs which were + * associated with the artifacts. + * + * @throws TskCoreException + */ + private List getJpegsFromWebDownload(SleuthkitCase caseDb, List artifacts) throws TskCoreException, InterruptedException { + final List jpegs = new ArrayList<>(); + for (BlackboardArtifact artifact : artifacts) { + if(Thread.currentThread().isInterrupted()) { + throw new InterruptedException(); + } + final Content sourceContent = caseDb.getContentById(artifact.getObjectID()); + addIfJpeg(jpegs, sourceContent); + } + return jpegs; + } + + /** + * Finds all JPEG source files from TSK_WEB_CACHE instances. + * + * @param caseDb The case database being searched. + * @param artifacts The list of artifacts to get jpegs from. + * + * @return The list of AbstractFiles representing jpegs which were + * associated with the artifacts. + */ + private List getJpegsFromWebCache(SleuthkitCase caseDb, List artifacts) throws TskCoreException, InterruptedException { + final BlackboardAttribute.Type TSK_PATH_ID = new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_PATH_ID); + final List jpegs = new ArrayList<>(); + for (BlackboardArtifact artifact : artifacts) { + if(Thread.currentThread().isInterrupted()) { + throw new InterruptedException(); + } + final BlackboardAttribute tskPathId = artifact.getAttribute(TSK_PATH_ID); + if (tskPathId != null) { + final Content sourceContent = caseDb.getContentById(tskPathId.getValueLong()); + addIfJpeg(jpegs, sourceContent); + } + } + return jpegs; + } + + /** + * Checks if the candidate source content is indeed a JPEG file. + * + * @param files The list of source content files which are jpegs to + * add to. + * @param sourceContent The source content to check and possibly add. + */ + private void addIfJpeg(List files, Content sourceContent) { + if ((sourceContent instanceof AbstractFile) && !(sourceContent instanceof DataSource)) { + final AbstractFile file = (AbstractFile) sourceContent; + if (JPG_EXTENSION.equals(file.getNameExtension()) + || JPG_MIME_TYPE.equals(file.getMIMEType())) { + files.add(file); + } + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchThumbnailRequest.java b/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchThumbnailRequest.java new file mode 100755 index 0000000000..45ce299e61 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchThumbnailRequest.java @@ -0,0 +1,96 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.discovery.search; + +import java.util.Objects; +import org.sleuthkit.datamodel.SleuthkitCase; + +/** + * Requests a thumbnail to be generated for a given Case, domain and size. + * IconSize should be a value obtained from ImageUtils. + */ +public class DomainSearchThumbnailRequest { + + private final SleuthkitCase sleuthkitCase; + private final String domain; + private final int iconSize; + + /** + * Construct a new DomainSearchThumbnailRequest. + * + * @param sleuthkitCase The case database for this thumbnail request. + * @param domain The domain name for this thumbnail request. + * @param iconSize The size of icon that this thumbnail request should + * retrieve. + */ + public DomainSearchThumbnailRequest(SleuthkitCase sleuthkitCase, + String domain, int iconSize) { + this.sleuthkitCase = sleuthkitCase; + this.domain = domain; + this.iconSize = iconSize; + } + + /** + * Get the case database for this thumbnail request. + * + * @return The case database for this thumbnail request. + */ + public SleuthkitCase getSleuthkitCase() { + return sleuthkitCase; + } + + /** + * Get the domain name for this thumbnail request. + * + * @return The domain name for this thumbnail request. + */ + public String getDomain() { + return domain; + } + + /** + * Get the size of icon that this thumbnail request should retrieve. + * + * @return The size of icon that this thumbnail request should retrieve. + */ + public int getIconSize() { + return iconSize; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof DomainSearchThumbnailRequest)) { + return false; + } + + DomainSearchThumbnailRequest otherRequest = (DomainSearchThumbnailRequest) other; + return this.sleuthkitCase == otherRequest.getSleuthkitCase() + && this.domain.equals(otherRequest.getDomain()) + && this.iconSize == otherRequest.getIconSize(); + } + + @Override + public int hashCode() { + return 79 * 5 + Objects.hash(this.domain, this.iconSize); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/search/FileSearch.java b/Core/src/org/sleuthkit/autopsy/discovery/search/FileSearch.java new file mode 100644 index 0000000000..4c25419357 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/FileSearch.java @@ -0,0 +1,312 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019-2020 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.discovery.search; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import org.apache.commons.lang.StringUtils; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; +import org.sleuthkit.autopsy.discovery.search.DiscoveryAttributes.AttributeType; +import org.sleuthkit.autopsy.discovery.search.DiscoveryKeyUtils.GroupKey; +import org.sleuthkit.autopsy.discovery.search.DiscoveryKeyUtils.SearchKey; +import org.sleuthkit.autopsy.textsummarizer.TextSummarizer; +import org.sleuthkit.autopsy.textsummarizer.TextSummary; + +/** + * Main class to perform the file search. + */ +public class FileSearch { + + private final static Logger logger = Logger.getLogger(FileSearch.class.getName()); + private static final int MAXIMUM_CACHE_SIZE = 10; + private static final Cache>> searchCache = CacheBuilder.newBuilder() + .maximumSize(MAXIMUM_CACHE_SIZE) + .build(); + + /** + * Run the file search and returns the SearchResults object for debugging. + * Caching new results for access at later time. + * + * @param userName The name of the user performing the search. + * @param filters The filters to apply + * @param groupAttributeType The attribute to use for grouping + * @param groupSortingType The method to use to sort the groups + * @param fileSortingMethod The method to use to sort the files within the + * groups + * @param caseDb The case database + * @param centralRepoDb The central repository database. Can be null if + * not needed. + * + * @return The raw search results + * + * @throws DiscoveryException + */ + static SearchResults runFileSearchDebug(String userName, + List filters, + AttributeType groupAttributeType, + Group.GroupSortingAlgorithm groupSortingType, + ResultsSorter.SortingMethod fileSortingMethod, + SleuthkitCase caseDb, CentralRepository centralRepoDb) throws DiscoveryException { + // Make a list of attributes that we want to add values for. This ensures the + // ResultFile objects will have all needed fields set when it's time to group + // and sort them. For example, if we're grouping by central repo frequency, we need + // to make sure we've loaded those values before grouping. + List attributesNeededForGroupingOrSorting = new ArrayList<>(); + attributesNeededForGroupingOrSorting.add(groupAttributeType); + attributesNeededForGroupingOrSorting.addAll(fileSortingMethod.getRequiredAttributes()); + + // Run the queries for each filter + List results = SearchFiltering.runQueries(filters, caseDb, centralRepoDb); + + // Add the data to resultFiles for any attributes needed for sorting and grouping + addAttributes(attributesNeededForGroupingOrSorting, results, caseDb, centralRepoDb); + + // Collect everything in the search results + SearchResults searchResults = new SearchResults(groupSortingType, groupAttributeType, fileSortingMethod); + searchResults.add(results); + + // Sort and group the results + searchResults.sortGroupsAndFiles(); + Map> resultHashMap = searchResults.toLinkedHashMap(); + SearchKey searchKey = new SearchKey(userName, filters, groupAttributeType, groupSortingType, fileSortingMethod); + synchronized (searchCache) { + searchCache.put(searchKey, resultHashMap); + } + return searchResults; + } + + /** + * Run the file search to get the group keys and sizes. Clears cache of + * search results, caching new results for access at later time. + * + * @param userName The name of the user performing the search. + * @param filters The filters to apply + * @param groupAttributeType The attribute to use for grouping + * @param groupSortingType The method to use to sort the groups + * @param fileSortingMethod The method to use to sort the files within the + * groups + * @param caseDb The case database + * @param centralRepoDb The central repository database. Can be null if + * not needed. + * + * @return A LinkedHashMap grouped and sorted according to the parameters + * + * @throws DiscoveryException + */ + public static Map getGroupSizes(String userName, + List filters, + AttributeType groupAttributeType, + Group.GroupSortingAlgorithm groupSortingType, + ResultsSorter.SortingMethod fileSortingMethod, + SleuthkitCase caseDb, CentralRepository centralRepoDb) throws DiscoveryException { + Map> searchResults = runFileSearch(userName, filters, + groupAttributeType, groupSortingType, fileSortingMethod, caseDb, centralRepoDb); + LinkedHashMap groupSizes = new LinkedHashMap<>(); + for (GroupKey groupKey : searchResults.keySet()) { + groupSizes.put(groupKey, searchResults.get(groupKey).size()); + } + return groupSizes; + } + + /** + * Get the files from the specified group from the cache, if the the group + * was not cached perform a search caching the groups. + * + * @param userName The name of the user performing the search. + * @param filters The filters to apply + * @param groupAttributeType The attribute to use for grouping + * @param groupSortingType The method to use to sort the groups + * @param fileSortingMethod The method to use to sort the files within the + * groups + * @param groupKey The key which uniquely identifies the group to + * get entries from + * @param startingEntry The first entry to return + * @param numberOfEntries The number of entries to return + * @param caseDb The case database + * @param centralRepoDb The central repository database. Can be null if + * not needed. + * + * @return A LinkedHashMap grouped and sorted according to the parameters + * + * @throws DiscoveryException + */ + public static List getFilesInGroup(String userName, + List filters, + AttributeType groupAttributeType, + Group.GroupSortingAlgorithm groupSortingType, + ResultsSorter.SortingMethod fileSortingMethod, + GroupKey groupKey, + int startingEntry, + int numberOfEntries, + SleuthkitCase caseDb, CentralRepository centralRepoDb) throws DiscoveryException { + //the group should be in the cache at this point + List filesInGroup = null; + SearchKey searchKey = new SearchKey(userName, filters, groupAttributeType, groupSortingType, fileSortingMethod); + Map> resultsMap; + synchronized (searchCache) { + resultsMap = searchCache.getIfPresent(searchKey); + } + if (resultsMap != null) { + filesInGroup = resultsMap.get(groupKey); + } + List page = new ArrayList<>(); + if (filesInGroup == null) { + logger.log(Level.INFO, "Group {0} was not cached, performing search to cache all groups again", groupKey); + runFileSearch(userName, filters, groupAttributeType, groupSortingType, fileSortingMethod, caseDb, centralRepoDb); + synchronized (searchCache) { + resultsMap = searchCache.getIfPresent(searchKey.getKeyString()); + } + if (resultsMap != null) { + filesInGroup = resultsMap.get(groupKey); + } + if (filesInGroup == null) { + logger.log(Level.WARNING, "Group {0} did not exist in cache or new search results", groupKey); + return page; //group does not exist + } + } + // Check that there is data after the starting point + if (filesInGroup.size() < startingEntry) { + logger.log(Level.WARNING, "Group only contains {0} files, starting entry of {1} is too large.", new Object[]{filesInGroup.size(), startingEntry}); + return page; + } + // Add files to the page + for (int i = startingEntry; (i < startingEntry + numberOfEntries) + && (i < filesInGroup.size()); i++) { + page.add(filesInGroup.get(i)); + } + return page; + } + + /** + * Get a summary for the specified AbstractFile. If no TextSummarizers exist + * get the beginning of the file. + * + * @param file The AbstractFile to summarize. + * + * @return The summary or beginning of the specified file as a String. + */ + @NbBundle.Messages({"FileSearch.documentSummary.noPreview=No preview available.", + "FileSearch.documentSummary.noBytes=No bytes read for document, unable to display preview."}) + public static TextSummary summarize(AbstractFile file) { + TextSummary summary = null; + TextSummarizer localSummarizer; + synchronized (searchCache) { + localSummarizer = SummaryHelpers.getLocalSummarizer(); + + } + if (localSummarizer != null) { + try { + //a summary of length 40 seems to fit without vertical scroll bars + summary = localSummarizer.summarize(file, 40); + } catch (IOException ex) { + return new TextSummary(Bundle.FileSearch_documentSummary_noPreview(), null, 0); + } + } + if (summary == null || StringUtils.isBlank(summary.getSummaryText())) { + //summary text was empty grab the beginning of the file + summary = SummaryHelpers.getDefaultSummary(file); + } + return summary; + } + + /** + * Run the file search. Caching new results for access at later time. + * + * @param userName The name of the user performing the search. + * @param filters The filters to apply + * @param groupAttributeType The attribute to use for grouping + * @param groupSortingType The method to use to sort the groups + * @param fileSortingMethod The method to use to sort the files within the + * groups + * @param caseDb The case database + * @param centralRepoDb The central repository database. Can be null if + * not needed. + * + * @return A LinkedHashMap grouped and sorted according to the parameters + * + * @throws DiscoveryException + */ + private static Map> runFileSearch(String userName, + List filters, + AttributeType groupAttributeType, + Group.GroupSortingAlgorithm groupSortingType, + ResultsSorter.SortingMethod fileSortingMethod, + SleuthkitCase caseDb, CentralRepository centralRepoDb) throws DiscoveryException { + + // Make a list of attributes that we want to add values for. This ensures the + // ResultFile objects will have all needed fields set when it's time to group + // and sort them. For example, if we're grouping by central repo frequency, we need + // to make sure we've loaded those values before grouping. + List attributesNeededForGroupingOrSorting = new ArrayList<>(); + attributesNeededForGroupingOrSorting.add(groupAttributeType); + attributesNeededForGroupingOrSorting.addAll(fileSortingMethod.getRequiredAttributes()); + + // Run the queries for each filter + List results = SearchFiltering.runQueries(filters, caseDb, centralRepoDb); + + // Add the data to resultFiles for any attributes needed for sorting and grouping + addAttributes(attributesNeededForGroupingOrSorting, results, caseDb, centralRepoDb); + + // Collect everything in the search results + SearchResults searchResults = new SearchResults(groupSortingType, groupAttributeType, fileSortingMethod); + searchResults.add(results); + Map> resultHashMap = searchResults.toLinkedHashMap(); + SearchKey searchKey = new SearchKey(userName, filters, groupAttributeType, groupSortingType, fileSortingMethod); + synchronized (searchCache) { + searchCache.put(searchKey, resultHashMap); + } + // Return a version of the results in general Java objects + return resultHashMap; + } + + /** + * Add any attributes corresponding to the attribute list to the given + * result files. For example, specifying the KeywordListAttribute will + * populate the list of keyword set names in the ResultFile objects. + * + * @param attrs The attributes to add to the list of result files + * @param results The result files + * @param caseDb The case database + * @param centralRepoDb The central repository database. Can be null if not + * needed. + * + * @throws DiscoveryException + */ + private static void addAttributes(List attrs, List results, SleuthkitCase caseDb, CentralRepository centralRepoDb) + throws DiscoveryException { + for (AttributeType attr : attrs) { + attr.addAttributeToResults(results, caseDb, centralRepoDb); + } + } + + private FileSearch() { + // Class should not be instantiated + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/FileGroup.java b/Core/src/org/sleuthkit/autopsy/discovery/search/Group.java similarity index 59% rename from Core/src/org/sleuthkit/autopsy/discovery/FileGroup.java rename to Core/src/org/sleuthkit/autopsy/discovery/search/Group.java index ad88550df6..688c854338 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/FileGroup.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/Group.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2019 Basis Technology Corp. + * Copyright 2019-2020 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,48 +16,49 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.search; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.openide.util.NbBundle.Messages; -import org.sleuthkit.autopsy.discovery.FileSearch.GroupKey; /** - * Class for storing files that belong to a particular group. + * Class for storing results that belong to a particular group. */ -class FileGroup implements Comparable { +public class Group implements Comparable { - private final FileGroup.GroupSortingAlgorithm groupSortingType; - private final GroupKey groupKey; - private final List files; + private final Group.GroupSortingAlgorithm groupSortingType; + private final DiscoveryKeyUtils.GroupKey groupKey; + private final List results; private final String displayName; /** - * Create a FileGroup object with its first file. + * Create a Group object with its first result. * * @param groupSortingType The method for sorting the group * @param groupKey The GroupKey for this group */ - FileGroup(FileGroup.GroupSortingAlgorithm groupSortingType, GroupKey groupKey) { + public Group(Group.GroupSortingAlgorithm groupSortingType, DiscoveryKeyUtils.GroupKey groupKey) { this.groupSortingType = groupSortingType; this.groupKey = groupKey; - files = new ArrayList<>(); + results = new ArrayList<>(); this.displayName = groupKey.getDisplayName(); } /** - * Add a ResultFile to the group. Will not be sorted at this time. + * Add a Result to the group. Will not be sorted at this time. * - * @param file The ResultFile to add to the FileGroup + * @param result The Result to add to the Group. */ - void addFile(ResultFile file) { - if (files.contains(file)) { - ResultFile existingCopy = files.get(files.indexOf(file)); //get the copy of this which exists in the list - existingCopy.addDuplicate(file.getFirstInstance()); + void addResult(Result result) { + if (result.getType() != SearchData.Type.DOMAIN && results.contains(result)) { + //dedupe files and show instances + ResultFile existingCopy = (ResultFile) results.get(results.indexOf(result)); //get the copy of this which exists in the list + existingCopy.addDuplicate(((ResultFile) result).getFirstInstance()); } else { - files.add(file); + //Domains and non files are not being deduped currently + results.add(result); } } @@ -66,7 +67,7 @@ class FileGroup implements Comparable { * * @return The display name of the group. */ - String getDisplayName() { + public String getDisplayName() { return displayName; // NON-NLS } @@ -75,28 +76,28 @@ class FileGroup implements Comparable { * * @return The unique key for the group. */ - GroupKey getGroupKey() { + public DiscoveryKeyUtils.GroupKey getGroupKey() { return groupKey; } /** - * Sort all the files in the group + * Sort all the results in the group */ - void sortFiles(FileSorter sorter) { - Collections.sort(files, sorter); + public void sortResults(ResultsSorter sorter) { + Collections.sort(results, sorter); } /** * Compare this group to another group for sorting. Uses the algorithm * specified in groupSortingType. * - * @param otherGroup the group to compare this one to + * @param otherGroup The group to compare this one to. * * @return -1 if this group should be displayed before the other group, 1 - * otherwise + * otherwise. */ @Override - public int compareTo(FileGroup otherGroup) { + public int compareTo(Group otherGroup) { switch (groupSortingType) { case BY_GROUP_SIZE: @@ -108,14 +109,14 @@ class FileGroup implements Comparable { } /** - * Compare two groups based on the group key + * Compare two groups based on the group key. * - * @param group1 - * @param group2 + * @param group1 The first group to be compared. + * @param group2 The second group to be compared. * - * @return -1 if group1 should be displayed before group2, 1 otherwise + * @return -1 if group1 should be displayed before group2, 1 otherwise. */ - private static int compareGroupsByGroupKey(FileGroup group1, FileGroup group2) { + private static int compareGroupsByGroupKey(Group group1, Group group2) { return group1.getGroupKey().compareTo(group2.getGroupKey()); } @@ -123,14 +124,14 @@ class FileGroup implements Comparable { * Compare two groups based on the group size. Falls back on the group key * if the groups are the same size. * - * @param group1 - * @param group2 + * @param group1 The first group to be compared. + * @param group2 The second group to be compared. * - * @return -1 if group1 should be displayed before group2, 1 otherwise + * @return -1 if group1 should be displayed before group2, 1 otherwise. */ - private static int compareGroupsBySize(FileGroup group1, FileGroup group2) { - if (group1.getFiles().size() != group2.getFiles().size()) { - return -1 * Long.compare(group1.getFiles().size(), group2.getFiles().size()); // High to low + private static int compareGroupsBySize(Group group1, Group group2) { + if (group1.getResults().size() != group2.getResults().size()) { + return -1 * Long.compare(group1.getResults().size(), group2.getResults().size()); // High to low } else { // If the groups have the same size, fall through to the BY_GROUP_NAME sorting return compareGroupsByGroupKey(group1, group2); @@ -142,7 +143,7 @@ class FileGroup implements Comparable { */ @Messages({"FileGroup.groupSortingAlgorithm.groupSize.text=Group Size", "FileGroup.groupSortingAlgorithm.groupName.text=Group Name"}) - enum GroupSortingAlgorithm { + public enum GroupSortingAlgorithm { BY_GROUP_NAME(Bundle.FileGroup_groupSortingAlgorithm_groupName_text()), // Sort using the group key (for example, if grouping by size sort from largest to smallest value) BY_GROUP_SIZE(Bundle.FileGroup_groupSortingAlgorithm_groupSize_text()); // Sort from largest to smallest group @@ -165,12 +166,12 @@ class FileGroup implements Comparable { } /** - * Get the list of ResultFile objects in the group + * Get the list of Result objects in the group. * - * @return List of ResultFile objects + * @return The list of Result objects. */ - List getFiles() { - return Collections.unmodifiableList(files); + public List getResults() { + return Collections.unmodifiableList(results); } } diff --git a/Core/src/org/sleuthkit/autopsy/discovery/search/Result.java b/Core/src/org/sleuthkit/autopsy/discovery/search/Result.java new file mode 100644 index 0000000000..cc4c97427b --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/Result.java @@ -0,0 +1,106 @@ +/* + * Autopsy + * + * Copyright 2020 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.discovery.search; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.TskData; + +/** + * Interface implemented by all types of results. + */ +public abstract class Result { + + private SearchData.Frequency frequency = SearchData.Frequency.UNKNOWN; + private final List tagNames = new ArrayList<>(); + + /** + * Get the Object ID for the data source the result is in. + * + * @return The Object ID of the data source the result is in. + */ + public abstract long getDataSourceObjectId(); + + /** + * Get the frequency of this result in the central repository. + * + * @return The Frequency enum. + */ + public SearchData.Frequency getFrequency() { + return frequency; + } + + /** + * Get the known status of the result. + * + * @return The Known status of the result. + */ + public abstract TskData.FileKnown getKnown(); + + /** + * Set the frequency of this result in the central repository. + * + * @param frequency The frequency of the result as an enum. + */ + final public void setFrequency(SearchData.Frequency frequency) { + this.frequency = frequency; + } + + /** + * Get the data source associated with this result. + * + * @return The data source this result came from. + * + * @throws TskCoreException + */ + public abstract Content getDataSource() throws TskCoreException; + + /** + * Get the type of this result. + * + * @return The type of items being searched for. + */ + public abstract SearchData.Type getType(); + + /** + * Add a tag name that matched this file. + * + * @param tagName + */ + public void addTagName(String tagName) { + if (!tagNames.contains(tagName)) { + tagNames.add(tagName); + } + + // Sort the list so the getTagNames() will be consistent regardless of the order added + Collections.sort(tagNames); + } + + /** + * Get the tag names for this file + * + * @return the tag names that matched this file. + */ + public List getTagNames() { + return Collections.unmodifiableList(tagNames); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/search/ResultDomain.java b/Core/src/org/sleuthkit/autopsy/discovery/search/ResultDomain.java new file mode 100644 index 0000000000..f91a9994c4 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/ResultDomain.java @@ -0,0 +1,139 @@ +/* + * Autopsy + * + * Copyright 2020 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.discovery.search; + +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.TskData; + +/** + * Container for domains that holds all necessary data for grouping and sorting. + */ +public class ResultDomain extends Result { + + private final String domain; + private final Long activityStart; + private final Long activityEnd; + private final Long totalVisits; + private final Long visitsInLast60; + private final Long filesDownloaded; + + private final Content dataSource; + private final long dataSourceId; + + /** + * Create a ResultDomain from a String. + * + * @param domain The domain the result is being created from. + */ + ResultDomain(String domain, Long activityStart, Long activityEnd, Long totalVisits, + Long visitsInLast60, Long filesDownloaded, Content dataSource) { + this.domain = domain; + this.dataSource = dataSource; + this.dataSourceId = dataSource.getId(); + this.activityStart = activityStart; + this.activityEnd = activityEnd; + this.totalVisits = totalVisits; + this.visitsInLast60 = visitsInLast60; + this.filesDownloaded = filesDownloaded; + } + + /** + * Get the domain represented as a String. + * + * @return The String representation of the domain this result is for. + */ + public String getDomain() { + return this.domain; + } + + /** + * Get the date of first activity for this domain. + * + * @return The date of first activity for this domain. + */ + public Long getActivityStart() { + return activityStart; + } + + /** + * Get the date of most recent activity for this domain. + * + * @return The date of most recent activity for this domain. + */ + public Long getActivityEnd() { + return activityEnd; + } + + /** + * Get the total number of visits that this domain has had. + * + * @return The total number of visits that this domain has had. + */ + public Long getTotalVisits() { + return totalVisits; + } + + /** + * Get the number of visits that this domain has had in the last 60 days. + * + * @return The number of visits that this domain has had in the last 60 + * days. + */ + public Long getVisitsInLast60() { + return visitsInLast60; + } + + /** + * Get the number of files downloaded associated with this domain. + * + * @return The number of files downloaded associated with this domain. + */ + public Long getFilesDownloaded() { + return filesDownloaded; + } + + @Override + public long getDataSourceObjectId() { + return this.dataSourceId; + } + + @Override + public Content getDataSource() throws TskCoreException { + return this.dataSource; + } + + @Override + public TskData.FileKnown getKnown() { + return TskData.FileKnown.UNKNOWN; + } + + @Override + public SearchData.Type getType() { + return SearchData.Type.DOMAIN; + } + + @Override + public String toString() { + return "[domain=" + this.domain + ", data_source=" + this.dataSourceId + ", start=" + + this.activityStart + ", end=" + this.activityEnd + ", totalVisits=" + this.totalVisits + ", visitsLast60=" + + this.visitsInLast60 + ", downloads=" + this.filesDownloaded + ", frequency=" + + this.getFrequency() + "]"; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ResultFile.java b/Core/src/org/sleuthkit/autopsy/discovery/search/ResultFile.java similarity index 83% rename from Core/src/org/sleuthkit/autopsy/discovery/ResultFile.java rename to Core/src/org/sleuthkit/autopsy/discovery/search/ResultFile.java index 73d57ccf37..59bfbf48f2 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ResultFile.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/ResultFile.java @@ -16,9 +16,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.search; -import org.sleuthkit.autopsy.discovery.FileSearchData.FileType; +import org.sleuthkit.autopsy.discovery.search.SearchData.Type; import org.sleuthkit.datamodel.AbstractFile; import java.util.ArrayList; import java.util.Collections; @@ -29,7 +29,9 @@ import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable; import org.sleuthkit.autopsy.coreutils.Logger; +import static org.sleuthkit.autopsy.discovery.search.SearchData.Type.OTHER; import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.ContentTag; import org.sleuthkit.datamodel.HashUtility; import org.sleuthkit.datamodel.Tag; @@ -39,27 +41,25 @@ import org.sleuthkit.datamodel.TskData; /** * Container for files that holds all necessary data for grouping and sorting. */ -class ResultFile { +public class ResultFile extends Result { private final static Logger logger = Logger.getLogger(ResultFile.class.getName()); - private FileSearchData.Frequency frequency; private final List keywordListNames; private final List hashSetNames; - private final List tagNames; private final List interestingSetNames; private final List objectDetectedNames; private final List instances = new ArrayList<>(); private DataResultViewerTable.Score currentScore = DataResultViewerTable.Score.NO_SCORE; private String scoreDescription = null; private boolean deleted = false; - private FileType fileType; + private Type fileType; /** * Create a ResultFile from an AbstractFile * * @param abstractFile */ - ResultFile(AbstractFile abstractFile) { + public ResultFile(AbstractFile abstractFile) { try { //call get uniquePath to cache the path abstractFile.getUniquePath(); @@ -72,31 +72,11 @@ class ResultFile { deleted = true; } updateScoreAndDescription(abstractFile); - this.frequency = FileSearchData.Frequency.UNKNOWN; keywordListNames = new ArrayList<>(); hashSetNames = new ArrayList<>(); - tagNames = new ArrayList<>(); interestingSetNames = new ArrayList<>(); objectDetectedNames = new ArrayList<>(); - fileType = FileType.fromMIMEtype(abstractFile.getMIMEType()); - } - - /** - * Get the frequency of this file in the central repository - * - * @return The Frequency enum - */ - FileSearchData.Frequency getFrequency() { - return frequency; - } - - /** - * Set the frequency of this file from the central repository - * - * @param frequency The frequency of the file as an enum - */ - void setFrequency(FileSearchData.Frequency frequency) { - this.frequency = frequency; + fileType = fromMIMEtype(abstractFile.getMIMEType()); } /** @@ -105,12 +85,12 @@ class ResultFile { * * @param duplicate The abstract file to add as a duplicate. */ - void addDuplicate(AbstractFile duplicate) { + public void addDuplicate(AbstractFile duplicate) { if (deleted && !duplicate.isDirNameFlagSet(TskData.TSK_FS_NAME_FLAG_ENUM.UNALLOC)) { deleted = false; } - if (fileType == FileType.OTHER) { - fileType = FileType.fromMIMEtype(duplicate.getMIMEType()); + if (fileType == Type.OTHER) { + fileType = fromMIMEtype(duplicate.getMIMEType()); } updateScoreAndDescription(duplicate); try { @@ -128,7 +108,7 @@ class ResultFile { * * @return The score of this ResultFile. */ - DataResultViewerTable.Score getScore() { + public DataResultViewerTable.Score getScore() { return currentScore; } @@ -137,7 +117,7 @@ class ResultFile { * * @return The score description of this ResultFile. */ - String getScoreDescription() { + public String getScoreDescription() { return scoreDescription; } @@ -147,7 +127,7 @@ class ResultFile { * * @return The deleted status of this ResultFile. */ - boolean isDeleted() { + public boolean isDeleted() { return deleted; } @@ -158,7 +138,7 @@ class ResultFile { * @return The list of AbstractFiles which have been identified as instances * of this file. */ - List getAllInstances() { + public List getAllInstances() { return Collections.unmodifiableList(instances); } @@ -167,7 +147,7 @@ class ResultFile { * * @return The FileType enum. */ - FileType getFileType() { + public Type getFileType() { return fileType; } @@ -176,7 +156,7 @@ class ResultFile { * * @param keywordListName */ - void addKeywordListName(String keywordListName) { + public void addKeywordListName(String keywordListName) { if (!keywordListNames.contains(keywordListName)) { keywordListNames.add(keywordListName); } @@ -190,7 +170,7 @@ class ResultFile { * * @return the keyword list names that matched this file. */ - List getKeywordListNames() { + public List getKeywordListNames() { return Collections.unmodifiableList(keywordListNames); } @@ -199,7 +179,7 @@ class ResultFile { * * @param hashSetName */ - void addHashSetName(String hashSetName) { + public void addHashSetName(String hashSetName) { if (!hashSetNames.contains(hashSetName)) { hashSetNames.add(hashSetName); } @@ -213,39 +193,16 @@ class ResultFile { * * @return The hash set names that matched this file. */ - List getHashSetNames() { + public List getHashSetNames() { return Collections.unmodifiableList(hashSetNames); } - /** - * Add a tag name that matched this file. - * - * @param tagName - */ - void addTagName(String tagName) { - if (!tagNames.contains(tagName)) { - tagNames.add(tagName); - } - - // Sort the list so the getTagNames() will be consistent regardless of the order added - Collections.sort(tagNames); - } - - /** - * Get the tag names for this file - * - * @return the tag names that matched this file. - */ - List getTagNames() { - return Collections.unmodifiableList(tagNames); - } - /** * Add an interesting file set name that matched this file. * * @param interestingSetName */ - void addInterestingSetName(String interestingSetName) { + public void addInterestingSetName(String interestingSetName) { if (!interestingSetNames.contains(interestingSetName)) { interestingSetNames.add(interestingSetName); } @@ -259,7 +216,7 @@ class ResultFile { * * @return the interesting item set names that matched this file. */ - List getInterestingSetNames() { + public List getInterestingSetNames() { return Collections.unmodifiableList(interestingSetNames); } @@ -268,7 +225,7 @@ class ResultFile { * * @param objectDetectedName */ - void addObjectDetectedName(String objectDetectedName) { + public void addObjectDetectedName(String objectDetectedName) { if (!objectDetectedNames.contains(objectDetectedName)) { objectDetectedNames.add(objectDetectedName); } @@ -282,7 +239,7 @@ class ResultFile { * * @return the objects detected in this file. */ - List getObjectDetectedNames() { + public List getObjectDetectedNames() { return Collections.unmodifiableList(objectDetectedNames); } @@ -291,7 +248,7 @@ class ResultFile { * * @return the AbstractFile object */ - AbstractFile getFirstInstance() { + public AbstractFile getFirstInstance() { return instances.get(0); } @@ -299,7 +256,7 @@ class ResultFile { public String toString() { return getFirstInstance().getName() + "(" + getFirstInstance().getId() + ") - " + getFirstInstance().getSize() + ", " + getFirstInstance().getParentPath() + ", " - + getFirstInstance().getDataSourceObjectId() + ", " + frequency.toString() + ", " + + getFirstInstance().getDataSourceObjectId() + ", " + getFrequency().toString() + ", " + String.join(",", keywordListNames) + ", " + getFirstInstance().getMIMEType(); } @@ -380,4 +337,40 @@ class ResultFile { } } } + + /** + * Get the enum matching the given MIME type. + * + * @param mimeType The MIME type for the file. + * + * @return the corresponding enum (will be OTHER if no types matched) + */ + public static Type fromMIMEtype(String mimeType) { + for (Type type : Type.values()) { + if (type.getMediaTypes().contains(mimeType)) { + return type; + } + } + return OTHER; + } + + @Override + public long getDataSourceObjectId() { + return getFirstInstance().getDataSourceObjectId(); + } + + @Override + public Content getDataSource() throws TskCoreException { + return getFirstInstance().getDataSource(); + } + + @Override + public TskData.FileKnown getKnown() { + return getFirstInstance().getKnown(); + } + + @Override + public Type getType() { + return fileType; + } } diff --git a/Core/src/org/sleuthkit/autopsy/discovery/search/ResultsSorter.java b/Core/src/org/sleuthkit/autopsy/discovery/search/ResultsSorter.java new file mode 100644 index 0000000000..b63d86a7f9 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/ResultsSorter.java @@ -0,0 +1,388 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.discovery.search; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import org.openide.util.NbBundle; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Class used to sort Results using the supplied method. + */ +public class ResultsSorter implements Comparator { + + private final List> comparators = new ArrayList<>(); + + /** + * Set up the sorter using the supplied sorting method. The sorting is + * defined by a list of Result comparators. These comparators will be run in + * order until one returns a non-zero result. + * + * @param method The method that should be used to sort the results. + */ + public ResultsSorter(SortingMethod method) { + + // Set up the primary comparators that should applied to the results + switch (method) { + case BY_DATA_SOURCE: + comparators.add(getDataSourceComparator()); + break; + case BY_FILE_SIZE: + comparators.add(getFileSizeComparator()); + break; + case BY_FILE_TYPE: + comparators.add(getTypeComparator()); + comparators.add(getMIMETypeComparator()); + break; + case BY_FREQUENCY: + comparators.add(getFrequencyComparator()); + break; + case BY_KEYWORD_LIST_NAMES: + comparators.add(getKeywordListNameComparator()); + break; + case BY_FULL_PATH: + comparators.add(getParentPathComparator()); + break; + case BY_FILE_NAME: + comparators.add(getFileNameComparator()); + break; + case BY_DOMAIN_NAME: + comparators.add(getDomainNameComparator()); + break; + default: + // The default comparator will be added afterward + break; + } + + // Add the default comparator to the end. This will ensure a consistent sort + // order regardless of the order the results were added to the list. + comparators.add(getDefaultComparator()); + } + + @Override + public int compare(Result result1, Result result2) { + + int result = 0; + for (Comparator comp : comparators) { + result = comp.compare(result1, result2); + if (result != 0) { + return result; + } + } + + // The results are the same + return result; + } + + /** + * Compare results using data source ID. Will order smallest to largest. + * + * @return -1 if result1 has the lower data source ID, 0 if equal, 1 + * otherwise. + */ + private static Comparator getDataSourceComparator() { + return (Result result1, Result result2) -> Long.compare(result1.getDataSourceObjectId(), result2.getDataSourceObjectId()); + } + + /** + * Compare results using their Type enum. Orders based on the ranking in the + * Type enum. + * + * @return -1 if result1 has the lower Type value, 0 if equal, 1 otherwise. + */ + private static Comparator getTypeComparator() { + return (Result result1, Result result2) -> Integer.compare(result1.getType().getRanking(), result2.getType().getRanking()); + } + + /** + * Compare files using a concatenated version of keyword list names. + * Alphabetical by the list names with files with no keyword list hits going + * last. + * + * @return -1 if result1 has the earliest combined keyword list name, 0 if + * equal, 1 otherwise. + */ + private static Comparator getKeywordListNameComparator() { + return (Result result1, Result result2) -> { + // Put empty lists at the bottom + if (result1.getType() == SearchData.Type.DOMAIN) { + return 0; + } + ResultFile file1 = (ResultFile) result1; + ResultFile file2 = (ResultFile) result2; + if (file1.getKeywordListNames().isEmpty()) { + if (file2.getKeywordListNames().isEmpty()) { + return 0; + } + return 1; + } else if (file2.getKeywordListNames().isEmpty()) { + return -1; + } + + String list1 = String.join(",", file1.getKeywordListNames()); + String list2 = String.join(",", file2.getKeywordListNames()); + return compareStrings(list1, list2); + }; + } + + /** + * Compare files based on parent path. Order alphabetically. + * + * @return -1 if result1's path comes first alphabetically, 0 if equal, 1 + * otherwise. + */ + private static Comparator getParentPathComparator() { + + return new Comparator() { + @Override + public int compare(Result result1, Result result2) { + if (result1.getType() == SearchData.Type.DOMAIN) { + return 0; + } + ResultFile file1 = (ResultFile) result1; + ResultFile file2 = (ResultFile) result2; + String file1ParentPath; + try { + file1ParentPath = file1.getFirstInstance().getParent().getUniquePath(); + } catch (TskCoreException ingored) { + file1ParentPath = file1.getFirstInstance().getParentPath(); + } + String file2ParentPath; + try { + file2ParentPath = file2.getFirstInstance().getParent().getUniquePath(); + } catch (TskCoreException ingored) { + file2ParentPath = file2.getFirstInstance().getParentPath(); + } + return compareStrings(file1ParentPath.toLowerCase(), file2ParentPath.toLowerCase()); + } + }; + } + + /** + * Compare results based on number of occurrences in the central repository. + * Order from most rare to least rare Frequency enum. + * + * @return -1 if result1's rarity is lower than result2, 0 if equal, 1 + * otherwise. + */ + private static Comparator getFrequencyComparator() { + return (Result result1, Result result2) -> Integer.compare(result1.getFrequency().getRanking(), result2.getFrequency().getRanking()); + } + + /** + * Compare files based on MIME type. Order is alphabetical. + * + * @return -1 if result1's MIME type comes before result2's, 0 if equal, 1 + * otherwise. + */ + private static Comparator getMIMETypeComparator() { + return (Result result1, Result result2) -> { + if (result1.getType() == SearchData.Type.DOMAIN) { + return 0; + } + return compareStrings(((ResultFile) result1).getFirstInstance().getMIMEType(), ((ResultFile) result2).getFirstInstance().getMIMEType()); + }; + } + + /** + * Compare files based on size. Order large to small. + * + * @return -1 if result1 is larger than result2, 0 if equal, 1 otherwise. + */ + private static Comparator getFileSizeComparator() { + return (Result result1, Result result2) -> { + if (result1.getType() == SearchData.Type.DOMAIN) { + return 0; + } + return -1 * Long.compare(((ResultFile) result1).getFirstInstance().getSize(), ((ResultFile) result2).getFirstInstance().getSize()); // Sort large to small + }; + } + + /** + * Compare files based on file name. Order alphabetically. + * + * @return -1 if result1 comes before result2, 0 if equal, 1 otherwise. + */ + private static Comparator getFileNameComparator() { + return (Result result1, Result result2) -> { + if (result1.getType() == SearchData.Type.DOMAIN) { + return 0; + } + return compareStrings(((ResultFile) result1).getFirstInstance().getName().toLowerCase(), (((ResultFile) result2).getFirstInstance().getName().toLowerCase())); + }; + } + + /** + * Sorts domain names in lexographical order, ignoring case. + * + * @return -1 if domain1 comes before domain2, 0 if equal, 1 otherwise. + */ + private static Comparator getDomainNameComparator() { + return (Result domain1, Result domain2) -> { + if (domain1.getType() != SearchData.Type.DOMAIN) { + return 0; + } + + ResultDomain first = (ResultDomain) domain1; + ResultDomain second = (ResultDomain) domain2; + return compareStrings(first.getDomain().toLowerCase(), second.getDomain().toLowerCase()); + }; + } + + /** + * Sorts results by most recent date time. + * + * @return -1 if domain1 comes before domain2, 0 if equal, 1 otherwise. + */ + private static Comparator getMostRecentDateTimeComparator() { + return (Result result1, Result result2) -> { + if (result1.getType() != SearchData.Type.DOMAIN) { + return 0; + } + + ResultDomain first = (ResultDomain) result1; + ResultDomain second = (ResultDomain) result2; + return Long.compare(second.getActivityEnd(), first.getActivityEnd()); + }; + } + + /** + * A final default comparison between two ResultFile objects. Currently this + * is on file name and then object ID. It can be changed but should always + * include something like the object ID to ensure a consistent sorting when + * the rest of the compared fields are the same. + * + * @return -1 if file1 comes before file2, 0 if equal, 1 otherwise. + */ + private static Comparator getDefaultComparator() { + return (Result result1, Result result2) -> { + // Compare file names and then object ID (to ensure a consistent sort) + if (result1.getType() == SearchData.Type.DOMAIN) { + return getFrequencyComparator().compare(result1, result2); + } else { + ResultFile file1 = (ResultFile) result1; + ResultFile file2 = (ResultFile) result2; + int result = getFileNameComparator().compare(file1, file2); + if (result == 0) { + return Long.compare(file1.getFirstInstance().getId(), file2.getFirstInstance().getId()); + } + return result; + } + + }; + } + + /** + * Compare two strings alphabetically. Nulls are allowed. + * + * @param s1 + * @param s2 + * + * @return -1 if s1 comes before s2, 0 if equal, 1 otherwise. + */ + private static int compareStrings(String s1, String s2) { + String string1 = s1 == null ? "" : s1; + String string2 = s2 == null ? "" : s2; + return string1.compareTo(string2); + + } + + /** + * Enum for selecting the primary method for sorting result files. + */ + @NbBundle.Messages({ + "FileSorter.SortingMethod.datasource.displayName=Data Source", + "FileSorter.SortingMethod.filename.displayName=File Name", + "FileSorter.SortingMethod.filesize.displayName=File Size", + "FileSorter.SortingMethod.filetype.displayName=File Type", + "FileSorter.SortingMethod.frequency.displayName=Central Repo Frequency", + "FileSorter.SortingMethod.keywordlist.displayName=Keyword List Names", + "FileSorter.SortingMethod.fullPath.displayName=Full Path", + "FileSorter.SortingMethod.domain.displayName=Domain"}) + public enum SortingMethod { + BY_FILE_NAME(new ArrayList<>(), + Bundle.FileSorter_SortingMethod_filename_displayName()), // Sort alphabetically by file name + BY_DATA_SOURCE(new ArrayList<>(), + Bundle.FileSorter_SortingMethod_datasource_displayName()), // Sort in increasing order of data source ID + BY_FILE_SIZE(new ArrayList<>(), + Bundle.FileSorter_SortingMethod_filesize_displayName()), // Sort in decreasing order of size + BY_FILE_TYPE(Arrays.asList(new DiscoveryAttributes.FileTypeAttribute()), + Bundle.FileSorter_SortingMethod_filetype_displayName()), // Sort in order of file type (defined in FileType enum), with secondary sort on MIME type + BY_FREQUENCY(Arrays.asList(new DiscoveryAttributes.FrequencyAttribute()), + Bundle.FileSorter_SortingMethod_frequency_displayName()), // Sort by decreasing rarity in the central repository + BY_KEYWORD_LIST_NAMES(Arrays.asList(new DiscoveryAttributes.KeywordListAttribute()), + Bundle.FileSorter_SortingMethod_keywordlist_displayName()), // Sort alphabetically by list of keyword list names found + BY_FULL_PATH(new ArrayList<>(), + Bundle.FileSorter_SortingMethod_fullPath_displayName()), // Sort alphabetically by path + BY_DOMAIN_NAME(new ArrayList<>(), + Bundle.FileSorter_SortingMethod_domain_displayName()); + + private final String displayName; + private final List requiredAttributes; + + /** + * Construct a new SortingMethod enum value. + * + * @param attributes The list of DiscoveryAttributes required by this + * enum value. + * @param displayName The display name for this enum value. + */ + SortingMethod(List attributes, String displayName) { + this.requiredAttributes = attributes; + this.displayName = displayName; + } + + @Override + public String toString() { + return displayName; + } + + /** + * Get the list of DiscoveryAttributes required by this enum value. + * + * @return The list of DiscoveryAttributes required by this enum value. + */ + public List getRequiredAttributes() { + return Collections.unmodifiableList(requiredAttributes); + } + + /** + * Get the list of enum values that are valid for ordering files. + * + * @return Enum values that can be used to ordering files. + */ + public static List getOptionsForOrderingFiles() { + return Arrays.asList(BY_FILE_SIZE, BY_FULL_PATH, BY_FILE_NAME, BY_DATA_SOURCE); + } + + /** + * Get the list of enum values that are valid for ordering files. + * + * @return Enum values that can be used to ordering files. + */ + public static List getOptionsForOrderingDomains() { + return Arrays.asList(BY_DOMAIN_NAME, BY_DATA_SOURCE); + } + + } +} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/search/SearchData.java b/Core/src/org/sleuthkit/autopsy/discovery/search/SearchData.java new file mode 100644 index 0000000000..909cfb1acd --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/SearchData.java @@ -0,0 +1,484 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.discovery.search; + +import com.google.common.collect.ImmutableSet; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coreutils.FileTypeUtils; +import org.sleuthkit.datamodel.BlackboardArtifact; + +/** + * Utility enums for searches made for files with Discovery. + */ +public final class SearchData { + + private final static long BYTES_PER_MB = 1000000; + private static final Set DOMAIN_ARTIFACT_TYPES = EnumSet.of(BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_BOOKMARK, BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_CACHE, BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_COOKIE, BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_DOWNLOAD, BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_HISTORY, BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_SEARCH_QUERY); + + /** + * Enum representing how often the result occurs in the Central Repository. + */ + @NbBundle.Messages({ + "SearchData.Frequency.unique.displayName=Unique (1)", + "SearchData.Frequency.rare.displayName=Rare (2-10)", + "SearchData.Frequency.common.displayName=Common (11 - 100)", + "SearchData.Frequency.verycommon.displayName=Very Common (100+)", + "SearchData.Frequency.known.displayName=Known (NSRL)", + "SearchData.Frequency.unknown.displayName=Unknown",}) + public enum Frequency { + UNIQUE(0, 1, Bundle.SearchData_Frequency_unique_displayName()), + RARE(1, 10, Bundle.SearchData_Frequency_rare_displayName()), + COMMON(2, 100, Bundle.SearchData_Frequency_common_displayName()), + VERY_COMMON(3, 0, Bundle.SearchData_Frequency_verycommon_displayName()), + KNOWN(4, 0, Bundle.SearchData_Frequency_known_displayName()), + UNKNOWN(5, 0, Bundle.SearchData_Frequency_unknown_displayName()); + + private final int ranking; + private final String displayName; + private final int maxOccur; + + /** + * Construct a new frequency enum value. + * + * @param ranking The rank for sorting. + * @param maxOccur The max occurrences this enum value is for. + * @param displayName The display name for this enum value. + */ + Frequency(int ranking, int maxOccur, String displayName) { + this.ranking = ranking; + this.maxOccur = maxOccur; + this.displayName = displayName; + } + + /** + * Get the rank for sorting. + * + * @return The rank (lower should be displayed first). + */ + public int getRanking() { + return ranking; + } + + /** + * Get the enum matching the given occurrence count. + * + * @param count Number of times a result is in the Central Repository. + * + * @return The corresponding enum. + */ + public static Frequency fromCount(long count) { + if (count <= UNIQUE.getMaxOccur()) { + return UNIQUE; + } else if (count <= RARE.getMaxOccur()) { + return RARE; + } else if (count <= COMMON.getMaxOccur()) { + return COMMON; + } + return VERY_COMMON; + } + + /** + * Get the list of enums that are valid for filtering when a CR is + * enabled. + * + * @return enums that can be used to filter with a CR. + */ + public static List getOptionsForFilteringWithCr() { + return Arrays.asList(UNIQUE, RARE, COMMON, VERY_COMMON, KNOWN); + } + + /** + * Get the list of enums that are valid for filtering when no CR is + * enabled. + * + * @return enums that can be used to filter without a CR. + */ + public static List getOptionsForFilteringWithoutCr() { + return Arrays.asList(KNOWN, UNKNOWN); + } + + @Override + public String toString() { + return displayName; + } + + /** + * Get the maximum number of occurrences this enum value is for. + * + * @return The maximum number of occurrences this enum value is for. + */ + public int getMaxOccur() { + return maxOccur; + } + } + + /** + * Enum representing the file size. + */ + @NbBundle.Messages({ + "SearchData.FileSize.XXLARGE.displayName=XXLarge", + "SearchData.FileSize.XLARGE.displayName=XLarge", + "SearchData.FileSize.LARGE.displayName=Large", + "SearchData.FileSize.MEDIUM.displayName=Medium", + "SearchData.FileSize.SMALL.displayName=Small", + "SearchData.FileSize.XSMALL.displayName=XSmall", + "SearchData.FileSize.10PlusGb=: 10GB+", + "SearchData.FileSize.5gbto10gb=: 5-10GB", + "SearchData.FileSize.1gbto5gb=: 1-5GB", + "SearchData.FileSize.100mbto1gb=: 100MB-1GB", + "SearchData.FileSize.200PlusMb=: 200MB+", + "SearchData.FileSize.50mbto200mb=: 50-200MB", + "SearchData.FileSize.500kbto100mb=: 500KB-100MB", + "SearchData.FileSize.1mbto50mb=: 1-50MB", + "SearchData.FileSize.100kbto1mb=: 100KB-1MB", + "SearchData.FileSize.16kbto100kb=: 16-100KB", + "SearchData.FileSize.upTo500kb=: 0-500KB", + "SearchData.FileSize.upTo16kb=: 0-16KB",}) + public enum FileSize { + XXLARGE_VIDEO(0, 10000 * BYTES_PER_MB, -1, Bundle.SearchData_FileSize_XXLARGE_displayName(), Bundle.SearchData_FileSize_10PlusGb()), + XLARGE_VIDEO(1, 5000 * BYTES_PER_MB, 10000 * BYTES_PER_MB, Bundle.SearchData_FileSize_XLARGE_displayName(), Bundle.SearchData_FileSize_5gbto10gb()), + LARGE_VIDEO(2, 1000 * BYTES_PER_MB, 5000 * BYTES_PER_MB, Bundle.SearchData_FileSize_LARGE_displayName(), Bundle.SearchData_FileSize_1gbto5gb()), + MEDIUM_VIDEO(3, 100 * BYTES_PER_MB, 1000 * BYTES_PER_MB, Bundle.SearchData_FileSize_MEDIUM_displayName(), Bundle.SearchData_FileSize_100mbto1gb()), + SMALL_VIDEO(4, 500000, 100 * BYTES_PER_MB, Bundle.SearchData_FileSize_SMALL_displayName(), Bundle.SearchData_FileSize_500kbto100mb()), + XSMALL_VIDEO(5, 0, 500000, Bundle.SearchData_FileSize_XSMALL_displayName(), Bundle.SearchData_FileSize_upTo500kb()), + XXLARGE_IMAGE(6, 200 * BYTES_PER_MB, -1, Bundle.SearchData_FileSize_XXLARGE_displayName(), Bundle.SearchData_FileSize_200PlusMb()), + XLARGE_IMAGE(7, 50 * BYTES_PER_MB, 200 * BYTES_PER_MB, Bundle.SearchData_FileSize_XLARGE_displayName(), Bundle.SearchData_FileSize_50mbto200mb()), + LARGE_IMAGE(8, 1 * BYTES_PER_MB, 50 * BYTES_PER_MB, Bundle.SearchData_FileSize_LARGE_displayName(), Bundle.SearchData_FileSize_1mbto50mb()), + MEDIUM_IMAGE(9, 100000, 1 * BYTES_PER_MB, Bundle.SearchData_FileSize_MEDIUM_displayName(), Bundle.SearchData_FileSize_100kbto1mb()), + SMALL_IMAGE(10, 16000, 100000, Bundle.SearchData_FileSize_SMALL_displayName(), Bundle.SearchData_FileSize_16kbto100kb()), + XSMALL_IMAGE(11, 0, 16000, Bundle.SearchData_FileSize_XSMALL_displayName(), Bundle.SearchData_FileSize_upTo16kb()); + + private final int ranking; // Must be unique for each value + private final long minBytes; // Note that the size must be strictly greater than this to match + private final long maxBytes; + private final String sizeGroup; + private final String displaySize; + final static long NO_MAXIMUM = -1; + + /** + * Construct a new FileSize enum value. + * + * @param ranking The rank for sorting. + * @param minB The minimum size included in this enum value. + * @param maxB The maximum size included in this enum value. + * @param displayName The display name for this enum value. + * @param displaySize The size to display in association with this enum + * value. + */ + FileSize(int ranking, long minB, long maxB, String displayName, String displaySize) { + this.ranking = ranking; + this.minBytes = minB; + if (maxB >= 0) { + this.maxBytes = maxB; + } else { + this.maxBytes = NO_MAXIMUM; + } + this.sizeGroup = displayName; + this.displaySize = displaySize; + } + + /** + * Get the enum corresponding to the given file size for image files. + * The file size must be strictly greater than minBytes. + * + * @param size The file size. + * + * @return The enum whose range contains the file size. + */ + public static FileSize fromImageSize(long size) { + if (size > XXLARGE_IMAGE.getMinBytes()) { + return XXLARGE_IMAGE; + } else if (size > XLARGE_IMAGE.getMinBytes()) { + return XLARGE_IMAGE; + } else if (size > LARGE_IMAGE.getMinBytes()) { + return LARGE_IMAGE; + } else if (size > MEDIUM_IMAGE.getMinBytes()) { + return MEDIUM_IMAGE; + } else if (size > SMALL_IMAGE.getMinBytes()) { + return SMALL_IMAGE; + } else { + return XSMALL_IMAGE; + } + } + + /** + * Get the enum corresponding to the given file size for video files. + * The file size must be strictly greater than minBytes. + * + * @param size The file size. + * + * @return The enum whose range contains the file size. + */ + public static FileSize fromVideoSize(long size) { + if (size > XXLARGE_VIDEO.getMinBytes()) { + return XXLARGE_VIDEO; + } else if (size > XLARGE_VIDEO.getMinBytes()) { + return XLARGE_VIDEO; + } else if (size > LARGE_VIDEO.getMinBytes()) { + return LARGE_VIDEO; + } else if (size > MEDIUM_VIDEO.getMinBytes()) { + return MEDIUM_VIDEO; + } else if (size > SMALL_VIDEO.getMinBytes()) { + return SMALL_VIDEO; + } else { + return XSMALL_VIDEO; + } + } + + /** + * Get the upper limit of the range. + * + * @return The maximum file size that will fit in this range. + */ + public long getMaxBytes() { + return maxBytes; + } + + /** + * Get the lower limit of the range. + * + * @return The maximum file size that is not part of this range. + */ + public long getMinBytes() { + return minBytes; + } + + /** + * Get the rank for sorting. + * + * @return The rank (lower should be displayed first). + */ + public int getRanking() { + return ranking; + } + + @Override + public String toString() { + return sizeGroup + displaySize; + } + + /** + * Get the name of the size group. For example Small. + * + * @return The name of the size group. For example Small. + */ + public String getSizeGroup() { + return sizeGroup; + } + + /** + * Get the list of enums that are valid for most file sizes. + * + * @return Enums that can be used to filter most file including images + * by size. + */ + public static List getDefaultSizeOptions() { + return Arrays.asList(XXLARGE_IMAGE, XLARGE_IMAGE, LARGE_IMAGE, MEDIUM_IMAGE, SMALL_IMAGE, XSMALL_IMAGE); + } + + /** + * Get the list of enums that are valid for video sizes. + * + * @return enums that can be used to filter videos by size. + */ + public static List getOptionsForVideos() { + return Arrays.asList(XXLARGE_VIDEO, XLARGE_VIDEO, LARGE_VIDEO, MEDIUM_VIDEO, SMALL_VIDEO, XSMALL_VIDEO); + } + } + + //Discovery uses a different list of document mime types than FileTypeUtils.FileTypeCategory.DOCUMENTS + private static final ImmutableSet DOCUMENT_MIME_TYPES + = new ImmutableSet.Builder() + .add("text/html", //NON-NLS + "text/csv", //NON-NLS + "application/rtf", //NON-NLS + "application/pdf", //NON-NLS + "application/xhtml+xml", //NON-NLS + "application/x-msoffice", //NON-NLS + "application/msword", //NON-NLS + "application/msword2", //NON-NLS + "application/vnd.wordperfect", //NON-NLS + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", //NON-NLS + "application/vnd.ms-powerpoint", //NON-NLS + "application/vnd.openxmlformats-officedocument.presentationml.presentation", //NON-NLS + "application/vnd.ms-excel", //NON-NLS + "application/vnd.ms-excel.sheet.4", //NON-NLS + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", //NON-NLS + "application/vnd.oasis.opendocument.presentation", //NON-NLS + "application/vnd.oasis.opendocument.spreadsheet", //NON-NLS + "application/vnd.oasis.opendocument.text" //NON-NLS + ).build(); + + private static final ImmutableSet IMAGE_UNSUPPORTED_DOC_TYPES + = new ImmutableSet.Builder() + .add("application/pdf", //NON-NLS + "application/xhtml+xml").build(); //NON-NLS + + /** + * Get the list of document types for which image extraction is not + * supported. + * + * @return The list of document types for which image extraction is not + * supported. + */ + public static Collection getDocTypesWithoutImageExtraction() { + return Collections.unmodifiableCollection(IMAGE_UNSUPPORTED_DOC_TYPES); + } + + /** + * Enum representing the type. + */ + @NbBundle.Messages({ + "SearchData.FileType.Audio.displayName=Audio", + "SearchData.FileType.Video.displayName=Video", + "SearchData.FileType.Image.displayName=Image", + "SearchData.FileType.Documents.displayName=Document", + "SearchData.FileType.Executables.displayName=Executable", + "SearchData.AttributeType.Domain.displayName=Domain", + "SearchData.FileType.Other.displayName=Other/Unknown"}) + public enum Type { + + IMAGE(0, Bundle.SearchData_FileType_Image_displayName(), FileTypeUtils.FileTypeCategory.IMAGE.getMediaTypes(), new ArrayList<>()), + AUDIO(1, Bundle.SearchData_FileType_Audio_displayName(), FileTypeUtils.FileTypeCategory.AUDIO.getMediaTypes(), new ArrayList<>()), + VIDEO(2, Bundle.SearchData_FileType_Video_displayName(), FileTypeUtils.FileTypeCategory.VIDEO.getMediaTypes(), new ArrayList<>()), + EXECUTABLE(3, Bundle.SearchData_FileType_Executables_displayName(), FileTypeUtils.FileTypeCategory.EXECUTABLE.getMediaTypes(), new ArrayList<>()), + DOCUMENT(4, Bundle.SearchData_FileType_Documents_displayName(), DOCUMENT_MIME_TYPES, new ArrayList<>()), + DOMAIN(6, Bundle.SearchData_AttributeType_Domain_displayName(), new ArrayList<>(), DOMAIN_ARTIFACT_TYPES), + OTHER(5, Bundle.SearchData_FileType_Other_displayName(), new ArrayList<>(), new ArrayList<>()); + + private final int ranking; // For ordering in the UI + private final String displayName; + private final Collection mediaTypes; + private final Collection artifactTypes; + + /** + * Construct a new Type enum value. + * + * @param value Integer value for comparison. + * @param displayName The display name for this type. + * @param mediaTypes The list of mime types this type is defined by + * if it is file type. + * @param artifactTypes The list of artifact types this type is defined + * by if it is an attribute type. + */ + Type(int value, String displayName, Collection mediaTypes, Collection artifactTypes) { + this.ranking = value; + this.displayName = displayName; + this.mediaTypes = mediaTypes; + this.artifactTypes = artifactTypes; + } + + /** + * Get the MIME types matching this category. + * + * @return Collection of MIME type strings + */ + public Collection getMediaTypes() { + return Collections.unmodifiableCollection(mediaTypes); + } + + /** + * Get the BlackboardArtifact types matching this category. + * + * @return Collection of BlackboardArtifact.ARTIFACT_TYPE objects. + */ + public Collection getArtifactTypes() { + return Collections.unmodifiableCollection(artifactTypes); + } + + @Override + public String toString() { + return displayName; + } + + /** + * Get the rank for sorting. + * + * @return the rank (lower should be displayed first) + */ + public int getRanking() { + return ranking; + } + + } + + /** + * Enum representing the score of the item. + */ + @NbBundle.Messages({ + "SearchData.Score.notable.displayName=Notable", + "SearchData.Score.interesting.displayName=Interesting", + "SearchData.Score.unknown.displayName=Unknown",}) + public enum Score { + NOTABLE(0, Bundle.SearchData_Score_notable_displayName()), + INTERESTING(1, Bundle.SearchData_Score_interesting_displayName()), + UNKNOWN(2, Bundle.SearchData_Score_unknown_displayName()); + + private final int ranking; + private final String displayName; + + /** + * Construct a new Score enum value. + * + * @param ranking The rank for sorting. + * @param displayName The display name for this enum value. + */ + Score(int ranking, String displayName) { + this.ranking = ranking; + this.displayName = displayName; + } + + /** + * Get the rank for sorting. + * + * @return The rank (lower should be displayed first). + */ + public int getRanking() { + return ranking; + } + + /** + * Get the list of enums that are valid for filtering. + * + * @return Enums that can be used to filter. + */ + public static List getOptionsForFiltering() { + return Arrays.asList(NOTABLE, INTERESTING); + } + + @Override + public String toString() { + return displayName; + } + } + + /** + * Private constructor for SearchData class. + */ + private SearchData() { + // Class should not be instantiated + } +} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/FileSearchFiltering.java b/Core/src/org/sleuthkit/autopsy/discovery/search/SearchFiltering.java similarity index 59% rename from Core/src/org/sleuthkit/autopsy/discovery/FileSearchFiltering.java rename to Core/src/org/sleuthkit/autopsy/discovery/search/SearchFiltering.java index adf301537a..9505cfa5e2 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/FileSearchFiltering.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/SearchFiltering.java @@ -16,51 +16,58 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.search; +import java.text.SimpleDateFormat; import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeNormalizationException; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoException; -import org.sleuthkit.autopsy.discovery.FileSearchData.FileSize; -import org.sleuthkit.autopsy.discovery.FileSearchData.FileType; -import org.sleuthkit.autopsy.discovery.FileSearchData.Frequency; -import org.sleuthkit.autopsy.discovery.FileSearchData.Score; +import org.sleuthkit.autopsy.discovery.search.SearchData.FileSize; +import org.sleuthkit.autopsy.discovery.search.SearchData.Type; +import org.sleuthkit.autopsy.discovery.search.SearchData.Frequency; +import org.sleuthkit.autopsy.discovery.search.SearchData.Score; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.DataSource; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TagName; import org.sleuthkit.datamodel.TskCoreException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; import java.util.List; +import java.util.Locale; +import java.util.StringJoiner; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import org.openide.util.NbBundle; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardAttribute; import org.sleuthkit.datamodel.TskData; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; +import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE; /** - * Run various filters to return a subset of files from the current case. + * Run various filters to return a subset of Results from the current case. */ -class FileSearchFiltering { +public class SearchFiltering { /** * Run the given filters to get a list of matching files. * - * @param filters The filters to run - * @param caseDb The case database - * @param crDb The central repo. Can be null as long as no filters need - * it. + * @param filters The filters to run. + * @param caseDb The case database. + * @param centralRepoDb The central repo. Can be null as long as no filters + * need it. * - * @return + * @return List of Results from the search performed. */ - static List runQueries(List filters, SleuthkitCase caseDb, CentralRepository centralRepoDb) throws FileSearchException { + static List runQueries(List filters, SleuthkitCase caseDb, CentralRepository centralRepoDb) throws DiscoveryException { if (caseDb == null) { - throw new FileSearchException("Case DB parameter is null"); // NON-NLS + throw new DiscoveryException("Case DB parameter is null"); // NON-NLS } // Combine all the SQL queries from the filters into one query String combinedQuery = ""; - for (FileFilter filter : filters) { + for (AbstractFilter filter : filters) { if (!filter.getWhereClause().isEmpty()) { if (!combinedQuery.isEmpty()) { combinedQuery += " AND "; // NON-NLS @@ -71,12 +78,12 @@ class FileSearchFiltering { if (combinedQuery.isEmpty()) { // The file search filter is required, so this should never be empty. - throw new FileSearchException("Selected filters do not include a case database query"); + throw new DiscoveryException("Selected filters do not include a case database query"); } try { return getResultList(filters, combinedQuery, caseDb, centralRepoDb); } catch (TskCoreException ex) { - throw new FileSearchException("Error querying case database", ex); // NON-NLS + throw new DiscoveryException("Error querying case database", ex); // NON-NLS } } @@ -86,17 +93,17 @@ class FileSearchFiltering { * @param filters The filters to run. * @param combinedQuery The query to get results files for. * @param caseDb The case database. - * @param crDb The central repo. Can be null as long as no filters + * @param centralRepoDb The central repo. Can be null as long as no filters * need it. * - * @return An ArrayList of ResultFiles returned by the query. + * @return An ArrayList of Results returned by the query. * * @throws TskCoreException - * @throws FileSearchException + * @throws DiscoveryException */ - private static List getResultList(List filters, String combinedQuery, SleuthkitCase caseDb, CentralRepository centralRepoDb) throws TskCoreException, FileSearchException { + private static List getResultList(List filters, String combinedQuery, SleuthkitCase caseDb, CentralRepository centralRepoDb) throws TskCoreException, DiscoveryException { // Get all matching abstract files - List resultList = new ArrayList<>(); + List resultList = new ArrayList<>(); List sqlResults = caseDb.findAllFilesWhere(combinedQuery); // If there are no results, return now @@ -110,7 +117,7 @@ class FileSearchFiltering { } // Now run any non-SQL filters. - for (FileFilter filter : filters) { + for (AbstractFilter filter : filters) { if (filter.useAlternateFilter()) { resultList = filter.applyAlternateFilter(resultList, caseDb, centralRepoDb); } @@ -123,74 +130,139 @@ class FileSearchFiltering { } /** - * Base class for the filters. + * A filter to specify date range for artifacts, start and end times should + * be in epoch seconds. */ - static abstract class FileFilter { + public static class ArtifactDateRangeFilter extends AbstractFilter { + + private final Long startDate; + private final Long endDate; + + // Attributes to search for date + private static List dateAttributes + = Arrays.asList( + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_CREATED, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_ACCESSED + ); /** - * Returns part of a query on the tsk_files table that can be AND-ed - * with other pieces + * Construct a new ArtifactDateRangeFilter. * - * @return the SQL query or an empty string if there is no SQL query for - * this filter. + * @param startDate The first date to include results for in the search. + * @param endDate The last date to include results for in the search. */ - abstract String getWhereClause(); - - /** - * Indicates whether this filter needs to use the secondary, non-SQL - * method applyAlternateFilter(). - * - * @return false by default - */ - boolean useAlternateFilter() { - return false; + public ArtifactDateRangeFilter(Long startDate, Long endDate) { + this.startDate = startDate; + this.endDate = endDate; } /** - * Run a secondary filter that does not operate on tsk_files. - * - * @param currentResults The current list of matching files; empty if no - * filters have yet been run. - * @param caseDb The case database - * @param centralRepoDb The central repo database. Can be null if the - * filter does not require it. - * - * @return The list of files that match this filter (and any that came - * before it) - * - * @throws FileSearchException + * Create a SQL clause containing the date time attribute types to + * search. */ - List applyAlternateFilter(List currentResults, SleuthkitCase caseDb, - CentralRepository centralRepoDb) throws FileSearchException { - return new ArrayList<>(); + static String createAttributeTypeClause() { + StringJoiner joiner = new StringJoiner(","); + for (BlackboardAttribute.ATTRIBUTE_TYPE type : dateAttributes) { + joiner.add("\'" + type.getTypeID() + "\'"); + } + return "attribute_type_id IN (" + joiner.toString() + ")"; } - /** - * Get a description of the selected filter. - * - * @return A description of the filter - */ - abstract String getDesc(); + @Override + public String getWhereClause() { + return createAttributeTypeClause() + + " AND (value_int64 BETWEEN " + startDate + " AND " + endDate + ")"; + } + + @NbBundle.Messages({"SearchFiltering.dateRangeFilter.lable=Activity date ", + "# {0} - startDate", + "SearchFiltering.dateRangeFilter.after=after: {0}", + "# {0} - endDate", + "SearchFiltering.dateRangeFilter.before=before: {0}", + "SearchFiltering.dateRangeFilter.and= and "}) + @Override + public String getDesc() { + String desc = ""; // NON-NLS + if (startDate > 0) { + desc += Bundle.SearchFiltering_dateRangeFilter_after(new SimpleDateFormat("yyyy/MM/dd", Locale.getDefault()).format(new Date(TimeUnit.SECONDS.toMillis(startDate)))); + } + if (endDate < 10000000000L) { //arbitrary time sometime in the 23rd century to check that they specified a date and the max date isn't being used + if (!desc.isEmpty()) { + desc += Bundle.SearchFiltering_dateRangeFilter_and(); + } + desc += Bundle.SearchFiltering_dateRangeFilter_before(new SimpleDateFormat("yyyy/MM/dd", Locale.getDefault()).format(new Date(TimeUnit.SECONDS.toMillis(endDate)))); + } + if (!desc.isEmpty()) { + desc = Bundle.SearchFiltering_dateRangeFilter_lable() + desc; + } + return desc; + } } /** - * A filter for specifying the file size + * A filter to specify artifact types. */ - static class SizeFilter extends FileFilter { + public static class ArtifactTypeFilter extends AbstractFilter { + + private final List types; + + /** + * Construct a new ArtifactTypeFilter. + * + * @param types The list of BlackboardArtifact types to include in + * results from. + */ + public ArtifactTypeFilter(List types) { + this.types = types; + } + + @Override + public String getWhereClause() { + StringJoiner joiner = new StringJoiner(","); + for (ARTIFACT_TYPE type : types) { + joiner.add("\'" + type.getTypeID() + "\'"); + } + + return "artifact_type_id IN (" + joiner + ")"; + } + + @NbBundle.Messages({"# {0} - artifactTypes", + "SearchFiltering.artifactTypeFilter.desc=Result type(s): {0}", + "SearchFiltering.artifactTypeFilter.or=, "}) + @Override + public String getDesc() { + String desc = ""; // NON-NLS + for (ARTIFACT_TYPE type : types) { + if (!desc.isEmpty()) { + desc += Bundle.SearchFiltering_artifactTypeFilter_or(); + } + desc += type.getDisplayName(); + } + desc = Bundle.SearchFiltering_artifactTypeFilter_desc(desc); + return desc; + } + + } + + /** + * A filter for specifying the file size. + */ + public static class SizeFilter extends AbstractFilter { private final List fileSizes; /** - * Create the SizeFilter + * Create the SizeFilter. * - * @param fileSizes the file sizes that should match + * @param fileSizes The file sizes that should match. */ - SizeFilter(List fileSizes) { + public SizeFilter(List fileSizes) { this.fileSizes = fileSizes; } @Override - String getWhereClause() { + public String getWhereClause() { String queryStr = ""; // NON-NLS for (FileSize size : fileSizes) { if (!queryStr.isEmpty()) { @@ -207,53 +279,53 @@ class FileSearchFiltering { @NbBundle.Messages({ "# {0} - filters", - "FileSearchFiltering.SizeFilter.desc=Size(s): {0}", - "FileSearchFiltering.SizeFilter.or=, "}) + "SearchFiltering.SizeFilter.desc=Size(s): {0}", + "SearchFiltering.SizeFilter.or=, "}) @Override - String getDesc() { + public String getDesc() { String desc = ""; // NON-NLS for (FileSize size : fileSizes) { if (!desc.isEmpty()) { - desc += Bundle.FileSearchFiltering_SizeFilter_or(); + desc += Bundle.SearchFiltering_SizeFilter_or(); } desc += size.getSizeGroup(); } - desc = Bundle.FileSearchFiltering_SizeFilter_desc(desc); + desc = Bundle.SearchFiltering_SizeFilter_desc(desc); return desc; } } /** * A utility class for the ParentFilter to store the search string and - * whether it is a full path or a substring. + * whether it is a full path or a sub-string. */ - static class ParentSearchTerm { + public static class ParentSearchTerm { private final String searchStr; private final boolean fullPath; private final boolean included; /** - * Create the ParentSearchTerm object + * Create the ParentSearchTerm object. * - * @param searchStr The string to search for in the file path + * @param searchStr The string to search for in the file path. * @param isFullPath True if the path should exactly match the given - * string, false to do a substring search + * string, false to do a sub-string search. * @param isIncluded True if the results must include the path, false if * the path should be excluded from the results. */ - ParentSearchTerm(String searchStr, boolean isFullPath, boolean isIncluded) { + public ParentSearchTerm(String searchStr, boolean isFullPath, boolean isIncluded) { this.searchStr = searchStr; this.fullPath = isFullPath; this.included = isIncluded; } /** - * Get the SQL term to search for + * Get the SQL term to search for. * - * @return The SQL for a where clause to search for a matching path + * @return The SQL for a where clause to search for a matching path. */ - String getSQLForTerm() { + public String getSQLForTerm() { // TODO - these should really be prepared statements if (isIncluded()) { if (isFullPath()) { @@ -271,66 +343,76 @@ class FileSearchFiltering { } @NbBundle.Messages({ - "FileSearchFiltering.ParentSearchTerm.fullString= (exact)", - "FileSearchFiltering.ParentSearchTerm.subString= (substring)", - "FileSearchFiltering.ParentSearchTerm.includeString= (include)", - "FileSearchFiltering.ParentSearchTerm.excludeString= (exclude)",}) + "SearchFiltering.ParentSearchTerm.fullString= (exact)", + "SearchFiltering.ParentSearchTerm.subString= (substring)", + "SearchFiltering.ParentSearchTerm.includeString= (include)", + "SearchFiltering.ParentSearchTerm.excludeString= (exclude)",}) @Override public String toString() { String returnString = getSearchStr(); if (isFullPath()) { - returnString += Bundle.FileSearchFiltering_ParentSearchTerm_fullString(); + returnString += Bundle.SearchFiltering_ParentSearchTerm_fullString(); } else { - returnString += Bundle.FileSearchFiltering_ParentSearchTerm_subString(); + returnString += Bundle.SearchFiltering_ParentSearchTerm_subString(); } if (isIncluded()) { - returnString += Bundle.FileSearchFiltering_ParentSearchTerm_includeString(); + returnString += Bundle.SearchFiltering_ParentSearchTerm_includeString(); } else { - returnString += Bundle.FileSearchFiltering_ParentSearchTerm_excludeString(); + returnString += Bundle.SearchFiltering_ParentSearchTerm_excludeString(); } return returnString; } /** - * @return the fullPath + * Is the search string the full path of the of the parent or is it a + * sub-string in the parent path? + * + * @return True if the search string is the full path of the parent, + * false if it is a sub-string. */ - boolean isFullPath() { + public boolean isFullPath() { return fullPath; } /** - * @return the included + * Should the search string be included in the path, or excluded from + * the path? + * + * @return True if the search string should be included, false if it + * should be excluded. */ - boolean isIncluded() { + public boolean isIncluded() { return included; } /** - * @return the searchStr + * Get the string being searched for by this filter. + * + * @return The string being searched for by this filter. */ - String getSearchStr() { + public String getSearchStr() { return searchStr; } } /** - * A filter for specifying parent path (either full path or substring) + * A filter for specifying parent path (either full path or substring). */ - static class ParentFilter extends FileFilter { + public static class ParentFilter extends AbstractFilter { private final List parentSearchTerms; /** - * Create the ParentFilter + * Create the ParentFilter. * - * @param parentSearchTerms Full paths or substrings to filter on + * @param parentSearchTerms Full paths or substrings to filter on. */ - ParentFilter(List parentSearchTerms) { + public ParentFilter(List parentSearchTerms) { this.parentSearchTerms = parentSearchTerms; } @Override - String getWhereClause() { + public String getWhereClause() { String includeQueryStr = ""; // NON-NLS String excludeQueryStr = ""; for (ParentSearchTerm searchTerm : parentSearchTerms) { @@ -361,53 +443,53 @@ class FileSearchFiltering { @NbBundle.Messages({ "# {0} - filters", - "FileSearchFiltering.ParentFilter.desc=Paths matching: {0}", - "FileSearchFiltering.ParentFilter.or=, ", - "FileSearchFiltering.ParentFilter.exact=(exact match)", - "FileSearchFiltering.ParentFilter.substring=(substring)", - "FileSearchFiltering.ParentFilter.included=(included)", - "FileSearchFiltering.ParentFilter.excluded=(excluded)"}) + "SearchFiltering.ParentFilter.desc=Paths matching: {0}", + "SearchFiltering.ParentFilter.or=, ", + "SearchFiltering.ParentFilter.exact=(exact match)", + "SearchFiltering.ParentFilter.substring=(substring)", + "SearchFiltering.ParentFilter.included=(included)", + "SearchFiltering.ParentFilter.excluded=(excluded)"}) @Override - String getDesc() { + public String getDesc() { String desc = ""; // NON-NLS for (ParentSearchTerm searchTerm : parentSearchTerms) { if (!desc.isEmpty()) { - desc += Bundle.FileSearchFiltering_ParentFilter_or(); + desc += Bundle.SearchFiltering_ParentFilter_or(); } if (searchTerm.isFullPath()) { - desc += searchTerm.getSearchStr() + Bundle.FileSearchFiltering_ParentFilter_exact(); + desc += searchTerm.getSearchStr() + Bundle.SearchFiltering_ParentFilter_exact(); } else { - desc += searchTerm.getSearchStr() + Bundle.FileSearchFiltering_ParentFilter_substring(); + desc += searchTerm.getSearchStr() + Bundle.SearchFiltering_ParentFilter_substring(); } if (searchTerm.isIncluded()) { - desc += Bundle.FileSearchFiltering_ParentFilter_included(); + desc += Bundle.SearchFiltering_ParentFilter_included(); } else { - desc += Bundle.FileSearchFiltering_ParentFilter_excluded(); + desc += Bundle.SearchFiltering_ParentFilter_excluded(); } } - desc = Bundle.FileSearchFiltering_ParentFilter_desc(desc); + desc = Bundle.SearchFiltering_ParentFilter_desc(desc); return desc; } } /** - * A filter for specifying data sources + * A filter for specifying data sources. */ - static class DataSourceFilter extends FileFilter { + public static class DataSourceFilter extends AbstractFilter { private final List dataSources; /** - * Create the DataSourceFilter + * Create the DataSourceFilter. * - * @param dataSources the data sources to filter on + * @param dataSources The data sources to filter on. */ - DataSourceFilter(List dataSources) { + public DataSourceFilter(List dataSources) { this.dataSources = dataSources; } @Override - String getWhereClause() { + public String getWhereClause() { String queryStr = ""; // NON-NLS for (DataSource ds : dataSources) { if (!queryStr.isEmpty()) { @@ -421,21 +503,21 @@ class FileSearchFiltering { @NbBundle.Messages({ "# {0} - filters", - "FileSearchFiltering.DataSourceFilter.desc=Data source(s): {0}", - "FileSearchFiltering.DataSourceFilter.or=, ", + "SearchFiltering.DataSourceFilter.desc=Data source(s): {0}", + "SearchFiltering.DataSourceFilter.or=, ", "# {0} - Data source name", "# {1} - Data source ID", - "FileSearchFiltering.DataSourceFilter.datasource={0}({1})",}) + "SearchFiltering.DataSourceFilter.datasource={0}({1})",}) @Override - String getDesc() { + public String getDesc() { String desc = ""; // NON-NLS for (DataSource ds : dataSources) { if (!desc.isEmpty()) { - desc += Bundle.FileSearchFiltering_DataSourceFilter_or(); + desc += Bundle.SearchFiltering_DataSourceFilter_or(); } - desc += Bundle.FileSearchFiltering_DataSourceFilter_datasource(ds.getName(), ds.getId()); + desc += Bundle.SearchFiltering_DataSourceFilter_datasource(ds.getName(), ds.getId()); } - desc = Bundle.FileSearchFiltering_DataSourceFilter_desc(desc); + desc = Bundle.SearchFiltering_DataSourceFilter_desc(desc); return desc; } } @@ -444,21 +526,21 @@ class FileSearchFiltering { * A filter for specifying keyword list names. A file must contain a keyword * from one of the given lists to pass. */ - static class KeywordListFilter extends FileFilter { + public static class KeywordListFilter extends AbstractFilter { private final List listNames; /** - * Create the KeywordListFilter + * Create the KeywordListFilter. * - * @param listNames + * @param listNames The list of keywords for this filter. */ - KeywordListFilter(List listNames) { + public KeywordListFilter(List listNames) { this.listNames = listNames; } @Override - String getWhereClause() { + public String getWhereClause() { String keywordListPart = concatenateNamesForSQL(listNames); String queryStr = "(obj_id IN (SELECT obj_id from blackboard_artifacts WHERE artifact_id IN " @@ -470,43 +552,43 @@ class FileSearchFiltering { @NbBundle.Messages({ "# {0} - filters", - "FileSearchFiltering.KeywordListFilter.desc=Keywords in list(s): {0}",}) + "SearchFiltering.KeywordListFilter.desc=Keywords in list(s): {0}",}) @Override - String getDesc() { - return Bundle.FileSearchFiltering_KeywordListFilter_desc(concatenateSetNamesForDisplay(listNames)); + public String getDesc() { + return Bundle.SearchFiltering_KeywordListFilter_desc(concatenateSetNamesForDisplay(listNames)); } } /** * A filter for specifying file types. */ - static class FileTypeFilter extends FileFilter { + public static class FileTypeFilter extends AbstractFilter { - private final List categories; + private final List categories; /** - * Create the FileTypeFilter + * Create the FileTypeFilter. * * @param categories List of file types to filter on */ - FileTypeFilter(List categories) { + public FileTypeFilter(List categories) { this.categories = categories; } /** - * Create the FileTypeFilter + * Create the FileTypeFilter. * - * @param category the file type to filter on + * @param category The file type to filter on. */ - FileTypeFilter(FileType category) { + public FileTypeFilter(Type category) { this.categories = new ArrayList<>(); this.categories.add(category); } @Override - String getWhereClause() { + public String getWhereClause() { String queryStr = ""; // NON-NLS - for (FileType cat : categories) { + for (Type cat : categories) { for (String type : cat.getMediaTypes()) { if (!queryStr.isEmpty()) { queryStr += ","; // NON-NLS @@ -520,18 +602,18 @@ class FileSearchFiltering { @NbBundle.Messages({ "# {0} - filters", - "FileSearchFiltering.FileTypeFilter.desc=Type: {0}", - "FileSearchFiltering.FileTypeFilter.or=, ",}) + "SearchFiltering.FileTypeFilter.desc=Type: {0}", + "SearchFiltering.FileTypeFilter.or=, ",}) @Override - String getDesc() { + public String getDesc() { String desc = ""; - for (FileType cat : categories) { + for (Type cat : categories) { if (!desc.isEmpty()) { - desc += Bundle.FileSearchFiltering_FileTypeFilter_or(); + desc += Bundle.SearchFiltering_FileTypeFilter_or(); } desc += cat.toString(); } - desc = Bundle.FileSearchFiltering_FileTypeFilter_desc(desc); + desc = Bundle.SearchFiltering_FileTypeFilter_desc(desc); return desc; } } @@ -539,48 +621,41 @@ class FileSearchFiltering { /** * A filter for specifying frequency in the central repository. */ - static class FrequencyFilter extends FileFilter { + public static class FrequencyFilter extends AbstractFilter { private final List frequencies; /** - * Create the FrequencyFilter + * Create the FrequencyFilter. * - * @param frequencies List of frequencies that will pass the filter + * @param frequencies List of frequencies that will pass the filter. */ - FrequencyFilter(List frequencies) { + public FrequencyFilter(List frequencies) { this.frequencies = frequencies; } @Override - String getWhereClause() { + public String getWhereClause() { // Since this relies on the central repository database, there is no // query on the case database. return ""; // NON-NLS } @Override - boolean useAlternateFilter() { + public boolean useAlternateFilter() { return true; } @Override - List applyAlternateFilter(List currentResults, SleuthkitCase caseDb, - CentralRepository centralRepoDb) throws FileSearchException { - - // We have to have run some kind of SQL filter before getting to this point, - // and should have checked afterward to see if the results were empty. - if (currentResults.isEmpty()) { - throw new FileSearchException("Can not run on empty list"); // NON-NLS - } - + public List applyAlternateFilter(List currentResults, SleuthkitCase caseDb, + CentralRepository centralRepoDb) throws DiscoveryException { // Set the frequency for each file - FileSearch.FrequencyAttribute freqAttr = new FileSearch.FrequencyAttribute(); - freqAttr.addAttributeToResultFiles(currentResults, caseDb, centralRepoDb); + DiscoveryAttributes.FrequencyAttribute freqAttr = new DiscoveryAttributes.FrequencyAttribute(); + freqAttr.addAttributeToResults(currentResults, caseDb, centralRepoDb); // If the frequency matches the filter, add the file to the results - List frequencyResults = new ArrayList<>(); - for (ResultFile file : currentResults) { + List frequencyResults = new ArrayList<>(); + for (Result file : currentResults) { if (frequencies.contains(file.getFrequency())) { frequencyResults.add(file); } @@ -590,18 +665,18 @@ class FileSearchFiltering { @NbBundle.Messages({ "# {0} - filters", - "FileSearchFiltering.FrequencyFilter.desc=Past occurrences: {0}", - "FileSearchFiltering.FrequencyFilter.or=, ",}) + "SearchFiltering.FrequencyFilter.desc=Past occurrences: {0}", + "SearchFiltering.FrequencyFilter.or=, ",}) @Override - String getDesc() { + public String getDesc() { String desc = ""; // NON-NLS for (Frequency freq : frequencies) { if (!desc.isEmpty()) { - desc += Bundle.FileSearchFiltering_FrequencyFilter_or(); + desc += Bundle.SearchFiltering_FrequencyFilter_or(); } desc += freq.toString(); } - return Bundle.FileSearchFiltering_FrequencyFilter_desc(desc); + return Bundle.SearchFiltering_FrequencyFilter_desc(desc); } } @@ -609,21 +684,21 @@ class FileSearchFiltering { * A filter for specifying hash set names. A file must match one of the * given sets to pass. */ - static class HashSetFilter extends FileFilter { + public static class HashSetFilter extends AbstractFilter { private final List setNames; /** - * Create the HashSetFilter + * Create the HashSetFilter. * - * @param setNames + * @param setNames The hash set names for this filter. */ - HashSetFilter(List setNames) { + public HashSetFilter(List setNames) { this.setNames = setNames; } @Override - String getWhereClause() { + public String getWhereClause() { String hashSetPart = concatenateNamesForSQL(setNames); String queryStr = "(obj_id IN (SELECT obj_id from blackboard_artifacts WHERE artifact_id IN " @@ -638,7 +713,7 @@ class FileSearchFiltering { "# {0} - filters", "FileSearchFiltering.HashSetFilter.desc=Hash set hits in set(s): {0}",}) @Override - String getDesc() { + public String getDesc() { return Bundle.FileSearchFiltering_HashSetFilter_desc(concatenateSetNamesForDisplay(setNames)); } } @@ -647,21 +722,21 @@ class FileSearchFiltering { * A filter for specifying interesting file set names. A file must match one * of the given sets to pass. */ - static class InterestingFileSetFilter extends FileFilter { + public static class InterestingFileSetFilter extends AbstractFilter { private final List setNames; /** - * Create the InterestingFileSetFilter + * Create the InterestingFileSetFilter. * - * @param setNames + * @param setNames The interesting file set names for this filter. */ - InterestingFileSetFilter(List setNames) { + public InterestingFileSetFilter(List setNames) { this.setNames = setNames; } @Override - String getWhereClause() { + public String getWhereClause() { String intItemSetPart = concatenateNamesForSQL(setNames); String queryStr = "(obj_id IN (SELECT obj_id from blackboard_artifacts WHERE artifact_id IN " @@ -674,10 +749,10 @@ class FileSearchFiltering { @NbBundle.Messages({ "# {0} - filters", - "FileSearchFiltering.InterestingItemSetFilter.desc=Interesting item hits in set(s): {0}",}) + "SearchFiltering.InterestingItemSetFilter.desc=Interesting item hits in set(s): {0}",}) @Override - String getDesc() { - return Bundle.FileSearchFiltering_InterestingItemSetFilter_desc(concatenateSetNamesForDisplay(setNames)); + public String getDesc() { + return Bundle.SearchFiltering_InterestingItemSetFilter_desc(concatenateSetNamesForDisplay(setNames)); } } @@ -685,21 +760,21 @@ class FileSearchFiltering { * A filter for specifying object types detected. A file must match one of * the given types to pass. */ - static class ObjectDetectionFilter extends FileFilter { + public static class ObjectDetectionFilter extends AbstractFilter { private final List typeNames; /** - * Create the ObjectDetectionFilter + * Create the ObjectDetectionFilter. * - * @param typeNames + * @param typeNames The type names for this filter. */ - ObjectDetectionFilter(List typeNames) { + public ObjectDetectionFilter(List typeNames) { this.typeNames = typeNames; } @Override - String getWhereClause() { + public String getWhereClause() { String objTypePart = concatenateNamesForSQL(typeNames); String queryStr = "(obj_id IN (SELECT obj_id from blackboard_artifacts WHERE artifact_id IN " @@ -712,32 +787,32 @@ class FileSearchFiltering { @NbBundle.Messages({ "# {0} - filters", - "FileSearchFiltering.ObjectDetectionFilter.desc=Objects detected in set(s): {0}",}) + "SearchFiltering.ObjectDetectionFilter.desc=Objects detected in set(s): {0}",}) @Override - String getDesc() { - return Bundle.FileSearchFiltering_ObjectDetectionFilter_desc(concatenateSetNamesForDisplay(typeNames)); + public String getDesc() { + return Bundle.SearchFiltering_ObjectDetectionFilter_desc(concatenateSetNamesForDisplay(typeNames)); } } /** * A filter for specifying the score. A file must have one of the given - * scores to pass + * scores to pass. */ - static class ScoreFilter extends FileFilter { + public static class ScoreFilter extends AbstractFilter { private final List scores; /** - * Create the ObjectDetectionFilter + * Create the ScoreFilter. * - * @param typeNames + * @param scores The list of scores for this filter. */ - ScoreFilter(List scores) { + public ScoreFilter(List scores) { this.scores = scores; } @Override - String getWhereClause() { + public String getWhereClause() { // Current algorithm: // "Notable" if the file is a match for a notable hashset or has been tagged with a notable tag. @@ -788,10 +863,10 @@ class FileSearchFiltering { @NbBundle.Messages({ "# {0} - filters", - "FileSearchFiltering.ScoreFilter.desc=Score(s) of : {0}",}) + "SearchFiltering.ScoreFilter.desc=Score(s) of : {0}",}) @Override - String getDesc() { - return Bundle.FileSearchFiltering_ScoreFilter_desc( + public String getDesc() { + return Bundle.SearchFiltering_ScoreFilter_desc( concatenateSetNamesForDisplay(scores.stream().map(p -> p.toString()).collect(Collectors.toList()))); } } @@ -800,21 +875,21 @@ class FileSearchFiltering { * A filter for specifying tag names. A file must contain one of the given * tags to pass. */ - static class TagsFilter extends FileFilter { + public static class TagsFilter extends AbstractFilter { private final List tagNames; /** - * Create the TagsFilter + * Create the TagsFilter. * - * @param tagNames + * @param tagNames The list of tag names for this filter. */ - TagsFilter(List tagNames) { + public TagsFilter(List tagNames) { this.tagNames = tagNames; } @Override - String getWhereClause() { + public String getWhereClause() { String tagIDs = ""; // NON-NLS for (TagName tagName : tagNames) { if (!tagIDs.isEmpty()) { @@ -833,7 +908,7 @@ class FileSearchFiltering { "FileSearchFiltering.TagsFilter.desc=Tagged {0}", "FileSearchFiltering.TagsFilter.or=, ",}) @Override - String getDesc() { + public String getDesc() { String desc = ""; // NON-NLS for (TagName name : tagNames) { if (!desc.isEmpty()) { @@ -849,17 +924,10 @@ class FileSearchFiltering { * A filter for specifying that the file must have user content suspected * data. */ - static class UserCreatedFilter extends FileFilter { - - /** - * Create the ExifFilter - */ - UserCreatedFilter() { - // Nothing to save - } + public static class UserCreatedFilter extends AbstractFilter { @Override - String getWhereClause() { + public String getWhereClause() { return "(obj_id IN (SELECT obj_id from blackboard_artifacts WHERE artifact_id IN " + "(SELECT artifact_id FROM blackboard_attributes WHERE artifact_type_id = " + BlackboardArtifact.ARTIFACT_TYPE.TSK_USER_CONTENT_SUSPECTED.getTypeID() + ")))"; @@ -868,7 +936,7 @@ class FileSearchFiltering { @NbBundle.Messages({ "FileSearchFiltering.UserCreatedFilter.desc=that contain EXIF data",}) @Override - String getDesc() { + public String getDesc() { return Bundle.FileSearchFiltering_UserCreatedFilter_desc(); } } @@ -877,67 +945,63 @@ class FileSearchFiltering { * A filter for specifying that the file must have been marked as notable in * the CR. */ - static class NotableFilter extends FileFilter { - - /** - * Create the NotableFilter - */ - NotableFilter() { - // Nothing to save - } + public static class NotableFilter extends AbstractFilter { @Override - String getWhereClause() { + public String getWhereClause() { // Since this relies on the central repository database, there is no // query on the case database. return ""; // NON-NLS } @Override - boolean useAlternateFilter() { + public boolean useAlternateFilter() { return true; } @Override - List applyAlternateFilter(List currentResults, SleuthkitCase caseDb, - CentralRepository centralRepoDb) throws FileSearchException { + public List applyAlternateFilter(List currentResults, SleuthkitCase caseDb, + CentralRepository centralRepoDb) throws DiscoveryException { if (centralRepoDb == null) { - throw new FileSearchException("Can not run Previously Notable filter with null Central Repository DB"); // NON-NLS + throw new DiscoveryException("Can not run Previously Notable filter with null Central Repository DB"); // NON-NLS } // We have to have run some kind of SQL filter before getting to this point, // and should have checked afterward to see if the results were empty. if (currentResults.isEmpty()) { - throw new FileSearchException("Can not run on empty list"); // NON-NLS + throw new DiscoveryException("Can not run on empty list"); // NON-NLS } // The matching files - List notableResults = new ArrayList<>(); + List notableResults = new ArrayList<>(); try { CorrelationAttributeInstance.Type type = CorrelationAttributeInstance.getDefaultCorrelationTypes().get(CorrelationAttributeInstance.FILES_TYPE_ID); - for (ResultFile file : currentResults) { + for (Result result : currentResults) { + ResultFile file = (ResultFile) result; + if (result.getType() == SearchData.Type.DOMAIN) { + break; + } if (file.getFirstInstance().getMd5Hash() != null && !file.getFirstInstance().getMd5Hash().isEmpty()) { - // Check if this file hash is marked as notable in the CR String value = file.getFirstInstance().getMd5Hash(); if (centralRepoDb.getCountArtifactInstancesKnownBad(type, value) > 0) { - notableResults.add(file); + notableResults.add(result); } } } return notableResults; } catch (CentralRepoException | CorrelationAttributeNormalizationException ex) { - throw new FileSearchException("Error querying central repository", ex); // NON-NLS + throw new DiscoveryException("Error querying central repository", ex); // NON-NLS } } @NbBundle.Messages({ "FileSearchFiltering.PreviouslyNotableFilter.desc=that were previously marked as notable",}) @Override - String getDesc() { + public String getDesc() { return Bundle.FileSearchFiltering_PreviouslyNotableFilter_desc(); } } @@ -945,21 +1009,28 @@ class FileSearchFiltering { /** * A filter for specifying if known files should be included. */ - static class KnownFilter extends FileFilter { + public static class KnownFilter extends AbstractFilter { @Override - String getWhereClause() { + public String getWhereClause() { return "known!=" + TskData.FileKnown.KNOWN.getFileKnownValue(); // NON-NLS } @NbBundle.Messages({ "FileSearchFiltering.KnownFilter.desc=which are not known"}) @Override - String getDesc() { + public String getDesc() { return Bundle.FileSearchFiltering_KnownFilter_desc(); } } + /** + * Concatenate the set names into a "," separated list. + * + * @param setNames The List of setNames to concatenate. + * + * @return The concatenated list for display. + */ @NbBundle.Messages({ "FileSearchFiltering.concatenateSetNamesForDisplay.comma=, ",}) private static String concatenateSetNamesForDisplay(List setNames) { @@ -977,9 +1048,9 @@ class FileSearchFiltering { * Concatenate the set names into an "OR" separated list. This does not do * any SQL-escaping. * - * @param setNames + * @param setNames The List of setNames to concatenate. * - * @return the list to use in the SQL query + * @return The concatenated list to use in the SQL query. */ private static String concatenateNamesForSQL(List setNames) { String result = ""; // NON-NLS @@ -992,7 +1063,10 @@ class FileSearchFiltering { return result; } - private FileSearchFiltering() { + /** + * Private constructor for SearchFiltering class. + */ + private SearchFiltering() { // Class should not be instantiated } } diff --git a/Core/src/org/sleuthkit/autopsy/discovery/SearchResults.java b/Core/src/org/sleuthkit/autopsy/discovery/search/SearchResults.java similarity index 62% rename from Core/src/org/sleuthkit/autopsy/discovery/SearchResults.java rename to Core/src/org/sleuthkit/autopsy/discovery/search/SearchResults.java index 29e6eb924d..8d07b03e22 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/SearchResults.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/SearchResults.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.search; import java.util.ArrayList; import java.util.Collections; @@ -25,19 +25,19 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; -import org.sleuthkit.autopsy.discovery.FileSearch.GroupKey; +import org.sleuthkit.autopsy.discovery.search.DiscoveryKeyUtils.GroupKey; /** * Class to hold the results of the filtering/grouping/sorting operations. */ class SearchResults { - private final FileGroup.GroupSortingAlgorithm groupSortingType; - private final FileSearch.AttributeType attrType; - private final FileSorter fileSorter; + private final Group.GroupSortingAlgorithm groupSortingType; + private final DiscoveryAttributes.AttributeType attrType; + private final ResultsSorter fileSorter; - private final Map groupMap = new HashMap<>(); - private List groupList = new ArrayList<>(); + private final Map groupMap = new HashMap<>(); + private List groupList = new ArrayList<>(); private static final long MAX_OUTPUT_FILES = 2000; // For debug UI - maximum number of lines to print @@ -50,11 +50,11 @@ class SearchResults { * @param fileSortingMethod The method that should be used to * sortGroupsAndFiles the files in each group. */ - SearchResults(FileGroup.GroupSortingAlgorithm groupSortingType, FileSearch.AttributeType attrType, - FileSorter.SortingMethod fileSortingMethod) { + SearchResults(Group.GroupSortingAlgorithm groupSortingType, DiscoveryAttributes.AttributeType attrType, + ResultsSorter.SortingMethod fileSortingMethod) { this.groupSortingType = groupSortingType; this.attrType = attrType; - this.fileSorter = new FileSorter(fileSortingMethod); + this.fileSorter = new ResultsSorter(fileSortingMethod); } /** @@ -62,9 +62,9 @@ class SearchResults { * the search is finished. */ SearchResults() { - this.groupSortingType = FileGroup.GroupSortingAlgorithm.BY_GROUP_NAME; - this.attrType = new FileSearch.FileSizeAttribute(); - this.fileSorter = new FileSorter(FileSorter.SortingMethod.BY_FILE_NAME); + this.groupSortingType = Group.GroupSortingAlgorithm.BY_GROUP_NAME; + this.attrType = new DiscoveryAttributes.FileSizeAttribute(); + this.fileSorter = new ResultsSorter(ResultsSorter.SortingMethod.BY_FILE_NAME); } /** @@ -72,15 +72,15 @@ class SearchResults { * * @param files The list of ResultFiles to add. */ - void add(List files) { - for (ResultFile file : files) { + void add(List results) { + for (Result result : results) { // Add the file to the appropriate group, creating it if necessary - FileSearch.GroupKey groupKey = attrType.getGroupKey(file); + GroupKey groupKey = attrType.getGroupKey(result); if (!groupMap.containsKey(groupKey)) { - groupMap.put(groupKey, new FileGroup(groupSortingType, groupKey)); + groupMap.put(groupKey, new Group(groupSortingType, groupKey)); } - groupMap.get(groupKey).addFile(file); + groupMap.get(groupKey).addResult(result); } } @@ -91,8 +91,8 @@ class SearchResults { void sortGroupsAndFiles() { // First sortGroupsAndFiles the files - for (FileGroup group : groupMap.values()) { - group.sortFiles(fileSorter); + for (Group group : groupMap.values()) { + group.sortResults(fileSorter); } // Now put the groups in a list and sortGroupsAndFiles them @@ -102,25 +102,25 @@ class SearchResults { @Override public String toString() { - String result = ""; + String resultString = ""; if (groupList == null) { - return result; + return resultString; } long count = 0; - for (FileGroup group : groupList) { - result += group.getDisplayName() + "\n"; + for (Group group : groupList) { + resultString += group.getDisplayName() + "\n"; - for (ResultFile file : group.getFiles()) { - result += " " + file.toString() + "\n"; + for (Result result : group.getResults()) { + resultString += " " + result.toString() + "\n"; count++; if (count > MAX_OUTPUT_FILES) { - result += "(truncated)"; - return result; + resultString += "(truncated)"; + return resultString; } } } - return result; + return resultString; } /** @@ -129,7 +129,7 @@ class SearchResults { * @return The list of group names. */ List getGroupNamesWithCounts() { - return groupList.stream().map(p -> p.getDisplayName() + " (" + p.getFiles().size() + ")").collect(Collectors.toList()); + return groupList.stream().map(p -> p.getDisplayName() + " (" + p.getResults().size() + ")").collect(Collectors.toList()); } /** @@ -139,13 +139,13 @@ class SearchResults { * * @return The list of result files. */ - List getResultFilesInGroup(String groupName) { + List getResultFilesInGroup(String groupName) { if (groupName != null) { final String modifiedGroupName = groupName.replaceAll(" \\([0-9]+\\)$", ""); - java.util.Optional fileGroup = groupList.stream().filter(p -> p.getDisplayName().equals(modifiedGroupName)).findFirst(); - if (fileGroup.isPresent()) { - return fileGroup.get().getFiles(); + java.util.Optional group = groupList.stream().filter(p -> p.getDisplayName().equals(modifiedGroupName)).findFirst(); + if (group.isPresent()) { + return group.get().getResults(); } } return new ArrayList<>(); @@ -156,15 +156,15 @@ class SearchResults { * * @return The grouped and sorted results. */ - Map> toLinkedHashMap() throws FileSearchException { - Map> map = new LinkedHashMap<>(); + Map> toLinkedHashMap() throws DiscoveryException { + Map> map = new LinkedHashMap<>(); // Sort the groups and files sortGroupsAndFiles(); // groupList is sorted and a LinkedHashMap will preserve that order. - for (FileGroup group : groupList) { - map.put(group.getGroupKey(), group.getFiles()); + for (Group group : groupList) { + map.put(group.getGroupKey(), group.getResults()); } return map; diff --git a/Core/src/org/sleuthkit/autopsy/discovery/search/SummaryHelpers.java b/Core/src/org/sleuthkit/autopsy/discovery/search/SummaryHelpers.java new file mode 100644 index 0000000000..c258801f70 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/SummaryHelpers.java @@ -0,0 +1,243 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.discovery.search; + +import com.google.common.io.Files; +import java.awt.Image; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Reader; +import java.nio.file.Paths; +import java.util.Collection; +import java.util.logging.Level; +import org.apache.commons.lang.StringUtils; +import org.openide.util.Lookup; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.coreutils.ImageUtils; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.textextractors.TextExtractor; +import org.sleuthkit.autopsy.textextractors.TextExtractorFactory; +import org.sleuthkit.autopsy.textsummarizer.TextSummarizer; +import org.sleuthkit.autopsy.textsummarizer.TextSummary; +import org.sleuthkit.autopsy.texttranslation.NoServiceProviderException; +import org.sleuthkit.autopsy.texttranslation.TextTranslationService; +import org.sleuthkit.autopsy.texttranslation.TranslationException; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Utility class for code which helps create summaries for Discovery. + */ +class SummaryHelpers { + + private static final int PREVIEW_SIZE = 256; + private final static Logger logger = Logger.getLogger(SummaryHelpers.class.getName()); + private static volatile TextSummarizer summarizerToUse = null; + + private SummaryHelpers() { + // Class should not be instantiated + } + + /** + * Get the default text summary for the document. + * + * @param file The file to summarize. + * + * @return The TextSummary object which is a default summary for the file. + */ + static TextSummary getDefaultSummary(AbstractFile file) { + Image image = null; + int countOfImages = 0; + try { + Content largestChild = null; + for (Content child : file.getChildren()) { + if (child instanceof AbstractFile && ImageUtils.isImageThumbnailSupported((AbstractFile) child)) { + countOfImages++; + if (largestChild == null || child.getSize() > largestChild.getSize()) { + largestChild = child; + } + } + } + if (largestChild != null) { + image = ImageUtils.getThumbnail(largestChild, ImageUtils.ICON_SIZE_LARGE); + } + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "Error getting children for file: " + file.getId(), ex); + } + image = image == null ? image : image.getScaledInstance(ImageUtils.ICON_SIZE_MEDIUM, ImageUtils.ICON_SIZE_MEDIUM, + Image.SCALE_SMOOTH); + String summaryText = null; + if (file.getMd5Hash() != null) { + try { + summaryText = getSavedSummary(Paths.get(Case.getCurrentCaseThrows().getCacheDirectory(), "summaries", file.getMd5Hash() + "-default-" + PREVIEW_SIZE + "-translated.txt").toString()); + } catch (NoCurrentCaseException ex) { + logger.log(Level.WARNING, "Unable to retrieve saved summary. No case is open.", ex); + } + } + if (StringUtils.isBlank(summaryText)) { + String firstLines = getFirstLines(file); + String translatedFirstLines = getTranslatedVersion(firstLines); + if (!StringUtils.isBlank(translatedFirstLines)) { + summaryText = translatedFirstLines; + if (file.getMd5Hash() != null) { + try { + saveSummary(summaryText, Paths.get(Case.getCurrentCaseThrows().getCacheDirectory(), "summaries", file.getMd5Hash() + "-default-" + PREVIEW_SIZE + "-translated.txt").toString()); + } catch (NoCurrentCaseException ex) { + logger.log(Level.WARNING, "Unable to save translated summary. No case is open.", ex); + } + } + } else { + summaryText = firstLines; + } + } + return new TextSummary(summaryText, image, countOfImages); + } + + /** + * Provide an English version of the specified String if it is not English, + * translation is enabled, and it can be translated. + * + * @param documentString The String to provide an English version of. + * + * @return The English version of the provided String, or null if no + * translation occurred. + */ + static String getTranslatedVersion(String documentString) { + try { + TextTranslationService translatorInstance = TextTranslationService.getInstance(); + if (translatorInstance.hasProvider()) { + String translatedResult = translatorInstance.translate(documentString); + if (translatedResult.isEmpty() == false) { + return translatedResult; + } + } + } catch (NoServiceProviderException | TranslationException ex) { + logger.log(Level.INFO, "Error translating string for summary", ex); + } + return null; + } + + /** + * Find and load a saved summary from the case folder for the specified + * file. + * + * @param summarySavePath The full path for the saved summary file. + * + * @return The summary found given the specified path, null if no summary + * was found. + */ + static String getSavedSummary(String summarySavePath) { + if (summarySavePath == null) { + return null; + } + File savedFile = new File(summarySavePath); + if (savedFile.exists()) { + try (BufferedReader bReader = new BufferedReader(new FileReader(savedFile))) { + // pass the path to the file as a parameter + StringBuilder sBuilder = new StringBuilder(PREVIEW_SIZE); + String sCurrentLine = bReader.readLine(); + while (sCurrentLine != null) { + sBuilder.append(sCurrentLine).append('\n'); + sCurrentLine = bReader.readLine(); + } + return sBuilder.toString(); + } catch (IOException ingored) { + //summary file may not exist or may be incomplete in which case return null so a summary can be generated + return null; //no saved summary was able to be found + } + } else { + try { //if the file didn't exist make sure the parent directories exist before we move on to creating a summary + Files.createParentDirs(savedFile); + } catch (IOException ex) { + logger.log(Level.WARNING, "Unable to create summaries directory in case folder for file at: " + summarySavePath, ex); + } + return null; //no saved summary was able to be found + } + + } + + /** + * Save a summary at the specified location. + * + * @param summary The text of the summary being saved. + * @param summarySavePath The full path for the saved summary file. + */ + static void saveSummary(String summary, String summarySavePath) { + if (summarySavePath == null) { + return; //can't save a summary if we don't have a path + } + try (FileWriter myWriter = new FileWriter(summarySavePath)) { + myWriter.write(summary); + } catch (IOException ex) { + logger.log(Level.WARNING, "Unable to save summary at: " + summarySavePath, ex); + } + } + + /** + * Get the beginning of text from the specified AbstractFile. + * + * @param file The AbstractFile to get text from. + * + * @return The beginning of text from the specified AbstractFile. + */ + static String getFirstLines(AbstractFile file) { + TextExtractor extractor; + try { + extractor = TextExtractorFactory.getExtractor(file, null); + } catch (TextExtractorFactory.NoTextExtractorFound ignored) { + //no extractor found, use Strings Extractor + extractor = TextExtractorFactory.getStringsExtractor(file, null); + } + + try (Reader reader = extractor.getReader()) { + char[] cbuf = new char[PREVIEW_SIZE]; + reader.read(cbuf, 0, PREVIEW_SIZE); + return new String(cbuf); + } catch (IOException ex) { + return Bundle.FileSearch_documentSummary_noBytes(); + } catch (TextExtractor.InitReaderException ex) { + return Bundle.FileSearch_documentSummary_noPreview(); + } + } + + /** + * Get the first TextSummarizer found by a lookup of TextSummarizers. + * + * @return The first TextSummarizer found by a lookup of TextSummarizers. + * + * @throws IOException + */ + static TextSummarizer getLocalSummarizer() { + if (summarizerToUse == null) { + Collection summarizers + = Lookup.getDefault().lookupAll(TextSummarizer.class + ); + if (!summarizers.isEmpty()) { + summarizerToUse = summarizers.iterator().next(); + } + } + return summarizerToUse; + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/AbstractDiscoveryFilterPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/AbstractDiscoveryFilterPanel.java similarity index 90% rename from Core/src/org/sleuthkit/autopsy/discovery/AbstractDiscoveryFilterPanel.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/AbstractDiscoveryFilterPanel.java index 24d8a8e946..3e4ab45592 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/AbstractDiscoveryFilterPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/AbstractDiscoveryFilterPanel.java @@ -16,8 +16,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; +import org.sleuthkit.autopsy.discovery.search.AbstractFilter; import java.awt.event.ActionListener; import javax.swing.JCheckBox; import javax.swing.JLabel; @@ -69,7 +70,8 @@ abstract class AbstractDiscoveryFilterPanel extends javax.swing.JPanel { /** * Check if this filter is configured to valid settings. * - * @return If the settings are invalid returns the error that has occurred, otherwise returns empty string. + * @return If the settings are invalid returns the error that has occurred, + * otherwise returns empty string. */ abstract String checkForError(); @@ -90,12 +92,12 @@ abstract class AbstractDiscoveryFilterPanel extends javax.swing.JPanel { } /** - * Get the FileFilter which is represented by this Panel. + * Get the AbstractFilter which is represented by this Panel. * - * @return The FileFilter for the selected settings, null if the settings - * are not in use. + * @return The AbstractFilter for the selected settings, null if the + * settings are not in use. */ - abstract FileSearchFiltering.FileFilter getFilter(); + abstract AbstractFilter getFilter(); /** * Remove listeners from the checkbox and the list if they exist. diff --git a/Core/src/org/sleuthkit/autopsy/discovery/AbstractFiltersPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/AbstractFiltersPanel.java similarity index 76% rename from Core/src/org/sleuthkit/autopsy/discovery/AbstractFiltersPanel.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/AbstractFiltersPanel.java index 2f718c3aa5..6e8137a6ed 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/AbstractFiltersPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/AbstractFiltersPanel.java @@ -16,8 +16,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; +import org.sleuthkit.autopsy.discovery.search.AbstractFilter; import java.awt.Component; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; @@ -31,6 +32,11 @@ import javax.swing.JSplitPane; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import org.apache.commons.lang3.StringUtils; +import org.sleuthkit.autopsy.discovery.search.DiscoveryAttributes.GroupingAttributeType; +import org.sleuthkit.autopsy.discovery.search.Group; +import org.sleuthkit.autopsy.discovery.search.ResultsSorter.SortingMethod; +import org.sleuthkit.autopsy.discovery.search.SearchData; +import org.sleuthkit.autopsy.discovery.search.SearchFiltering; /** * Abstract class extending JPanel for displaying all the filters associated @@ -52,6 +58,9 @@ abstract class AbstractFiltersPanel extends JPanel implements ActionListener, Li private final JPanel secondColumnPanel = new JPanel(); private int firstColumnY = 0; private int secondColumnY = 0; + private SortingMethod lastSortingMethod = SortingMethod.BY_FILE_NAME; + private GroupingAttributeType lastGroupingAttributeType = GroupingAttributeType.PARENT_PATH; + private Group.GroupSortingAlgorithm lastGroupSortingAlg = Group.GroupSortingAlgorithm.BY_GROUP_SIZE; /** * Setup necessary for implementations of this abstract class. @@ -66,7 +75,7 @@ abstract class AbstractFiltersPanel extends JPanel implements ActionListener, Li * * @return The type of results this panel filters. */ - abstract FileSearchData.FileType getFileType(); + abstract SearchData.Type getType(); /** * Add a DiscoveryFilterPanel to the specified column with the specified @@ -242,12 +251,15 @@ abstract class AbstractFiltersPanel extends JPanel implements ActionListener, Li * * @return The list of filters selected by the user. */ - synchronized List getFilters() { - List filtersToUse = new ArrayList<>(); - filtersToUse.add(new FileSearchFiltering.FileTypeFilter(getFileType())); + synchronized List getFilters() { + + List filtersToUse = new ArrayList<>(); + if (getType() != SearchData.Type.DOMAIN) { //Domain type does not have a file type + filtersToUse.add(new SearchFiltering.FileTypeFilter(getType())); + } for (AbstractDiscoveryFilterPanel filterPanel : filters) { if (filterPanel.getCheckbox().isSelected()) { - FileSearchFiltering.FileFilter filter = filterPanel.getFilter(); + AbstractFilter filter = filterPanel.getFilter(); if (filter != null) { filtersToUse.add(filter); } @@ -265,4 +277,60 @@ abstract class AbstractFiltersPanel extends JPanel implements ActionListener, Li } } + /** + * Get the most recently used sorting method. + * + * @return The most recently used sorting method. + */ + SortingMethod getLastSortingMethod() { + return lastSortingMethod; + } + + /** + * Set the most recently used sorting method. + * + * @param lastSortingMethod The most recently used sorting method. + */ + final void setLastSortingMethod(SortingMethod lastSortingMethod) { + this.lastSortingMethod = lastSortingMethod; + } + + /** + * Get the most recently used grouping attribute. + * + * @return The most recently used grouping attribute. + */ + GroupingAttributeType getLastGroupingAttributeType() { + return lastGroupingAttributeType; + } + + /** + * Set the most recently used grouping attribute. + * + * @param lastGroupingAttributeType The most recently used grouping + * attribute. + */ + final void setLastGroupingAttributeType(GroupingAttributeType lastGroupingAttributeType) { + this.lastGroupingAttributeType = lastGroupingAttributeType; + } + + /** + * Get the most recently used group sorting algorithm. + * + * @return The most recently used group sorting algorithm. + */ + Group.GroupSortingAlgorithm getLastGroupSortingAlg() { + return lastGroupSortingAlg; + } + + /** + * Set the group sorting algorithm that was used most recently. + * + * @param lastGroupSortingAlg The most recently used group sorting + * algorithm. + */ + final void setLastGroupSortingAlg(Group.GroupSortingAlgorithm lastGroupSortingAlg) { + this.lastGroupSortingAlg = lastGroupSortingAlg; + } + } diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/ArtifactTypeFilterPanel.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/ArtifactTypeFilterPanel.form new file mode 100644 index 0000000000..e0d00591cc --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/ArtifactTypeFilterPanel.form @@ -0,0 +1,69 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/ArtifactTypeFilterPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/ArtifactTypeFilterPanel.java new file mode 100644 index 0000000000..089c18bf13 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/ArtifactTypeFilterPanel.java @@ -0,0 +1,195 @@ +/* + * Autopsy + * + * Copyright 2020 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.discovery.ui; + +import java.util.ArrayList; +import java.util.List; +import org.sleuthkit.autopsy.discovery.search.AbstractFilter; +import javax.swing.DefaultListModel; +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JList; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.discovery.search.SearchData; +import org.sleuthkit.autopsy.discovery.search.SearchFiltering.ArtifactTypeFilter; +import org.sleuthkit.datamodel.BlackboardArtifact; + +/** + * Filter for selection of a specific Artifact type to limit results to. + */ +class ArtifactTypeFilterPanel extends AbstractDiscoveryFilterPanel { + + private static final long serialVersionUID = 1L; + + /** + * Creates new form ArtifactTypeFilterPanel + */ + ArtifactTypeFilterPanel() { + initComponents(); + setUpArtifactTypeFilter(); + + } + + /** + * Initialize the data source filter. + */ + private void setUpArtifactTypeFilter() { + int count = 0; + DefaultListModel artifactTypeModel = (DefaultListModel) artifactList.getModel(); + artifactTypeModel.removeAllElements(); + for (BlackboardArtifact.ARTIFACT_TYPE artifactType : SearchData.Type.DOMAIN.getArtifactTypes()) { + artifactTypeModel.add(count, new ArtifactTypeItem(artifactType)); + count++; + } + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + artifactTypeCheckbox = new javax.swing.JCheckBox(); + artifactTypeScrollPane = new javax.swing.JScrollPane(); + artifactList = new javax.swing.JList<>(); + + org.openide.awt.Mnemonics.setLocalizedText(artifactTypeCheckbox, org.openide.util.NbBundle.getMessage(ArtifactTypeFilterPanel.class, "ArtifactTypeFilterPanel.artifactTypeCheckbox.text")); // NOI18N + artifactTypeCheckbox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + artifactTypeCheckboxActionPerformed(evt); + } + }); + + setPreferredSize(new java.awt.Dimension(27, 27)); + + artifactTypeScrollPane.setPreferredSize(new java.awt.Dimension(27, 27)); + + artifactList.setModel(new DefaultListModel()); + artifactList.setEnabled(false); + artifactTypeScrollPane.setViewportView(artifactList); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(artifactTypeScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(artifactTypeScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + ); + }// //GEN-END:initComponents + + private void artifactTypeCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_artifactTypeCheckboxActionPerformed + artifactTypeScrollPane.setEnabled(artifactTypeCheckbox.isSelected()); + artifactList.setEnabled(artifactTypeCheckbox.isSelected()); + }//GEN-LAST:event_artifactTypeCheckboxActionPerformed + + @Override + void configurePanel(boolean selected, int[] indicesSelected) { + artifactTypeCheckbox.setSelected(selected); + if (artifactTypeCheckbox.isEnabled() && artifactTypeCheckbox.isSelected()) { + artifactTypeScrollPane.setEnabled(true); + artifactList.setEnabled(true); + if (indicesSelected != null) { + artifactList.setSelectedIndices(indicesSelected); + } + } else { + artifactTypeScrollPane.setEnabled(false); + artifactList.setEnabled(false); + } + } + + @Override + JCheckBox getCheckbox() { + return artifactTypeCheckbox; + } + + @Override + JList getList() { + return artifactList; + } + + @Override + JLabel getAdditionalLabel() { + return null; + } + + @NbBundle.Messages({"ArtifactTypeFilterPanel.selectionNeeded.text=At least one Result type must be selected."}) + @Override + String checkForError() { + if (artifactTypeCheckbox.isSelected() && artifactList.getSelectedValuesList().isEmpty()) { + return Bundle.ArtifactTypeFilterPanel_selectionNeeded_text(); + } + return ""; + } + + @Override + AbstractFilter getFilter() { + if (artifactTypeCheckbox.isSelected() && !artifactList.getSelectedValuesList().isEmpty()) { + List artifactTypeList = new ArrayList<>(); + for (ArtifactTypeItem item : artifactList.getSelectedValuesList()) { + artifactTypeList.add(item.getArtifactType()); + } + return new ArtifactTypeFilter(artifactTypeList); + } + return null; + } + + /** + * Utility class to allow us to display the AritfactType display name + * instead of the name. + */ + private class ArtifactTypeItem { + + private final BlackboardArtifact.ARTIFACT_TYPE artifactType; + + /** + * Construct a new ArtifactTypeItem. + * + * @param ds The artifact type being wrapped. + */ + ArtifactTypeItem(BlackboardArtifact.ARTIFACT_TYPE artifactType) { + this.artifactType = artifactType; + } + + /** + * Get the ArtifactType represented by this ArtifactTypeItem. + * + * @return The ArtifactType represented by this ArtifactTypeItem. + */ + BlackboardArtifact.ARTIFACT_TYPE getArtifactType() { + return artifactType; + } + + @Override + public String toString() { + return artifactType.getDisplayName(); + } + } + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JList artifactList; + private javax.swing.JCheckBox artifactTypeCheckbox; + private javax.swing.JScrollPane artifactTypeScrollPane; + // End of variables declaration//GEN-END:variables +} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/Bundle.properties b/Core/src/org/sleuthkit/autopsy/discovery/ui/Bundle.properties new file mode 100644 index 0000000000..5fc4ff56e6 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/Bundle.properties @@ -0,0 +1,60 @@ +# To change this license header, choose License Headers in Project Properties. +# To change this template file, choose Tools | Templates +# and open the template in the editor. +ResultsPanel.currentPageLabel.text=Page: - +DiscoveryDialog.sortingPanel.border.title=Step 3: Choose display settings +DiscoveryDialog.searchButton.text=Search +DiscoveryDialog.domainsButton.text=Domains +DiscoveryDialog.groupByLabel.text=Group By: +DiscoveryDialog.step1Label.text=Step 1: Choose result type +DiscoveryDialog.orderByLabel.text=Order Within Groups By: +DiscoveryDialog.documentsButton.text=Documents +DiscoveryDialog.orderGroupsByLabel.text=Order Groups By: +DiscoveryDialog.videosButton.text=Videos +DiscoveryDialog.imagesButton.text=Images +VideoFilterPanel.videoFiltersSplitPane.border.title=Step 2: Filter which videos to show +DataSourceFilterPanel.dataSourceCheckbox.text=Data Source: +ParentFolderFilterPanel.parentLabel.text_1=(All will be used) +ParentFolderFilterPanel.parentCheckbox.text_1=Parent Folder: +ParentFolderFilterPanel.addButton.text_1=Add +ParentFolderFilterPanel.deleteButton.text_1=Delete +ParentFolderFilterPanel.excludeRadioButton.text_1=Exclude +ParentFolderFilterPanel.substringRadioButton.text_1=Substring +ParentFolderFilterPanel.includeRadioButton.text_1=Include +ParentFolderFilterPanel.fullRadioButton.text_1=Full +UserCreatedFilterPanel.userCreatedCheckbox.text_1=Possibly User Created +GroupListPanel.groupKeyList.border.title=Groups +ResultsSplitPaneDivider.detailsLabel.text=Details Area +ResultsSplitPaneDivider.showButton.text= +ResultsSplitPaneDivider.hideButton.text= +ImageFilterPanel.imageFiltersSplitPane.toolTipText= +ImageFilterPanel.imageFiltersSplitPane.border.title=Step 2: Filter which images to show +ArtifactTypeFilterPanel.artifactTypeCheckbox.text=Result Type: +InterestingItemsFilterPanel.interestingItemsCheckbox.text=Interesting Item: +DocumentPanel.fileSizeLabel.toolTipText= +DocumentPanel.isDeletedLabel.toolTipText= +DomainFilterPanel.domainFiltersSplitPane.toolTipText= +DomainFilterPanel.domainFiltersSplitPane.border.title=Step 2: Filter which domains to show +SizeFilterPanel.sizeCheckbox.text=File Size: +DateFilterPanel.endCheckBox.text=End: +DateFilterPanel.startCheckBox.text=Start: +DateFilterPanel.daysLabel.text=days of activity +ImageThumbnailPanel.isDeletedLabel.toolTipText= +ResultsPanel.pageControlsLabel.text=Pages: +ResultsPanel.currentPageLabel.text=Page: - +ResultsPanel.pageSizeLabel.text=Page Size: +ResultsPanel.gotoPageLabel.text=Go to Page: +# To change this license header, choose License Headers in Project Properties. +# To change this template file, choose Tools | Templates +# and open the template in the editor. +HashSetFilterPanel.hashSetCheckbox.text=Hash Set: +PastOccurrencesFilterPanel.pastOccurrencesCheckbox.text=Past Occurrences: +DocumentFilterPanel.documentsFiltersSplitPane.border.title=Step 2: Filter which documents to show +ObjectDetectedFilterPanel.text=Object Detected: +DetailsPanel.instancesList.border.title=Instances +DateFilterPanel.mostRecentRadioButton.text=Only last: +DateFilterPanel.dateFilterCheckBox.text=Date Filter: +DomainSummaryPanel.activityLabel.text= +DomainSummaryPanel.pagesLabel.text= +DomainSummaryPanel.filesDownloadedLabel.text= +DomainSummaryPanel.totalVisitsLabel.text= diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/discovery/ui/Bundle.properties-MERGED new file mode 100644 index 0000000000..7c2c5e6757 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/Bundle.properties-MERGED @@ -0,0 +1,146 @@ +ArtifactTypeFilterPanel.selectionNeeded.text=At least one Result type must be selected. +CTL_OpenDiscoveryAction=Discovery +DataSourceFilterPanel.error.text=At least one data source must be selected. +# {0} - dataSourceName +DataSourceModuleWrapper.exifModule.text=Picture Analyzer module was not run on data source: {0}\n +# {0} - dataSourceName +DataSourceModuleWrapper.fileTypeModule.text=File Type Identification module was not run on data source: {0}\n +# {0} - dataSourceName +DataSourceModuleWrapper.hashModule.text=Hash Lookup module was not run on data source: {0}\n +# {0} - timeZone +DateFilterPanel.dateRange.text=Date Range ({0}): +DateFilterPanel.invalidRange.text=Range or Only Last must be selected. +DateFilterPanel.startAfterEnd.text=Start date should be before the end date when both are enabled. +DateFilterPanel.startOrEndNeeded.text=A start or end date must be specified to use the range filter. +DiscoveryDialog.name.text=Discovery +DiscoveryTopComponent.additionalFilters.text=; +DiscoveryTopComponent.cancelButton.text=Cancel Search +DiscoveryTopComponent.domainSearch.text=Type: Domain +DiscoveryTopComponent.name=\ Discovery +DiscoveryTopComponent.newSearch.text=New Search +DiscoveryTopComponent.searchCancelled.text=Search has been cancelled. +# {0} - search +DiscoveryTopComponent.searchComplete.text=Results with {0} +DiscoveryTopComponent.searchError.text=Error no type specified for search. +# {0} - searchType +DiscoveryTopComponent.searchInProgress.text=Performing search for results of type {0}. Please wait. +DiscoveryUiUtility.bytes.text=bytes +DiscoveryUiUtility.gigaBytes.text=GB +DiscoveryUiUtility.kiloBytes.text=KB +DiscoveryUiUtility.megaBytes.text=MB +# {0} - fileSize +# {1} - units +DiscoveryUiUtility.sizeLabel.text=Size: {0} {1} +DiscoveryUiUtility.terraBytes.text=TB +# {0} - file name +DiscoveryUiUtils.genVideoThumb.progress.text=extracting temporary file {0} +DiscoveryUiUtils.isDeleted.text=All instances of file are deleted. +DiscoveryUiUtils.resultsIncomplete.text=Discovery results may be incomplete +# {0} - otherInstanceCount +DocumentPanel.nameLabel.more.text=\ and {0} more +DocumentPanel.noImageExtraction.text=0 of ? images +DocumentPanel.numberOfImages.noImages=No images +# {0} - numberOfImages +DocumentPanel.numberOfImages.text=1 of {0} images +DocumentWrapper.previewInitialValue=Preview not generated yet. +# {0} - startDate +# {1} - endDate +DomainSummaryPanel.activity.text=Activity: {0} to {1} +DomainSummaryPanel.downloads.text=Files downloaded: +DomainSummaryPanel.loadingImages.text=Loading thumbnail... +DomainSummaryPanel.pages.text=Pages in past 60 days: +DomainSummaryPanel.totalPages.text=Total visits: +GroupsListPanel.noDomainResults.message.text=No domains were found for the selected filters.\n\nReminder:\n -The Recent Activity module must be run on each data source you want to find results in.\n -The Central Repository module must be run on each data source if you want to filter or sort by past occurrences.\n -The iOS Analyzer (iLEAPP) module must be run on each data source which contains data from an iOS device.\n +GroupsListPanel.noFileResults.message.text=No files were found for the selected filters.\n\nReminder:\n -The File Type Identification module must be run on each data source you want to find results in.\n -The Hash Lookup module must be run on each data source if you want to filter by past occurrence.\n -The Picture Analyzer module must be run on each data source if you are filtering by User Created content. +GroupsListPanel.noResults.title.text=No results found +HashSetFilterPanel.error.text=At least one hash set name must be selected. +ImageThumbnailPanel.isDeleted.text=All instances of file are deleted. +# {0} - otherInstanceCount +ImageThumbnailPanel.nameLabel.more.text=\ and {0} more +InterestingItemsFilterPanel.error.text=At least one interesting file set name must be selected. +ObjectDetectedFilterPanel.error.text=At least one object type name must be selected. +ParentFolderFilterPanel.error.text=At least one parent path must be entered. +PastOccurrencesFilterPanel.error.text=At least one value in the past occurrence filter must be selected. +# {0} - currentPage +# {1} - totalPages +ResultsPanel.currentPage.displayValue=Page: {0} of {1} +# To change this license header, choose License Headers in Project Properties. +# To change this template file, choose Tools | Templates +# and open the template in the editor. +ResultsPanel.currentPageLabel.text=Page: - +DiscoveryDialog.sortingPanel.border.title=Step 3: Choose display settings +DiscoveryDialog.searchButton.text=Search +DiscoveryDialog.domainsButton.text=Domains +DiscoveryDialog.groupByLabel.text=Group By: +DiscoveryDialog.step1Label.text=Step 1: Choose result type +DiscoveryDialog.orderByLabel.text=Order Within Groups By: +DiscoveryDialog.documentsButton.text=Documents +DiscoveryDialog.orderGroupsByLabel.text=Order Groups By: +DiscoveryDialog.videosButton.text=Videos +DiscoveryDialog.imagesButton.text=Images +ResultsPanel.documentPreview.text=Document preview creation cancelled. +# {0} - selectedPage +# {1} - maxPage +ResultsPanel.invalidPageNumber.message=The selected page number {0} does not exist. Please select a value from 1 to {1}. +ResultsPanel.invalidPageNumber.title=Invalid Page Number +ResultsPanel.openInExternalViewer.name=Open in External Viewer +ResultsPanel.unableToCreate.text=Unable to create summary. +ResultsPanel.viewFileInDir.name=View File in Directory +SizeFilterPanel.error.text=At least one size must be selected. +VideoFilterPanel.videoFiltersSplitPane.border.title=Step 2: Filter which videos to show +DataSourceFilterPanel.dataSourceCheckbox.text=Data Source: +ParentFolderFilterPanel.parentLabel.text_1=(All will be used) +ParentFolderFilterPanel.parentCheckbox.text_1=Parent Folder: +ParentFolderFilterPanel.addButton.text_1=Add +ParentFolderFilterPanel.deleteButton.text_1=Delete +ParentFolderFilterPanel.excludeRadioButton.text_1=Exclude +ParentFolderFilterPanel.substringRadioButton.text_1=Substring +ParentFolderFilterPanel.includeRadioButton.text_1=Include +ParentFolderFilterPanel.fullRadioButton.text_1=Full +UserCreatedFilterPanel.userCreatedCheckbox.text_1=Possibly User Created +GroupListPanel.groupKeyList.border.title=Groups +ResultsSplitPaneDivider.detailsLabel.text=Details Area +ResultsSplitPaneDivider.showButton.text= +ResultsSplitPaneDivider.hideButton.text= +ImageFilterPanel.imageFiltersSplitPane.toolTipText= +ImageFilterPanel.imageFiltersSplitPane.border.title=Step 2: Filter which images to show +ArtifactTypeFilterPanel.artifactTypeCheckbox.text=Result Type: +InterestingItemsFilterPanel.interestingItemsCheckbox.text=Interesting Item: +DocumentPanel.fileSizeLabel.toolTipText= +DocumentPanel.isDeletedLabel.toolTipText= +DomainFilterPanel.domainFiltersSplitPane.toolTipText= +DomainFilterPanel.domainFiltersSplitPane.border.title=Step 2: Filter which domains to show +SizeFilterPanel.sizeCheckbox.text=File Size: +DateFilterPanel.endCheckBox.text=End: +DateFilterPanel.startCheckBox.text=Start: +DateFilterPanel.daysLabel.text=days of activity +ImageThumbnailPanel.isDeletedLabel.toolTipText= +ResultsPanel.pageControlsLabel.text=Pages: +ResultsPanel.currentPageLabel.text=Page: - +ResultsPanel.pageSizeLabel.text=Page Size: +ResultsPanel.gotoPageLabel.text=Go to Page: +# To change this license header, choose License Headers in Project Properties. +# To change this template file, choose Tools | Templates +# and open the template in the editor. +HashSetFilterPanel.hashSetCheckbox.text=Hash Set: +PastOccurrencesFilterPanel.pastOccurrencesCheckbox.text=Past Occurrences: +DocumentFilterPanel.documentsFiltersSplitPane.border.title=Step 2: Filter which documents to show +ObjectDetectedFilterPanel.text=Object Detected: +DetailsPanel.instancesList.border.title=Instances +DateFilterPanel.mostRecentRadioButton.text=Only last: +DateFilterPanel.dateFilterCheckBox.text=Date Filter: +DomainSummaryPanel.activityLabel.text= +DomainSummaryPanel.pagesLabel.text= +DomainSummaryPanel.filesDownloadedLabel.text= +DomainSummaryPanel.totalVisitsLabel.text= +VideoThumbnailPanel.bytes.text=bytes +VideoThumbnailPanel.deleted.text=All instances of file are deleted. +VideoThumbnailPanel.gigaBytes.text=GB +VideoThumbnailPanel.kiloBytes.text=KB +VideoThumbnailPanel.megaBytes.text=MB +# {0} - otherInstanceCount +VideoThumbnailPanel.nameLabel.more.text=\ and {0} more +# {0} - fileSize +# {1} - units +VideoThumbnailPanel.sizeLabel.text=Size: {0} {1} +VideoThumbnailPanel.terraBytes.text=TB diff --git a/Core/src/org/sleuthkit/autopsy/discovery/DataSourceFilterPanel.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/DataSourceFilterPanel.form similarity index 96% rename from Core/src/org/sleuthkit/autopsy/discovery/DataSourceFilterPanel.form rename to Core/src/org/sleuthkit/autopsy/discovery/ui/DataSourceFilterPanel.form index 44ea93cd5a..3f2a9d4a1d 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/DataSourceFilterPanel.form +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DataSourceFilterPanel.form @@ -5,7 +5,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/discovery/DataSourceFilterPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/DataSourceFilterPanel.java similarity index 89% rename from Core/src/org/sleuthkit/autopsy/discovery/DataSourceFilterPanel.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/DataSourceFilterPanel.java index a2a2cf4de9..ab54df4341 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/DataSourceFilterPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DataSourceFilterPanel.java @@ -16,8 +16,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; +import java.util.Collections; +import org.sleuthkit.autopsy.discovery.search.AbstractFilter; import java.util.List; import java.util.logging.Level; import java.util.stream.Collectors; @@ -25,8 +27,10 @@ import javax.swing.DefaultListModel; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JList; +import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.discovery.search.SearchFiltering; import org.sleuthkit.datamodel.DataSource; import org.sleuthkit.datamodel.TskCoreException; @@ -138,9 +142,13 @@ final class DataSourceFilterPanel extends AbstractDiscoveryFilterPanel { try { DefaultListModel dsListModel = (DefaultListModel) dataSourceList.getModel(); dsListModel.removeAllElements(); - for (DataSource ds : Case.getCurrentCase().getSleuthkitCase().getDataSources()) { + List dataSources = Case.getCurrentCase().getSleuthkitCase().getDataSources(); + Collections.sort(dataSources, (DataSource ds1, DataSource ds2) -> ds1.getName().compareToIgnoreCase(ds2.getName())); + for (DataSource ds : dataSources) { dsListModel.add(count, new DataSourceItem(ds)); + count++; } + } catch (TskCoreException ex) { logger.log(Level.SEVERE, "Error loading data sources", ex); dataSourceCheckbox.setEnabled(false); @@ -185,19 +193,20 @@ final class DataSourceFilterPanel extends AbstractDiscoveryFilterPanel { } } + @NbBundle.Messages({"DataSourceFilterPanel.error.text=At least one data source must be selected."}) @Override String checkForError() { if (dataSourceCheckbox.isSelected() && dataSourceList.getSelectedValuesList().isEmpty()) { - return "At least one size must be selected"; + return Bundle.DataSourceFilterPanel_error_text(); } return ""; } @Override - FileSearchFiltering.FileFilter getFilter() { + AbstractFilter getFilter() { if (dataSourceCheckbox.isSelected()) { List dataSources = dataSourceList.getSelectedValuesList().stream().map(t -> t.getDataSource()).collect(Collectors.toList()); - return new FileSearchFiltering.DataSourceFilter(dataSources); + return new SearchFiltering.DataSourceFilter(dataSources); } return null; } diff --git a/Core/src/org/sleuthkit/autopsy/discovery/DataSourceModulesWrapper.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/DataSourceModulesWrapper.java similarity index 96% rename from Core/src/org/sleuthkit/autopsy/discovery/DataSourceModulesWrapper.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/DataSourceModulesWrapper.java index 9d48827b30..3cdd98b0f9 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/DataSourceModulesWrapper.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DataSourceModulesWrapper.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.modules.filetypeid.FileTypeIdModuleFactory; @@ -49,7 +49,7 @@ class DataSourceModulesWrapper { "# {0} - dataSourceName", "DataSourceModuleWrapper.fileTypeModule.text=File Type Identification module was not run on data source: {0}\n", "# {0} - dataSourceName", - "DataSourceModuleWrapper.exifModule.text=Exif Parser module was not run on data source: {0}\n" + "DataSourceModuleWrapper.exifModule.text=Picture Analyzer module was not run on data source: {0}\n" }) /** * Get the message which indicates which modules were not run on this data diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/DateFilterPanel.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/DateFilterPanel.form new file mode 100644 index 0000000000..276a4f9ef5 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DateFilterPanel.form @@ -0,0 +1,208 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/DateFilterPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/DateFilterPanel.java new file mode 100644 index 0000000000..528a5d1bc2 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DateFilterPanel.java @@ -0,0 +1,357 @@ +/* + * Autopsy + * + * Copyright 2020 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.discovery.ui; + +import com.github.lgooddatepicker.optionalusertools.DateChangeListener; +import com.github.lgooddatepicker.zinternaltools.DateChangeEvent; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.time.LocalDate; +import java.time.Period; +import java.time.ZoneId; +import org.sleuthkit.autopsy.discovery.search.AbstractFilter; +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JSpinner; +import javax.swing.event.ListSelectionListener; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.communications.Utils; +import org.sleuthkit.autopsy.discovery.search.SearchFiltering; + +/** + * Filter panel for allowing the user to filter on date. + */ +class DateFilterPanel extends AbstractDiscoveryFilterPanel { + + private static final long serialVersionUID = 1L; + private static final long SECS_PER_DAY = 86400; + + /** + * Creates new form DateFilterPanel. + */ + @NbBundle.Messages({"# {0} - timeZone", + "DateFilterPanel.dateRange.text=Date Range ({0}):"}) + DateFilterPanel() { + initComponents(); + rangeRadioButton.setText(Bundle.DateFilterPanel_dateRange_text(Utils.getUserPreferredZoneId().toString())); + //Disable manual entry in the spinner + ((JSpinner.DefaultEditor) daysSpinner.getEditor()).getTextField().setEditable(false); + //Disable manual entry in the date pickers + startDatePicker.getComponentDateTextField().setEditable(false); + endDatePicker.getComponentDateTextField().setEditable(false); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + buttonGroup1 = new javax.swing.ButtonGroup(); + dateFilterCheckBox = new javax.swing.JCheckBox(); + jPanel1 = new javax.swing.JPanel(); + daysSpinner = new javax.swing.JSpinner(); + daysLabel = new javax.swing.JLabel(); + mostRecentRadioButton = new javax.swing.JRadioButton(); + startCheckBox = new javax.swing.JCheckBox(); + startDatePicker = new com.github.lgooddatepicker.components.DatePicker(); + endDatePicker = new com.github.lgooddatepicker.components.DatePicker(); + endCheckBox = new javax.swing.JCheckBox(); + rangeRadioButton = new javax.swing.JRadioButton(); + + org.openide.awt.Mnemonics.setLocalizedText(dateFilterCheckBox, org.openide.util.NbBundle.getMessage(DateFilterPanel.class, "DateFilterPanel.dateFilterCheckBox.text")); // NOI18N + dateFilterCheckBox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + dateFilterCheckBoxActionPerformed(evt); + } + }); + + daysSpinner.setModel(new javax.swing.SpinnerNumberModel(7, 1, 100000, 1)); + daysSpinner.setEditor(new javax.swing.JSpinner.NumberEditor(daysSpinner, "")); + daysSpinner.setEnabled(false); + daysSpinner.setPreferredSize(new java.awt.Dimension(75, 26)); + daysSpinner.setValue(7); + + org.openide.awt.Mnemonics.setLocalizedText(daysLabel, org.openide.util.NbBundle.getMessage(DateFilterPanel.class, "DateFilterPanel.daysLabel.text")); // NOI18N + daysLabel.setEnabled(false); + + buttonGroup1.add(mostRecentRadioButton); + mostRecentRadioButton.setSelected(true); + org.openide.awt.Mnemonics.setLocalizedText(mostRecentRadioButton, org.openide.util.NbBundle.getMessage(DateFilterPanel.class, "DateFilterPanel.mostRecentRadioButton.text")); // NOI18N + mostRecentRadioButton.setEnabled(false); + mostRecentRadioButton.addChangeListener(new javax.swing.event.ChangeListener() { + public void stateChanged(javax.swing.event.ChangeEvent evt) { + mostRecentRadioButtonStateChanged(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(startCheckBox, org.openide.util.NbBundle.getMessage(DateFilterPanel.class, "DateFilterPanel.startCheckBox.text")); // NOI18N + startCheckBox.setEnabled(false); + startCheckBox.addChangeListener(new javax.swing.event.ChangeListener() { + public void stateChanged(javax.swing.event.ChangeEvent evt) { + startCheckBoxStateChanged(evt); + } + }); + + startDatePicker.setDate(LocalDate.now()); + startDatePicker.setEnabled(false); + startDatePicker.setMinimumSize(new java.awt.Dimension(60, 22)); + startDatePicker.setPreferredSize(new java.awt.Dimension(110, 22)); + + endDatePicker.setDate(LocalDate.now()); + endDatePicker.setEnabled(false); + endDatePicker.setMinimumSize(new java.awt.Dimension(60, 22)); + endDatePicker.setPreferredSize(new java.awt.Dimension(110, 22)); + + org.openide.awt.Mnemonics.setLocalizedText(endCheckBox, org.openide.util.NbBundle.getMessage(DateFilterPanel.class, "DateFilterPanel.endCheckBox.text")); // NOI18N + endCheckBox.setEnabled(false); + endCheckBox.addChangeListener(new javax.swing.event.ChangeListener() { + public void stateChanged(javax.swing.event.ChangeEvent evt) { + endCheckBoxStateChanged(evt); + } + }); + + buttonGroup1.add(rangeRadioButton); + rangeRadioButton.setEnabled(false); + rangeRadioButton.addChangeListener(new javax.swing.event.ChangeListener() { + public void stateChanged(javax.swing.event.ChangeEvent evt) { + rangeRadioButtonStateChanged(evt); + } + }); + + javax.swing.GroupLayout jPanel1Layout = new javax.swing.GroupLayout(jPanel1); + jPanel1.setLayout(jPanel1Layout); + jPanel1Layout.setHorizontalGroup( + jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(jPanel1Layout.createSequentialGroup() + .addComponent(mostRecentRadioButton, javax.swing.GroupLayout.PREFERRED_SIZE, 90, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(daysSpinner, javax.swing.GroupLayout.PREFERRED_SIZE, 80, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(daysLabel, javax.swing.GroupLayout.DEFAULT_SIZE, 132, Short.MAX_VALUE)) + .addComponent(rangeRadioButton, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel1Layout.createSequentialGroup() + .addGap(30, 30, 30) + .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(endCheckBox) + .addComponent(startCheckBox)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(endDatePicker, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(startDatePicker, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))) + ); + jPanel1Layout.setVerticalGroup( + jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, jPanel1Layout.createSequentialGroup() + .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(mostRecentRadioButton) + .addComponent(daysSpinner, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(daysLabel)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(rangeRadioButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(startCheckBox) + .addComponent(startDatePicker, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(endCheckBox) + .addComponent(endDatePicker, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGap(0, 0, 0)) + ); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGap(0, 0, 0) + .addComponent(jPanel1, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGap(0, 0, 0)) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGap(0, 0, 0) + .addComponent(jPanel1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGap(8, 8, 8)) + ); + }// //GEN-END:initComponents + + private void startCheckBoxStateChanged(javax.swing.event.ChangeEvent evt) {//GEN-FIRST:event_startCheckBoxStateChanged + startDatePicker.setEnabled(startCheckBox.isEnabled() && startCheckBox.isSelected()); + }//GEN-LAST:event_startCheckBoxStateChanged + + private void endCheckBoxStateChanged(javax.swing.event.ChangeEvent evt) {//GEN-FIRST:event_endCheckBoxStateChanged + endDatePicker.setEnabled(endCheckBox.isEnabled() && endCheckBox.isSelected()); + }//GEN-LAST:event_endCheckBoxStateChanged + + private void dateFilterCheckBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_dateFilterCheckBoxActionPerformed + rangeRadioButton.setEnabled(dateFilterCheckBox.isSelected()); + mostRecentRadioButton.setEnabled(dateFilterCheckBox.isSelected()); + rangeRadioButton.firePropertyChange("DateFilterChange", !rangeRadioButton.isEnabled(), rangeRadioButton.isEnabled()); + mostRecentRadioButton.firePropertyChange("DateFilterChange", !mostRecentRadioButton.isEnabled(), mostRecentRadioButton.isEnabled()); + }//GEN-LAST:event_dateFilterCheckBoxActionPerformed + + private void mostRecentRadioButtonStateChanged(javax.swing.event.ChangeEvent evt) {//GEN-FIRST:event_mostRecentRadioButtonStateChanged + daysSpinner.setEnabled(mostRecentRadioButton.isSelected()); + daysLabel.setEnabled(mostRecentRadioButton.isSelected()); + }//GEN-LAST:event_mostRecentRadioButtonStateChanged + + private void rangeRadioButtonStateChanged(javax.swing.event.ChangeEvent evt) {//GEN-FIRST:event_rangeRadioButtonStateChanged + startCheckBox.setEnabled(rangeRadioButton.isEnabled() && rangeRadioButton.isSelected()); + endCheckBox.setEnabled(rangeRadioButton.isEnabled() && rangeRadioButton.isSelected()); + startCheckBox.firePropertyChange("StartButtonChange", true, false); + endCheckBox.firePropertyChange("EndButtonChange", true, false); + }//GEN-LAST:event_rangeRadioButtonStateChanged + + @Override + void configurePanel(boolean selected, int[] indicesSelected) { + dateFilterCheckBox.setSelected(selected); + if (dateFilterCheckBox.isEnabled() && dateFilterCheckBox.isSelected()) { + mostRecentRadioButton.setEnabled(true); + rangeRadioButton.setEnabled(true); + mostRecentRadioButton.setSelected(true); + } else { + mostRecentRadioButton.setEnabled(false); + rangeRadioButton.setEnabled(false); + } + } + + @Override + JCheckBox getCheckbox() { + return dateFilterCheckBox; + } + + @Override + JList getList() { + return null; + } + + @Override + JLabel getAdditionalLabel() { + return null; + } + + @Override + void addListeners(ActionListener actionListener, ListSelectionListener listListener) { + dateFilterCheckBox.addActionListener(actionListener); + startCheckBox.addActionListener(actionListener); + endCheckBox.addActionListener(actionListener); + rangeRadioButton.addActionListener(actionListener); + mostRecentRadioButton.addActionListener(actionListener); + startDatePicker.addDateChangeListener(new DateChangeListener() { + @Override + public void dateChanged(DateChangeEvent event) { + actionListener.actionPerformed(new ActionEvent(startDatePicker, ActionEvent.ACTION_PERFORMED, "StartDateChanged")); + } + }); + endDatePicker.addDateChangeListener(new DateChangeListener() { + @Override + public void dateChanged(DateChangeEvent event) { + actionListener.actionPerformed(new ActionEvent(endDatePicker, ActionEvent.ACTION_PERFORMED, "EndDateChanged")); + } + }); + } + + @Override + void removeListeners() { + for (ActionListener listener : dateFilterCheckBox.getActionListeners()) { + dateFilterCheckBox.removeActionListener(listener); + } + for (ActionListener listener : rangeRadioButton.getActionListeners()) { + rangeRadioButton.removeActionListener(listener); + } + for (ActionListener listener : mostRecentRadioButton.getActionListeners()) { + mostRecentRadioButton.removeActionListener(listener); + } + for (ActionListener listener : rangeRadioButton.getActionListeners()) { + rangeRadioButton.removeActionListener(listener); + } + for (ActionListener listener : startCheckBox.getActionListeners()) { + startCheckBox.removeActionListener(listener); + } + for (ActionListener listener : endCheckBox.getActionListeners()) { + endCheckBox.removeActionListener(listener); + } + for (DateChangeListener listener : endDatePicker.getDateChangeListeners()) { + endDatePicker.removeDateChangeListener(listener); + } + for (DateChangeListener listener : startDatePicker.getDateChangeListeners()) { + startDatePicker.removeDateChangeListener(listener); + } + } + + @NbBundle.Messages({"DateFilterPanel.invalidRange.text=Range or Only Last must be selected.", + "DateFilterPanel.startOrEndNeeded.text=A start or end date must be specified to use the range filter.", + "DateFilterPanel.startAfterEnd.text=Start date should be before the end date when both are enabled."}) + @Override + String checkForError() { + if (dateFilterCheckBox.isSelected()) { + if (!(rangeRadioButton.isSelected() || mostRecentRadioButton.isSelected())) { + return Bundle.DateFilterPanel_invalidRange_text(); + } else if (rangeRadioButton.isSelected() && !(startCheckBox.isSelected() || endCheckBox.isSelected())) { + return Bundle.DateFilterPanel_startOrEndNeeded_text(); + } else if (startCheckBox.isSelected() && endCheckBox.isSelected() && startDatePicker.getDate().isAfter(endDatePicker.getDate())) { + //if the dates are equal it will effectively search just that day due to the rounding up of the end date in the getFilter code + return Bundle.DateFilterPanel_startAfterEnd_text(); + } + } + return ""; + } + + @Override + AbstractFilter getFilter() { + if (dateFilterCheckBox.isSelected()) { + LocalDate startDate = LocalDate.MIN; + LocalDate endDate = LocalDate.MAX; + ZoneId zone = Utils.getUserPreferredZoneId(); + if (rangeRadioButton.isSelected() && (startCheckBox.isSelected() || endCheckBox.isSelected())) { + if (startCheckBox.isSelected() && startDatePicker.getDate() != null) { + startDate = startDatePicker.getDate(); + } + if (endCheckBox.isSelected() && endDatePicker.getDate() != null) { + endDate = endDatePicker.getDate(); + } + } else if (dateFilterCheckBox.isSelected() && mostRecentRadioButton.isSelected()) { + endDate = LocalDate.now(); + startDate = LocalDate.now().minus(Period.ofDays((Integer) daysSpinner.getValue())); + } + return new SearchFiltering.ArtifactDateRangeFilter(startDate.atStartOfDay(zone).toEpochSecond(), endDate.atStartOfDay(zone).toEpochSecond() + SECS_PER_DAY);//to insure end date is inclusive + } + return null; + } + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.ButtonGroup buttonGroup1; + private javax.swing.JCheckBox dateFilterCheckBox; + private javax.swing.JLabel daysLabel; + private javax.swing.JSpinner daysSpinner; + private javax.swing.JCheckBox endCheckBox; + private com.github.lgooddatepicker.components.DatePicker endDatePicker; + private javax.swing.JPanel jPanel1; + private javax.swing.JRadioButton mostRecentRadioButton; + private javax.swing.JRadioButton rangeRadioButton; + private javax.swing.JCheckBox startCheckBox; + private com.github.lgooddatepicker.components.DatePicker startDatePicker; + // End of variables declaration//GEN-END:variables +} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/DetailsPanel.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/DetailsPanel.form similarity index 97% rename from Core/src/org/sleuthkit/autopsy/discovery/DetailsPanel.form rename to Core/src/org/sleuthkit/autopsy/discovery/ui/DetailsPanel.form index 32f78a9645..bd3d8c5af9 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/DetailsPanel.form +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DetailsPanel.form @@ -106,7 +106,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/discovery/DetailsPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/DetailsPanel.java similarity index 99% rename from Core/src/org/sleuthkit/autopsy/discovery/DetailsPanel.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/DetailsPanel.java index 6b0a37e276..e302669278 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/DetailsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DetailsPanel.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; import com.google.common.eventbus.Subscribe; import java.awt.Component; @@ -39,6 +39,7 @@ import org.sleuthkit.autopsy.corecomponents.TableFilterNode; import org.sleuthkit.autopsy.datamodel.FileNode; import org.sleuthkit.autopsy.directorytree.ExternalViewerAction; import org.sleuthkit.autopsy.directorytree.ViewContextAction; +import org.sleuthkit.autopsy.discovery.search.DiscoveryEventUtils; import org.sleuthkit.autopsy.modules.hashdatabase.AddContentToHashDbAction; import org.sleuthkit.autopsy.timeline.actions.ViewFileInTimelineAction; import org.sleuthkit.datamodel.AbstractFile; diff --git a/Core/src/org/sleuthkit/autopsy/discovery/DiscoveryDialog.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryDialog.form similarity index 86% rename from Core/src/org/sleuthkit/autopsy/discovery/DiscoveryDialog.form rename to Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryDialog.form index 19116a24fe..522cd9b43e 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/DiscoveryDialog.form +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryDialog.form @@ -6,9 +6,6 @@ - - -
@@ -48,12 +45,14 @@ - + - + - - + + + + @@ -78,6 +77,7 @@ + @@ -91,7 +91,7 @@
- + @@ -118,7 +118,7 @@ - + @@ -148,7 +148,7 @@ - + @@ -165,7 +165,7 @@ - + @@ -191,6 +191,26 @@ + + + + + + + + + + + + + + + + + + + +
@@ -240,7 +260,7 @@ - + @@ -259,7 +279,7 @@ - + @@ -338,7 +358,7 @@ - + @@ -349,7 +369,7 @@ - + @@ -360,7 +380,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/discovery/DiscoveryDialog.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryDialog.java similarity index 80% rename from Core/src/org/sleuthkit/autopsy/discovery/DiscoveryDialog.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryDialog.java index 4a2713ba94..f2f42bd113 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/DiscoveryDialog.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryDialog.java @@ -16,17 +16,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; +import org.sleuthkit.autopsy.discovery.search.AbstractFilter; import static java.awt.BorderLayout.CENTER; import java.awt.Color; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; +import java.util.ArrayList; import java.util.EnumSet; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.logging.Level; +import javax.swing.SwingUtilities; import org.apache.commons.lang.StringUtils; import org.openide.util.NbBundle.Messages; import org.openide.windows.WindowManager; @@ -35,24 +40,26 @@ import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoException; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.discovery.FileGroup.GroupSortingAlgorithm; -import static org.sleuthkit.autopsy.discovery.FileGroup.GroupSortingAlgorithm.BY_GROUP_SIZE; -import org.sleuthkit.autopsy.discovery.FileSearch.GroupingAttributeType; -import static org.sleuthkit.autopsy.discovery.FileSearch.GroupingAttributeType.PARENT_PATH; -import org.sleuthkit.autopsy.discovery.FileSorter.SortingMethod; +import org.sleuthkit.autopsy.discovery.search.DiscoveryAttributes; +import org.sleuthkit.autopsy.discovery.search.DiscoveryEventUtils; +import org.sleuthkit.autopsy.discovery.search.Group; +import org.sleuthkit.autopsy.discovery.search.Group.GroupSortingAlgorithm; +import org.sleuthkit.autopsy.discovery.search.DiscoveryAttributes.GroupingAttributeType; +import org.sleuthkit.autopsy.discovery.search.ResultsSorter; +import org.sleuthkit.autopsy.discovery.search.ResultsSorter.SortingMethod; +import org.sleuthkit.autopsy.discovery.search.SearchData; import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.autopsy.ingest.ModuleDataEvent; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardAttribute; import org.sleuthkit.datamodel.TskCoreException; -import static org.sleuthkit.autopsy.discovery.FileSorter.SortingMethod.BY_FILE_NAME; /** * Dialog for displaying the controls and filters for configuration of a * Discovery search. */ final class DiscoveryDialog extends javax.swing.JDialog { - + private static final Set CASE_EVENTS_OF_INTEREST = EnumSet.of(Case.Events.CURRENT_CASE, Case.Events.DATA_SOURCE_ADDED, Case.Events.DATA_SOURCE_DELETED); private static final Set INGEST_MODULE_EVENTS_OF_INTEREST = EnumSet.of(IngestManager.IngestModuleEvent.DATA_ADDED); @@ -61,12 +68,13 @@ final class DiscoveryDialog extends javax.swing.JDialog { private ImageFilterPanel imageFilterPanel = null; private VideoFilterPanel videoFilterPanel = null; private DocumentFilterPanel documentFilterPanel = null; + private DomainFilterPanel domainFilterPanel = null; private static final Color SELECTED_COLOR = new Color(216, 230, 242); private static final Color UNSELECTED_COLOR = new Color(240, 240, 240); private SearchWorker searchWorker = null; private static DiscoveryDialog discDialog; private static volatile boolean shouldUpdate = false; - private FileSearchData.FileType fileType = FileSearchData.FileType.IMAGE; + private SearchData.Type type = SearchData.Type.IMAGE; private final PropertyChangeListener listener; private final Set objectsDetected = new HashSet<>(); private final Set interestingItems = new HashSet<>(); @@ -103,12 +111,41 @@ final class DiscoveryDialog extends javax.swing.JDialog { } } }; - for (GroupSortingAlgorithm groupSortAlgorithm : GroupSortingAlgorithm.values()) { - groupSortingComboBox.addItem(groupSortAlgorithm); - } updateSearchSettings(); + groupByCombobox.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent event) { + if (event.getStateChange() == ItemEvent.SELECTED) { + SwingUtilities.invokeLater(() -> { + getSelectedFilterPanel().setLastGroupingAttributeType(groupByCombobox.getItemAt(groupByCombobox.getSelectedIndex())); + }); + + } + } + }); + orderByCombobox.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent event) { + if (event.getStateChange() == ItemEvent.SELECTED) { + SwingUtilities.invokeLater(() -> { + getSelectedFilterPanel().setLastSortingMethod(orderByCombobox.getItemAt(orderByCombobox.getSelectedIndex())); + }); + } + } + }); + groupSortingComboBox.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent event) { + if (event.getStateChange() == ItemEvent.SELECTED) { + SwingUtilities.invokeLater(() -> { + getSelectedFilterPanel().setLastGroupSortingAlg(groupSortingComboBox.getItemAt(groupSortingComboBox.getSelectedIndex())); + }); + } + } + }); Case.addEventTypeSubscriber(CASE_EVENTS_OF_INTEREST, this.new CasePropertyChangeListener()); IngestManager.getInstance().addIngestModuleEventListener(INGEST_MODULE_EVENTS_OF_INTEREST, this.new ModuleChangeListener()); + setPreferredSize(new java.awt.Dimension(1000, 650)); } /** @@ -122,43 +159,90 @@ final class DiscoveryDialog extends javax.swing.JDialog { imageFilterPanel = new ImageFilterPanel(); videoFilterPanel = new VideoFilterPanel(); documentFilterPanel = new DocumentFilterPanel(); + domainFilterPanel = new DomainFilterPanel(); + unselectAllButtons(); imagesButton.setSelected(true); imagesButton.setEnabled(false); imagesButton.setBackground(SELECTED_COLOR); imagesButton.setForeground(Color.BLACK); + type = SearchData.Type.IMAGE; + add(imageFilterPanel, CENTER); + imageFilterPanel.addPropertyChangeListener(listener); + updateComboBoxes(); + pack(); + repaint(); + } + + /** + * Set the type buttons to a default state where none are selected. + */ + private void unselectAllButtons() { + imagesButton.setSelected(false); + imagesButton.setEnabled(true); + imagesButton.setBackground(UNSELECTED_COLOR); videosButton.setSelected(false); videosButton.setEnabled(true); videosButton.setBackground(UNSELECTED_COLOR); documentsButton.setSelected(false); documentsButton.setEnabled(true); documentsButton.setBackground(UNSELECTED_COLOR); - fileType = FileSearchData.FileType.IMAGE; - add(imageFilterPanel, CENTER); - imageFilterPanel.addPropertyChangeListener(listener); - updateComboBoxes(); - groupSortingComboBox.setSelectedItem(BY_GROUP_SIZE); - pack(); - repaint(); + domainsButton.setSelected(false); + domainsButton.setEnabled(true); + domainsButton.setBackground(UNSELECTED_COLOR); } /** * Private helper method to perform update of comboboxes update. */ private void updateComboBoxes() { - groupByCombobox.removeAllItems(); // Set up the grouping attributes - for (FileSearch.GroupingAttributeType type : FileSearch.GroupingAttributeType.getOptionsForGrouping()) { - addTypeToGroupByComboBox(type); + List groupingAttrs = new ArrayList<>(); + List sortingMethods = new ArrayList<>(); + groupByCombobox.removeAllItems(); + if (type == SearchData.Type.DOMAIN) { + groupingAttrs.addAll(GroupingAttributeType.getOptionsForGroupingForDomains()); + sortingMethods.addAll(SortingMethod.getOptionsForOrderingDomains()); + } else { + groupingAttrs.addAll(GroupingAttributeType.getOptionsForGroupingForFiles()); + sortingMethods.addAll(SortingMethod.getOptionsForOrderingFiles()); } - groupByCombobox.setSelectedItem(PARENT_PATH); + for (GroupingAttributeType groupingType : groupingAttrs) { + addTypeToGroupByComboBox(groupingType); + } + groupByCombobox.setSelectedItem(getSelectedFilterPanel().getLastGroupingAttributeType()); orderByCombobox.removeAllItems(); // Set up the file order list - for (FileSorter.SortingMethod method : FileSorter.SortingMethod.getOptionsForOrdering()) { + for (SortingMethod method : sortingMethods) { if (method != SortingMethod.BY_FREQUENCY || CentralRepository.isEnabled()) { orderByCombobox.addItem(method); } } - orderByCombobox.setSelectedItem(BY_FILE_NAME); + orderByCombobox.setSelectedItem(getSelectedFilterPanel().getLastSortingMethod()); + groupSortingComboBox.removeAllItems(); + for (GroupSortingAlgorithm groupSortAlgorithm : GroupSortingAlgorithm.values()) { + groupSortingComboBox.addItem(groupSortAlgorithm); + } + groupSortingComboBox.setSelectedItem(getSelectedFilterPanel().getLastGroupSortingAlg()); + } + + /** + * Private helper method to get the correct panel for the selected type. + * + * @return The panel that corresponds to the currently selected type. + */ + private AbstractFiltersPanel getSelectedFilterPanel() { + switch (type) { + case IMAGE: + return imageFilterPanel; + case VIDEO: + return videoFilterPanel; + case DOCUMENT: + return documentFilterPanel; + case DOMAIN: + return domainFilterPanel; + default: + return imageFilterPanel; + } } /** @@ -196,27 +280,12 @@ final class DiscoveryDialog extends javax.swing.JDialog { } /** - * Validate the current filter settings of the selected type. + * Validate the filter settings for File type filters. */ synchronized void validateDialog() { - switch (fileType) { - case IMAGE: - if (imageFilterPanel != null) { - imageFilterPanel.validateFields(); - } - return; - case VIDEO: - if (videoFilterPanel != null) { - videoFilterPanel.validateFields(); - } - return; - case DOCUMENTS: - if (documentFilterPanel != null) { - documentFilterPanel.validateFields(); - } - break; - default: - break; + AbstractFiltersPanel panel = getSelectedFilterPanel(); + if (panel != null) { + panel.validateFields(); } } @@ -235,6 +304,7 @@ final class DiscoveryDialog extends javax.swing.JDialog { documentsButton = new javax.swing.JButton(); javax.swing.JLabel step1Label = new javax.swing.JLabel(); javax.swing.Box.Filler filler1 = new javax.swing.Box.Filler(new java.awt.Dimension(104, 0), new java.awt.Dimension(104, 0), new java.awt.Dimension(104, 32767)); + domainsButton = new javax.swing.JButton(); javax.swing.JPanel displaySettingsPanel = new javax.swing.JPanel(); searchButton = new javax.swing.JButton(); errorLabel = new javax.swing.JLabel(); @@ -248,7 +318,6 @@ final class DiscoveryDialog extends javax.swing.JDialog { setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE); setMinimumSize(new java.awt.Dimension(600, 300)); - setPreferredSize(new java.awt.Dimension(1000, 650)); imagesButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/pictures-icon.png"))); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(imagesButton, org.openide.util.NbBundle.getMessage(DiscoveryDialog.class, "DiscoveryDialog.imagesButton.text")); // NOI18N @@ -292,6 +361,17 @@ final class DiscoveryDialog extends javax.swing.JDialog { org.openide.awt.Mnemonics.setLocalizedText(step1Label, org.openide.util.NbBundle.getMessage(DiscoveryDialog.class, "DiscoveryDialog.step1Label.text")); // NOI18N + domainsButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/domain-32.png"))); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(domainsButton, org.openide.util.NbBundle.getMessage(DiscoveryDialog.class, "DiscoveryDialog.domainsButton.text")); // NOI18N + domainsButton.setDisabledIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/domain-32.png"))); // NOI18N + domainsButton.setDisabledSelectedIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/domain-32.png"))); // NOI18N + domainsButton.setFocusable(false); + domainsButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + domainsButtonActionPerformed(evt); + } + }); + javax.swing.GroupLayout toolBarPanelLayout = new javax.swing.GroupLayout(toolBarPanel); toolBarPanel.setLayout(toolBarPanelLayout); toolBarPanelLayout.setHorizontalGroup( @@ -306,13 +386,18 @@ final class DiscoveryDialog extends javax.swing.JDialog { .addComponent(videosButton, javax.swing.GroupLayout.PREFERRED_SIZE, 110, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(documentsButton) - .addContainerGap(370, Short.MAX_VALUE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(domainsButton) + .addContainerGap(190, Short.MAX_VALUE)) .addGroup(toolBarPanelLayout.createSequentialGroup() .addComponent(step1Label, javax.swing.GroupLayout.PREFERRED_SIZE, 243, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(filler1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addContainerGap(391, Short.MAX_VALUE)))) ); + + toolBarPanelLayout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {documentsButton, domainsButton, imagesButton, videosButton}); + toolBarPanelLayout.setVerticalGroup( toolBarPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(toolBarPanelLayout.createSequentialGroup() @@ -324,7 +409,8 @@ final class DiscoveryDialog extends javax.swing.JDialog { .addGroup(toolBarPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(videosButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addComponent(imagesButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addComponent(documentsButton, javax.swing.GroupLayout.PREFERRED_SIZE, 43, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(documentsButton, javax.swing.GroupLayout.PREFERRED_SIZE, 43, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(domainsButton, javax.swing.GroupLayout.PREFERRED_SIZE, 43, javax.swing.GroupLayout.PREFERRED_SIZE)) .addGap(8, 8, 8)) ); @@ -417,18 +503,14 @@ final class DiscoveryDialog extends javax.swing.JDialog { private void imagesButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_imagesButtonActionPerformed removeAllPanels(); add(imageFilterPanel, CENTER); + unselectAllButtons(); imagesButton.setSelected(true); imagesButton.setEnabled(false); imagesButton.setBackground(SELECTED_COLOR); imagesButton.setForeground(Color.BLACK); - videosButton.setSelected(false); - videosButton.setEnabled(true); - videosButton.setBackground(UNSELECTED_COLOR); - documentsButton.setSelected(false); - documentsButton.setEnabled(true); - documentsButton.setBackground(UNSELECTED_COLOR); - fileType = FileSearchData.FileType.IMAGE; + type = SearchData.Type.IMAGE; imageFilterPanel.addPropertyChangeListener(listener); + updateComboBoxes(); validateDialog(); pack(); repaint(); @@ -437,18 +519,14 @@ final class DiscoveryDialog extends javax.swing.JDialog { private void videosButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_videosButtonActionPerformed removeAllPanels(); add(videoFilterPanel, CENTER); - imagesButton.setSelected(false); - imagesButton.setEnabled(true); - imagesButton.setBackground(UNSELECTED_COLOR); + unselectAllButtons(); videosButton.setSelected(true); videosButton.setEnabled(false); videosButton.setBackground(SELECTED_COLOR); videosButton.setForeground(Color.BLACK); - documentsButton.setSelected(false); - documentsButton.setEnabled(true); - documentsButton.setBackground(UNSELECTED_COLOR); videoFilterPanel.addPropertyChangeListener(listener); - fileType = FileSearchData.FileType.VIDEO; + type = SearchData.Type.VIDEO; + updateComboBoxes(); validateDialog(); pack(); repaint(); @@ -457,18 +535,14 @@ final class DiscoveryDialog extends javax.swing.JDialog { private void documentsButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_documentsButtonActionPerformed removeAllPanels(); add(documentFilterPanel, CENTER); + unselectAllButtons(); documentsButton.setSelected(true); documentsButton.setEnabled(false); documentsButton.setBackground(SELECTED_COLOR); documentsButton.setForeground(Color.BLACK); - videosButton.setSelected(false); - videosButton.setEnabled(true); - videosButton.setBackground(UNSELECTED_COLOR); - imagesButton.setSelected(false); - imagesButton.setEnabled(true); - imagesButton.setBackground(UNSELECTED_COLOR); - fileType = FileSearchData.FileType.DOCUMENTS; + type = SearchData.Type.DOCUMENT; documentFilterPanel.addPropertyChangeListener(listener); + updateComboBoxes(); validateDialog(); pack(); repaint(); @@ -482,6 +556,10 @@ final class DiscoveryDialog extends javax.swing.JDialog { remove(imageFilterPanel); imageFilterPanel.removePropertyChangeListener(listener); } + if (domainFilterPanel != null) { + remove(domainFilterPanel); + domainFilterPanel.removePropertyChangeListener(listener); + } if (documentFilterPanel != null) { remove(documentFilterPanel); documentFilterPanel.removePropertyChangeListener(listener); @@ -503,22 +581,28 @@ final class DiscoveryDialog extends javax.swing.JDialog { tc.open(); } tc.resetTopComponent(); - List filters; + List filters; if (videosButton.isSelected()) { filters = videoFilterPanel.getFilters(); } else if (documentsButton.isSelected()) { filters = documentFilterPanel.getFilters(); - } else { + } else if (imagesButton.isSelected()) { filters = imageFilterPanel.getFilters(); + } else if (domainsButton.isSelected()) { + filters = domainFilterPanel.getFilters(); + } else { + logger.log(Level.SEVERE, "No filter type selected"); + filters = new ArrayList<>(); } - DiscoveryEventUtils.getDiscoveryEventBus().post(new DiscoveryEventUtils.SearchStartedEvent(fileType)); + + DiscoveryEventUtils.getDiscoveryEventBus().post(new DiscoveryEventUtils.SearchStartedEvent(type)); // Get the grouping attribute and group sorting method - FileSearch.AttributeType groupingAttr = groupByCombobox.getItemAt(groupByCombobox.getSelectedIndex()).getAttributeType(); - FileGroup.GroupSortingAlgorithm groupSortAlgorithm = groupSortingComboBox.getItemAt(groupSortingComboBox.getSelectedIndex()); + DiscoveryAttributes.AttributeType groupingAttr = groupByCombobox.getItemAt(groupByCombobox.getSelectedIndex()).getAttributeType(); + Group.GroupSortingAlgorithm groupSortAlgorithm = groupSortingComboBox.getItemAt(groupSortingComboBox.getSelectedIndex()); // Get the file sorting method - FileSorter.SortingMethod fileSort = (FileSorter.SortingMethod) orderByCombobox.getSelectedItem(); + ResultsSorter.SortingMethod fileSort = (ResultsSorter.SortingMethod) orderByCombobox.getSelectedItem(); CentralRepository centralRepoDb = null; if (CentralRepository.isEnabled()) { try { @@ -528,13 +612,29 @@ final class DiscoveryDialog extends javax.swing.JDialog { logger.log(Level.SEVERE, "Error loading central repository database, no central repository options will be available for Discovery", ex); } } - searchWorker = new SearchWorker(centralRepoDb, filters, groupingAttr, groupSortAlgorithm, fileSort); + searchWorker = new SearchWorker(centralRepoDb, type, filters, groupingAttr, groupSortAlgorithm, fileSort); searchWorker.execute(); dispose(); tc.toFront(); tc.requestActive(); }//GEN-LAST:event_searchButtonActionPerformed - + + private void domainsButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_domainsButtonActionPerformed + removeAllPanels(); + add(domainFilterPanel, CENTER); + unselectAllButtons(); + domainsButton.setSelected(true); + domainsButton.setEnabled(false); + domainsButton.setBackground(SELECTED_COLOR); + domainsButton.setForeground(Color.BLACK); + type = SearchData.Type.DOMAIN; + domainFilterPanel.addPropertyChangeListener(listener); + updateComboBoxes(); + validateDialog(); + pack(); + repaint(); + }//GEN-LAST:event_domainsButtonActionPerformed + @Override public void dispose() { setVisible(false); @@ -569,6 +669,7 @@ final class DiscoveryDialog extends javax.swing.JDialog { // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JButton documentsButton; + private javax.swing.JButton domainsButton; private javax.swing.JLabel errorLabel; private javax.swing.JComboBox groupByCombobox; private javax.swing.JComboBox groupSortingComboBox; @@ -583,7 +684,7 @@ final class DiscoveryDialog extends javax.swing.JDialog { * filters available. */ private class CasePropertyChangeListener implements PropertyChangeListener { - + @Override @SuppressWarnings("fallthrough") public void propertyChange(PropertyChangeEvent evt) { diff --git a/Core/src/org/sleuthkit/autopsy/discovery/DiscoveryExtractAction.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryExtractAction.java similarity index 97% rename from Core/src/org/sleuthkit/autopsy/discovery/DiscoveryExtractAction.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryExtractAction.java index acaf0ccc99..7965878017 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/DiscoveryExtractAction.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryExtractAction.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; import org.sleuthkit.autopsy.directorytree.actionhelpers.ExtractActionHelper; import java.awt.event.ActionEvent; diff --git a/Core/src/org/sleuthkit/autopsy/discovery/DiscoveryThumbnailChildren.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryThumbnailChildren.java similarity index 98% rename from Core/src/org/sleuthkit/autopsy/discovery/DiscoveryThumbnailChildren.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryThumbnailChildren.java index bdcf2f876f..dce68e3a41 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/DiscoveryThumbnailChildren.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryThumbnailChildren.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; import java.util.Arrays; import java.util.HashSet; diff --git a/Core/src/org/sleuthkit/autopsy/discovery/DiscoveryTopComponent.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryTopComponent.form similarity index 96% rename from Core/src/org/sleuthkit/autopsy/discovery/DiscoveryTopComponent.form rename to Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryTopComponent.form index 4a1f471190..54630599ec 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/DiscoveryTopComponent.form +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryTopComponent.form @@ -105,8 +105,8 @@ - - + + diff --git a/Core/src/org/sleuthkit/autopsy/discovery/DiscoveryTopComponent.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryTopComponent.java similarity index 86% rename from Core/src/org/sleuthkit/autopsy/discovery/DiscoveryTopComponent.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryTopComponent.java index 633bf5c612..03a271dcdd 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/DiscoveryTopComponent.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryTopComponent.java @@ -16,8 +16,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; +import org.sleuthkit.autopsy.discovery.search.AbstractFilter; import com.google.common.eventbus.Subscribe; import java.awt.BorderLayout; import java.awt.Color; @@ -36,24 +37,27 @@ import org.openide.windows.RetainLocation; import org.openide.windows.TopComponent; import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.coreutils.ThreadConfined; -import org.sleuthkit.autopsy.discovery.FileSearchFiltering.FileFilter; +import org.sleuthkit.autopsy.discovery.search.DiscoveryEventUtils; +import org.sleuthkit.autopsy.discovery.search.SearchData.Type; +import static org.sleuthkit.autopsy.discovery.search.SearchData.Type.DOMAIN; /** * Create a dialog for displaying the Discovery results. */ -@TopComponent.Description(preferredID = "Discovery", persistenceType = TopComponent.PERSISTENCE_NEVER) +@TopComponent.Description(preferredID = "DiscoveryTc", persistenceType = TopComponent.PERSISTENCE_NEVER) @TopComponent.Registration(mode = "discovery", openAtStartup = false) @RetainLocation("discovery") @NbBundle.Messages("DiscoveryTopComponent.name= Discovery") public final class DiscoveryTopComponent extends TopComponent { private static final long serialVersionUID = 1L; - private static final String PREFERRED_ID = "Discovery"; // NON-NLS + private static final String PREFERRED_ID = "DiscoveryTc"; // NON-NLS private static final int ANIMATION_INCREMENT = 30; private volatile static int resultsAreaSize = 250; private final GroupListPanel groupListPanel; private final DetailsPanel detailsPanel; private final ResultsPanel resultsPanel; + private Type searchType; private int dividerLocation = -1; private SwingAnimator animator = null; @@ -144,6 +148,7 @@ public final class DiscoveryTopComponent extends TopComponent { protected void componentClosed() { DiscoveryDialog.getDiscoveryDialogInstance().cancelSearch(); DiscoveryEventUtils.getDiscoveryEventBus().post(new DiscoveryEventUtils.ClearInstanceSelectionEvent()); + DiscoveryEventUtils.getDiscoveryEventBus().post(new DiscoveryEventUtils.CancelBackgroundTasksEvent()); DiscoveryEventUtils.getDiscoveryEventBus().unregister(this); DiscoveryEventUtils.getDiscoveryEventBus().unregister(groupListPanel); DiscoveryEventUtils.getDiscoveryEventBus().unregister(resultsPanel); @@ -182,7 +187,7 @@ public final class DiscoveryTopComponent extends TopComponent { add(mainSplitPane, java.awt.BorderLayout.CENTER); - org.openide.awt.Mnemonics.setLocalizedText(newSearchButton, org.openide.util.NbBundle.getMessage(DiscoveryTopComponent.class, "FileSearchDialog.cancelButton.text")); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(newSearchButton, Bundle.DiscoveryTopComponent_cancelButton_text()); newSearchButton.setMaximumSize(new java.awt.Dimension(110, 26)); newSearchButton.setMinimumSize(new java.awt.Dimension(110, 26)); newSearchButton.setPreferredSize(new java.awt.Dimension(110, 26)); @@ -257,17 +262,19 @@ public final class DiscoveryTopComponent extends TopComponent { */ @Subscribe void handleDetailsVisibleEvent(DiscoveryEventUtils.DetailsVisibleEvent detailsVisibleEvent) { - if (animator != null && animator.isRunning()) { - animator.stop(); - animator = null; + if (resultsPanel.getActiveType() != DOMAIN) { + if (animator != null && animator.isRunning()) { + animator.stop(); + animator = null; + } + dividerLocation = rightSplitPane.getDividerLocation(); + if (detailsVisibleEvent.isShowDetailsArea()) { + animator = new SwingAnimator(new ShowDetailsAreaCallback()); + } else { + animator = new SwingAnimator(new HideDetailsAreaCallback()); + } + animator.start(); } - dividerLocation = rightSplitPane.getDividerLocation(); - if (detailsVisibleEvent.isShowDetailsArea()) { - animator = new SwingAnimator(new ShowDetailsAreaCallback()); - } else { - animator = new SwingAnimator(new HideDetailsAreaCallback()); - } - animator.start(); } /** @@ -278,12 +285,16 @@ public final class DiscoveryTopComponent extends TopComponent { */ @Messages({"DiscoveryTopComponent.cancelButton.text=Cancel Search", "# {0} - searchType", - "DiscoveryTopComponent.searchInProgress.text=Performing search for results of type {0}. Please wait."}) + "DiscoveryTopComponent.searchInProgress.text=Performing search for results of type {0}. Please wait.", + "DiscoveryTopComponent.searchError.text=Error no type specified for search."}) @Subscribe void handleSearchStartedEvent(DiscoveryEventUtils.SearchStartedEvent searchStartedEvent) { newSearchButton.setText(Bundle.DiscoveryTopComponent_cancelButton_text()); progressMessageTextArea.setForeground(Color.red); - progressMessageTextArea.setText(Bundle.DiscoveryTopComponent_searchInProgress_text(searchStartedEvent.getType().name())); + searchType = searchStartedEvent.getType(); + progressMessageTextArea.setText(Bundle.DiscoveryTopComponent_searchInProgress_text(searchType.name())); + rightSplitPane.getComponent(1).setVisible(searchStartedEvent.getType() != DOMAIN); + rightSplitPane.getComponent(2).setVisible(searchStartedEvent.getType() != DOMAIN); } /** @@ -295,11 +306,22 @@ public final class DiscoveryTopComponent extends TopComponent { @Subscribe @Messages({"DiscoveryTopComponent.newSearch.text=New Search", "# {0} - search", - "DiscoveryTopComponent.searchComplete.text=Results with {0}"}) + "DiscoveryTopComponent.searchComplete.text=Results with {0}", + "DiscoveryTopComponent.domainSearch.text=Type: Domain", + "DiscoveryTopComponent.additionalFilters.text=; "}) void handleSearchCompleteEvent(DiscoveryEventUtils.SearchCompleteEvent searchCompleteEvent) { newSearchButton.setText(Bundle.DiscoveryTopComponent_newSearch_text()); progressMessageTextArea.setForeground(Color.black); - progressMessageTextArea.setText(Bundle.DiscoveryTopComponent_searchComplete_text(searchCompleteEvent.getFilters().stream().map(FileFilter::getDesc).collect(Collectors.joining("; ")))); + String descriptionText = ""; + if (searchType == DOMAIN) { + //domain does not have a file type filter to add the type information so it is manually added + descriptionText = Bundle.DiscoveryTopComponent_domainSearch_text(); + if (!searchCompleteEvent.getFilters().isEmpty()) { + descriptionText += Bundle.DiscoveryTopComponent_additionalFilters_text(); + } + } + descriptionText += searchCompleteEvent.getFilters().stream().map(AbstractFilter::getDesc).collect(Collectors.joining("; ")); + progressMessageTextArea.setText(Bundle.DiscoveryTopComponent_searchComplete_text(descriptionText)); progressMessageTextArea.setCaretPosition(0); } diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryUiUtils.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryUiUtils.java new file mode 100644 index 0000000000..a1a15616e7 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryUiUtils.java @@ -0,0 +1,542 @@ +/* + * Autopsy + * + * Copyright 2020 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.discovery.ui; + +import com.google.common.io.Files; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Image; +import java.awt.Point; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import javax.imageio.ImageIO; +import javax.swing.ImageIcon; +import javax.swing.JComponent; +import javax.swing.JOptionPane; +import javax.swing.JScrollPane; +import javax.swing.JTextPane; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.FilenameUtils; +import org.imgscalr.Scalr; +import org.netbeans.api.progress.ProgressHandle; +import org.opencv.core.Mat; +import org.opencv.highgui.VideoCapture; +import org.openide.util.ImageUtilities; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.corelibs.ScalrWrapper; +import org.sleuthkit.autopsy.coreutils.ImageUtils; +import org.sleuthkit.autopsy.coreutils.Logger; +import static org.sleuthkit.autopsy.coreutils.VideoUtils.getVideoFileInTempDir; +import org.sleuthkit.autopsy.datamodel.ContentUtils; +import org.sleuthkit.autopsy.discovery.search.ResultFile; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.DataSource; +import org.sleuthkit.datamodel.IngestJobInfo; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Utility class for the various user interface elements used by Discovery. + */ +final class DiscoveryUiUtils { + + private final static Logger logger = Logger.getLogger(DiscoveryUiUtils.class.getName()); + private static final int BYTE_UNIT_CONVERSION = 1000; + private static final int ICON_SIZE = 16; + private static final String RED_CIRCLE_ICON_PATH = "org/sleuthkit/autopsy/images/red-circle-exclamation.png"; + private static final String YELLOW_CIRCLE_ICON_PATH = "org/sleuthkit/autopsy/images/yellow-circle-yield.png"; + private static final String DELETE_ICON_PATH = "org/sleuthkit/autopsy/images/file-icon-deleted.png"; + private static final String UNSUPPORTED_DOC_PATH = "org/sleuthkit/autopsy/images/image-extraction-not-supported.png"; + private static final ImageIcon INTERESTING_SCORE_ICON = new ImageIcon(ImageUtilities.loadImage(YELLOW_CIRCLE_ICON_PATH, false)); + private static final ImageIcon NOTABLE_SCORE_ICON = new ImageIcon(ImageUtilities.loadImage(RED_CIRCLE_ICON_PATH, false)); + private static final ImageIcon DELETED_ICON = new ImageIcon(ImageUtilities.loadImage(DELETE_ICON_PATH, false)); + private static final ImageIcon UNSUPPORTED_DOCUMENT_THUMBNAIL = new ImageIcon(ImageUtilities.loadImage(UNSUPPORTED_DOC_PATH, false)); + private static final String THUMBNAIL_FORMAT = "png"; //NON-NLS + private static final String VIDEO_THUMBNAIL_DIR = "video-thumbnails"; //NON-NLS + private static final BufferedImage VIDEO_DEFAULT_IMAGE = getDefaultVideoThumbnail(); + + @NbBundle.Messages({"# {0} - fileSize", + "# {1} - units", + "DiscoveryUiUtility.sizeLabel.text=Size: {0} {1}", + "DiscoveryUiUtility.bytes.text=bytes", + "DiscoveryUiUtility.kiloBytes.text=KB", + "DiscoveryUiUtility.megaBytes.text=MB", + "DiscoveryUiUtility.gigaBytes.text=GB", + "DiscoveryUiUtility.terraBytes.text=TB"}) + /** + * Convert a size in bytes to a string with representing the size in the + * largest units which represent the value as being greater than or equal to + * one. Result will be rounded down to the nearest whole number of those + * units. + * + * @param bytes Size in bytes. + */ + static String getFileSizeString(long bytes) { + long size = bytes; + int unitsSwitchValue = 0; + while (size > BYTE_UNIT_CONVERSION && unitsSwitchValue < 4) { + size /= BYTE_UNIT_CONVERSION; + unitsSwitchValue++; + } + String units; + switch (unitsSwitchValue) { + case 1: + units = Bundle.DiscoveryUiUtility_kiloBytes_text(); + break; + case 2: + units = Bundle.DiscoveryUiUtility_megaBytes_text(); + break; + case 3: + units = Bundle.DiscoveryUiUtility_gigaBytes_text(); + break; + case 4: + units = Bundle.DiscoveryUiUtility_terraBytes_text(); + break; + default: + units = Bundle.DiscoveryUiUtility_bytes_text(); + break; + } + return Bundle.DiscoveryUiUtility_sizeLabel_text(size, units); + } + + /** + * Get the image to use when the document type does not support image + * extraction. + * + * @return An image that indicates we don't know if there are images. + */ + static ImageIcon getUnsupportedImageThumbnail() { + return UNSUPPORTED_DOCUMENT_THUMBNAIL; + } + + /** + * Get the names of the sets which exist in the case database for the + * specified artifact and attribute types. + * + * @param artifactType The artifact type to get the list of sets for. + * @param setNameAttribute The attribute type which contains the set names. + * + * @return A list of set names which exist in the case for the specified + * artifact and attribute types. + * + * @throws TskCoreException + */ + static List getSetNames(BlackboardArtifact.ARTIFACT_TYPE artifactType, BlackboardAttribute.ATTRIBUTE_TYPE setNameAttribute) throws TskCoreException { + List arts = Case.getCurrentCase().getSleuthkitCase().getBlackboardArtifacts(artifactType); + List setNames = new ArrayList<>(); + for (BlackboardArtifact art : arts) { + for (BlackboardAttribute attr : art.getAttributes()) { + if (attr.getAttributeType().getTypeID() == setNameAttribute.getTypeID()) { + String setName = attr.getValueString(); + if (!setNames.contains(setName)) { + setNames.add(setName); + } + } + } + } + Collections.sort(setNames); + return setNames; + } + + /** + * Helper method to see if point is on the icon. + * + * @param comp The component to check if the cursor is over the icon of + * @param point The point the cursor is at. + * + * @return True if the point is over the icon, false otherwise. + */ + static boolean isPointOnIcon(Component comp, Point point) { + return comp instanceof JComponent && point.x >= comp.getX() && point.x <= comp.getX() + ICON_SIZE && point.y >= comp.getY() && point.y <= comp.getY() + ICON_SIZE; + } + + /** + * Method to set the icon and tool tip text for a label to show deleted + * status. + * + * @param isDeleted True if the label should reflect deleted status, + * false otherwise. + * @param isDeletedLabel The label to set the icon and tooltip for. + */ + @NbBundle.Messages({"DiscoveryUiUtils.isDeleted.text=All instances of file are deleted."}) + static void setDeletedIcon(boolean isDeleted, javax.swing.JLabel isDeletedLabel) { + if (isDeleted) { + isDeletedLabel.setIcon(DELETED_ICON); + isDeletedLabel.setToolTipText(Bundle.DiscoveryUiUtils_isDeleted_text()); + } else { + isDeletedLabel.setIcon(null); + isDeletedLabel.setToolTipText(null); + } + } + + /** + * Method to set the icon and tool tip text for a label to show the score. + * + * @param resultFile The result file which the label should reflect the + * score of. + * @param scoreLabel The label to set the icon and tooltip for. + */ + static void setScoreIcon(ResultFile resultFile, javax.swing.JLabel scoreLabel) { + switch (resultFile.getScore()) { + case NOTABLE_SCORE: + scoreLabel.setIcon(NOTABLE_SCORE_ICON); + break; + case INTERESTING_SCORE: + scoreLabel.setIcon(INTERESTING_SCORE_ICON); + break; + case NO_SCORE: // empty case - this is interpreted as an intentional fall-through + default: + scoreLabel.setIcon(null); + break; + } + scoreLabel.setToolTipText(resultFile.getScoreDescription()); + } + + /** + * Get the size of the icons used by the UI. + * + * @return + */ + static int getIconSize() { + return ICON_SIZE; + } + + /** + * Helper method to display an error message when the results of the + * Discovery Top component may be incomplete. + */ + @NbBundle.Messages({"DiscoveryUiUtils.resultsIncomplete.text=Discovery results may be incomplete"}) + static void displayErrorMessage(DiscoveryDialog dialog) { + //check if modules run and assemble message + try { + SleuthkitCase skCase = Case.getCurrentCaseThrows().getSleuthkitCase(); + Map dataSourceIngestModules = new HashMap<>(); + for (DataSource dataSource : skCase.getDataSources()) { + dataSourceIngestModules.put(dataSource.getId(), new DataSourceModulesWrapper(dataSource.getName())); + } + + for (IngestJobInfo jobInfo : skCase.getIngestJobs()) { + dataSourceIngestModules.get(jobInfo.getObjectId()).updateModulesRun(jobInfo); + } + String message = ""; + for (DataSourceModulesWrapper dsmodulesWrapper : dataSourceIngestModules.values()) { + message += dsmodulesWrapper.getMessage(); + } + if (!message.isEmpty()) { + JScrollPane messageScrollPane = new JScrollPane(); + JTextPane messageTextPane = new JTextPane(); + messageTextPane.setText(message); + messageTextPane.setVisible(true); + messageTextPane.setEditable(false); + messageTextPane.setCaretPosition(0); + messageScrollPane.setMaximumSize(new Dimension(600, 100)); + messageScrollPane.setPreferredSize(new Dimension(600, 100)); + messageScrollPane.setViewportView(messageTextPane); + JOptionPane.showMessageDialog(dialog, messageScrollPane, Bundle.DiscoveryUiUtils_resultsIncomplete_text(), JOptionPane.PLAIN_MESSAGE); + } + } catch (NoCurrentCaseException | TskCoreException ex) { + logger.log(Level.WARNING, "Exception while determining which modules have been run for Discovery", ex); + } + dialog.validateDialog(); + } + + /** + * Get the video thumbnails for a file which exists in a + * VideoThumbnailsWrapper and update the VideoThumbnailsWrapper to include + * them. + * + * @param thumbnailWrapper the object which contains the file to generate + * thumbnails for. + * + */ + @NbBundle.Messages({"# {0} - file name", + "DiscoveryUiUtils.genVideoThumb.progress.text=extracting temporary file {0}"}) + static void getVideoThumbnails(VideoThumbnailsWrapper thumbnailWrapper) { + AbstractFile file = thumbnailWrapper.getResultFile().getFirstInstance(); + String cacheDirectory; + try { + cacheDirectory = Case.getCurrentCaseThrows().getCacheDirectory(); + } catch (NoCurrentCaseException ex) { + cacheDirectory = null; + logger.log(Level.WARNING, "Unable to get cache directory, video thumbnails will not be saved", ex); + } + if (cacheDirectory == null || file.getMd5Hash() == null || !Paths.get(cacheDirectory, VIDEO_THUMBNAIL_DIR, file.getMd5Hash()).toFile().exists()) { + java.io.File tempFile; + try { + tempFile = getVideoFileInTempDir(file); + } catch (NoCurrentCaseException ex) { + logger.log(Level.WARNING, "Exception while getting open case.", ex); //NON-NLS + int[] framePositions = new int[]{ + 0, + 0, + 0, + 0}; + thumbnailWrapper.setThumbnails(createDefaultThumbnailList(VIDEO_DEFAULT_IMAGE), framePositions); + return; + } + if (tempFile.exists() == false || tempFile.length() < file.getSize()) { + ProgressHandle progress = ProgressHandle.createHandle(Bundle.DiscoveryUiUtils_genVideoThumb_progress_text(file.getName())); + progress.start(100); + try { + Files.createParentDirs(tempFile); + if (Thread.interrupted()) { + int[] framePositions = new int[]{ + 0, + 0, + 0, + 0}; + thumbnailWrapper.setThumbnails(createDefaultThumbnailList(VIDEO_DEFAULT_IMAGE), framePositions); + return; + } + ContentUtils.writeToFile(file, tempFile, progress, null, true); + } catch (IOException ex) { + logger.log(Level.WARNING, "Error extracting temporary file for " + file.getParentPath() + "/" + file.getName(), ex); //NON-NLS + } finally { + progress.finish(); + } + } + VideoCapture videoFile = new VideoCapture(); // will contain the video + BufferedImage bufferedImage = null; + + try { + if (!videoFile.open(tempFile.toString())) { + logger.log(Level.WARNING, "Error opening {0} for preview generation.", file.getParentPath() + "/" + file.getName()); //NON-NLS + int[] framePositions = new int[]{ + 0, + 0, + 0, + 0}; + thumbnailWrapper.setThumbnails(createDefaultThumbnailList(VIDEO_DEFAULT_IMAGE), framePositions); + return; + } + double fps = videoFile.get(5); // gets frame per second + double totalFrames = videoFile.get(7); // gets total frames + if (fps <= 0 || totalFrames <= 0) { + logger.log(Level.WARNING, "Error getting fps or total frames for {0}", file.getParentPath() + "/" + file.getName()); //NON-NLS + int[] framePositions = new int[]{ + 0, + 0, + 0, + 0}; + thumbnailWrapper.setThumbnails(createDefaultThumbnailList(VIDEO_DEFAULT_IMAGE), framePositions); + return; + } + if (Thread.interrupted()) { + int[] framePositions = new int[]{ + 0, + 0, + 0, + 0}; + thumbnailWrapper.setThumbnails(createDefaultThumbnailList(VIDEO_DEFAULT_IMAGE), framePositions); + return; + } + + double duration = 1000 * (totalFrames / fps); //total milliseconds + + int[] framePositions = new int[]{ + (int) (duration * .01), + (int) (duration * .25), + (int) (duration * .5), + (int) (duration * .75),}; + + Mat imageMatrix = new Mat(); + List videoThumbnails = new ArrayList<>(); + if (cacheDirectory == null || file.getMd5Hash() == null) { + cacheDirectory = null; + } else { + try { + FileUtils.forceMkdir(Paths.get(cacheDirectory, VIDEO_THUMBNAIL_DIR, file.getMd5Hash()).toFile()); + } catch (IOException ex) { + cacheDirectory = null; + logger.log(Level.WARNING, "Unable to make video thumbnails directory, thumbnails will not be saved", ex); + } + } + for (int i = 0; i < framePositions.length; i++) { + if (!videoFile.set(0, framePositions[i])) { + logger.log(Level.WARNING, "Error seeking to " + framePositions[i] + "ms in {0}", file.getParentPath() + "/" + file.getName()); //NON-NLS + // If we can't set the time, continue to the next frame position and try again. + + videoThumbnails.add(VIDEO_DEFAULT_IMAGE); + if (cacheDirectory != null) { + try { + ImageIO.write(VIDEO_DEFAULT_IMAGE, THUMBNAIL_FORMAT, + Paths.get(cacheDirectory, VIDEO_THUMBNAIL_DIR, file.getMd5Hash(), i + "-" + framePositions[i] + "." + THUMBNAIL_FORMAT).toFile()); //NON-NLS) + } catch (IOException ex) { + logger.log(Level.WARNING, "Unable to save default video thumbnail for " + file.getMd5Hash() + " at frame position " + framePositions[i], ex); + } + } + continue; + } + // Read the frame into the image/matrix. + if (!videoFile.read(imageMatrix)) { + logger.log(Level.WARNING, "Error reading frame at " + framePositions[i] + "ms from {0}", file.getParentPath() + "/" + file.getName()); //NON-NLS + // If the image is bad for some reason, continue to the next frame position and try again. + videoThumbnails.add(VIDEO_DEFAULT_IMAGE); + if (cacheDirectory != null) { + try { + ImageIO.write(VIDEO_DEFAULT_IMAGE, THUMBNAIL_FORMAT, + Paths.get(cacheDirectory, VIDEO_THUMBNAIL_DIR, file.getMd5Hash(), i + "-" + framePositions[i] + "." + THUMBNAIL_FORMAT).toFile()); //NON-NLS) + } catch (IOException ex) { + logger.log(Level.WARNING, "Unable to save default video thumbnail for " + file.getMd5Hash() + " at frame position " + framePositions[i], ex); + } + } + + continue; + } + // If the image is empty, return since no buffered image can be created. + if (imageMatrix.empty()) { + videoThumbnails.add(VIDEO_DEFAULT_IMAGE); + if (cacheDirectory != null) { + try { + ImageIO.write(VIDEO_DEFAULT_IMAGE, THUMBNAIL_FORMAT, + Paths.get(cacheDirectory, VIDEO_THUMBNAIL_DIR, file.getMd5Hash(), i + "-" + framePositions[i] + "." + THUMBNAIL_FORMAT).toFile()); //NON-NLS) + } catch (IOException ex) { + logger.log(Level.WARNING, "Unable to save default video thumbnail for " + file.getMd5Hash() + " at frame position " + framePositions[i], ex); + } + } + continue; + } + + int matrixColumns = imageMatrix.cols(); + int matrixRows = imageMatrix.rows(); + + // Convert the matrix that contains the frame to a buffered image. + if (bufferedImage == null) { + bufferedImage = new BufferedImage(matrixColumns, matrixRows, BufferedImage.TYPE_3BYTE_BGR); + } + + byte[] data = new byte[matrixRows * matrixColumns * (int) (imageMatrix.elemSize())]; + imageMatrix.get(0, 0, data); //copy the image to data + + if (imageMatrix.channels() == 3) { + for (int k = 0; k < data.length; k += 3) { + byte temp = data[k]; + data[k] = data[k + 2]; + data[k + 2] = temp; + } + } + + bufferedImage.getRaster().setDataElements(0, 0, matrixColumns, matrixRows, data); + if (Thread.interrupted()) { + thumbnailWrapper.setThumbnails(videoThumbnails, framePositions); + try { + FileUtils.forceDelete(Paths.get(cacheDirectory, VIDEO_THUMBNAIL_DIR, file.getMd5Hash()).toFile()); + } catch (IOException ex) { + logger.log(Level.WARNING, "Unable to delete directory for cancelled video thumbnail process", ex); + } + return; + } + BufferedImage thumbnail = ScalrWrapper.resize(bufferedImage, Scalr.Method.SPEED, Scalr.Mode.FIT_TO_HEIGHT, ImageUtils.ICON_SIZE_LARGE, ImageUtils.ICON_SIZE_MEDIUM, Scalr.OP_ANTIALIAS); + //We are height limited here so it can be wider than it can be tall.Scalr maintains the aspect ratio. + videoThumbnails.add(thumbnail); + if (cacheDirectory != null) { + try { + ImageIO.write(thumbnail, THUMBNAIL_FORMAT, + Paths.get(cacheDirectory, VIDEO_THUMBNAIL_DIR, file.getMd5Hash(), i + "-" + framePositions[i] + "." + THUMBNAIL_FORMAT).toFile()); //NON-NLS) + } catch (IOException ex) { + logger.log(Level.WARNING, "Unable to save video thumbnail for " + file.getMd5Hash() + " at frame position " + framePositions[i], ex); + } + } + } + thumbnailWrapper.setThumbnails(videoThumbnails, framePositions); + } finally { + videoFile.release(); // close the file} + } + } else { + loadSavedThumbnails(cacheDirectory, thumbnailWrapper, VIDEO_DEFAULT_IMAGE); + } + } + + /** + * Get the default image to display when a thumbnail is not available. + * + * @return The default video thumbnail. + */ + private static BufferedImage getDefaultVideoThumbnail() { + try { + return ImageIO.read(ImageUtils.class + .getResourceAsStream("/org/sleuthkit/autopsy/images/failedToCreateVideoThumb.png"));//NON-NLS + } catch (IOException ex) { + logger.log(Level.SEVERE, "Failed to load 'failed to create video' placeholder.", ex); //NON-NLS + } + return null; + } + + /** + * Load the thumbnails that exist in the cache directory for the specified + * video file. + * + * @param cacheDirectory The directory which exists for the video + * thumbnails. + * @param thumbnailWrapper The VideoThumbnailWrapper object which contains + * information about the file and the thumbnails + * associated with it. + */ + private static void loadSavedThumbnails(String cacheDirectory, VideoThumbnailsWrapper thumbnailWrapper, BufferedImage failedVideoThumbImage) { + int[] framePositions = new int[4]; + List videoThumbnails = new ArrayList<>(); + int thumbnailNumber = 0; + String md5 = thumbnailWrapper.getResultFile().getFirstInstance().getMd5Hash(); + for (String fileName : Paths.get(cacheDirectory, VIDEO_THUMBNAIL_DIR, md5).toFile().list()) { + try { + videoThumbnails.add(ImageIO.read(Paths.get(cacheDirectory, VIDEO_THUMBNAIL_DIR, md5, fileName).toFile())); + } catch (IOException ex) { + videoThumbnails.add(failedVideoThumbImage); + logger.log(Level.WARNING, "Unable to read saved video thumbnail " + fileName + " for " + md5, ex); + } + int framePos = Integer.valueOf(FilenameUtils.getBaseName(fileName).substring(2)); + framePositions[thumbnailNumber] = framePos; + thumbnailNumber++; + } + thumbnailWrapper.setThumbnails(videoThumbnails, framePositions); + } + + /** + * Private helper method for creating video thumbnails, for use when no + * thumbnails are created. + * + * @return List containing the default thumbnail. + */ + private static List createDefaultThumbnailList(BufferedImage failedVideoThumbImage) { + List videoThumbnails = new ArrayList<>(); + videoThumbnails.add(failedVideoThumbImage); + videoThumbnails.add(failedVideoThumbImage); + videoThumbnails.add(failedVideoThumbImage); + videoThumbnails.add(failedVideoThumbImage); + return videoThumbnails; + } + + /** + * Private constructor for DiscoveryUiUtils utility class. + */ + private DiscoveryUiUtils() { + //private constructor in a utility class intentionally left blank + } +} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/DocumentFilterPanel.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentFilterPanel.form similarity index 95% rename from Core/src/org/sleuthkit/autopsy/discovery/DocumentFilterPanel.form rename to Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentFilterPanel.form index a1c49c4c07..16b22c3672 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/DocumentFilterPanel.form +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentFilterPanel.form @@ -58,14 +58,14 @@ + - + - diff --git a/Core/src/org/sleuthkit/autopsy/discovery/DocumentFilterPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentFilterPanel.java similarity index 91% rename from Core/src/org/sleuthkit/autopsy/discovery/DocumentFilterPanel.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentFilterPanel.java index fcd26fed08..44e303ccc1 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/DocumentFilterPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentFilterPanel.java @@ -16,9 +16,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; +import org.sleuthkit.autopsy.discovery.search.SearchData; /** * Class which displays all filters available for the Documents search type. @@ -26,7 +27,7 @@ import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; final class DocumentFilterPanel extends AbstractFiltersPanel { private static final long serialVersionUID = 1L; - private static final FileSearchData.FileType FILE_TYPE = FileSearchData.FileType.DOCUMENTS; + private static final SearchData.Type TYPE = SearchData.Type.DOCUMENT; /** * Constructs a new DocumentFilterPanel. @@ -34,7 +35,7 @@ final class DocumentFilterPanel extends AbstractFiltersPanel { DocumentFilterPanel() { super(); initComponents(); - SizeFilterPanel sizeFilterPanel = new SizeFilterPanel(FILE_TYPE); + SizeFilterPanel sizeFilterPanel = new SizeFilterPanel(TYPE); int[] sizeIndicesSelected = {3, 4, 5}; addFilter(sizeFilterPanel, true, sizeIndicesSelected, 0); addFilter(new DataSourceFilterPanel(), false, null, 0); @@ -44,7 +45,7 @@ final class DocumentFilterPanel extends AbstractFiltersPanel { } else { pastOccurrencesIndices = new int[]{2, 3, 4}; } - addFilter(new PastOccurrencesFilterPanel(), true, pastOccurrencesIndices, 0); + addFilter(new PastOccurrencesFilterPanel(TYPE), true, pastOccurrencesIndices, 0); addFilter(new HashSetFilterPanel(), false, null, 1); addFilter(new InterestingItemsFilterPanel(), false, null, 1); addFilter(new ParentFolderFilterPanel(), false, null, 1); @@ -91,8 +92,8 @@ final class DocumentFilterPanel extends AbstractFiltersPanel { add(documentFiltersScrollPane, java.awt.BorderLayout.CENTER); }// //GEN-END:initComponents @Override - FileSearchData.FileType getFileType() { - return FILE_TYPE; + SearchData.Type getType() { + return TYPE; } // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JSplitPane documentsFiltersSplitPane; diff --git a/Core/src/org/sleuthkit/autopsy/discovery/DocumentPanel.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentPanel.form similarity index 88% rename from Core/src/org/sleuthkit/autopsy/discovery/DocumentPanel.form rename to Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentPanel.form index bb29cdb8b4..9329d6f976 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/DocumentPanel.form +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentPanel.form @@ -80,16 +80,16 @@ - + - + - + - + @@ -100,20 +100,20 @@ - + - + - +
- + diff --git a/Core/src/org/sleuthkit/autopsy/discovery/DocumentPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentPanel.java similarity index 91% rename from Core/src/org/sleuthkit/autopsy/discovery/DocumentPanel.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentPanel.java index 2852c7579a..08bdd70791 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/DocumentPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentPanel.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; import java.awt.Color; import java.awt.Component; @@ -29,6 +29,7 @@ import javax.swing.JList; import javax.swing.ListCellRenderer; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.corecomponents.AutoWrappingJTextPane; +import org.sleuthkit.autopsy.discovery.search.SearchData; /** * Class which displays a preview and details about a document. @@ -67,15 +68,15 @@ class DocumentPanel extends javax.swing.JPanel implements ListCellRenderer 0) { numberOfImagesLabel.setText(Bundle.DocumentPanel_numberOfImages_text(value.getSummary().getNumberOfImages())); sampleImageLabel.setIcon(new ImageIcon(value.getSummary().getSampleImage())); - } else if (FileSearchData.getDocTypesWithoutImageExtraction().contains(value.getResultFile().getFirstInstance().getMIMEType())) { + } else if (SearchData.getDocTypesWithoutImageExtraction().contains(value.getResultFile().getFirstInstance().getMIMEType())) { numberOfImagesLabel.setText(Bundle.DocumentPanel_noImageExtraction_text()); sampleImageLabel.setIcon(DiscoveryUiUtils.getUnsupportedImageThumbnail()); } else { diff --git a/Core/src/org/sleuthkit/autopsy/discovery/DocumentPreviewViewer.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentPreviewViewer.form similarity index 100% rename from Core/src/org/sleuthkit/autopsy/discovery/DocumentPreviewViewer.form rename to Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentPreviewViewer.form diff --git a/Core/src/org/sleuthkit/autopsy/discovery/DocumentPreviewViewer.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentPreviewViewer.java similarity index 99% rename from Core/src/org/sleuthkit/autopsy/discovery/DocumentPreviewViewer.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentPreviewViewer.java index 6d79268b75..0ef1173c0e 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/DocumentPreviewViewer.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentPreviewViewer.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; import java.util.ArrayList; import java.util.List; diff --git a/Core/src/org/sleuthkit/autopsy/discovery/DocumentWrapper.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentWrapper.java similarity index 95% rename from Core/src/org/sleuthkit/autopsy/discovery/DocumentWrapper.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentWrapper.java index 02a2b0530e..1ea0df4ba4 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/DocumentWrapper.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentWrapper.java @@ -16,9 +16,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.discovery.search.ResultFile; import org.sleuthkit.autopsy.textsummarizer.TextSummary; /** diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainFilterPanel.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainFilterPanel.form new file mode 100644 index 0000000000..4d4d511b7a --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainFilterPanel.form @@ -0,0 +1,86 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainFilterPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainFilterPanel.java new file mode 100644 index 0000000000..bce5577054 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainFilterPanel.java @@ -0,0 +1,104 @@ +/* + * Autopsy + * + * Copyright 2020 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.discovery.ui; + +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; +import org.sleuthkit.autopsy.discovery.search.DiscoveryAttributes; +import org.sleuthkit.autopsy.discovery.search.ResultsSorter; +import org.sleuthkit.autopsy.discovery.search.SearchData; + +/** + * Filter panel for searching domain attributes with Discovery. + */ +public class DomainFilterPanel extends AbstractFiltersPanel { + + private static final long serialVersionUID = 1L; + private static final SearchData.Type TYPE = SearchData.Type.DOMAIN; + + /** + * Creates new form DomainFilterPanel. + */ + public DomainFilterPanel() { + super(); + initComponents(); + addFilter(new DataSourceFilterPanel(), false, null, 0); + addFilter(new ArtifactTypeFilterPanel(), false, null, 1); + addFilter(new DateFilterPanel(), false, null, 1); + int[] pastOccurrencesIndices = null; + if (CentralRepository.isEnabled()) { + pastOccurrencesIndices = new int[]{2, 3, 4}; + } + addFilter(new PastOccurrencesFilterPanel(TYPE), true, pastOccurrencesIndices, 0); + addPanelsToScrollPane(domainFiltersSplitPane); + setLastGroupingAttributeType(DiscoveryAttributes.GroupingAttributeType.MOST_RECENT_DATE); + setLastSortingMethod(ResultsSorter.SortingMethod.BY_DOMAIN_NAME); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + javax.swing.JScrollPane domainFiltersScrollPane = new javax.swing.JScrollPane(); + javax.swing.JPanel domainFiltersPanel = new javax.swing.JPanel(); + domainFiltersSplitPane = new javax.swing.JSplitPane(); + + setPreferredSize(new java.awt.Dimension(225, 70)); + setLayout(new java.awt.BorderLayout()); + + domainFiltersSplitPane.setBorder(javax.swing.BorderFactory.createTitledBorder(org.openide.util.NbBundle.getMessage(DomainFilterPanel.class, "DomainFilterPanel.domainFiltersSplitPane.border.title"))); // NOI18N + domainFiltersSplitPane.setResizeWeight(0.5); + domainFiltersSplitPane.setToolTipText(org.openide.util.NbBundle.getMessage(DomainFilterPanel.class, "DomainFilterPanel.domainFiltersSplitPane.toolTipText")); // NOI18N + + javax.swing.GroupLayout domainFiltersPanelLayout = new javax.swing.GroupLayout(domainFiltersPanel); + domainFiltersPanel.setLayout(domainFiltersPanelLayout); + domainFiltersPanelLayout.setHorizontalGroup( + domainFiltersPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(domainFiltersPanelLayout.createSequentialGroup() + .addGap(8, 8, 8) + .addComponent(domainFiltersSplitPane) + .addGap(8, 8, 8)) + ); + domainFiltersPanelLayout.setVerticalGroup( + domainFiltersPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(domainFiltersPanelLayout.createSequentialGroup() + .addGap(8, 8, 8) + .addComponent(domainFiltersSplitPane) + .addGap(8, 8, 8)) + ); + + domainFiltersScrollPane.setViewportView(domainFiltersPanel); + + add(domainFiltersScrollPane, java.awt.BorderLayout.CENTER); + }// //GEN-END:initComponents + + @Override + SearchData.Type getType() { + return TYPE; + } + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JSplitPane domainFiltersSplitPane; + // End of variables declaration//GEN-END:variables +} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainSummaryPanel.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainSummaryPanel.form new file mode 100644 index 0000000000..19ecc82ca2 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainSummaryPanel.form @@ -0,0 +1,128 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainSummaryPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainSummaryPanel.java new file mode 100644 index 0000000000..c6979663a7 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainSummaryPanel.java @@ -0,0 +1,185 @@ +/* + * Autopsy + * + * Copyright 2020 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.discovery.ui; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Point; +import java.awt.event.MouseEvent; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import javax.swing.ImageIcon; +import javax.swing.JComponent; +import javax.swing.JList; +import javax.swing.ListCellRenderer; +import org.openide.util.NbBundle; + +/** + * Class which displays a preview and details about a domain. + */ +class DomainSummaryPanel extends javax.swing.JPanel implements ListCellRenderer { + + private static final long serialVersionUID = 1L; + private static final Color SELECTION_COLOR = new Color(0, 120, 215); + private static final SimpleDateFormat dateFormat = new SimpleDateFormat("MMM dd yyyy", Locale.getDefault()); + + /** + * Creates new form DomainPanel. + */ + DomainSummaryPanel() { + initComponents(); + domainNameLabel.setFont(domainNameLabel.getFont().deriveFont(domainNameLabel.getFont().getStyle(), domainNameLabel.getFont().getSize() + 6)); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + domainNameLabel = new javax.swing.JLabel(); + sampleImageLabel = new javax.swing.JLabel(); + numberOfImagesLabel = new javax.swing.JLabel(); + activityLabel = new javax.swing.JLabel(); + pagesLabel = new javax.swing.JLabel(); + filesDownloadedLabel = new javax.swing.JLabel(); + totalVisitsLabel = new javax.swing.JLabel(); + + setBorder(javax.swing.BorderFactory.createEtchedBorder()); + + sampleImageLabel.setBorder(javax.swing.BorderFactory.createEtchedBorder()); + sampleImageLabel.setIconTextGap(0); + sampleImageLabel.setMaximumSize(new java.awt.Dimension(100, 100)); + sampleImageLabel.setMinimumSize(new java.awt.Dimension(100, 100)); + sampleImageLabel.setPreferredSize(new java.awt.Dimension(100, 100)); + + org.openide.awt.Mnemonics.setLocalizedText(activityLabel, org.openide.util.NbBundle.getMessage(DomainSummaryPanel.class, "DomainSummaryPanel.activityLabel.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(pagesLabel, org.openide.util.NbBundle.getMessage(DomainSummaryPanel.class, "DomainSummaryPanel.pagesLabel.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(filesDownloadedLabel, org.openide.util.NbBundle.getMessage(DomainSummaryPanel.class, "DomainSummaryPanel.filesDownloadedLabel.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(totalVisitsLabel, org.openide.util.NbBundle.getMessage(DomainSummaryPanel.class, "DomainSummaryPanel.totalVisitsLabel.text")); // NOI18N + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(domainNameLabel, javax.swing.GroupLayout.DEFAULT_SIZE, 539, Short.MAX_VALUE) + .addGap(47, 47, 47)) + .addComponent(activityLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(pagesLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(filesDownloadedLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(totalVisitsLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addComponent(numberOfImagesLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(sampleImageLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .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.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(numberOfImagesLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 17, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(sampleImageLabel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(layout.createSequentialGroup() + .addComponent(domainNameLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 32, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(activityLabel) + .addGap(11, 11, 11) + .addComponent(totalVisitsLabel) + .addGap(11, 11, 11) + .addComponent(pagesLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(filesDownloadedLabel) + .addGap(0, 0, Short.MAX_VALUE))) + .addContainerGap()) + ); + }// //GEN-END:initComponents + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JLabel activityLabel; + private javax.swing.JLabel domainNameLabel; + private javax.swing.JLabel filesDownloadedLabel; + private javax.swing.JLabel numberOfImagesLabel; + private javax.swing.JLabel pagesLabel; + private javax.swing.JLabel sampleImageLabel; + private javax.swing.JLabel totalVisitsLabel; + // End of variables declaration//GEN-END:variables + + @NbBundle.Messages({"# {0} - startDate", + "# {1} - endDate", + "DomainSummaryPanel.activity.text=Activity: {0} to {1}", + "DomainSummaryPanel.pages.text=Pages in past 60 days: ", + "DomainSummaryPanel.totalPages.text=Total visits: ", + "DomainSummaryPanel.downloads.text=Files downloaded: ", + "DomainSummaryPanel.loadingImages.text=Loading thumbnail..."}) + @Override + public Component getListCellRendererComponent(JList list, DomainWrapper value, int index, boolean isSelected, boolean cellHasFocus) { + domainNameLabel.setText(value.getResultDomain().getDomain()); + String startDate = dateFormat.format(new Date(value.getResultDomain().getActivityStart() * 1000)); + String endDate = dateFormat.format(new Date(value.getResultDomain().getActivityEnd() * 1000)); + activityLabel.setText(Bundle.DomainSummaryPanel_activity_text(startDate, endDate)); + totalVisitsLabel.setText(Bundle.DomainSummaryPanel_totalPages_text() + value.getResultDomain().getTotalVisits()); + pagesLabel.setText(Bundle.DomainSummaryPanel_pages_text() + value.getResultDomain().getVisitsInLast60()); + filesDownloadedLabel.setText(Bundle.DomainSummaryPanel_downloads_text() + value.getResultDomain().getFilesDownloaded()); + if (value.getThumbnail() == null) { + numberOfImagesLabel.setText(Bundle.DomainSummaryPanel_loadingImages_text()); + sampleImageLabel.setIcon(null); + } else { + numberOfImagesLabel.setText(null); + sampleImageLabel.setIcon(new ImageIcon(value.getThumbnail())); + } + setBackground(isSelected ? SELECTION_COLOR : list.getBackground()); + return this; + } + + @Override + public String getToolTipText(MouseEvent event) { + if (event != null) { + //gets tooltip of internal panel item mouse is over + Point point = event.getPoint(); + for (Component comp : getComponents()) { + if (DiscoveryUiUtils.isPointOnIcon(comp, point)) { + String toolTip = ((JComponent) comp).getToolTipText(); + if (toolTip == null || toolTip.isEmpty()) { + return null; + } else { + return toolTip; + } + } + } + } + return null; + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainSummaryViewer.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainSummaryViewer.form new file mode 100644 index 0000000000..22296c0178 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainSummaryViewer.form @@ -0,0 +1,49 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainSummaryViewer.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainSummaryViewer.java new file mode 100644 index 0000000000..593ae49949 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainSummaryViewer.java @@ -0,0 +1,86 @@ +/* + * Autopsy + * + * Copyright 2020 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.discovery.ui; + +import javax.swing.DefaultListModel; + +/** + * A JPanel to display domain summaries. + */ +public class DomainSummaryViewer extends javax.swing.JPanel { + + private static final long serialVersionUID = 1L; + + private final DefaultListModel domainListModel = new DefaultListModel<>(); + + /** + * Clear the list of documents being displayed. + */ + void clearViewer() { + synchronized (this) { + domainListModel.removeAllElements(); + domainScrollPane.getVerticalScrollBar().setValue(0); + } + } + + /** + * Creates new form DomainSummaryPanel + */ + public DomainSummaryViewer() { + initComponents(); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + domainScrollPane = new javax.swing.JScrollPane(); + javax.swing.JList domainList = new javax.swing.JList<>(); + + setLayout(new java.awt.BorderLayout()); + + domainList.setModel(domainListModel); + domainList.setCellRenderer(new DomainSummaryPanel()); + domainScrollPane.setViewportView(domainList); + + add(domainScrollPane, java.awt.BorderLayout.CENTER); + }// //GEN-END:initComponents + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JScrollPane domainScrollPane; + // End of variables declaration//GEN-END:variables + + /** + * Add the summary for a domain to the panel. + * + * @param domainWrapper The object which contains the domain summary which + * will be displayed. + */ + void addDomain(DomainWrapper domainWrapper) { + synchronized (this) { + domainListModel.addElement(domainWrapper); + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainWrapper.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainWrapper.java new file mode 100644 index 0000000000..b3ee3999ac --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainWrapper.java @@ -0,0 +1,72 @@ +/* + * Autopsy + * + * Copyright 2020 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.discovery.ui; + +import java.awt.Image; +import org.sleuthkit.autopsy.discovery.search.ResultDomain; + +/** + * Class to wrap all the information necessary for a domain summary to be + * displayed. + */ +public class DomainWrapper { + + private final ResultDomain domain; + private Image thumbnail = null; + + /** + * Construct a new DocumentWrapper. + * + * @param file The ResultFile which represents the document which the + * summary is created for. + */ + DomainWrapper(ResultDomain domain) { + this.domain = domain; + } + + /** + * Set the thumbnail which exists. + * + * @param thumbnail The image object which will be used to represent this + * domain object. + */ + void setThumbnail(Image thumbnail) { + this.thumbnail = thumbnail; + } + + /** + * Get the ResultDomain which represents the Domain the summary was created + * for. + * + * @return The ResultDomain which represents the domain attribute which the + * summary was created for. + */ + ResultDomain getResultDomain() { + return domain; + } + + /** + * Get the image to be used for the domain. + * + * @return The image which represents the domain. + */ + Image getThumbnail() { + return thumbnail; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/GroupListPanel.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/GroupListPanel.form similarity index 95% rename from Core/src/org/sleuthkit/autopsy/discovery/GroupListPanel.form rename to Core/src/org/sleuthkit/autopsy/discovery/ui/GroupListPanel.form index be51027b3a..47ead7ce0b 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/GroupListPanel.form +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/GroupListPanel.form @@ -46,7 +46,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/discovery/GroupListPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/GroupListPanel.java similarity index 80% rename from Core/src/org/sleuthkit/autopsy/discovery/GroupListPanel.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/GroupListPanel.java index 3617ce3670..714d5202f0 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/GroupListPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/GroupListPanel.java @@ -16,8 +16,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; +import org.sleuthkit.autopsy.discovery.search.AbstractFilter; import com.google.common.eventbus.Subscribe; import java.awt.Cursor; import java.awt.Graphics2D; @@ -30,8 +31,13 @@ import javax.swing.JList; import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import org.openide.util.NbBundle.Messages; -import org.sleuthkit.autopsy.discovery.FileSearch.GroupKey; -import org.sleuthkit.autopsy.discovery.FileSearchData.FileType; +import org.sleuthkit.autopsy.discovery.search.DiscoveryAttributes; +import org.sleuthkit.autopsy.discovery.search.DiscoveryEventUtils; +import org.sleuthkit.autopsy.discovery.search.DiscoveryKeyUtils.GroupKey; +import org.sleuthkit.autopsy.discovery.search.Group; +import org.sleuthkit.autopsy.discovery.search.ResultsSorter; +import org.sleuthkit.autopsy.discovery.search.SearchData.Type; +import static org.sleuthkit.autopsy.discovery.search.SearchData.Type.DOMAIN; /** * Panel to display the list of groups which are provided by a search. @@ -39,12 +45,12 @@ import org.sleuthkit.autopsy.discovery.FileSearchData.FileType; final class GroupListPanel extends javax.swing.JPanel { private static final long serialVersionUID = 1L; - private FileType resultType = null; + private Type type = null; private Map groupMap = null; - private List searchfilters; - private FileSearch.AttributeType groupingAttribute; - private FileGroup.GroupSortingAlgorithm groupSort; - private FileSorter.SortingMethod fileSortMethod; + private List searchfilters; + private DiscoveryAttributes.AttributeType groupingAttribute; + private Group.GroupSortingAlgorithm groupSort; + private ResultsSorter.SortingMethod resultSortMethod; private GroupKey selectedGroupKey; /** @@ -61,15 +67,20 @@ final class GroupListPanel extends javax.swing.JPanel { */ @Subscribe void handleSearchStartedEvent(DiscoveryEventUtils.SearchStartedEvent searchStartedEvent) { - resultType = searchStartedEvent.getType(); + type = searchStartedEvent.getType(); groupKeyList.setListData(new GroupKey[0]); } - @Messages({"GroupsListPanel.noResults.message.text=No results were found for the selected filters.\n\n" + @Messages({"GroupsListPanel.noFileResults.message.text=No files were found for the selected filters.\n\n" + "Reminder:\n" + " -The File Type Identification module must be run on each data source you want to find results in.\n" + " -The Hash Lookup module must be run on each data source if you want to filter by past occurrence.\n" - + " -The Exif module must be run on each data source if you are filtering by User Created content.", + + " -The Picture Analyzer module must be run on each data source if you are filtering by User Created content.", + "GroupsListPanel.noDomainResults.message.text=No domains were found for the selected filters.\n\n" + + "Reminder:\n" + + " -The Recent Activity module must be run on each data source you want to find results in.\n" + + " -The Central Repository module must be run on each data source if you want to filter or sort by past occurrences.\n" + + " -The iOS Analyzer (iLEAPP) module must be run on each data source which contains data from an iOS device.\n", "GroupsListPanel.noResults.title.text=No results found"}) /** * Subscribe to and update list of groups in response to @@ -83,14 +94,19 @@ final class GroupListPanel extends javax.swing.JPanel { searchfilters = searchCompleteEvent.getFilters(); groupingAttribute = searchCompleteEvent.getGroupingAttr(); groupSort = searchCompleteEvent.getGroupSort(); - fileSortMethod = searchCompleteEvent.getFileSort(); + resultSortMethod = searchCompleteEvent.getResultSort(); groupKeyList.setListData(groupMap.keySet().toArray(new GroupKey[groupMap.keySet().size()])); SwingUtilities.invokeLater(() -> { if (groupKeyList.getModel().getSize() > 0) { groupKeyList.setSelectedIndex(0); + } else if (type == DOMAIN) { + JOptionPane.showMessageDialog(DiscoveryTopComponent.getTopComponent(), + Bundle.GroupsListPanel_noDomainResults_message_text(), + Bundle.GroupsListPanel_noResults_title_text(), + JOptionPane.PLAIN_MESSAGE); } else { JOptionPane.showMessageDialog(DiscoveryTopComponent.getTopComponent(), - Bundle.GroupsListPanel_noResults_message_text(), + Bundle.GroupsListPanel_noFileResults_message_text(), Bundle.GroupsListPanel_noResults_title_text(), JOptionPane.PLAIN_MESSAGE); } @@ -172,7 +188,7 @@ final class GroupListPanel extends javax.swing.JPanel { if (selectedGroup.equals(groupKey)) { selectedGroupKey = groupKey; DiscoveryEventUtils.getDiscoveryEventBus().post(new DiscoveryEventUtils.GroupSelectedEvent( - searchfilters, groupingAttribute, groupSort, fileSortMethod, selectedGroupKey, groupMap.get(selectedGroupKey), resultType)); + searchfilters, groupingAttribute, groupSort, resultSortMethod, selectedGroupKey, groupMap.get(selectedGroupKey), type)); break; } } @@ -206,10 +222,9 @@ final class GroupListPanel extends javax.swing.JPanel { if (newValue instanceof GroupKey) { String valueString = newValue.toString(); setToolTipText(valueString); - valueString += " (" + groupMap.get(newValue) + ")"; - if (groupingAttribute instanceof FileSearch.ParentPathAttribute) { + if (groupingAttribute instanceof DiscoveryAttributes.ParentPathAttribute) { // Using the list FontRenderContext instead of this because // the label RenderContext was sometimes null, but this should work. FontRenderContext context = ((Graphics2D) list.getGraphics()).getFontRenderContext(); @@ -227,7 +242,6 @@ final class GroupListPanel extends javax.swing.JPanel { int charactersToShow = (int) Math.ceil((valueString.length() - charToRemove) / 2); valueString = valueString.substring(0, charactersToShow) + " ... " + valueString.substring(valueString.length() - charactersToShow); } - } newValue = valueString; } diff --git a/Core/src/org/sleuthkit/autopsy/discovery/HashSetFilterPanel.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/HashSetFilterPanel.form similarity index 95% rename from Core/src/org/sleuthkit/autopsy/discovery/HashSetFilterPanel.form rename to Core/src/org/sleuthkit/autopsy/discovery/ui/HashSetFilterPanel.form index 8f83bc0e77..56c033db74 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/HashSetFilterPanel.form +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/HashSetFilterPanel.form @@ -5,7 +5,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/discovery/HashSetFilterPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/HashSetFilterPanel.java similarity index 92% rename from Core/src/org/sleuthkit/autopsy/discovery/HashSetFilterPanel.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/HashSetFilterPanel.java index eaad0f06f2..4698c43dc3 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/HashSetFilterPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/HashSetFilterPanel.java @@ -16,15 +16,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; +import org.sleuthkit.autopsy.discovery.search.AbstractFilter; import java.util.List; import java.util.logging.Level; import javax.swing.DefaultListModel; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JList; +import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.discovery.search.SearchFiltering; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardAttribute; import org.sleuthkit.datamodel.TskCoreException; @@ -147,10 +150,11 @@ final class HashSetFilterPanel extends AbstractDiscoveryFilterPanel { return null; } + @NbBundle.Messages({"HashSetFilterPanel.error.text=At least one hash set name must be selected."}) @Override String checkForError() { if (hashSetCheckbox.isSelected() && hashSetList.getSelectedValuesList().isEmpty()) { - return "At least one hash set name must be selected"; + return Bundle.HashSetFilterPanel_error_text(); } return ""; } @@ -161,9 +165,9 @@ final class HashSetFilterPanel extends AbstractDiscoveryFilterPanel { } @Override - FileSearchFiltering.FileFilter getFilter() { + AbstractFilter getFilter() { if (hashSetCheckbox.isSelected()) { - return new FileSearchFiltering.HashSetFilter(hashSetList.getSelectedValuesList()); + return new SearchFiltering.HashSetFilter(hashSetList.getSelectedValuesList()); } return null; } diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ImageFilterPanel.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/ImageFilterPanel.form similarity index 92% rename from Core/src/org/sleuthkit/autopsy/discovery/ImageFilterPanel.form rename to Core/src/org/sleuthkit/autopsy/discovery/ui/ImageFilterPanel.form index 7a3374f5cb..0eab977a06 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ImageFilterPanel.form +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/ImageFilterPanel.form @@ -61,13 +61,13 @@ - + - + diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ImageFilterPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/ImageFilterPanel.java similarity index 91% rename from Core/src/org/sleuthkit/autopsy/discovery/ImageFilterPanel.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/ImageFilterPanel.java index dfe8a9de31..308cdd569b 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ImageFilterPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/ImageFilterPanel.java @@ -16,9 +16,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; +import org.sleuthkit.autopsy.discovery.search.SearchData; /** * Panel for displaying all the filters associated with the Image type. @@ -26,7 +27,7 @@ import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; final class ImageFilterPanel extends AbstractFiltersPanel { private static final long serialVersionUID = 1L; - private static final FileSearchData.FileType FILE_TYPE = FileSearchData.FileType.IMAGE; + private static final SearchData.Type TYPE = SearchData.Type.IMAGE; /** * Creates new form ImageFilterPanel. @@ -34,7 +35,7 @@ final class ImageFilterPanel extends AbstractFiltersPanel { ImageFilterPanel() { super(); initComponents(); - SizeFilterPanel sizeFilterPanel = new SizeFilterPanel(FILE_TYPE); + SizeFilterPanel sizeFilterPanel = new SizeFilterPanel(TYPE); int[] sizeIndicesSelected = {3, 4, 5}; addFilter(sizeFilterPanel, true, sizeIndicesSelected, 0); addFilter(new DataSourceFilterPanel(), false, null, 0); @@ -44,7 +45,7 @@ final class ImageFilterPanel extends AbstractFiltersPanel { } else { pastOccurrencesIndices = new int[]{2, 3, 4}; } - addFilter(new PastOccurrencesFilterPanel(), true, pastOccurrencesIndices, 0); + addFilter(new PastOccurrencesFilterPanel(TYPE), true, pastOccurrencesIndices, 0); addFilter(new UserCreatedFilterPanel(), false, null, 1); addFilter(new HashSetFilterPanel(), false, null, 1); addFilter(new InterestingItemsFilterPanel(), false, null, 1); @@ -96,11 +97,12 @@ final class ImageFilterPanel extends AbstractFiltersPanel { }// //GEN-END:initComponents @Override - FileSearchData.FileType getFileType() { - return FILE_TYPE; + SearchData.Type getType() { + return TYPE; } // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JSplitPane imageFiltersSplitPane; // End of variables declaration//GEN-END:variables + } diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ImageThumbnailPanel.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/ImageThumbnailPanel.form similarity index 89% rename from Core/src/org/sleuthkit/autopsy/discovery/ImageThumbnailPanel.form rename to Core/src/org/sleuthkit/autopsy/discovery/ui/ImageThumbnailPanel.form index c3ae30091c..46c5d3601f 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ImageThumbnailPanel.form +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/ImageThumbnailPanel.form @@ -102,16 +102,16 @@ - + - + - + - + @@ -122,13 +122,13 @@ - + - + - + diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ImageThumbnailPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/ImageThumbnailPanel.java similarity index 91% rename from Core/src/org/sleuthkit/autopsy/discovery/ImageThumbnailPanel.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/ImageThumbnailPanel.java index 8f7b90ad08..d294fa866d 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ImageThumbnailPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/ImageThumbnailPanel.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; import java.awt.Color; import java.awt.Component; @@ -76,15 +76,15 @@ final class ImageThumbnailPanel extends javax.swing.JPanel implements ListCellRe isDeletedLabel.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/file-icon-deleted.png"))); // NOI18N isDeletedLabel.setToolTipText(org.openide.util.NbBundle.getMessage(ImageThumbnailPanel.class, "ImageThumbnailPanel.isDeletedLabel.toolTipText")); // NOI18N - isDeletedLabel.setMaximumSize(new Dimension(org.sleuthkit.autopsy.discovery.DiscoveryUiUtils.getIconSize(),org.sleuthkit.autopsy.discovery.DiscoveryUiUtils.getIconSize())); - isDeletedLabel.setMinimumSize(new Dimension(org.sleuthkit.autopsy.discovery.DiscoveryUiUtils.getIconSize(),org.sleuthkit.autopsy.discovery.DiscoveryUiUtils.getIconSize())); - isDeletedLabel.setPreferredSize(new Dimension(org.sleuthkit.autopsy.discovery.DiscoveryUiUtils.getIconSize(),org.sleuthkit.autopsy.discovery.DiscoveryUiUtils.getIconSize())); + isDeletedLabel.setMaximumSize(new Dimension(org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize(),org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize())); + isDeletedLabel.setMinimumSize(new Dimension(org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize(),org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize())); + isDeletedLabel.setPreferredSize(new Dimension(org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize(),org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize())); scoreLabel.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/red-circle-exclamation.png"))); // NOI18N scoreLabel.setToolTipText(""); - scoreLabel.setMaximumSize(new Dimension(org.sleuthkit.autopsy.discovery.DiscoveryUiUtils.getIconSize(),org.sleuthkit.autopsy.discovery.DiscoveryUiUtils.getIconSize())); - scoreLabel.setMinimumSize(new Dimension(org.sleuthkit.autopsy.discovery.DiscoveryUiUtils.getIconSize(),org.sleuthkit.autopsy.discovery.DiscoveryUiUtils.getIconSize())); - scoreLabel.setPreferredSize(new Dimension(org.sleuthkit.autopsy.discovery.DiscoveryUiUtils.getIconSize(),org.sleuthkit.autopsy.discovery.DiscoveryUiUtils.getIconSize())); + scoreLabel.setMaximumSize(new Dimension(org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize(),org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize())); + scoreLabel.setMinimumSize(new Dimension(org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize(),org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize())); + scoreLabel.setPreferredSize(new Dimension(org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize(),org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize())); javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ImageThumbnailViewer.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/ImageThumbnailViewer.form similarity index 100% rename from Core/src/org/sleuthkit/autopsy/discovery/ImageThumbnailViewer.form rename to Core/src/org/sleuthkit/autopsy/discovery/ui/ImageThumbnailViewer.form diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ImageThumbnailViewer.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/ImageThumbnailViewer.java similarity index 98% rename from Core/src/org/sleuthkit/autopsy/discovery/ImageThumbnailViewer.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/ImageThumbnailViewer.java index b1f5ce97e7..273cca8023 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ImageThumbnailViewer.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/ImageThumbnailViewer.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; import java.util.ArrayList; import java.util.List; diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ImageThumbnailWrapper.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/ImageThumbnailWrapper.java similarity index 95% rename from Core/src/org/sleuthkit/autopsy/discovery/ImageThumbnailWrapper.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/ImageThumbnailWrapper.java index b4e7bb0b01..67b7e2dab0 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ImageThumbnailWrapper.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/ImageThumbnailWrapper.java @@ -16,10 +16,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; import java.awt.Image; import org.sleuthkit.autopsy.coreutils.ImageUtils; +import org.sleuthkit.autopsy.discovery.search.ResultFile; /** * Class to wrap all the information necessary for an image thumbnail to be diff --git a/Core/src/org/sleuthkit/autopsy/discovery/InterestingItemsFilterPanel.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/InterestingItemsFilterPanel.form similarity index 95% rename from Core/src/org/sleuthkit/autopsy/discovery/InterestingItemsFilterPanel.form rename to Core/src/org/sleuthkit/autopsy/discovery/ui/InterestingItemsFilterPanel.form index ff3b7cea10..c5a8660cdd 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/InterestingItemsFilterPanel.form +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/InterestingItemsFilterPanel.form @@ -5,7 +5,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/discovery/InterestingItemsFilterPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/InterestingItemsFilterPanel.java similarity index 92% rename from Core/src/org/sleuthkit/autopsy/discovery/InterestingItemsFilterPanel.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/InterestingItemsFilterPanel.java index a073d6b928..b77956937d 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/InterestingItemsFilterPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/InterestingItemsFilterPanel.java @@ -16,15 +16,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; +import org.sleuthkit.autopsy.discovery.search.AbstractFilter; import java.util.List; import java.util.logging.Level; import javax.swing.DefaultListModel; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JList; +import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.discovery.search.SearchFiltering; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardAttribute; import org.sleuthkit.datamodel.TskCoreException; @@ -142,10 +145,11 @@ final class InterestingItemsFilterPanel extends AbstractDiscoveryFilterPanel { return null; } + @NbBundle.Messages({"InterestingItemsFilterPanel.error.text=At least one interesting file set name must be selected."}) @Override String checkForError() { if (interestingItemsCheckbox.isSelected() && interestingItemsList.getSelectedValuesList().isEmpty()) { - return "At least one interesting file set name must be selected"; + return Bundle.InterestingItemsFilterPanel_error_text(); } return ""; } @@ -163,9 +167,9 @@ final class InterestingItemsFilterPanel extends AbstractDiscoveryFilterPanel { } @Override - FileSearchFiltering.FileFilter getFilter() { + AbstractFilter getFilter() { if (interestingItemsCheckbox.isSelected()) { - return new FileSearchFiltering.InterestingFileSetFilter(interestingItemsList.getSelectedValuesList()); + return new SearchFiltering.InterestingFileSetFilter(interestingItemsList.getSelectedValuesList()); } return null; } diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ObjectDetectedFilterPanel.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/ObjectDetectedFilterPanel.form similarity index 100% rename from Core/src/org/sleuthkit/autopsy/discovery/ObjectDetectedFilterPanel.form rename to Core/src/org/sleuthkit/autopsy/discovery/ui/ObjectDetectedFilterPanel.form diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ObjectDetectedFilterPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/ObjectDetectedFilterPanel.java similarity index 92% rename from Core/src/org/sleuthkit/autopsy/discovery/ObjectDetectedFilterPanel.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/ObjectDetectedFilterPanel.java index 335ca8af43..eb1c1525c5 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ObjectDetectedFilterPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/ObjectDetectedFilterPanel.java @@ -16,15 +16,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; +import org.sleuthkit.autopsy.discovery.search.AbstractFilter; import java.util.List; import java.util.logging.Level; import javax.swing.DefaultListModel; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JList; +import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.discovery.search.SearchFiltering; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardAttribute; import org.sleuthkit.datamodel.TskCoreException; @@ -152,11 +155,11 @@ final class ObjectDetectedFilterPanel extends AbstractDiscoveryFilterPanel { JLabel getAdditionalLabel() { return null; } - + @NbBundle.Messages({"ObjectDetectedFilterPanel.error.text=At least one object type name must be selected."}) @Override String checkForError() { if (objectsCheckbox.isSelected() && objectsList.getSelectedValuesList().isEmpty()) { - return "At least one object type name must be selected"; + return Bundle.ObjectDetectedFilterPanel_error_text(); } return ""; } @@ -167,9 +170,9 @@ final class ObjectDetectedFilterPanel extends AbstractDiscoveryFilterPanel { } @Override - FileSearchFiltering.FileFilter getFilter() { + AbstractFilter getFilter() { if (objectsCheckbox.isSelected()) { - return new FileSearchFiltering.ObjectDetectionFilter(objectsList.getSelectedValuesList()); + return new SearchFiltering.ObjectDetectionFilter(objectsList.getSelectedValuesList()); } return null; } diff --git a/Core/src/org/sleuthkit/autopsy/discovery/OpenDiscoveryAction.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/OpenDiscoveryAction.java similarity index 92% rename from Core/src/org/sleuthkit/autopsy/discovery/OpenDiscoveryAction.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/OpenDiscoveryAction.java index fbe3160dcd..33228a6a9b 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/OpenDiscoveryAction.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/OpenDiscoveryAction.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; import java.awt.Component; import javax.swing.ImageIcon; @@ -38,8 +38,7 @@ import org.sleuthkit.autopsy.casemodule.Case; */ @ActionID(category = "Tools", id = "org.sleuthkit.autopsy.newpackage.OpenDiscoveryAction") @ActionReferences(value = { - @ActionReference(path = "Menu/Tools", position = 105) - , + @ActionReference(path = "Menu/Tools", position = 105), @ActionReference(path = "Toolbars/Case", position = 105)}) @ActionRegistration(displayName = "#CTL_OpenDiscoveryAction", lazy = false) @NbBundle.Messages({"CTL_OpenDiscoveryAction=Discovery"}) @@ -62,15 +61,13 @@ public final class OpenDiscoveryAction extends CallableSystemAction implements P return Case.isCaseOpen(); } - @NbBundle.Messages({"OpenDiscoveryAction.resultsIncomplete.text=Discovery results may be incomplete"}) - @Override public void performAction() { SwingUtilities.invokeLater(() -> { final DiscoveryDialog discDialog = DiscoveryDialog.getDiscoveryDialogInstance(); discDialog.cancelSearch(); DiscoveryUiUtils.displayErrorMessage(discDialog); - discDialog.setVisible(true); + discDialog.setVisible(true); }); } diff --git a/Core/src/org/sleuthkit/autopsy/discovery/PageWorker.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/PageWorker.java similarity index 58% rename from Core/src/org/sleuthkit/autopsy/discovery/PageWorker.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/PageWorker.java index 6eda6c6c3a..491e618683 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/PageWorker.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/PageWorker.java @@ -16,16 +16,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; +import org.sleuthkit.autopsy.discovery.search.AbstractFilter; import java.util.List; import java.util.ArrayList; import java.util.logging.Level; import javax.swing.SwingWorker; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.discovery.FileSearch.GroupKey; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; +import org.sleuthkit.autopsy.discovery.search.DiscoveryAttributes; +import org.sleuthkit.autopsy.discovery.search.DiscoveryEventUtils; +import org.sleuthkit.autopsy.discovery.search.DiscoveryKeyUtils.GroupKey; +import org.sleuthkit.autopsy.discovery.search.Group; +import org.sleuthkit.autopsy.discovery.search.FileSearch; +import org.sleuthkit.autopsy.discovery.search.SearchData; +import org.sleuthkit.autopsy.discovery.search.DiscoveryException; +import org.sleuthkit.autopsy.discovery.search.DomainSearch; +import org.sleuthkit.autopsy.discovery.search.ResultsSorter; +import org.sleuthkit.autopsy.discovery.search.Result; /** * SwingWorker to retrieve the contents of a page. @@ -34,16 +44,16 @@ final class PageWorker extends SwingWorker { private final static Logger logger = Logger.getLogger(PageWorker.class.getName()); private static final String USER_NAME_PROPERTY = "user.name"; //NON-NLS - private final List searchfilters; - private final FileSearch.AttributeType groupingAttribute; - private final FileGroup.GroupSortingAlgorithm groupSort; - private final FileSorter.SortingMethod fileSortMethod; + private final List searchfilters; + private final DiscoveryAttributes.AttributeType groupingAttribute; + private final Group.GroupSortingAlgorithm groupSort; + private final ResultsSorter.SortingMethod fileSortMethod; private final GroupKey groupKey; private final int startingEntry; private final int pageSize; - private final FileSearchData.FileType resultType; + private final SearchData.Type resultType; private final CentralRepository centralRepo; - private final List results = new ArrayList<>(); + private final List results = new ArrayList<>(); /** * Construct a new PageWorker. @@ -61,9 +71,9 @@ final class PageWorker extends SwingWorker { * @param resultType The type of files which exist in the group. * @param centralRepo The central repository to be used. */ - PageWorker(List searchfilters, FileSearch.AttributeType groupingAttribute, - FileGroup.GroupSortingAlgorithm groupSort, FileSorter.SortingMethod fileSortMethod, GroupKey groupKey, - int startingEntry, int pageSize, FileSearchData.FileType resultType, CentralRepository centralRepo) { + PageWorker(List searchfilters, DiscoveryAttributes.AttributeType groupingAttribute, + Group.GroupSortingAlgorithm groupSort, ResultsSorter.SortingMethod fileSortMethod, GroupKey groupKey, + int startingEntry, int pageSize, SearchData.Type resultType, CentralRepository centralRepo) { this.searchfilters = searchfilters; this.groupingAttribute = groupingAttribute; this.groupSort = groupSort; @@ -80,12 +90,21 @@ final class PageWorker extends SwingWorker { try { // Run the search - results.addAll(FileSearch.getFilesInGroup(System.getProperty(USER_NAME_PROPERTY), searchfilters, - groupingAttribute, - groupSort, - fileSortMethod, groupKey, startingEntry, pageSize, - Case.getCurrentCase().getSleuthkitCase(), centralRepo)); - } catch (FileSearchException ex) { + if (resultType == SearchData.Type.DOMAIN) { + DomainSearch domainSearch = new DomainSearch(); + results.addAll(domainSearch.getDomainsInGroup(System.getProperty(USER_NAME_PROPERTY), searchfilters, + groupingAttribute, + groupSort, + fileSortMethod, groupKey, startingEntry, pageSize, + Case.getCurrentCase().getSleuthkitCase(), centralRepo)); + } else { + results.addAll(FileSearch.getFilesInGroup(System.getProperty(USER_NAME_PROPERTY), searchfilters, + groupingAttribute, + groupSort, + fileSortMethod, groupKey, startingEntry, pageSize, + Case.getCurrentCase().getSleuthkitCase(), centralRepo)); + } + } catch (DiscoveryException ex) { logger.log(Level.SEVERE, "Error running file search test", ex); cancel(true); } diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ParentFolderFilterPanel.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/ParentFolderFilterPanel.form similarity index 93% rename from Core/src/org/sleuthkit/autopsy/discovery/ParentFolderFilterPanel.form rename to Core/src/org/sleuthkit/autopsy/discovery/ui/ParentFolderFilterPanel.form index b350ab42b3..fe21dc8607 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ParentFolderFilterPanel.form +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/ParentFolderFilterPanel.form @@ -5,7 +5,7 @@ - + @@ -24,7 +24,7 @@ - + @@ -160,7 +160,7 @@ - + @@ -172,7 +172,7 @@ - + @@ -183,7 +183,7 @@ - + @@ -194,7 +194,7 @@ - + @@ -202,7 +202,7 @@ - + @@ -222,7 +222,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ParentFolderFilterPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/ParentFolderFilterPanel.java similarity index 94% rename from Core/src/org/sleuthkit/autopsy/discovery/ParentFolderFilterPanel.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/ParentFolderFilterPanel.java index 7f10b38692..a2a0d9615f 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ParentFolderFilterPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/ParentFolderFilterPanel.java @@ -16,15 +16,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; +import org.sleuthkit.autopsy.discovery.search.AbstractFilter; import java.util.ArrayList; import java.util.List; import javax.swing.DefaultListModel; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JList; -import org.sleuthkit.autopsy.discovery.FileSearchFiltering.ParentSearchTerm; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.discovery.search.SearchFiltering; +import org.sleuthkit.autopsy.discovery.search.SearchFiltering.ParentSearchTerm; /** * Panel to allow configuration of the Parent Folder filter. @@ -32,7 +35,7 @@ import org.sleuthkit.autopsy.discovery.FileSearchFiltering.ParentSearchTerm; final class ParentFolderFilterPanel extends AbstractDiscoveryFilterPanel { private static final long serialVersionUID = 1L; - private DefaultListModel parentListModel; + private DefaultListModel parentListModel; private static final String[] DEFAULT_IGNORED_PATHS = {"/Windows/", "/Program Files/"}; //NON-NLS /** @@ -49,7 +52,7 @@ final class ParentFolderFilterPanel extends AbstractDiscoveryFilterPanel { private void setUpParentPathFilter() { fullRadioButton.setSelected(true); includeRadioButton.setSelected(true); - parentListModel = (DefaultListModel) parentList.getModel(); + parentListModel = (DefaultListModel) parentList.getModel(); for (String ignorePath : DEFAULT_IGNORED_PATHS) { parentListModel.add(parentListModel.size(), new ParentSearchTerm(ignorePath, false, false)); } @@ -277,11 +280,12 @@ final class ParentFolderFilterPanel extends AbstractDiscoveryFilterPanel { return parentLabel; } + @NbBundle.Messages({"ParentFolderFilterPanel.error.text=At least one parent path must be entered."}) @Override String checkForError() { // Parent uses everything in the box if (parentCheckbox.isSelected() && getParentPaths().isEmpty()) { - return "At least one parent path must be entered"; + return Bundle.ParentFolderFilterPanel_error_text(); } return ""; } @@ -291,8 +295,8 @@ final class ParentFolderFilterPanel extends AbstractDiscoveryFilterPanel { * * @return The list of entered ParentSearchTerm objects */ - private List getParentPaths() { - List results = new ArrayList<>(); + private List getParentPaths() { + List results = new ArrayList<>(); for (int i = 0; i < parentListModel.getSize(); i++) { results.add(parentListModel.get(i)); } @@ -305,9 +309,9 @@ final class ParentFolderFilterPanel extends AbstractDiscoveryFilterPanel { } @Override - FileSearchFiltering.FileFilter getFilter() { + AbstractFilter getFilter() { if (parentCheckbox.isSelected()) { - return new FileSearchFiltering.ParentFilter(getParentPaths()); + return new SearchFiltering.ParentFilter(getParentPaths()); } return null; } diff --git a/Core/src/org/sleuthkit/autopsy/discovery/PastOccurrencesFilterPanel.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/PastOccurrencesFilterPanel.form similarity index 95% rename from Core/src/org/sleuthkit/autopsy/discovery/PastOccurrencesFilterPanel.form rename to Core/src/org/sleuthkit/autopsy/discovery/ui/PastOccurrencesFilterPanel.form index 37ea6de900..bfe666aee3 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/PastOccurrencesFilterPanel.form +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/PastOccurrencesFilterPanel.form @@ -5,7 +5,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/discovery/PastOccurrencesFilterPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/PastOccurrencesFilterPanel.java similarity index 77% rename from Core/src/org/sleuthkit/autopsy/discovery/PastOccurrencesFilterPanel.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/PastOccurrencesFilterPanel.java index f80bfbfad5..b19c793556 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/PastOccurrencesFilterPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/PastOccurrencesFilterPanel.java @@ -16,14 +16,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; import javax.swing.DefaultListModel; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JList; +import org.openide.util.NbBundle; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; -import org.sleuthkit.autopsy.discovery.FileSearchData.Frequency; +import org.sleuthkit.autopsy.discovery.search.AbstractFilter; +import org.sleuthkit.autopsy.discovery.search.SearchData; +import org.sleuthkit.autopsy.discovery.search.SearchData.Frequency; +import org.sleuthkit.autopsy.discovery.search.SearchData.Type; +import org.sleuthkit.autopsy.discovery.search.SearchFiltering; /** * Panel to allow configuration of the Past Occurrences filter. @@ -31,12 +36,14 @@ import org.sleuthkit.autopsy.discovery.FileSearchData.Frequency; final class PastOccurrencesFilterPanel extends AbstractDiscoveryFilterPanel { private static final long serialVersionUID = 1L; + private final Type type; /** * Creates new form PastOccurrencesFilterPanel. */ - PastOccurrencesFilterPanel() { + PastOccurrencesFilterPanel(Type type) { initComponents(); + this.type = type; setUpFrequencyFilter(); } @@ -96,15 +103,19 @@ final class PastOccurrencesFilterPanel extends AbstractDiscoveryFilterPanel { */ private void setUpFrequencyFilter() { int count = 0; - DefaultListModel frequencyListModel = (DefaultListModel) crFrequencyList.getModel(); + DefaultListModel frequencyListModel = (DefaultListModel) crFrequencyList.getModel(); frequencyListModel.removeAllElements(); if (!CentralRepository.isEnabled()) { - for (FileSearchData.Frequency freq : FileSearchData.Frequency.getOptionsForFilteringWithoutCr()) { - frequencyListModel.add(count, freq); + if (type != Type.DOMAIN) { + for (SearchData.Frequency freq : SearchData.Frequency.getOptionsForFilteringWithoutCr()) { + frequencyListModel.add(count, freq); + } } } else { - for (FileSearchData.Frequency freq : FileSearchData.Frequency.getOptionsForFilteringWithCr()) { - frequencyListModel.add(count, freq); + for (SearchData.Frequency freq : SearchData.Frequency.getOptionsForFilteringWithCr()) { + if (type != Type.DOMAIN || freq != SearchData.Frequency.KNOWN) { + frequencyListModel.add(count, freq); + } } } } @@ -117,7 +128,10 @@ final class PastOccurrencesFilterPanel extends AbstractDiscoveryFilterPanel { @Override void configurePanel(boolean selected, int[] indicesSelected) { - pastOccurrencesCheckbox.setSelected(selected); + boolean canBeFilteredOn = type != Type.DOMAIN || CentralRepository.isEnabled(); + pastOccurrencesCheckbox.setEnabled(canBeFilteredOn); + pastOccurrencesCheckbox.setSelected(selected && canBeFilteredOn); + if (pastOccurrencesCheckbox.isEnabled() && pastOccurrencesCheckbox.isSelected()) { crFrequencyScrollPane.setEnabled(true); crFrequencyList.setEnabled(true); @@ -140,10 +154,11 @@ final class PastOccurrencesFilterPanel extends AbstractDiscoveryFilterPanel { return null; } + @NbBundle.Messages({"PastOccurrencesFilterPanel.error.text=At least one value in the past occurrence filter must be selected."}) @Override String checkForError() { if (pastOccurrencesCheckbox.isSelected() && crFrequencyList.getSelectedValuesList().isEmpty()) { - return "At least one value in the past occurrence filter must be selected"; + return Bundle.PastOccurrencesFilterPanel_error_text(); } return ""; } @@ -154,9 +169,9 @@ final class PastOccurrencesFilterPanel extends AbstractDiscoveryFilterPanel { } @Override - FileSearchFiltering.FileFilter getFilter() { + AbstractFilter getFilter() { if (pastOccurrencesCheckbox.isSelected()) { - return new FileSearchFiltering.FrequencyFilter(crFrequencyList.getSelectedValuesList()); + return new SearchFiltering.FrequencyFilter(crFrequencyList.getSelectedValuesList()); } return null; } diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ResultsPanel.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/ResultsPanel.form similarity index 97% rename from Core/src/org/sleuthkit/autopsy/discovery/ResultsPanel.form rename to Core/src/org/sleuthkit/autopsy/discovery/ui/ResultsPanel.form index cad2e006cd..d256c2374e 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ResultsPanel.form +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/ResultsPanel.form @@ -79,7 +79,7 @@ - + @@ -127,7 +127,7 @@ - + @@ -153,7 +153,7 @@ - + @@ -199,7 +199,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ResultsPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/ResultsPanel.java similarity index 83% rename from Core/src/org/sleuthkit/autopsy/discovery/ResultsPanel.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/ResultsPanel.java index fc86f7a963..c077f65f22 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ResultsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/ResultsPanel.java @@ -16,8 +16,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; +import org.sleuthkit.autopsy.discovery.search.AbstractFilter; import com.google.common.eventbus.Subscribe; import java.awt.Cursor; import java.awt.Image; @@ -32,13 +33,28 @@ import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import javax.swing.SwingWorker; import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoException; import org.sleuthkit.autopsy.coreutils.ImageUtils; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.discovery.FileSearch.GroupKey; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; +import org.sleuthkit.autopsy.discovery.search.DiscoveryAttributes; +import org.sleuthkit.autopsy.discovery.search.DiscoveryEventUtils; +import org.sleuthkit.autopsy.discovery.search.DiscoveryKeyUtils.GroupKey; +import org.sleuthkit.autopsy.discovery.search.DomainSearch; +import org.sleuthkit.autopsy.discovery.search.DomainSearchThumbnailRequest; +import org.sleuthkit.autopsy.discovery.search.Group; +import org.sleuthkit.autopsy.discovery.search.FileSearch; +import org.sleuthkit.autopsy.discovery.search.SearchData; +import org.sleuthkit.autopsy.discovery.search.ResultsSorter; +import org.sleuthkit.autopsy.discovery.search.Result; +import org.sleuthkit.autopsy.discovery.search.ResultDomain; +import org.sleuthkit.autopsy.discovery.search.ResultFile; +import static org.sleuthkit.autopsy.discovery.search.SearchData.Type.DOMAIN; import org.sleuthkit.autopsy.textsummarizer.TextSummary; +import org.sleuthkit.datamodel.SleuthkitCase; /** * Panel for displaying of Discovery results and handling the paging of those @@ -51,14 +67,15 @@ final class ResultsPanel extends javax.swing.JPanel { private final VideoThumbnailViewer videoThumbnailViewer; private final ImageThumbnailViewer imageThumbnailViewer; private final DocumentPreviewViewer documentPreviewViewer; - private List searchFilters; - private FileSearch.AttributeType groupingAttribute; - private FileGroup.GroupSortingAlgorithm groupSort; - private FileSorter.SortingMethod fileSortMethod; + private final DomainSummaryViewer domainSummaryViewer; + private List searchFilters; + private DiscoveryAttributes.AttributeType groupingAttribute; + private Group.GroupSortingAlgorithm groupSort; + private ResultsSorter.SortingMethod fileSortMethod; private GroupKey selectedGroupKey; private int currentPage = 0; private int previousPageSize = 10; - private FileSearchData.FileType resultType; + private SearchData.Type resultType; private int groupSize = 0; private PageWorker pageWorker; private final List> resultContentWorkers = new ArrayList<>(); @@ -73,8 +90,9 @@ final class ResultsPanel extends javax.swing.JPanel { imageThumbnailViewer = new ImageThumbnailViewer(); videoThumbnailViewer = new VideoThumbnailViewer(); documentPreviewViewer = new DocumentPreviewViewer(); + domainSummaryViewer = new DomainSummaryViewer(); videoThumbnailViewer.addListSelectionListener((e) -> { - if (resultType == FileSearchData.FileType.VIDEO) { + if (resultType == SearchData.Type.VIDEO) { if (!e.getValueIsAdjusting()) { //send populateMesage DiscoveryEventUtils.getDiscoveryEventBus().post(new DiscoveryEventUtils.PopulateInstancesListEvent(getInstancesForSelected())); @@ -85,7 +103,7 @@ final class ResultsPanel extends javax.swing.JPanel { } }); imageThumbnailViewer.addListSelectionListener((e) -> { - if (resultType == FileSearchData.FileType.IMAGE) { + if (resultType == SearchData.Type.IMAGE) { if (!e.getValueIsAdjusting()) { //send populateMesage DiscoveryEventUtils.getDiscoveryEventBus().post(new DiscoveryEventUtils.PopulateInstancesListEvent(getInstancesForSelected())); @@ -97,7 +115,7 @@ final class ResultsPanel extends javax.swing.JPanel { } }); documentPreviewViewer.addListSelectionListener((e) -> { - if (resultType == FileSearchData.FileType.DOCUMENTS) { + if (resultType == SearchData.Type.DOCUMENT) { if (!e.getValueIsAdjusting()) { //send populateMesage DiscoveryEventUtils.getDiscoveryEventBus().post(new DiscoveryEventUtils.PopulateInstancesListEvent(getInstancesForSelected())); @@ -107,6 +125,11 @@ final class ResultsPanel extends javax.swing.JPanel { } } }); + //JIRA-TODO 6307 Add listener for domainSummaryViewer when 6782, 6773, and the other details area related stories are done + } + + SearchData.Type getActiveType() { + return resultType; } /** @@ -123,7 +146,7 @@ final class ResultsPanel extends javax.swing.JPanel { return videoThumbnailViewer.getInstancesForSelected(); case IMAGE: return imageThumbnailViewer.getInstancesForSelected(); - case DOCUMENTS: + case DOCUMENT: return documentPreviewViewer.getInstancesForSelected(); default: break; @@ -153,8 +176,9 @@ final class ResultsPanel extends javax.swing.JPanel { void handlePageRetrievedEvent(DiscoveryEventUtils.PageRetrievedEvent pageRetrievedEvent) { SwingUtilities.invokeLater(() -> { //send populateMesage - DiscoveryEventUtils.getDiscoveryEventBus().post(new DiscoveryEventUtils.PopulateInstancesListEvent(getInstancesForSelected())); - + if (pageRetrievedEvent.getType() != DOMAIN) { + DiscoveryEventUtils.getDiscoveryEventBus().post(new DiscoveryEventUtils.PopulateInstancesListEvent(getInstancesForSelected())); + } currentPage = pageRetrievedEvent.getPageNumber(); updateControls(); resetResultViewer(); @@ -168,10 +192,14 @@ final class ResultsPanel extends javax.swing.JPanel { populateVideoViewer(pageRetrievedEvent.getSearchResults()); resultsViewerPanel.add(videoThumbnailViewer); break; - case DOCUMENTS: + case DOCUMENT: populateDocumentViewer(pageRetrievedEvent.getSearchResults()); resultsViewerPanel.add(documentPreviewViewer); break; + case DOMAIN: + populateDomainViewer(pageRetrievedEvent.getSearchResults()); + resultsViewerPanel.add(domainSummaryViewer); + break; default: break; } @@ -181,6 +209,17 @@ final class ResultsPanel extends javax.swing.JPanel { } ); } + + @Subscribe + void handleCancelBackgroundTasksEvent(DiscoveryEventUtils.CancelBackgroundTasksEvent cancelEvent) { + for (SwingWorker thumbWorker : resultContentWorkers) { + if (!thumbWorker.isDone()) { + thumbWorker.cancel(true); + } + } + + resultContentWorkers.clear(); + } /** * Reset the result viewer and any associate workers to a default empty @@ -190,6 +229,7 @@ final class ResultsPanel extends javax.swing.JPanel { resultsViewerPanel.remove(imageThumbnailViewer); resultsViewerPanel.remove(videoThumbnailViewer); resultsViewerPanel.remove(documentPreviewViewer); + resultsViewerPanel.remove(domainSummaryViewer); //cancel any unfished thumb workers for (SwingWorker thumbWorker : resultContentWorkers) { if (!thumbWorker.isDone()) { @@ -201,17 +241,18 @@ final class ResultsPanel extends javax.swing.JPanel { videoThumbnailViewer.clearViewer(); imageThumbnailViewer.clearViewer(); documentPreviewViewer.clearViewer(); + domainSummaryViewer.clearViewer(); } /** * Populate the video thumbnail viewer, cancelling any thumbnails which are * currently being created first. * - * @param files The list of ResultFiles to populate the video viewer with. + * @param results The list of ResultFiles to populate the video viewer with. */ - synchronized void populateVideoViewer(List files) { - for (ResultFile file : files) { - VideoThumbnailWorker thumbWorker = new VideoThumbnailWorker(file); + synchronized void populateVideoViewer(List results) { + for (Result result : results) { + VideoThumbnailWorker thumbWorker = new VideoThumbnailWorker((ResultFile) result); thumbWorker.execute(); //keep track of thumb worker for possible cancelation resultContentWorkers.add(thumbWorker); @@ -222,11 +263,11 @@ final class ResultsPanel extends javax.swing.JPanel { * Populate the image thumbnail viewer, cancelling any thumbnails which are * currently being created first. * - * @param files The list of ResultFiles to populate the image viewer with. + * @param results The list of ResultFiles to populate the image viewer with. */ - synchronized void populateImageViewer(List files) { - for (ResultFile file : files) { - ImageThumbnailWorker thumbWorker = new ImageThumbnailWorker(file); + synchronized void populateImageViewer(List results) { + for (Result result : results) { + ImageThumbnailWorker thumbWorker = new ImageThumbnailWorker((ResultFile) result); thumbWorker.execute(); //keep track of thumb worker for possible cancelation resultContentWorkers.add(thumbWorker); @@ -237,17 +278,43 @@ final class ResultsPanel extends javax.swing.JPanel { * Populate the document preview viewer, cancelling any content which is * currently being created first. * - * @param files The list of ResultFiles to populate the image viewer with. + * @param results The list of ResultFiles to populate the document viewer + * with. */ - synchronized void populateDocumentViewer(List files) { - for (ResultFile file : files) { - DocumentPreviewWorker documentWorker = new DocumentPreviewWorker(file); + synchronized void populateDocumentViewer(List results) { + for (Result result : results) { + DocumentPreviewWorker documentWorker = new DocumentPreviewWorker((ResultFile) result); documentWorker.execute(); //keep track of thumb worker for possible cancelation resultContentWorkers.add(documentWorker); } } + /** + * Populate the domain summary viewer, cancelling any content which is + * currently being created first. + * + * @param results The list of ResultDomains to populate the domain summary + * viewer with. + */ + synchronized void populateDomainViewer(List results) { + SleuthkitCase currentCase; + try { + currentCase = Case.getCurrentCaseThrows().getSleuthkitCase(); + } catch (NoCurrentCaseException ex) { + // Do nothing, case has been closed. + return; + } + + for (Result result : results) { + DomainThumbnailWorker domainWorker = new DomainThumbnailWorker( + currentCase, (ResultDomain) result); + domainWorker.execute(); + //keep track of thumb worker for possible cancelation + resultContentWorkers.add(domainWorker); + } + } + /** * Subscribe and respond to GroupSelectedEvents. * @@ -259,10 +326,11 @@ final class ResultsPanel extends javax.swing.JPanel { searchFilters = groupSelectedEvent.getFilters(); groupingAttribute = groupSelectedEvent.getGroupingAttr(); groupSort = groupSelectedEvent.getGroupSort(); - fileSortMethod = groupSelectedEvent.getFileSort(); + fileSortMethod = groupSelectedEvent.getResultSort(); selectedGroupKey = groupSelectedEvent.getGroupKey(); resultType = groupSelectedEvent.getResultType(); groupSize = groupSelectedEvent.getGroupSize(); + resetResultViewer(); setPage(0); }); } @@ -282,6 +350,7 @@ final class ResultsPanel extends javax.swing.JPanel { videoThumbnailViewer.clearViewer(); imageThumbnailViewer.clearViewer(); documentPreviewViewer.clearViewer(); + domainSummaryViewer.clearViewer(); resultsViewerPanel.revalidate(); resultsViewerPanel.repaint(); }); @@ -646,7 +715,7 @@ final class ResultsPanel extends javax.swing.JPanel { @Override protected Void doInBackground() throws Exception { - FileSearch.getVideoThumbnails(thumbnailWrapper); + DiscoveryUiUtils.getVideoThumbnails(thumbnailWrapper); return null; } @@ -752,4 +821,54 @@ final class ResultsPanel extends javax.swing.JPanel { } + /** + * Swing worker to handle the retrieval of domain thumbnails and population + * of the Domain Summary Viewer. + */ + private class DomainThumbnailWorker extends SwingWorker { + + private final DomainWrapper domainWrapper; + private final SleuthkitCase caseDb; + + /** + * Construct a new DomainThumbnailWorker. + * + * @param file The ResultFile which represents the domain attribute the + * preview is being retrieved for. + */ + DomainThumbnailWorker(SleuthkitCase caseDb, ResultDomain domain) { + this.caseDb = caseDb; + domainWrapper = new DomainWrapper(domain); + domainSummaryViewer.addDomain(domainWrapper); + } + + @Override + protected Void doInBackground() throws Exception { + DomainSearch domainSearch = new DomainSearch(); + DomainSearchThumbnailRequest request = new DomainSearchThumbnailRequest( + caseDb, + domainWrapper.getResultDomain().getDomain(), + ImageUtils.ICON_SIZE_LARGE + ); + + Image thumbnail = domainSearch.getThumbnail(request); + domainWrapper.setThumbnail(thumbnail); + return null; + } + + @Override + protected void done() { + try { + get(); + } catch (ExecutionException ex) { + domainWrapper.setThumbnail(null); + logger.log(Level.WARNING, "Fatal error getting thumbnail for domain.", ex); + } catch (InterruptedException | CancellationException ignored) { + domainWrapper.setThumbnail(null); + //we want to do nothing in response to this since we allow it to be cancelled + } + domainSummaryViewer.repaint(); + } + + } } diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ResultsSplitPaneDivider.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/ResultsSplitPaneDivider.form similarity index 92% rename from Core/src/org/sleuthkit/autopsy/discovery/ResultsSplitPaneDivider.form rename to Core/src/org/sleuthkit/autopsy/discovery/ui/ResultsSplitPaneDivider.form index 48ae94d8a9..864e5d198c 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ResultsSplitPaneDivider.form +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/ResultsSplitPaneDivider.form @@ -24,7 +24,7 @@ - + @@ -54,7 +54,7 @@ - + @@ -66,10 +66,10 @@ - + - + @@ -91,10 +91,10 @@ - + - + diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ResultsSplitPaneDivider.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/ResultsSplitPaneDivider.java similarity index 95% rename from Core/src/org/sleuthkit/autopsy/discovery/ResultsSplitPaneDivider.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/ResultsSplitPaneDivider.java index 0a89f28711..cffb2dac7f 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ResultsSplitPaneDivider.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/ResultsSplitPaneDivider.java @@ -16,10 +16,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; import java.awt.Cursor; +import org.sleuthkit.autopsy.discovery.search.DiscoveryEventUtils; + /** * Panel for separating the results list from the details area. */ @@ -54,7 +56,7 @@ final class ResultsSplitPaneDivider extends javax.swing.JPanel { org.openide.awt.Mnemonics.setLocalizedText(detailsLabel, org.openide.util.NbBundle.getMessage(ResultsSplitPaneDivider.class, "ResultsSplitPaneDivider.detailsLabel.text")); // NOI18N detailsLabel.setFocusable(false); - hideButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/discovery/arrow-down.png"))); // NOI18N + hideButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/discovery/ui/arrow-down.png"))); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(hideButton, org.openide.util.NbBundle.getMessage(ResultsSplitPaneDivider.class, "ResultsSplitPaneDivider.hideButton.text")); // NOI18N hideButton.setBorder(null); hideButton.setFocusable(false); @@ -66,7 +68,7 @@ final class ResultsSplitPaneDivider extends javax.swing.JPanel { } }); - showButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/discovery/arrow-up.png"))); // NOI18N + showButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/discovery/ui/arrow-up.png"))); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(showButton, org.openide.util.NbBundle.getMessage(ResultsSplitPaneDivider.class, "ResultsSplitPaneDivider.showButton.text")); // NOI18N showButton.setBorder(null); showButton.setFocusable(false); @@ -85,7 +87,7 @@ final class ResultsSplitPaneDivider extends javax.swing.JPanel { .addGroup(layout.createSequentialGroup() .addContainerGap() .addComponent(detailsLabel) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 251, Short.MAX_VALUE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 199, Short.MAX_VALUE) .addComponent(showButton) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(hideButton) diff --git a/Core/src/org/sleuthkit/autopsy/discovery/SearchWorker.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/SearchWorker.java similarity index 56% rename from Core/src/org/sleuthkit/autopsy/discovery/SearchWorker.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/SearchWorker.java index 1a513afda1..7c6863ce62 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/SearchWorker.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/SearchWorker.java @@ -16,8 +16,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; +import org.sleuthkit.autopsy.discovery.search.AbstractFilter; import java.util.LinkedHashMap; import javax.swing.SwingWorker; import java.util.List; @@ -25,8 +26,16 @@ import java.util.Map; import java.util.logging.Level; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.discovery.FileSearch.GroupKey; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; +import org.sleuthkit.autopsy.discovery.search.DiscoveryAttributes; +import org.sleuthkit.autopsy.discovery.search.DiscoveryEventUtils; +import org.sleuthkit.autopsy.discovery.search.DiscoveryKeyUtils.GroupKey; +import org.sleuthkit.autopsy.discovery.search.Group; +import org.sleuthkit.autopsy.discovery.search.FileSearch; +import org.sleuthkit.autopsy.discovery.search.DiscoveryException; +import org.sleuthkit.autopsy.discovery.search.DomainSearch; +import org.sleuthkit.autopsy.discovery.search.ResultsSorter; +import org.sleuthkit.autopsy.discovery.search.SearchData; /** * SwingWorker to perform search on a background thread. @@ -35,11 +44,12 @@ final class SearchWorker extends SwingWorker { private final static Logger logger = Logger.getLogger(SearchWorker.class.getName()); private static final String USER_NAME_PROPERTY = "user.name"; //NON-NLS - private final List filters; - private final FileSearch.AttributeType groupingAttr; - private final FileSorter.SortingMethod fileSort; - private final FileGroup.GroupSortingAlgorithm groupSortAlgorithm; + private final List filters; + private final DiscoveryAttributes.AttributeType groupingAttr; + private final ResultsSorter.SortingMethod fileSort; + private final Group.GroupSortingAlgorithm groupSortAlgorithm; private final CentralRepository centralRepoDb; + private final SearchData.Type searchType; private final Map results = new LinkedHashMap<>(); /** @@ -52,8 +62,9 @@ final class SearchWorker extends SwingWorker { * @param groupSort The Algorithm to sort groups by. * @param fileSortMethod The SortingMethod to use for files. */ - SearchWorker(CentralRepository centralRepo, List searchfilters, FileSearch.AttributeType groupingAttribute, FileGroup.GroupSortingAlgorithm groupSort, FileSorter.SortingMethod fileSortMethod) { + SearchWorker(CentralRepository centralRepo, SearchData.Type type, List searchfilters, DiscoveryAttributes.AttributeType groupingAttribute, Group.GroupSortingAlgorithm groupSort, ResultsSorter.SortingMethod fileSortMethod) { centralRepoDb = centralRepo; + searchType = type; filters = searchfilters; groupingAttr = groupingAttribute; groupSortAlgorithm = groupSort; @@ -64,12 +75,21 @@ final class SearchWorker extends SwingWorker { protected Void doInBackground() throws Exception { try { // Run the search - results.putAll(FileSearch.getGroupSizes(System.getProperty(USER_NAME_PROPERTY), filters, - groupingAttr, - groupSortAlgorithm, - fileSort, - Case.getCurrentCase().getSleuthkitCase(), centralRepoDb)); - } catch (FileSearchException ex) { + if (searchType == SearchData.Type.DOMAIN) { + DomainSearch domainSearch = new DomainSearch(); + results.putAll(domainSearch.getGroupSizes(System.getProperty(USER_NAME_PROPERTY), filters, + groupingAttr, + groupSortAlgorithm, + fileSort, + Case.getCurrentCase().getSleuthkitCase(), centralRepoDb)); + } else { + results.putAll(FileSearch.getGroupSizes(System.getProperty(USER_NAME_PROPERTY), filters, + groupingAttr, + groupSortAlgorithm, + fileSort, + Case.getCurrentCase().getSleuthkitCase(), centralRepoDb)); + } + } catch (DiscoveryException ex) { logger.log(Level.SEVERE, "Error running file search test", ex); cancel(true); } diff --git a/Core/src/org/sleuthkit/autopsy/discovery/SizeFilterPanel.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/SizeFilterPanel.form similarity index 96% rename from Core/src/org/sleuthkit/autopsy/discovery/SizeFilterPanel.form rename to Core/src/org/sleuthkit/autopsy/discovery/ui/SizeFilterPanel.form index 1b77a4329e..fd63ea077d 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/SizeFilterPanel.form +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/SizeFilterPanel.form @@ -5,7 +5,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/discovery/SizeFilterPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/SizeFilterPanel.java similarity index 88% rename from Core/src/org/sleuthkit/autopsy/discovery/SizeFilterPanel.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/SizeFilterPanel.java index 1e78b9a079..0585750aa4 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/SizeFilterPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/SizeFilterPanel.java @@ -16,15 +16,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; +import org.sleuthkit.autopsy.discovery.search.AbstractFilter; import java.util.ArrayList; import java.util.List; import javax.swing.DefaultListModel; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JList; -import org.sleuthkit.autopsy.discovery.FileSearchData.FileSize; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.discovery.search.SearchData; +import org.sleuthkit.autopsy.discovery.search.SearchData.FileSize; +import org.sleuthkit.autopsy.discovery.search.SearchFiltering; /** * Panel to allow configuration of the Size Filter. @@ -38,7 +42,7 @@ final class SizeFilterPanel extends AbstractDiscoveryFilterPanel { * * @param type The type of result being searched for. */ - SizeFilterPanel(FileSearchData.FileType type) { + SizeFilterPanel(SearchData.Type type) { initComponents(); setUpSizeFilter(type); } @@ -133,7 +137,7 @@ final class SizeFilterPanel extends AbstractDiscoveryFilterPanel { /** * Initialize the file size filter. */ - private void setUpSizeFilter(FileSearchData.FileType fileType) { + private void setUpSizeFilter(SearchData.Type fileType) { int count = 0; DefaultListModel sizeListModel = (DefaultListModel) sizeList.getModel(); sizeListModel.removeAllElements(); @@ -142,7 +146,7 @@ final class SizeFilterPanel extends AbstractDiscoveryFilterPanel { sizeListModel.add(count, size); } } else { - List sizes; + List sizes; switch (fileType) { case VIDEO: sizes = FileSize.getOptionsForVideos(); @@ -150,7 +154,7 @@ final class SizeFilterPanel extends AbstractDiscoveryFilterPanel { case IMAGE: sizes = FileSize.getDefaultSizeOptions(); break; - case DOCUMENTS: + case DOCUMENT: sizes = FileSize.getDefaultSizeOptions(); break; default: @@ -163,10 +167,11 @@ final class SizeFilterPanel extends AbstractDiscoveryFilterPanel { } } + @NbBundle.Messages({"SizeFilterPanel.error.text=At least one size must be selected."}) @Override String checkForError() { if (sizeCheckbox.isSelected() && sizeList.getSelectedValuesList().isEmpty()) { - return "At least one size must be selected"; + return Bundle.SizeFilterPanel_error_text(); } return ""; @@ -178,9 +183,9 @@ final class SizeFilterPanel extends AbstractDiscoveryFilterPanel { } @Override - FileSearchFiltering.FileFilter getFilter() { + AbstractFilter getFilter() { if (sizeCheckbox.isSelected()) { - return new FileSearchFiltering.SizeFilter(sizeList.getSelectedValuesList()); + return new SearchFiltering.SizeFilter(sizeList.getSelectedValuesList()); } return null; } diff --git a/Core/src/org/sleuthkit/autopsy/discovery/SwingAnimator.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/SwingAnimator.java similarity index 98% rename from Core/src/org/sleuthkit/autopsy/discovery/SwingAnimator.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/SwingAnimator.java index 2dac8559bb..cb32183eb2 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/SwingAnimator.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/SwingAnimator.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; diff --git a/Core/src/org/sleuthkit/autopsy/discovery/SwingAnimatorCallback.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/SwingAnimatorCallback.java similarity index 96% rename from Core/src/org/sleuthkit/autopsy/discovery/SwingAnimatorCallback.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/SwingAnimatorCallback.java index 86f7d2f7fb..2393f7957b 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/SwingAnimatorCallback.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/SwingAnimatorCallback.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; /** * diff --git a/Core/src/org/sleuthkit/autopsy/discovery/UserCreatedFilterPanel.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/UserCreatedFilterPanel.form similarity index 93% rename from Core/src/org/sleuthkit/autopsy/discovery/UserCreatedFilterPanel.form rename to Core/src/org/sleuthkit/autopsy/discovery/ui/UserCreatedFilterPanel.form index 520bf47dc3..a6fc200308 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/UserCreatedFilterPanel.form +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/UserCreatedFilterPanel.form @@ -5,7 +5,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/discovery/UserCreatedFilterPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/UserCreatedFilterPanel.java similarity index 92% rename from Core/src/org/sleuthkit/autopsy/discovery/UserCreatedFilterPanel.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/UserCreatedFilterPanel.java index 6c0ca9d67a..d65596633a 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/UserCreatedFilterPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/UserCreatedFilterPanel.java @@ -16,11 +16,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; +import org.sleuthkit.autopsy.discovery.search.AbstractFilter; import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JList; +import org.sleuthkit.autopsy.discovery.search.SearchFiltering; /** * Panel to allow configuration of the User Created Filter. @@ -98,9 +100,9 @@ final class UserCreatedFilterPanel extends AbstractDiscoveryFilterPanel { } @Override - FileSearchFiltering.FileFilter getFilter() { + AbstractFilter getFilter() { if (userCreatedCheckbox.isSelected()) { - return new FileSearchFiltering.UserCreatedFilter(); + return new SearchFiltering.UserCreatedFilter(); } return null; } diff --git a/Core/src/org/sleuthkit/autopsy/discovery/VideoFilterPanel.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/VideoFilterPanel.form similarity index 95% rename from Core/src/org/sleuthkit/autopsy/discovery/VideoFilterPanel.form rename to Core/src/org/sleuthkit/autopsy/discovery/ui/VideoFilterPanel.form index 1a853b425a..914d724f3d 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/VideoFilterPanel.form +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/VideoFilterPanel.form @@ -71,7 +71,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/discovery/VideoFilterPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/VideoFilterPanel.java similarity index 91% rename from Core/src/org/sleuthkit/autopsy/discovery/VideoFilterPanel.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/VideoFilterPanel.java index 97cd8e818d..82b0d030bc 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/VideoFilterPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/VideoFilterPanel.java @@ -16,9 +16,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; +import org.sleuthkit.autopsy.discovery.search.SearchData; /** * Panel for displaying all filters available for the searches of type Video. @@ -26,7 +27,7 @@ import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; final class VideoFilterPanel extends AbstractFiltersPanel { private static final long serialVersionUID = 1L; - private static final FileSearchData.FileType FILE_TYPE = FileSearchData.FileType.VIDEO; + private static final SearchData.Type TYPE = SearchData.Type.VIDEO; /** * Creates new form VideoFilterPanel. @@ -34,7 +35,7 @@ final class VideoFilterPanel extends AbstractFiltersPanel { VideoFilterPanel() { super(); initComponents(); - SizeFilterPanel sizeFilterPanel = new SizeFilterPanel(FILE_TYPE); + SizeFilterPanel sizeFilterPanel = new SizeFilterPanel(TYPE); int[] sizeIndicesSelected = {3, 4, 5}; addFilter(sizeFilterPanel, true, sizeIndicesSelected, 0); addFilter(new DataSourceFilterPanel(), false, null, 0); @@ -44,7 +45,7 @@ final class VideoFilterPanel extends AbstractFiltersPanel { } else { pastOccurrencesIndices = new int[]{2, 3, 4}; } - addFilter(new PastOccurrencesFilterPanel(), true, pastOccurrencesIndices, 0); + addFilter(new PastOccurrencesFilterPanel(TYPE), true, pastOccurrencesIndices, 0); addFilter(new UserCreatedFilterPanel(), false, null, 1); addFilter(new HashSetFilterPanel(), false, null, 1); addFilter(new InterestingItemsFilterPanel(), false, null, 1); @@ -97,8 +98,8 @@ final class VideoFilterPanel extends AbstractFiltersPanel { add(videoFiltersScrollPane, java.awt.BorderLayout.CENTER); }// //GEN-END:initComponents @Override - FileSearchData.FileType getFileType() { - return FILE_TYPE; + SearchData.Type getType() { + return TYPE; } // Variables declaration - do not modify//GEN-BEGIN:variables diff --git a/Core/src/org/sleuthkit/autopsy/discovery/VideoThumbnailPanel.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/VideoThumbnailPanel.form similarity index 88% rename from Core/src/org/sleuthkit/autopsy/discovery/VideoThumbnailPanel.form rename to Core/src/org/sleuthkit/autopsy/discovery/ui/VideoThumbnailPanel.form index 522acb16c8..759e61c6d2 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/VideoThumbnailPanel.form +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/VideoThumbnailPanel.form @@ -78,13 +78,13 @@ - + - + - + @@ -94,13 +94,13 @@ - + - + - + diff --git a/Core/src/org/sleuthkit/autopsy/discovery/VideoThumbnailPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/VideoThumbnailPanel.java similarity index 94% rename from Core/src/org/sleuthkit/autopsy/discovery/VideoThumbnailPanel.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/VideoThumbnailPanel.java index d8845f565a..7a62937582 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/VideoThumbnailPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/VideoThumbnailPanel.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; import java.awt.Color; import java.awt.Component; @@ -113,14 +113,14 @@ final class VideoThumbnailPanel extends javax.swing.JPanel implements ListCellRe imagePanel.setLayout(new java.awt.GridBagLayout()); scoreLabel.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/red-circle-exclamation.png"))); // NOI18N - scoreLabel.setMaximumSize(new Dimension(org.sleuthkit.autopsy.discovery.DiscoveryUiUtils.getIconSize(),org.sleuthkit.autopsy.discovery.DiscoveryUiUtils.getIconSize())); - scoreLabel.setMinimumSize(new Dimension(org.sleuthkit.autopsy.discovery.DiscoveryUiUtils.getIconSize(),org.sleuthkit.autopsy.discovery.DiscoveryUiUtils.getIconSize())); - scoreLabel.setPreferredSize(new Dimension(org.sleuthkit.autopsy.discovery.DiscoveryUiUtils.getIconSize(),org.sleuthkit.autopsy.discovery.DiscoveryUiUtils.getIconSize())); + scoreLabel.setMaximumSize(new Dimension(org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize(),org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize())); + scoreLabel.setMinimumSize(new Dimension(org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize(),org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize())); + scoreLabel.setPreferredSize(new Dimension(org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize(),org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize())); deletedLabel.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/file-icon-deleted.png"))); // NOI18N - deletedLabel.setMaximumSize(new Dimension(org.sleuthkit.autopsy.discovery.DiscoveryUiUtils.getIconSize(),org.sleuthkit.autopsy.discovery.DiscoveryUiUtils.getIconSize())); - deletedLabel.setMinimumSize(new Dimension(org.sleuthkit.autopsy.discovery.DiscoveryUiUtils.getIconSize(),org.sleuthkit.autopsy.discovery.DiscoveryUiUtils.getIconSize())); - deletedLabel.setPreferredSize(new Dimension(org.sleuthkit.autopsy.discovery.DiscoveryUiUtils.getIconSize(),org.sleuthkit.autopsy.discovery.DiscoveryUiUtils.getIconSize())); + deletedLabel.setMaximumSize(new Dimension(org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize(),org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize())); + deletedLabel.setMinimumSize(new Dimension(org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize(),org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize())); + deletedLabel.setPreferredSize(new Dimension(org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize(),org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize())); javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); diff --git a/Core/src/org/sleuthkit/autopsy/discovery/VideoThumbnailViewer.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/VideoThumbnailViewer.form similarity index 97% rename from Core/src/org/sleuthkit/autopsy/discovery/VideoThumbnailViewer.form rename to Core/src/org/sleuthkit/autopsy/discovery/ui/VideoThumbnailViewer.form index aa8875a52d..54c180973c 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/VideoThumbnailViewer.form +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/VideoThumbnailViewer.form @@ -35,11 +35,11 @@ - + - +
diff --git a/Core/src/org/sleuthkit/autopsy/discovery/VideoThumbnailViewer.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/VideoThumbnailViewer.java similarity index 95% rename from Core/src/org/sleuthkit/autopsy/discovery/VideoThumbnailViewer.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/VideoThumbnailViewer.java index bb3c5a30ea..8824e6f5d8 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/VideoThumbnailViewer.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/VideoThumbnailViewer.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; import java.util.ArrayList; import java.util.List; @@ -104,7 +104,7 @@ final class VideoThumbnailViewer extends javax.swing.JPanel { thumbnailList.setModel(thumbnailListModel); thumbnailList.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); - thumbnailList.setCellRenderer(new org.sleuthkit.autopsy.discovery.VideoThumbnailPanel()); + thumbnailList.setCellRenderer(new org.sleuthkit.autopsy.discovery.ui.VideoThumbnailPanel()); thumbnailListScrollPane.setViewportView(thumbnailList); add(thumbnailListScrollPane, java.awt.BorderLayout.CENTER); @@ -112,7 +112,7 @@ final class VideoThumbnailViewer extends javax.swing.JPanel { // Variables declaration - do not modify//GEN-BEGIN:variables - private javax.swing.JList thumbnailList; + private javax.swing.JList thumbnailList; private javax.swing.JScrollPane thumbnailListScrollPane; // End of variables declaration//GEN-END:variables diff --git a/Core/src/org/sleuthkit/autopsy/discovery/VideoThumbnailsWrapper.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/VideoThumbnailsWrapper.java similarity index 96% rename from Core/src/org/sleuthkit/autopsy/discovery/VideoThumbnailsWrapper.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/VideoThumbnailsWrapper.java index 52c188ac80..6b312bab36 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/VideoThumbnailsWrapper.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/VideoThumbnailsWrapper.java @@ -16,12 +16,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.discovery; +package org.sleuthkit.autopsy.discovery.ui; import java.awt.Image; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import org.sleuthkit.autopsy.discovery.search.ResultFile; /** * Class to wrap all the information necessary for video thumbnails to be diff --git a/Core/src/org/sleuthkit/autopsy/discovery/arrow-down.png b/Core/src/org/sleuthkit/autopsy/discovery/ui/arrow-down.png similarity index 100% rename from Core/src/org/sleuthkit/autopsy/discovery/arrow-down.png rename to Core/src/org/sleuthkit/autopsy/discovery/ui/arrow-down.png diff --git a/Core/src/org/sleuthkit/autopsy/discovery/arrow-up.png b/Core/src/org/sleuthkit/autopsy/discovery/ui/arrow-up.png similarity index 100% rename from Core/src/org/sleuthkit/autopsy/discovery/arrow-up.png rename to Core/src/org/sleuthkit/autopsy/discovery/ui/arrow-up.png diff --git a/Core/src/org/sleuthkit/autopsy/guiutils/RefreshThrottler.java b/Core/src/org/sleuthkit/autopsy/guiutils/RefreshThrottler.java index 2610642761..79893039f7 100755 --- a/Core/src/org/sleuthkit/autopsy/guiutils/RefreshThrottler.java +++ b/Core/src/org/sleuthkit/autopsy/guiutils/RefreshThrottler.java @@ -30,8 +30,8 @@ import org.sleuthkit.autopsy.ingest.IngestManager; /** * Utility class that can be used by UI nodes to reduce the number of - * potentially expensive UI refresh events when DATA_ADDED and CONTENT_CHANGED - * ingest manager events are received. + * potentially expensive UI refresh events when DATA_ADDED, CONTENT_CHANGED, and + * FILE_DONE ingest manager events are received. */ public class RefreshThrottler { @@ -66,7 +66,10 @@ public class RefreshThrottler { private static final long MIN_SECONDS_BETWEEN_REFRESH = 5; - private static final Set INGEST_MODULE_EVENTS_OF_INTEREST = EnumSet.of(IngestManager.IngestModuleEvent.DATA_ADDED, IngestManager.IngestModuleEvent.CONTENT_CHANGED); + private static final Set INGEST_MODULE_EVENTS_OF_INTEREST = EnumSet.of( + IngestManager.IngestModuleEvent.DATA_ADDED, + IngestManager.IngestModuleEvent.CONTENT_CHANGED, + IngestManager.IngestModuleEvent.FILE_DONE); /** * A RefreshTask is scheduled to run when an event arrives and there isn't @@ -96,7 +99,8 @@ public class RefreshThrottler { pcl = (PropertyChangeEvent evt) -> { String eventType = evt.getPropertyName(); if (eventType.equals(IngestManager.IngestModuleEvent.DATA_ADDED.toString()) - || eventType.equals(IngestManager.IngestModuleEvent.CONTENT_CHANGED.toString())) { + || eventType.equals(IngestManager.IngestModuleEvent.CONTENT_CHANGED.toString()) + || eventType.equals(IngestManager.IngestModuleEvent.FILE_DONE.toString())) { if (!refresher.isRefreshRequired(evt)) { return; } diff --git a/Core/src/org/sleuthkit/autopsy/guiutils/SimpleListCellRenderer.java b/Core/src/org/sleuthkit/autopsy/guiutils/SimpleListCellRenderer.java new file mode 100755 index 0000000000..589ce35a34 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/guiutils/SimpleListCellRenderer.java @@ -0,0 +1,42 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.guiutils; + +import java.awt.Component; +import javax.swing.DefaultListCellRenderer; +import javax.swing.JLabel; +import javax.swing.JList; + +/** + * Simple extension of DefaultListCellRenderer that adds support for tooltips. + * The the tooltip text will be the same as the label text. + */ +public class SimpleListCellRenderer extends DefaultListCellRenderer{ + + private static final long serialVersionUID = 1L; + + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { + JLabel label = (JLabel)super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + label.setToolTipText(label.getText()); + + return label; + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/guiutils/SimpleTableCellRenderer.java b/Core/src/org/sleuthkit/autopsy/guiutils/SimpleTableCellRenderer.java new file mode 100755 index 0000000000..bbc5d995f5 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/guiutils/SimpleTableCellRenderer.java @@ -0,0 +1,41 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.guiutils; + +import java.awt.Component; +import javax.swing.JLabel; +import javax.swing.JTable; +import javax.swing.table.DefaultTableCellRenderer; + +/** + * Simple Cell renderer for JTables that will set the value of the labels tooltip + * to be the same as the label. + */ +public class SimpleTableCellRenderer extends DefaultTableCellRenderer{ + + private static final long serialVersionUID = 1L; + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { + JLabel label = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + label.setToolTipText(label.getText()); + return label; + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/images/cache.png b/Core/src/org/sleuthkit/autopsy/images/cache.png new file mode 100644 index 0000000000..fe645e5d4a Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/images/cache.png differ diff --git a/Core/src/org/sleuthkit/autopsy/images/clipboard.png b/Core/src/org/sleuthkit/autopsy/images/clipboard.png new file mode 100644 index 0000000000..96617fa957 Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/images/clipboard.png differ diff --git a/Core/src/org/sleuthkit/autopsy/images/domain-32.png b/Core/src/org/sleuthkit/autopsy/images/domain-32.png new file mode 100644 index 0000000000..ba62d997b9 Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/images/domain-32.png differ diff --git a/Core/src/org/sleuthkit/autopsy/images/metadata.png b/Core/src/org/sleuthkit/autopsy/images/metadata.png new file mode 100644 index 0000000000..181a9b18d6 Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/images/metadata.png differ diff --git a/Core/src/org/sleuthkit/autopsy/images/objects.png b/Core/src/org/sleuthkit/autopsy/images/objects.png new file mode 100644 index 0000000000..08318a751a Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/images/objects.png differ diff --git a/Core/src/org/sleuthkit/autopsy/images/os-account.png b/Core/src/org/sleuthkit/autopsy/images/os-account.png new file mode 100644 index 0000000000..7cb45bf426 Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/images/os-account.png differ diff --git a/Core/src/org/sleuthkit/autopsy/images/user-content.png b/Core/src/org/sleuthkit/autopsy/images/user-content.png new file mode 100644 index 0000000000..680e2ad7dc Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/images/user-content.png differ diff --git a/Core/src/org/sleuthkit/autopsy/images/web-account-type.png b/Core/src/org/sleuthkit/autopsy/images/web-account-type.png new file mode 100644 index 0000000000..cf12fef643 Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/images/web-account-type.png differ diff --git a/Core/src/org/sleuthkit/autopsy/images/web-form.png b/Core/src/org/sleuthkit/autopsy/images/web-form.png new file mode 100644 index 0000000000..ada14fbcec Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/images/web-form.png differ diff --git a/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/Bundle.properties b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/Bundle.properties new file mode 100644 index 0000000000..0036d4dd6f --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/Bundle.properties @@ -0,0 +1,3 @@ +ILeappAnalyzerIngestModule.init.exception.msg=Unable to find {0}. +ILeappAnalyzerIngestModule.processing.file=Processing file {0} +ILeappAnalyzerIngestModule.parsing.file=Parsing file {0} \ No newline at end of file diff --git a/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/Bundle.properties-MERGED new file mode 100644 index 0000000000..b4f350f478 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/Bundle.properties-MERGED @@ -0,0 +1,29 @@ +ILeappAnalyzerIngestModule.completed=iLeapp Processing Completed +ILeappAnalyzerIngestModule.error.creating.output.dir=Error creating iLeapp module output directory. +ILeappAnalyzerIngestModule.error.ileapp.file.processor.init=Failure to initialize ILeappProcessFile +ILeappAnalyzerIngestModule.error.running.iLeapp=Error running iLeapp, see log file. +ILeappAnalyzerIngestModule.executable.not.found=iLeapp Executable Not Found. +ILeappAnalyzerIngestModule.has.run=iLeapp +ILeappAnalyzerIngestModule.iLeapp.cancelled=iLeapp run was canceled +ILeappAnalyzerIngestModule.init.exception.msg=Unable to find {0}. +ILeappAnalyzerIngestModule.processing.file=Processing file {0} +ILeappAnalyzerIngestModule.parsing.file=Parsing file {0} +ILeappAnalyzerIngestModule.report.name=iLeapp Html Report +ILeappAnalyzerIngestModule.requires.windows=iLeapp module requires windows. +ILeappAnalyzerIngestModule.running.iLeapp=Running iLeapp +ILeappAnalyzerIngestModule.starting.iLeapp=Starting iLeapp +ILeappAnalyzerModuleFactory_moduleDesc=Uses iLEAPP to analyze logical acquisitions of iOS devices. +ILeappAnalyzerModuleFactory_moduleName=iOS Analyzer (iLEAPP) +ILeappFileProcessor.cannot.load.artifact.xml=Cannor load xml artifact file. +ILeappFileProcessor.cannotBuildXmlParser=Cannot buld an XML parser. +ILeappFileProcessor.completed=iLeapp Processing Completed +ILeappFileProcessor.error.creating.new.artifacts=Error creating new artifacts. +ILeappFileProcessor.error.creating.output.dir=Error creating iLeapp module output directory. +ILeappFileProcessor.error.reading.iLeapp.directory=Error reading iLeapp Output Directory +ILeappFileProcessor.error.running.iLeapp=Error running iLeapp, see log file. +ILeappFileProcessor.has.run=iLeapp +ILeappFileProcessor.iLeapp.cancelled=iLeapp run was canceled +ILeappFileProcessor.postartifacts_error=Error posting Blackboard Artifact +ILeappFileProcessor.running.iLeapp=Running iLeapp +ILeappFileProcessor.starting.iLeapp=Starting iLeapp +ILeappFileProcessor_cannotParseXml=Cannot Parse XML file. diff --git a/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ILeappAnalyzerIngestModule.java b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ILeappAnalyzerIngestModule.java new file mode 100644 index 0000000000..80baf4a25d --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ILeappAnalyzerIngestModule.java @@ -0,0 +1,267 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.modules.ileappanalyzer; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.SimpleDateFormat; +import java.util.List; +import java.util.ArrayList; +import java.util.Locale; +import java.util.logging.Level; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.openide.modules.InstalledFileLocator; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.casemodule.Case; +import static org.sleuthkit.autopsy.casemodule.Case.getCurrentCase; +import org.sleuthkit.autopsy.casemodule.services.FileManager; +import org.sleuthkit.autopsy.coreutils.ExecUtil; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.PlatformUtil; +import org.sleuthkit.autopsy.ingest.DataSourceIngestModule; +import org.sleuthkit.autopsy.ingest.DataSourceIngestModuleProcessTerminator; +import org.sleuthkit.autopsy.ingest.DataSourceIngestModuleProgress; +import org.sleuthkit.autopsy.ingest.IngestJobContext; +import org.sleuthkit.autopsy.ingest.IngestMessage; +import org.sleuthkit.autopsy.ingest.IngestServices; +import org.sleuthkit.autopsy.ingest.IngestModule.IngestModuleException; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.LocalFilesDataSource; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Data source ingest module that runs iLeapp against logical iOS files. + */ +public class ILeappAnalyzerIngestModule implements DataSourceIngestModule { + + private static final Logger logger = Logger.getLogger(ILeappAnalyzerIngestModule.class.getName()); + private static final String MODULE_NAME = ILeappAnalyzerModuleFactory.getModuleName(); + + private static final String ILEAPP = "iLeapp"; //NON-NLS + private static final String ILEAPP_EXECUTABLE = "ileapp.exe";//NON-NLS + + private File iLeappExecutable; + + private IngestJobContext context; + + private ILeappFileProcessor iLeappFileProcessor; + + ILeappAnalyzerIngestModule() { + // This constructor is intentionally empty. Nothing special is needed here. + } + + @NbBundle.Messages({ + "ILeappAnalyzerIngestModule.executable.not.found=iLeapp Executable Not Found.", + "ILeappAnalyzerIngestModule.requires.windows=iLeapp module requires windows.", + "ILeappAnalyzerIngestModule.error.ileapp.file.processor.init=Failure to initialize ILeappProcessFile"}) + @Override + public void startUp(IngestJobContext context) throws IngestModuleException { + this.context = context; + + if (false == PlatformUtil.isWindowsOS()) { + throw new IngestModuleException(Bundle.ILeappAnalyzerIngestModule_requires_windows()); + } + + try { + iLeappFileProcessor = new ILeappFileProcessor(); + } catch (IOException | IngestModuleException ex) { + throw new IngestModuleException(Bundle.ILeappAnalyzerIngestModule_error_ileapp_file_processor_init(), ex); + } + + try { + iLeappExecutable = locateExecutable(ILEAPP_EXECUTABLE); + } catch (FileNotFoundException exception) { + logger.log(Level.WARNING, "iLeapp executable not found.", exception); //NON-NLS + throw new IngestModuleException(Bundle.ILeappAnalyzerIngestModule_executable_not_found(), exception); + } + + } + + @NbBundle.Messages({ + "ILeappAnalyzerIngestModule.error.running.iLeapp=Error running iLeapp, see log file.", + "ILeappAnalyzerIngestModule.error.creating.output.dir=Error creating iLeapp module output directory.", + "ILeappAnalyzerIngestModule.starting.iLeapp=Starting iLeapp", + "ILeappAnalyzerIngestModule.running.iLeapp=Running iLeapp", + "ILeappAnalyzerIngestModule.has.run=iLeapp", + "ILeappAnalyzerIngestModule.iLeapp.cancelled=iLeapp run was canceled", + "ILeappAnalyzerIngestModule.completed=iLeapp Processing Completed", + "ILeappAnalyzerIngestModule.report.name=iLeapp Html Report"}) + @Override + public ProcessResult process(Content dataSource, DataSourceIngestModuleProgress statusHelper) { + + if (!(context.getDataSource() instanceof LocalFilesDataSource)) { + return ProcessResult.OK; + } + + statusHelper.progress(Bundle.ILeappAnalyzerIngestModule_starting_iLeapp(), 0); + + List iLeappFilesToProcess = findiLeappFilesToProcess(dataSource); + + statusHelper.switchToDeterminate(iLeappFilesToProcess.size()); + + Integer filesProcessedCount = 0; + + Case currentCase = Case.getCurrentCase(); + for (AbstractFile iLeappFile : iLeappFilesToProcess) { + + String currentTime = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss z", Locale.US).format(System.currentTimeMillis());//NON-NLS + Path moduleOutputPath = Paths.get(currentCase.getModuleDirectory(), ILEAPP, currentTime); + try { + Files.createDirectories(moduleOutputPath); + } catch (IOException ex) { + logger.log(Level.SEVERE, String.format("Error creating iLeapp output directory %s", moduleOutputPath.toString()), ex); + return ProcessResult.ERROR; + } + + statusHelper.progress(NbBundle.getMessage(this.getClass(), "ILeappAnalyzerIngestModule.processing.file", iLeappFile.getName()), filesProcessedCount); + ProcessBuilder iLeappCommand = buildiLeappCommand(moduleOutputPath, iLeappFile.getLocalAbsPath(), iLeappFile.getNameExtension()); + try { + int result = ExecUtil.execute(iLeappCommand, new DataSourceIngestModuleProcessTerminator(context, true)); + if (result != 0) { + logger.log(Level.SEVERE, String.format("Error running iLeapp, error code returned %d", result)); //NON-NLS + return ProcessResult.ERROR; + } + + addILeappReportToReports(moduleOutputPath, currentCase); + + } catch (IOException ex) { + logger.log(Level.SEVERE, String.format("Error when trying to execute iLeapp program against file %s", iLeappFile.getLocalAbsPath()), ex); + return ProcessResult.ERROR; + } + + if (context.dataSourceIngestIsCancelled()) { + logger.log(Level.INFO, "ILeapp Analyser ingest module run was canceled"); //NON-NLS + return ProcessResult.OK; + } + + ProcessResult fileProcessorResult = iLeappFileProcessor.processFiles(dataSource, moduleOutputPath, iLeappFile); + + if (fileProcessorResult == ProcessResult.ERROR) { + return ProcessResult.ERROR; + } + + filesProcessedCount++; + } + + IngestMessage message = IngestMessage.createMessage(IngestMessage.MessageType.DATA, + Bundle.ILeappAnalyzerIngestModule_has_run(), + Bundle.ILeappAnalyzerIngestModule_completed()); + IngestServices.getInstance().postMessage(message); + return ProcessResult.OK; + } + + /** + * Find the files that will be processed by the iLeapp program + * + * @param dataSource + * + * @return List of abstract files to process. + */ + private List findiLeappFilesToProcess(Content dataSource) { + + List iLeappFiles = new ArrayList<>(); + + FileManager fileManager = getCurrentCase().getServices().getFileManager(); + + // findFiles use the SQL wildcard % in the file name + try { + iLeappFiles = fileManager.findFiles(dataSource, "%", "/"); //NON-NLS + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "No files found to process"); //NON-NLS + return iLeappFiles; + } + + List iLeappFilesToProcess = new ArrayList<>(); + for (AbstractFile iLeappFile : iLeappFiles) { + if (((iLeappFile.getLocalAbsPath() != null) + && (!iLeappFile.getNameExtension().isEmpty() && (!iLeappFile.isVirtual()))) + && ((iLeappFile.getName().toLowerCase().contains(".zip") || (iLeappFile.getName().toLowerCase().contains(".tar"))) + || iLeappFile.getName().toLowerCase().contains(".tgz"))) { + iLeappFilesToProcess.add(iLeappFile); + + } + } + + return iLeappFilesToProcess; + } + + private ProcessBuilder buildiLeappCommand(Path moduleOutputPath, String sourceFilePath, String iLeappFileSystemType) { + + ProcessBuilder processBuilder = buildProcessWithRunAsInvoker( + "\"" + iLeappExecutable + "\"", //NON-NLS + "-t", iLeappFileSystemType, //NON-NLS + "-i", String.format("\"%s\"",sourceFilePath), //NON-NLS + "-o", String.format("\"%s\"",moduleOutputPath.toString()) + ); + processBuilder.redirectError(moduleOutputPath.resolve("iLeapp_err.txt").toFile()); //NON-NLS + processBuilder.redirectOutput(moduleOutputPath.resolve("iLeapp_out.txt").toFile()); //NON-NLS + return processBuilder; + } + + static private ProcessBuilder buildProcessWithRunAsInvoker(String... commandLine) { + ProcessBuilder processBuilder = new ProcessBuilder(commandLine); + /* + * Add an environment variable to force log2timeline/psort to run with + * the same permissions Autopsy uses. + */ + processBuilder.environment().put("__COMPAT_LAYER", "RunAsInvoker"); //NON-NLS + return processBuilder; + } + + private static File locateExecutable(String executableName) throws FileNotFoundException { + String executableToFindName = Paths.get(ILEAPP, executableName).toString(); + + File exeFile = InstalledFileLocator.getDefault().locate(executableToFindName, ILeappAnalyzerIngestModule.class.getPackage().getName(), false); + if (null == exeFile || exeFile.canExecute() == false) { + throw new FileNotFoundException(executableName + " executable not found."); + } + return exeFile; + } + + /** + * Find the index.html file in the iLeapp output directory so it can be + * added to reports + */ + private void addILeappReportToReports(Path iLeappOutputDir, Case currentCase) { + List allIndexFiles = new ArrayList<>(); + + try (Stream walk = Files.walk(iLeappOutputDir)) { + + allIndexFiles = walk.map(x -> x.toString()) + .filter(f -> f.toLowerCase().endsWith("index.html")).collect(Collectors.toList()); + + if (!allIndexFiles.isEmpty()) { + currentCase.addReport(allIndexFiles.get(0), MODULE_NAME, Bundle.ILeappAnalyzerIngestModule_report_name()); + } + + } catch (IOException | UncheckedIOException | TskCoreException ex) { + // catch the error and continue on as report is not added + logger.log(Level.WARNING, String.format("Error finding index file in path %s", iLeappOutputDir.toString()), ex); + } + + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ILeappAnalyzerModuleFactory.java b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ILeappAnalyzerModuleFactory.java new file mode 100644 index 0000000000..166c455d55 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ILeappAnalyzerModuleFactory.java @@ -0,0 +1,67 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.modules.ileappanalyzer; + +import org.openide.util.NbBundle; +import org.openide.util.lookup.ServiceProvider; +import org.sleuthkit.autopsy.coreutils.Version; +import org.sleuthkit.autopsy.ingest.DataSourceIngestModule; +import org.sleuthkit.autopsy.ingest.IngestModuleFactory; +import org.sleuthkit.autopsy.ingest.IngestModuleFactoryAdapter; +import org.sleuthkit.autopsy.ingest.IngestModuleIngestJobSettings; + +/** + * A factory that creates data source ingest modules that will run iLeapp + * against logical files and saves the output to module output. + */ +@ServiceProvider(service = IngestModuleFactory.class) +public class ILeappAnalyzerModuleFactory extends IngestModuleFactoryAdapter { + + @NbBundle.Messages({"ILeappAnalyzerModuleFactory_moduleName=iOS Analyzer (iLEAPP)"}) + static String getModuleName() { + return Bundle.ILeappAnalyzerModuleFactory_moduleName(); + } + + @Override + public String getModuleDisplayName() { + return getModuleName(); + } + + @NbBundle.Messages({"ILeappAnalyzerModuleFactory_moduleDesc=Uses iLEAPP to analyze logical acquisitions of iOS devices."}) + @Override + public String getModuleDescription() { + return Bundle.ILeappAnalyzerModuleFactory_moduleDesc(); + } + + @Override + public String getModuleVersionNumber() { + return Version.getVersion(); + } + + @Override + public boolean isDataSourceIngestModuleFactory() { + return true; + } + + @Override + public DataSourceIngestModule createDataSourceIngestModule(IngestModuleIngestJobSettings ingestJobOptions) { + return new ILeappAnalyzerIngestModule(); + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ILeappFileProcessor.java b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ILeappFileProcessor.java new file mode 100644 index 0000000000..cb5ab516a6 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ILeappFileProcessor.java @@ -0,0 +1,447 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.modules.ileappanalyzer; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.List; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import static java.util.Locale.US; +import java.util.Map; +import java.util.logging.Level; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import org.apache.commons.io.FilenameUtils; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.PlatformUtil; +import org.sleuthkit.autopsy.ingest.IngestModule.IngestModuleException; +import org.sleuthkit.autopsy.ingest.IngestModule.ProcessResult; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.Blackboard; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.TskException; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +/** + * Find and process output from iLeapp program and bring into Autopsy + */ +public final class ILeappFileProcessor { + + private static final Logger logger = Logger.getLogger(ILeappFileProcessor.class.getName()); + private static final String MODULE_NAME = ILeappAnalyzerModuleFactory.getModuleName(); + + private static final String XMLFILE = "ileap-artifact-attribute-reference.xml"; //NON-NLS + + private final Map tsvFiles; + private final Map tsvFileArtifacts; + private final Map tsvFileArtifactComments; + private final Map>> tsvFileAttributes; + + public ILeappFileProcessor() throws IOException, IngestModuleException { + this.tsvFiles = new HashMap<>(); + this.tsvFileArtifacts = new HashMap<>(); + this.tsvFileArtifactComments = new HashMap<>(); + this.tsvFileAttributes = new HashMap<>(); + + configExtractor(); + loadConfigFile(); + + } + + @NbBundle.Messages({ + "ILeappFileProcessor.error.running.iLeapp=Error running iLeapp, see log file.", + "ILeappFileProcessor.error.creating.output.dir=Error creating iLeapp module output directory.", + "ILeappFileProcessor.starting.iLeapp=Starting iLeapp", + "ILeappFileProcessor.running.iLeapp=Running iLeapp", + "ILeappFileProcessor.has.run=iLeapp", + "ILeappFileProcessor.iLeapp.cancelled=iLeapp run was canceled", + "ILeappFileProcessor.completed=iLeapp Processing Completed", + "ILeappFileProcessor.error.reading.iLeapp.directory=Error reading iLeapp Output Directory"}) + + public ProcessResult processFiles(Content dataSource, Path moduleOutputPath, AbstractFile iLeappFile) { + + try { + List iLeappTsvOutputFiles = findTsvFiles(moduleOutputPath); + processiLeappFiles(iLeappTsvOutputFiles, iLeappFile); + } catch (IOException | IngestModuleException ex) { + logger.log(Level.SEVERE, String.format("Error trying to process iLeapp output files in directory %s. ", moduleOutputPath.toString()), ex); //NON-NLS + return ProcessResult.ERROR; + } + + return ProcessResult.OK; + } + + /** + * Find the tsv files in the iLeapp output directory and match them to files + * we know we want to process and return the list to process those files. + */ + private List findTsvFiles(Path iLeappOutputDir) throws IngestModuleException { + List allTsvFiles = new ArrayList<>(); + List foundTsvFiles = new ArrayList<>(); + + try (Stream walk = Files.walk(iLeappOutputDir)) { + + allTsvFiles = walk.map(x -> x.toString()) + .filter(f -> f.toLowerCase().endsWith(".tsv")).collect(Collectors.toList()); + + for (String tsvFile : allTsvFiles) { + if (tsvFiles.containsKey(FilenameUtils.getName(tsvFile))) { + foundTsvFiles.add(tsvFile); + } + } + + } catch (IOException | UncheckedIOException e) { + throw new IngestModuleException(Bundle.ILeappFileProcessor_error_reading_iLeapp_directory() + iLeappOutputDir.toString(), e); + } + + return foundTsvFiles; + + } + + /** + * Process the iLeapp files that were found that match the xml mapping file + * + * @param iLeappFilesToProcess List of files to process + * @param iLeappImageFile Abstract file to create artifact for + * + * @throws FileNotFoundException + * @throws IOException + */ + private void processiLeappFiles(List iLeappFilesToProcess, AbstractFile iLeappImageFile) throws FileNotFoundException, IOException, IngestModuleException { + List bbartifacts = new ArrayList<>(); + + for (String iLeappFileName : iLeappFilesToProcess) { + String fileName = FilenameUtils.getName(iLeappFileName); + File iLeappFile = new File(iLeappFileName); + if (tsvFileAttributes.containsKey(fileName)) { + List> attrList = tsvFileAttributes.get(fileName); + try { + BlackboardArtifact.Type artifactType = Case.getCurrentCase().getSleuthkitCase().getArtifactType(tsvFileArtifacts.get(fileName)); + + processFile(iLeappFile, attrList, fileName, artifactType, bbartifacts, iLeappImageFile); + + } catch (TskCoreException ex) { + // check this + throw new IngestModuleException(String.format("Error getting Blackboard Artifact Type for %s", tsvFileArtifacts.get(fileName)), ex); + } + } + + } + + if (!bbartifacts.isEmpty()) { + postArtifacts(bbartifacts); + } + + } + + private void processFile(File iLeappFile, List> attrList, String fileName, BlackboardArtifact.Type artifactType, + List bbartifacts, AbstractFile iLeappImageFile) throws FileNotFoundException, IOException, IngestModuleException { + try (BufferedReader reader = new BufferedReader(new FileReader(iLeappFile))) { + String line = reader.readLine(); + // Check first line, if it is null then no heading so nothing to match to, close and go to next file. + if (line != null) { + Map columnNumberToProcess = findColumnsToProcess(line, attrList); + line = reader.readLine(); + while (line != null) { + Collection bbattributes = processReadLine(line, columnNumberToProcess, fileName); + if (!bbattributes.isEmpty()) { + BlackboardArtifact bbartifact = createArtifactWithAttributes(artifactType.getTypeID(), iLeappImageFile, bbattributes); + if (bbartifact != null) { + bbartifacts.add(bbartifact); + } + } + line = reader.readLine(); + } + } + } + + } + + /** + * Process the line read and create the necessary attributes for it + * + * @param line a tsv line to process that was read + * @param columnNumberToProcess Which columns to process in the tsv line + * @param fileName name of file begin processed + * + * @return + */ + private Collection processReadLine(String line, Map columnNumberToProcess, String fileName) throws IngestModuleException { + String[] columnValues = line.split("\\t"); + + Collection bbattributes = new ArrayList(); + + for (Map.Entry columnToProcess : columnNumberToProcess.entrySet()) { + Integer columnNumber = columnToProcess.getKey(); + String attributeName = columnToProcess.getValue(); + + try { + BlackboardAttribute.Type attributeType = Case.getCurrentCase().getSleuthkitCase().getAttributeType(attributeName.toUpperCase()); + if (attributeType == null) { + break; + } + String attrType = attributeType.getValueType().getLabel().toUpperCase(); + checkAttributeType(bbattributes, attrType, columnValues, columnNumber, attributeType, fileName); + } catch (TskCoreException ex) { + throw new IngestModuleException(String.format("Error getting Attribute type for Attribute Name %s", attributeName), ex); //NON-NLS + } + } + + if (tsvFileArtifactComments.containsKey(fileName)) { + bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_COMMENT, MODULE_NAME, tsvFileArtifactComments.get(fileName))); + } + + return bbattributes; + + } + + private void checkAttributeType(Collection bbattributes, String attrType, String[] columnValues, Integer columnNumber, BlackboardAttribute.Type attributeType, + String fileName) { + if (attrType.matches("STRING")) { + bbattributes.add(new BlackboardAttribute(attributeType, MODULE_NAME, columnValues[columnNumber])); + } else if (attrType.matches("INTEGER")) { + bbattributes.add(new BlackboardAttribute(attributeType, MODULE_NAME, Integer.valueOf(columnValues[columnNumber]))); + } else if (attrType.matches("LONG")) { + bbattributes.add(new BlackboardAttribute(attributeType, MODULE_NAME, Long.valueOf(columnValues[columnNumber]))); + } else if (attrType.matches("DOUBLE")) { + bbattributes.add(new BlackboardAttribute(attributeType, MODULE_NAME, Double.valueOf(columnValues[columnNumber]))); + } else if (attrType.matches("BYTE")) { + bbattributes.add(new BlackboardAttribute(attributeType, MODULE_NAME, Byte.valueOf(columnValues[columnNumber]))); + } else if (attrType.matches("DATETIME")) { + // format of data should be the same in all the data and the format is 2020-03-28 01:00:17 + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-d HH:mm:ss", US); + Long dateLong = Long.valueOf(0); + try { + Date newDate = dateFormat.parse(columnValues[columnNumber]); + dateLong = newDate.getTime() / 1000; + bbattributes.add(new BlackboardAttribute(attributeType, MODULE_NAME, dateLong)); + } catch (ParseException ex) { + // catching error and displaying date that could not be parsed + // we set the timestamp to 0 and continue on processing + logger.log(Level.WARNING, String.format("Failed to parse date/time %s for attribute type %s in file %s.", columnValues[columnNumber], attributeType.getDisplayName(), fileName)); //NON-NLS + } + } else if (attrType.matches("JSON")) { + + bbattributes.add(new BlackboardAttribute(attributeType, MODULE_NAME, columnValues[columnNumber])); + } else { + // Log this and continue on with processing + logger.log(Level.WARNING, String.format("Attribute Type %s not defined.", attrType)); //NON-NLS + } + + } + + /** + * Process the first line of the tsv file which has the headings. Match the + * headings to the columns in the XML mapping file so we know which columns + * to process. + * + * @param line a tsv heading line of the columns in the file + * @param attrList the list of headings we want to process + * + * @return the numbered column(s) and attribute(s) we want to use for the + * column(s) + */ + private Map findColumnsToProcess(String line, List> attrList) { + String[] columnNames = line.split("\\t"); + HashMap columnsToProcess = new HashMap<>(); + + Integer columnPosition = 0; + for (String columnName : columnNames) { + // for some reason the first column of the line has unprintable characters so removing them + String cleanColumnName = columnName.replaceAll("[^\\n\\r\\t\\p{Print}]", ""); + for (List atList : attrList) { + if (atList.contains(cleanColumnName.toLowerCase())) { + columnsToProcess.put(columnPosition, atList.get(0)); + break; + } + } + columnPosition++; + } + + return columnsToProcess; + } + + @NbBundle.Messages({ + "ILeappFileProcessor.cannot.load.artifact.xml=Cannor load xml artifact file.", + "ILeappFileProcessor.cannotBuildXmlParser=Cannot buld an XML parser.", + "ILeappFileProcessor_cannotParseXml=Cannot Parse XML file.", + "ILeappFileProcessor.postartifacts_error=Error posting Blackboard Artifact", + "ILeappFileProcessor.error.creating.new.artifacts=Error creating new artifacts." + }) + + /** + * Read the XML config file and load the mappings into maps + */ + private void loadConfigFile() throws IngestModuleException { + Document xmlinput; + try { + String path = PlatformUtil.getUserConfigDirectory() + File.separator + XMLFILE; + File f = new File(path); + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + DocumentBuilder db = dbf.newDocumentBuilder(); + xmlinput = db.parse(f); + + } catch (IOException e) { + throw new IngestModuleException(Bundle.ILeappFileProcessor_cannot_load_artifact_xml() + e.getLocalizedMessage(), e); //NON-NLS + } catch (ParserConfigurationException pce) { + throw new IngestModuleException(Bundle.ILeappFileProcessor_cannotBuildXmlParser() + pce.getLocalizedMessage(), pce); //NON-NLS + } catch (SAXException sxe) { + throw new IngestModuleException(Bundle.ILeappFileProcessor_cannotParseXml() + sxe.getLocalizedMessage(), sxe); //NON-NLS + } + + getFileNode(xmlinput); + getArtifactNode(xmlinput); + getAttributeNodes(xmlinput); + + } + + private void getFileNode(Document xmlinput) { + + NodeList nlist = xmlinput.getElementsByTagName("FileName"); //NON-NLS + + for (int i = 0; i < nlist.getLength(); i++) { + NamedNodeMap nnm = nlist.item(i).getAttributes(); + tsvFiles.put(nnm.getNamedItem("filename").getNodeValue(), nnm.getNamedItem("description").getNodeValue()); + + } + + } + + private void getArtifactNode(Document xmlinput) { + + NodeList artifactNlist = xmlinput.getElementsByTagName("ArtifactName"); //NON-NLS + for (int k = 0; k < artifactNlist.getLength(); k++) { + NamedNodeMap nnm = artifactNlist.item(k).getAttributes(); + String artifactName = nnm.getNamedItem("artifactname").getNodeValue(); + String comment = nnm.getNamedItem("comment").getNodeValue(); + String parentName = artifactNlist.item(k).getParentNode().getAttributes().getNamedItem("filename").getNodeValue(); + + tsvFileArtifacts.put(parentName, artifactName); + + if (!comment.toLowerCase().matches("null")) { + tsvFileArtifactComments.put(parentName, comment); + } + } + + } + + private void getAttributeNodes(Document xmlinput) { + + NodeList attributeNlist = xmlinput.getElementsByTagName("AttributeName"); //NON-NLS + for (int k = 0; k < attributeNlist.getLength(); k++) { + List attributeList = new ArrayList<>(); + NamedNodeMap nnm = attributeNlist.item(k).getAttributes(); + String attributeName = nnm.getNamedItem("attributename").getNodeValue(); + if (!attributeName.toLowerCase().matches("null")) { + String columnName = nnm.getNamedItem("columnName").getNodeValue(); + String required = nnm.getNamedItem("required").getNodeValue(); + String parentName = attributeNlist.item(k).getParentNode().getParentNode().getAttributes().getNamedItem("filename").getNodeValue(); + + attributeList.add(attributeName.toLowerCase()); + attributeList.add(columnName.toLowerCase()); + attributeList.add(required.toLowerCase()); + + if (tsvFileAttributes.containsKey(parentName)) { + List> attrList = tsvFileAttributes.get(parentName); + attrList.add(attributeList); + tsvFileAttributes.replace(parentName, attrList); + } else { + List> attrList = new ArrayList<>(); + attrList.add(attributeList); + tsvFileAttributes.put(parentName, attrList); + } + } + + } + } + /** + * Generic method for creating a blackboard artifact with attributes + * + * @param type is a blackboard.artifact_type enum to determine + * which type the artifact should be + * @param abstractFile is the AbstractFile object that needs to have the + * artifact added for it + * @param bbattributes is the collection of blackboard attributes that + * need to be added to the artifact after the + * artifact has been created + * + * @return The newly-created artifact, or null on error + */ + private BlackboardArtifact createArtifactWithAttributes(int type, AbstractFile abstractFile, Collection bbattributes) { + try { + BlackboardArtifact bbart = abstractFile.newArtifact(type); + bbart.addAttributes(bbattributes); + return bbart; + } catch (TskException ex) { + logger.log(Level.WARNING, Bundle.ILeappFileProcessor_error_creating_new_artifacts(), ex); //NON-NLS + } + return null; + } + + /** + * Method to post a list of BlackboardArtifacts to the blackboard. + * + * @param artifacts A list of artifacts. IF list is empty or null, the + * function will return. + */ + void postArtifacts(Collection artifacts) { + if (artifacts == null || artifacts.isEmpty()) { + return; + } + + try { + Case.getCurrentCase().getSleuthkitCase().getBlackboard().postArtifacts(artifacts, MODULE_NAME); + } catch (Blackboard.BlackboardException ex) { + logger.log(Level.SEVERE, Bundle.ILeappFileProcessor_postartifacts_error(), ex); //NON-NLS + } + } + + /** + * Extract the iLeapp config xml file to the user directory to process + * + * @throws org.sleuthkit.autopsy.ingest.IngestModule.IngestModuleException + */ + private void configExtractor() throws IOException { + PlatformUtil.extractResourceToUserConfigDir(ILeappFileProcessor.class, XMLFILE, true); + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ileap-artifact-attribute-reference.xml b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ileap-artifact-attribute-reference.xml new file mode 100644 index 0000000000..0959cbdcc4 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ileap-artifact-attribute-reference.xml @@ -0,0 +1,538 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.form b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.form index cca6a8b5c8..5655a0ddd9 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.form +++ b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.form @@ -329,6 +329,7 @@ + @@ -401,9 +402,12 @@ - + + + + diff --git a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.java b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.java index 7b41f71e25..7b518da9c4 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2018 Basis Technology Corp. + * Copyright 2011-2020 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -46,6 +46,8 @@ import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.corecomponents.OptionsPanel; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import org.sleuthkit.autopsy.coreutils.ModuleSettings; +import org.sleuthkit.autopsy.guiutils.SimpleListCellRenderer; import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.autopsy.ingest.IngestModuleGlobalSettingsPanel; import org.sleuthkit.autopsy.ingest.IngestProfiles; @@ -87,6 +89,14 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp private final String filterDialogTitle; private final String ruleDialogTitle; private boolean canBeEnabled = true; + + private static final String XML_EXTENSION = "xml"; + + private final JFileChooser importFileChooser; + private static final String LAST_IMPORT_PATH_KEY = "InterestingFilesRuleSetLastImport"; + + private final JFileChooser exportFileChooser; + private static final String LAST_EXPORT_PATH_KEY = "InterestingFilesRuleSetLastExport"; // The following is a map of interesting files set names to interesting // files set definitions. It is a snapshot of the files set definitions @@ -145,6 +155,22 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp enableButtons(); }); canBeEnabled = !IngestManager.getInstance().isIngestRunning(); + + this.importFileChooser = new JFileChooser(); + this.exportFileChooser = new JFileChooser(); + configureFileChooser(importFileChooser); + configureFileChooser(exportFileChooser); + } + + /** + * Configure the file chooser for rule set imports and exports. + */ + private void configureFileChooser(JFileChooser fileChooser) { + FileNameExtensionFilter autopsyFilter = new FileNameExtensionFilter( + NbBundle.getMessage(this.getClass(), "FilesSetDefsPanel.interesting.fileExtensionFilterLbl"), XML_EXTENSION); + fileChooser.addChoosableFileFilter(autopsyFilter); + fileChooser.setAcceptAllFileFilterUsed(false); + fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); } @NbBundle.Messages({"FilesSetDefsPanel.Interesting.Title=Global Interesting Items Settings", @@ -648,6 +674,7 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp rulesList.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); rulesListScrollPane.setViewportView(rulesList); + rulesList.setCellRenderer(new SimpleListCellRenderer()); setDescScrollPanel.setMinimumSize(new java.awt.Dimension(10, 22)); setDescScrollPanel.setPreferredSize(new java.awt.Dimension(14, 40)); @@ -676,6 +703,7 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp setsList.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); setsListScrollPane.setViewportView(setsList); + setsList.setCellRenderer(new SimpleListCellRenderer()); fileNameButtonGroup.add(fileNameExtensionRadioButton); org.openide.awt.Mnemonics.setLocalizedText(fileNameExtensionRadioButton, org.openide.util.NbBundle.getMessage(FilesSetDefsPanel.class, "FilesSetDefsPanel.fileNameExtensionRadioButton.text")); // NOI18N @@ -1128,16 +1156,13 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp private void importSetButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_importSetButtonActionPerformed //save currently selected value as default value to select FilesSet selectedSet = this.setsList.getSelectedValue(); - JFileChooser chooser = new JFileChooser(); - final String EXTENSION = "xml"; //NON-NLS - FileNameExtensionFilter autopsyFilter = new FileNameExtensionFilter( - NbBundle.getMessage(this.getClass(), "FilesSetDefsPanel.interesting.fileExtensionFilterLbl"), EXTENSION); - chooser.addChoosableFileFilter(autopsyFilter); - chooser.setAcceptAllFileFilterUsed(false); - chooser.setFileSelectionMode(JFileChooser.FILES_ONLY); - int returnVal = chooser.showOpenDialog(this); + + File lastFolder = getLastUsedDirectory(LAST_IMPORT_PATH_KEY); + importFileChooser.setCurrentDirectory(lastFolder); + + int returnVal = importFileChooser.showOpenDialog(this); if (returnVal == JFileChooser.APPROVE_OPTION) { - File selFile = chooser.getSelectedFile(); + File selFile = importFileChooser.getSelectedFile(); if (selFile == null) { JOptionPane.showMessageDialog(this, Bundle.FilesSetDefsPanel_importSetButtonActionPerformed_noFilesSelected(), @@ -1146,6 +1171,9 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp logger.warning("Selected file was null, when trying to import interesting files set definitions"); return; } + + ModuleSettings.setConfigSetting(ModuleSettings.MAIN_SETTINGS, LAST_IMPORT_PATH_KEY, selFile.getParent()); + Collection importedSets; try { importedSets = InterestingItemsFilesSetSettings.readDefinitionsXML(selFile).values(); //read the xml from that path @@ -1188,6 +1216,27 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp }//GEN-LAST:event_importSetButtonActionPerformed + /** + * Get the last used directory from ModuleSettings, using the value + * associated with the input key as the directory path. + * + * @param key The input key to search in module settings. + * @return A directory instance if a value was found and the path is still + * valid, or null otherwise. + */ + private File getLastUsedDirectory(String key) { + File lastFolder = null; + if (ModuleSettings.settingExists(ModuleSettings.MAIN_SETTINGS, key)) { + final String lastDirectory = ModuleSettings.getConfigSetting(ModuleSettings.MAIN_SETTINGS, key); + File lastDirectoryFile = new File(lastDirectory); + // Only select it if it exists. + if (lastDirectoryFile.exists()) { + lastFolder = lastDirectoryFile; + } + } + return lastFolder; + } + /** * From the files sets that can be imported, this method rectifies any * conflicts that may occur. @@ -1417,18 +1466,16 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp private void exportSetButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exportSetButtonActionPerformed //display warning that existing filessets with duplicate names will be overwritten //create file chooser to get xml filefinal String FEATURE_NAME = NbBundle.getMessage(this.getClass(), - JFileChooser chooser = new JFileChooser(); - final String EXTENSION = "xml"; //NON-NLS - FileNameExtensionFilter autopsyFilter = new FileNameExtensionFilter( - NbBundle.getMessage(this.getClass(), "FilesSetDefsPanel.interesting.fileExtensionFilterLbl"), EXTENSION); - chooser.addChoosableFileFilter(autopsyFilter); - chooser.setSelectedFile(new File(this.setsList.getSelectedValue().getName())); - chooser.setFileSelectionMode(JFileChooser.FILES_ONLY); - int returnVal = chooser.showSaveDialog(this); + exportFileChooser.setSelectedFile(new File(this.setsList.getSelectedValue().getName())); + + final File lastDirectory = getLastUsedDirectory(LAST_EXPORT_PATH_KEY); + exportFileChooser.setCurrentDirectory(lastDirectory); + + int returnVal = exportFileChooser.showSaveDialog(this); if (returnVal == JFileChooser.APPROVE_OPTION) { final String FEATURE_NAME = NbBundle.getMessage(this.getClass(), "FilesSetDefsPanel.interesting.exportButtonAction.featureName"); - File selFile = chooser.getSelectedFile(); + File selFile = exportFileChooser.getSelectedFile(); if (selFile == null) { JOptionPane.showMessageDialog(this, NbBundle.getMessage(this.getClass(), "FilesSetDefsPanel.interesting.failExportMsg"), @@ -1437,10 +1484,13 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp logger.warning("Selected file was null, when trying to export interesting files set definitions"); return; } + + ModuleSettings.setConfigSetting(ModuleSettings.MAIN_SETTINGS, LAST_EXPORT_PATH_KEY, selFile.getParent()); + //force append extension if not given String fileAbs = selFile.getAbsolutePath(); - if (!fileAbs.endsWith("." + EXTENSION)) { - fileAbs = fileAbs + "." + EXTENSION; + if (!fileAbs.endsWith("." + XML_EXTENSION)) { + fileAbs = fileAbs + "." + XML_EXTENSION; selFile = new File(fileAbs); } if (selFile.exists()) { diff --git a/Core/src/org/sleuthkit/autopsy/modules/photoreccarver/PhotoRecCarverFileIngestModule.java b/Core/src/org/sleuthkit/autopsy/modules/photoreccarver/PhotoRecCarverFileIngestModule.java index 7b9a6db4c7..d887f3ba4c 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/photoreccarver/PhotoRecCarverFileIngestModule.java +++ b/Core/src/org/sleuthkit/autopsy/modules/photoreccarver/PhotoRecCarverFileIngestModule.java @@ -86,7 +86,7 @@ final class PhotoRecCarverFileIngestModule implements FileIngestModule { static final boolean DEFAULT_CONFIG_INCLUDE_ELSE_EXCLUDE = false; - private static final String PHOTOREC_TEMP_SUBDIR = "Photorec"; + private static final String PHOTOREC_TEMP_SUBDIR = "PhotoRec Carver"; // NON-NLS Note that we need the space in this dir name (JIRA-6878) private static final String PHOTOREC_DIRECTORY = "photorec_exec"; //NON-NLS private static final String PHOTOREC_SUBDIRECTORY = "bin"; //NON-NLS private static final String PHOTOREC_EXECUTABLE = "photorec_win.exe"; //NON-NLS @@ -321,9 +321,9 @@ final class PhotoRecCarverFileIngestModule implements FileIngestModule { ProcessBuilder processAndSettings = new ProcessBuilder( executableFile.toString(), "/d", // NON-NLS - outputDirPath.toAbsolutePath().toString() + File.separator + PHOTOREC_RESULTS_BASE, + String.format("\"%s\"", Paths.get(outputDirPath.toAbsolutePath().toString(), PHOTOREC_RESULTS_BASE).toString()), "/cmd", // NON-NLS - tempFilePath.toFile().toString()); + String.format("\"%s\"",tempFilePath.toFile().toString())); processAndSettings.command().add(this.optionsString); @@ -468,7 +468,8 @@ final class PhotoRecCarverFileIngestModule implements FileIngestModule { try { // The last instance of this module for an ingest job cleans out // the working paths map entry for the job and deletes the temp dir. - FileUtil.deleteDir(this.rootTempDirPath.toFile()); + WorkingPaths paths = PhotoRecCarverFileIngestModule.pathsByJob.remove(this.jobId); + FileUtil.deleteDir(new File(paths.getTempDirPath().toString())); postSummary(jobId); } catch (SecurityException ex) { logger.log(Level.SEVERE, "Error shutting down PhotoRec carver module", ex); // NON-NLS diff --git a/Core/src/org/sleuthkit/autopsy/modules/pictureanalyzer/impls/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/modules/pictureanalyzer/impls/Bundle.properties-MERGED index 349e005b1b..569370ec6e 100755 --- a/Core/src/org/sleuthkit/autopsy/modules/pictureanalyzer/impls/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/modules/pictureanalyzer/impls/Bundle.properties-MERGED @@ -1,3 +1,2 @@ ExifProcessor.indexError.message=Failed to post EXIF Metadata artifact(s). -ExifProcessor.module_name=Exif Parser -ExifProcessor.userContent.description=EXIF metadata exists for this file. +ExifProcessor.userContent.description=EXIF metadata data exists for this file. diff --git a/Core/src/org/sleuthkit/autopsy/modules/pictureanalyzer/impls/EXIFProcessor.java b/Core/src/org/sleuthkit/autopsy/modules/pictureanalyzer/impls/EXIFProcessor.java index c2c5051050..5a48fcf7bf 100755 --- a/Core/src/org/sleuthkit/autopsy/modules/pictureanalyzer/impls/EXIFProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/modules/pictureanalyzer/impls/EXIFProcessor.java @@ -44,6 +44,7 @@ import org.sleuthkit.autopsy.ingest.IngestJobContext; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import org.sleuthkit.autopsy.modules.pictureanalyzer.PictureAnalyzerIngestModuleFactory; import org.sleuthkit.datamodel.Blackboard; import org.sleuthkit.datamodel.BlackboardArtifact; import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_METADATA_EXIF; @@ -70,11 +71,10 @@ public class EXIFProcessor implements PictureProcessor { @Override @NbBundle.Messages({ "ExifProcessor.indexError.message=Failed to post EXIF Metadata artifact(s).", - "ExifProcessor.userContent.description=EXIF metadata exists for this file.", - "ExifProcessor.module_name=Exif Parser" + "ExifProcessor.userContent.description=EXIF metadata data exists for this file." }) public void process(IngestJobContext context, AbstractFile file) { - final String MODULE_NAME = Bundle.ExifProcessor_module_name(); + final String MODULE_NAME = PictureAnalyzerIngestModuleFactory.getModuleName(); try (BufferedInputStream bin = new BufferedInputStream(new ReadContentInputStream(file));) { diff --git a/Core/src/org/sleuthkit/autopsy/modules/pictureanalyzer/impls/HEICProcessor.java b/Core/src/org/sleuthkit/autopsy/modules/pictureanalyzer/impls/HEICProcessor.java index 8d4588aa98..0b3296be97 100755 --- a/Core/src/org/sleuthkit/autopsy/modules/pictureanalyzer/impls/HEICProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/modules/pictureanalyzer/impls/HEICProcessor.java @@ -48,6 +48,7 @@ import org.sleuthkit.autopsy.coreutils.ExecUtil; import org.sleuthkit.autopsy.coreutils.FileUtil; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.PlatformUtil; +import org.sleuthkit.autopsy.ingest.FileIngestModuleProcessTerminator; import org.sleuthkit.autopsy.ingest.IngestJobContext; import org.sleuthkit.autopsy.ingest.IngestServices; import org.sleuthkit.autopsy.ingest.ModuleContentEvent; @@ -71,7 +72,7 @@ public class HEICProcessor implements PictureProcessor { private static final int EXIT_SUCCESS = 0; private static final String HEIC_MODULE_FOLDER = "HEIC"; - private static final long TIMEOUT_IN_MS = TimeUnit.MILLISECONDS.convert(2, TimeUnit.MINUTES); + private static final long TIMEOUT_IN_SEC = TimeUnit.SECONDS.convert(2, TimeUnit.MINUTES); // Windows location private static final String IMAGE_MAGICK_FOLDER = "ImageMagick-7.0.10-27-portable-Q16-x64"; @@ -188,16 +189,13 @@ public class HEICProcessor implements PictureProcessor { // Any additional images found within the HEIC container will be // formatted as fileName-1.jpg, fileName-2.jpg, etc. final ProcessBuilder processBuilder = new ProcessBuilder() - .command(IMAGE_MAGICK_PATH.toString(), - localDiskCopy.toString(), - outputFile.toString()); + .command(String.format("\"%s\"",IMAGE_MAGICK_PATH.toString()), + String.format("\"%s\"",localDiskCopy.toString()), + String.format("\"%s\"",outputFile.toString())); processBuilder.redirectError(imageMagickErrorOutput.toFile()); - final long startTime = System.currentTimeMillis(); - final int exitStatus = ExecUtil.execute(processBuilder, () -> { - return context.fileIngestIsCancelled() || System.currentTimeMillis() - startTime >= TIMEOUT_IN_MS; - }); + final int exitStatus = ExecUtil.execute(processBuilder, new FileIngestModuleProcessTerminator(context, TIMEOUT_IN_SEC)); if (context.fileIngestIsCancelled()) { return; diff --git a/Core/src/org/sleuthkit/autopsy/modules/plaso/PlasoIngestModule.java b/Core/src/org/sleuthkit/autopsy/modules/plaso/PlasoIngestModule.java index d359332f84..d1838c2798 100755 --- a/Core/src/org/sleuthkit/autopsy/modules/plaso/PlasoIngestModule.java +++ b/Core/src/org/sleuthkit/autopsy/modules/plaso/PlasoIngestModule.java @@ -235,8 +235,8 @@ public class PlasoIngestModule implements DataSourceIngestModule { "--parsers", "\"" + parsersString + "\"",//NON-NLS "--no_dependencies_check", //NON-NLS "--workers", String.valueOf(LOG2TIMELINE_WORKERS),//NON-NLS - moduleOutputPath.resolve(PLASO).toString(), - image.getPaths()[0] + String.format("\"%s\"",moduleOutputPath.resolve(PLASO).toString()), + String.format("\"%s\"",image.getPaths()[0]) ); processBuilder.redirectError(moduleOutputPath.resolve("log2timeline_err.txt").toFile()); //NON-NLS return processBuilder; @@ -256,8 +256,8 @@ public class PlasoIngestModule implements DataSourceIngestModule { ProcessBuilder processBuilder = buildProcessWithRunAsInvoker( "\"" + psortExecutable + "\"", //NON-NLS "-o", "4n6time_sqlite", //NON-NLS - "-w", moduleOutputPath.resolve("plasodb.db3").toString(), //NON-NLS - moduleOutputPath.resolve(PLASO).toString() + "-w", String.format("\"%s\"",moduleOutputPath.resolve("plasodb.db3").toString()), //NON-NLS + String.format("\"%s\"",moduleOutputPath.resolve(PLASO).toString()) ); processBuilder.redirectOutput(moduleOutputPath.resolve("psort_output.txt").toFile()); //NON-NLS diff --git a/Core/src/org/sleuthkit/autopsy/report/infrastructure/PortableCaseInterestingItemsListPanel.java b/Core/src/org/sleuthkit/autopsy/report/infrastructure/PortableCaseInterestingItemsListPanel.java index 55da66a73d..dd11137a0a 100644 --- a/Core/src/org/sleuthkit/autopsy/report/infrastructure/PortableCaseInterestingItemsListPanel.java +++ b/Core/src/org/sleuthkit/autopsy/report/infrastructure/PortableCaseInterestingItemsListPanel.java @@ -207,7 +207,9 @@ class PortableCaseInterestingItemsListPanel extends javax.swing.JPanel { setFont(list.getFont()); setBackground(list.getBackground()); setForeground(list.getForeground()); - setText(value + " (" + setCounts.get(value) + ")"); // NON-NLS + String text = value + " (" + setCounts.get(value) + ")"; + setText(text); // NON-NLS + setToolTipText(text); return this; } return new JLabel(); diff --git a/Core/src/org/sleuthkit/autopsy/report/infrastructure/PortableCaseTagsListPanel.java b/Core/src/org/sleuthkit/autopsy/report/infrastructure/PortableCaseTagsListPanel.java index 247db77517..2e4a8c98cd 100644 --- a/Core/src/org/sleuthkit/autopsy/report/infrastructure/PortableCaseTagsListPanel.java +++ b/Core/src/org/sleuthkit/autopsy/report/infrastructure/PortableCaseTagsListPanel.java @@ -203,7 +203,9 @@ class PortableCaseTagsListPanel extends javax.swing.JPanel { setFont(list.getFont()); setBackground(list.getBackground()); setForeground(list.getForeground()); - setText(value + " (" + tagCounts.get(value) + ")"); // NON-NLS + String text = value + " (" + tagCounts.get(value) + ")"; + setText(text); + setToolTipText(text); return this; } return new JLabel(); diff --git a/Core/src/org/sleuthkit/autopsy/report/modules/html/HTMLReport.java b/Core/src/org/sleuthkit/autopsy/report/modules/html/HTMLReport.java index 6c49926907..70bb171d5d 100644 --- a/Core/src/org/sleuthkit/autopsy/report/modules/html/HTMLReport.java +++ b/Core/src/org/sleuthkit/autopsy/report/modules/html/HTMLReport.java @@ -336,6 +336,27 @@ public class HTMLReport implements TableReportModule { case TSK_REMOTE_DRIVE: in = getClass().getResourceAsStream("/org/sleuthkit/autopsy/report/images/drive_network.png"); //NON-NLS break; + case TSK_OS_ACCOUNT: + in = getClass().getResourceAsStream("/org/sleuthkit/autopsy/images/os-account.png"); //NON-NLS + break; + case TSK_OBJECT_DETECTED: + in = getClass().getResourceAsStream("/org/sleuthkit/autopsy/images/objects.png"); //NON-NLS + break; + case TSK_WEB_FORM_AUTOFILL: + in = getClass().getResourceAsStream("/org/sleuthkit/autopsy/images/web-form.png"); //NON-NLS + break; + case TSK_WEB_CACHE: + in = getClass().getResourceAsStream("/org/sleuthkit/autopsy/images/cache.png"); //NON-NLS + break; + case TSK_USER_CONTENT_SUSPECTED: + in = getClass().getResourceAsStream("/org/sleuthkit/autopsy/images/user-content.png"); //NON-NLS + break; + case TSK_METADATA: + in = getClass().getResourceAsStream("/org/sleuthkit/autopsy/images/metadata.png"); //NON-NLS + break; + case TSK_CLIPBOARD_CONTENT: + in = getClass().getResourceAsStream("/org/sleuthkit/autopsy/images/clipboard.png"); //NON-NLS + break; case TSK_ACCOUNT: in = getClass().getResourceAsStream("/org/sleuthkit/autopsy/report/images/accounts.png"); //NON-NLS break; @@ -357,6 +378,9 @@ public class HTMLReport implements TableReportModule { case TSK_VERIFICATION_FAILED: in = getClass().getResourceAsStream("/org/sleuthkit/autopsy/images/validationFailed.png"); //NON-NLS break; + case TSK_WEB_ACCOUNT_TYPE: + in = getClass().getResourceAsStream("/org/sleuthkit/autopsy/images/web-account-type.png.png"); //NON-NLS + break; default: logger.log(Level.WARNING, "useDataTypeIcon: unhandled artifact type = {0}", dataType); //NON-NLS in = getClass().getResourceAsStream("/org/sleuthkit/autopsy/report/images/star.png"); //NON-NLS diff --git a/Core/src/org/sleuthkit/autopsy/report/modules/portablecase/PortableCaseReportModule.java b/Core/src/org/sleuthkit/autopsy/report/modules/portablecase/PortableCaseReportModule.java index 3843eb29f4..29dc530aa0 100644 --- a/Core/src/org/sleuthkit/autopsy/report/modules/portablecase/PortableCaseReportModule.java +++ b/Core/src/org/sleuthkit/autopsy/report/modules/portablecase/PortableCaseReportModule.java @@ -1195,11 +1195,10 @@ public class PortableCaseReportModule implements ReportModule { } /** - * Copy the sorceFolder to destBaseFolder\appName. + * Copy the sorceFolder to destBaseFolder/appName. * * @param sourceFolder Autopsy installation directory. * @param destBaseFolder Report base direction. - * @param appName Name of the application being copied. * * @throws IOException */ @@ -1330,10 +1329,10 @@ public class PortableCaseReportModule implements ReportModule { File zipFile = Paths.get(tempZipFolder.getAbsolutePath(), caseName + ".zip").toFile(); // NON-NLS ProcessBuilder procBuilder = new ProcessBuilder(); procBuilder.command( - sevenZipExe.getAbsolutePath(), + String.format("\"%s\"",sevenZipExe.getAbsolutePath()), "a", // Add to archive - zipFile.getAbsolutePath(), - dirToCompress.toAbsolutePath().toString(), + String.format("\"%s\"",zipFile.getAbsolutePath()), + String.format("\"%s\"",dirToCompress.toAbsolutePath().toString()), chunkOption ); diff --git a/Core/src/org/sleuthkit/autopsy/report/modules/portablecase/PortableCaseReportModuleSettings.java b/Core/src/org/sleuthkit/autopsy/report/modules/portablecase/PortableCaseReportModuleSettings.java index 1f37b7fd9d..05f1fdbc54 100755 --- a/Core/src/org/sleuthkit/autopsy/report/modules/portablecase/PortableCaseReportModuleSettings.java +++ b/Core/src/org/sleuthkit/autopsy/report/modules/portablecase/PortableCaseReportModuleSettings.java @@ -83,6 +83,7 @@ public class PortableCaseReportModuleSettings implements ReportModuleSettings { this.chunkSize = ChunkSize.NONE; this.allTagsSelected = true; this.allSetsSelected = true; + this.shouldIncludeApplication = false; } PortableCaseReportModuleSettings(List setNames, List tagNames, @@ -93,6 +94,7 @@ public class PortableCaseReportModuleSettings implements ReportModuleSettings { this.chunkSize = chunkSize; this.allTagsSelected = allTagsSelected; this.allSetsSelected = allSetsSelected; + this.shouldIncludeApplication = false; } @Override diff --git a/Core/src/org/sleuthkit/autopsy/report/modules/taggedhashes/SaveTaggedHashesToHashDbConfigPanel.java b/Core/src/org/sleuthkit/autopsy/report/modules/taggedhashes/SaveTaggedHashesToHashDbConfigPanel.java index 9520b28706..081aa03b61 100644 --- a/Core/src/org/sleuthkit/autopsy/report/modules/taggedhashes/SaveTaggedHashesToHashDbConfigPanel.java +++ b/Core/src/org/sleuthkit/autopsy/report/modules/taggedhashes/SaveTaggedHashesToHashDbConfigPanel.java @@ -221,7 +221,10 @@ class SaveTaggedHashesToHashDbConfigPanel extends javax.swing.JPanel { setFont(list.getFont()); setBackground(list.getBackground()); setForeground(list.getForeground()); - setText(TagUtils.getDecoratedTagDisplayName(value)); + + String text = TagUtils.getDecoratedTagDisplayName(value); + setText(text); + this.setToolTipText(text); return this; } return new JLabel(); diff --git a/Core/src/org/sleuthkit/autopsy/resultviewers/summary/DataSourceSummaryResultViewer.java b/Core/src/org/sleuthkit/autopsy/resultviewers/summary/DataSourceSummaryResultViewer.java index 4cc90e9843..12dfe741d4 100644 --- a/Core/src/org/sleuthkit/autopsy/resultviewers/summary/DataSourceSummaryResultViewer.java +++ b/Core/src/org/sleuthkit/autopsy/resultviewers/summary/DataSourceSummaryResultViewer.java @@ -116,7 +116,9 @@ public class DataSourceSummaryResultViewer extends AbstractDataResultViewer { this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); try { - summaryPanel.setDataSource(dataSource); + if (summaryPanel != null) { + summaryPanel.setDataSource(dataSource); + } } finally { this.setCursor(null); } @@ -133,5 +135,12 @@ public class DataSourceSummaryResultViewer extends AbstractDataResultViewer { add(summaryPanel, BorderLayout.CENTER); } + @Override + public void clearComponent() { + summaryPanel.close(); + summaryPanel = null; + } + private DataSourceSummaryTabbedPane summaryPanel; + } diff --git a/Core/test/unit/src/org/sleuthkit/autopsy/discovery/search/DomainSearchCacheLoaderTest.java b/Core/test/unit/src/org/sleuthkit/autopsy/discovery/search/DomainSearchCacheLoaderTest.java new file mode 100755 index 0000000000..c7c8570aca --- /dev/null +++ b/Core/test/unit/src/org/sleuthkit/autopsy/discovery/search/DomainSearchCacheLoaderTest.java @@ -0,0 +1,157 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.discovery.search; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import static org.mockito.Mockito.*; +import static org.junit.Assert.*; +import org.junit.Test; +import org.sleuthkit.autopsy.discovery.search.DiscoveryKeyUtils.GroupKey; +import org.sleuthkit.autopsy.discovery.search.DiscoveryKeyUtils.SearchKey; +import org.sleuthkit.datamodel.TskCoreException; + +public class DomainSearchCacheLoaderTest { + + @Test + public void load_GroupByDataSourceSortByGroupNameAndDomain() throws DiscoveryException, TskCoreException, SQLException, InterruptedException { + DomainSearchCacheLoader loader = mock(DomainSearchCacheLoader.class); + List domains = Arrays.asList( + DomainSearchTestUtils.mockDomainResult("google.com", 10, 100, 3, 5, 4, 110), + DomainSearchTestUtils.mockDomainResult("yahoo.com", 1, 5, 3, 7, 20, 100), + DomainSearchTestUtils.mockDomainResult("google.com", 5, 20, 3, 1, 4, 105), + DomainSearchTestUtils.mockDomainResult("facebook.com", 2, 2, 3, 1, 3, 110), + DomainSearchTestUtils.mockDomainResult("abc.com", 1, 2, 3, 3, 4, 100), + DomainSearchTestUtils.mockDomainResult("xyz.com", 1, 2, 3, 3, 4, 20) + ); + + SearchKey key = new SearchKey(null, new ArrayList<>(), + new DiscoveryAttributes.DataSourceAttribute(), + Group.GroupSortingAlgorithm.BY_GROUP_NAME, + ResultsSorter.SortingMethod.BY_DOMAIN_NAME); + + when(loader.getResultDomainsFromDatabase(key)).thenReturn(domains); + when(loader.load(key)).thenCallRealMethod(); + Map> results = loader.load(key); + assertEquals(4, results.size()); + for(List group : results.values()) { + ResultDomain previous = null; + for(Result result : group) { + ResultDomain current = (ResultDomain) result; + if (previous != null) { + assertTrue(previous.getDomain().compareTo(current.getDomain()) < 0); + } + previous = current; + } + } + } + + @Test + public void load_GroupByNothingByGroupNameAndDomain() throws DiscoveryException, TskCoreException, SQLException, InterruptedException { + DomainSearchCacheLoader loader = mock(DomainSearchCacheLoader.class); + List domains = Arrays.asList( + DomainSearchTestUtils.mockDomainResult("google.com", 10, 100, 1, 5, 4, 110), + DomainSearchTestUtils.mockDomainResult("yahoo.com", 1, 5, 1, 7, 20, 100), + DomainSearchTestUtils.mockDomainResult("facebook.com", 2, 2, 1, 1, 3, 110), + DomainSearchTestUtils.mockDomainResult("abc.com", 1, 2, 1, 3, 4, 100), + DomainSearchTestUtils.mockDomainResult("xyz.com", 1, 2, 1, 3, 4, 20) + ); + + SearchKey key = new SearchKey(null, new ArrayList<>(), + new DiscoveryAttributes.NoGroupingAttribute(), + Group.GroupSortingAlgorithm.BY_GROUP_NAME, + ResultsSorter.SortingMethod.BY_DOMAIN_NAME); + + when(loader.getResultDomainsFromDatabase(key)).thenReturn(domains); + when(loader.load(key)).thenCallRealMethod(); + Map> results = loader.load(key); + assertEquals(1, results.size()); + for(List group : results.values()) { + ResultDomain previous = null; + for(Result result : group) { + ResultDomain current = (ResultDomain) result; + if (previous != null) { + assertTrue(previous.getDomain().compareTo(current.getDomain()) < 0); + } + previous = current; + } + } + } + + @Test + public void load_GroupByNothingSortByNameAndDataSource() throws DiscoveryException, TskCoreException, SQLException, InterruptedException { + DomainSearchCacheLoader loader = mock(DomainSearchCacheLoader.class); + List domains = Arrays.asList( + DomainSearchTestUtils.mockDomainResult("google.com", 10, 100, 7, 5, 4, 110), + DomainSearchTestUtils.mockDomainResult("yahoo.com", 1, 5, 7, 7, 20, 100) + ); + + SearchKey key = new SearchKey(null, new ArrayList<>(), + new DiscoveryAttributes.NoGroupingAttribute(), + Group.GroupSortingAlgorithm.BY_GROUP_NAME, + ResultsSorter.SortingMethod.BY_DATA_SOURCE); + + when(loader.getResultDomainsFromDatabase(key)).thenReturn(domains); + when(loader.load(key)).thenCallRealMethod(); + Map> results = loader.load(key); + assertEquals(1, results.size()); + for(List group : results.values()) { + ResultDomain previous = null; + for(Result result : group) { + ResultDomain current = (ResultDomain) result; + if (previous != null) { + assertTrue(Long.compare(previous.getDataSource().getId(), current.getDataSource().getId()) < 0); + } + previous = current; + } + } + } + + @Test + public void load_GroupByDataSourceBySizeAndName() throws DiscoveryException, TskCoreException, SQLException, InterruptedException { + DomainSearchCacheLoader loader = mock(DomainSearchCacheLoader.class); + List domains = Arrays.asList( + DomainSearchTestUtils.mockDomainResult("google.com", 10, 100, 7, 5, 4, 110), + DomainSearchTestUtils.mockDomainResult("yahoo.com", 1, 5, 7, 7, 20, 100) + ); + + SearchKey key = new SearchKey(null, new ArrayList<>(), + new DiscoveryAttributes.DataSourceAttribute(), + Group.GroupSortingAlgorithm.BY_GROUP_SIZE, + ResultsSorter.SortingMethod.BY_DOMAIN_NAME); + + when(loader.getResultDomainsFromDatabase(key)).thenReturn(domains); + when(loader.load(key)).thenCallRealMethod(); + Map> results = loader.load(key); + assertEquals(2, results.size()); + for(List group : results.values()) { + ResultDomain previous = null; + for(Result result : group) { + ResultDomain current = (ResultDomain) result; + if (previous != null) { + assertTrue(previous.getDomain().compareTo(current.getDomain()) < 0); + } + previous = current; + } + } + } +} diff --git a/Core/test/unit/src/org/sleuthkit/autopsy/discovery/search/DomainSearchTest.java b/Core/test/unit/src/org/sleuthkit/autopsy/discovery/search/DomainSearchTest.java new file mode 100755 index 0000000000..76fa103104 --- /dev/null +++ b/Core/test/unit/src/org/sleuthkit/autopsy/discovery/search/DomainSearchTest.java @@ -0,0 +1,410 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.discovery.search; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Test; + +import static org.mockito.Mockito.*; +import static org.junit.Assert.*; +import org.sleuthkit.autopsy.discovery.search.DiscoveryKeyUtils.GroupKey; + +public class DomainSearchTest { + + @Test + public void groupSizes_SingleGroup_ShouldHaveSizeFour() throws DiscoveryException { + DomainSearchCache cache = mock(DomainSearchCache.class); + + DummyKey groupOne = new DummyKey("1"); + Map> dummyData = new HashMap>() { + { + put(groupOne, Arrays.asList( + DomainSearchTestUtils.mockDomainResult("google.com"), + DomainSearchTestUtils.mockDomainResult("yahoo.com"), + DomainSearchTestUtils.mockDomainResult("bing.com"), + DomainSearchTestUtils.mockDomainResult("amazon.com")) + ); + } + }; + when(cache.get(null, new ArrayList<>(), null, null, null, null, null)).thenReturn(dummyData); + + DomainSearch domainSearch = new DomainSearch(cache, null); + Map sizes = domainSearch.getGroupSizes(null, + new ArrayList<>(), null, null, null, null, null); + assertEquals(4, sizes.get(groupOne).longValue()); + } + + @Test + public void groupSizes_MultipleGroups_ShouldHaveCorrectGroupSizes() throws DiscoveryException { + DomainSearchCache cache = mock(DomainSearchCache.class); + + DummyKey groupOne = new DummyKey("1"); + DummyKey groupTwo = new DummyKey("2"); + DummyKey groupThree = new DummyKey("3"); + + Map> dummyData = new HashMap>() { + { + put(groupOne, Arrays.asList( + DomainSearchTestUtils.mockDomainResult("google.com"), + DomainSearchTestUtils.mockDomainResult("yahoo.com"), + DomainSearchTestUtils.mockDomainResult("bing.com"), + DomainSearchTestUtils.mockDomainResult("amazon.com")) + ); + put(groupTwo, Arrays.asList( + DomainSearchTestUtils.mockDomainResult("facebook.com"), + DomainSearchTestUtils.mockDomainResult("spotify.com"), + DomainSearchTestUtils.mockDomainResult("netbeans.com")) + ); + put(groupThree, Arrays.asList( + DomainSearchTestUtils.mockDomainResult("youtube.com")) + ); + } + }; + + when(cache.get(null, new ArrayList<>(), null, null, null, null, null)).thenReturn(dummyData); + + DomainSearch domainSearch = new DomainSearch(cache, null); + Map sizes = domainSearch.getGroupSizes(null, + new ArrayList<>(), null, null, null, null, null); + assertEquals(4, sizes.get(groupOne).longValue()); + assertEquals(3, sizes.get(groupTwo).longValue()); + assertEquals(1, sizes.get(groupThree).longValue()); + } + + @Test + public void groupSizes_EmptyGroup_ShouldBeSizeZero() throws DiscoveryException { + DomainSearchCache cache = mock(DomainSearchCache.class); + + when(cache.get(null, new ArrayList<>(), null, null, null, null, null)).thenReturn(new HashMap<>()); + + DomainSearch domainSearch = new DomainSearch(cache, null); + Map sizes = domainSearch.getGroupSizes(null, + new ArrayList<>(), null, null, null, null, null); + assertEquals(0, sizes.size()); + } + + @Test + public void getDomains_SingleGroupFullPage_ShouldContainAllDomains() throws DiscoveryException { + DomainSearchCache cache = mock(DomainSearchCache.class); + + DummyKey groupOne = new DummyKey("1"); + List domains = Arrays.asList( + DomainSearchTestUtils.mockDomainResult("google.com"), + DomainSearchTestUtils.mockDomainResult("yahoo.com"), + DomainSearchTestUtils.mockDomainResult("bing.com"), + DomainSearchTestUtils.mockDomainResult("amazon.com")); + + Map> dummyData = new HashMap>() { + { + put(groupOne, domains); + } + }; + + when(cache.get(null, new ArrayList<>(), null, null, null, null, null)).thenReturn(dummyData); + + DomainSearch domainSearch = new DomainSearch(cache, null); + List firstPage = domainSearch.getDomainsInGroup(null, + new ArrayList<>(), null, null, null, groupOne, 0, 3, null, null); + assertEquals(3, firstPage.size()); + for (int i = 0; i < firstPage.size(); i++) { + assertEquals(domains.get(i), firstPage.get(i)); + } + } + + @Test + public void getDomains_SingleGroupOverSizedPage_ShouldContainAllDomains() throws DiscoveryException { + DomainSearchCache cache = mock(DomainSearchCache.class); + + DummyKey groupOne = new DummyKey("1"); + List domains = Arrays.asList( + DomainSearchTestUtils.mockDomainResult("google.com"), + DomainSearchTestUtils.mockDomainResult("yahoo.com"), + DomainSearchTestUtils.mockDomainResult("bing.com"), + DomainSearchTestUtils.mockDomainResult("amazon.com")); + + Map> dummyData = new HashMap>() { + { + put(groupOne, domains); + } + }; + + when(cache.get(null, new ArrayList<>(), null, null, null, null, null)).thenReturn(dummyData); + + DomainSearch domainSearch = new DomainSearch(cache, null); + List firstPage = domainSearch.getDomainsInGroup(null, + new ArrayList<>(), null, null, null, groupOne, 0, 100, null, null); + assertEquals(4, firstPage.size()); + for (int i = 0; i < firstPage.size(); i++) { + assertEquals(domains.get(i), firstPage.get(i)); + } + } + + @Test + public void getDomains_SingleGroupHalfPage_ShouldContainHalfDomains() throws DiscoveryException { + DomainSearchCache cache = mock(DomainSearchCache.class); + + DummyKey groupOne = new DummyKey("1"); + List domains = Arrays.asList( + DomainSearchTestUtils.mockDomainResult("google.com"), + DomainSearchTestUtils.mockDomainResult("yahoo.com"), + DomainSearchTestUtils.mockDomainResult("bing.com"), + DomainSearchTestUtils.mockDomainResult("amazon.com")); + + Map> dummyData = new HashMap>() { + { + put(groupOne, domains); + } + }; + + when(cache.get(null, new ArrayList<>(), null, null, null, null, null)).thenReturn(dummyData); + + DomainSearch domainSearch = new DomainSearch(cache, null); + List firstPage = domainSearch.getDomainsInGroup(null, + new ArrayList<>(), null, null, null, groupOne, 0, 2, null, null); + assertEquals(2, firstPage.size()); + for (int i = 0; i < firstPage.size(); i++) { + assertEquals(domains.get(i), firstPage.get(i)); + } + } + + @Test + public void getDomains_SingleGroupLastPageLastDomain_ShouldContainLastDomain() throws DiscoveryException { + DomainSearchCache cache = mock(DomainSearchCache.class); + + DummyKey groupOne = new DummyKey("1"); + List domains = Arrays.asList( + DomainSearchTestUtils.mockDomainResult("google.com"), + DomainSearchTestUtils.mockDomainResult("yahoo.com"), + DomainSearchTestUtils.mockDomainResult("bing.com"), + DomainSearchTestUtils.mockDomainResult("amazon.com")); + + Map> dummyData = new HashMap>() { + { + put(groupOne, domains); + } + }; + + when(cache.get(null, new ArrayList<>(), null, null, null, null, null)).thenReturn(dummyData); + + DomainSearch domainSearch = new DomainSearch(cache, null); + List firstPage = domainSearch.getDomainsInGroup(null, + new ArrayList<>(), null, null, null, groupOne, 3, 1, null, null); + assertEquals(1, firstPage.size()); + assertEquals(domains.get(domains.size() - 1), firstPage.get(0)); + } + + @Test + public void getDomains_SingleGroupOversizedOffset_ShouldContainNoDomains() throws DiscoveryException { + DomainSearchCache cache = mock(DomainSearchCache.class); + + DummyKey groupOne = new DummyKey("1"); + List domains = Arrays.asList( + DomainSearchTestUtils.mockDomainResult("google.com"), + DomainSearchTestUtils.mockDomainResult("yahoo.com"), + DomainSearchTestUtils.mockDomainResult("bing.com"), + DomainSearchTestUtils.mockDomainResult("amazon.com")); + + Map> dummyData = new HashMap>() { + { + put(groupOne, domains); + } + }; + + when(cache.get(null, new ArrayList<>(), null, null, null, null, null)).thenReturn(dummyData); + + DomainSearch domainSearch = new DomainSearch(cache, null); + List firstPage = domainSearch.getDomainsInGroup(null, + new ArrayList<>(), null, null, null, groupOne, 20, 5, null, null); + assertEquals(0, firstPage.size()); + } + + @Test + public void getDomains_SingleGroupZeroSizedPage_ShouldContainNoDomains() throws DiscoveryException { + DomainSearchCache cache = mock(DomainSearchCache.class); + + DummyKey groupOne = new DummyKey("1"); + List domains = Arrays.asList( + DomainSearchTestUtils.mockDomainResult("google.com"), + DomainSearchTestUtils.mockDomainResult("yahoo.com"), + DomainSearchTestUtils.mockDomainResult("bing.com"), + DomainSearchTestUtils.mockDomainResult("amazon.com")); + + Map> dummyData = new HashMap>() { + { + put(groupOne, domains); + } + }; + + when(cache.get(null, new ArrayList<>(), null, null, null, null, null)).thenReturn(dummyData); + + DomainSearch domainSearch = new DomainSearch(cache, null); + List firstPage = domainSearch.getDomainsInGroup(null, + new ArrayList<>(), null, null, null, groupOne, 0, 0, null, null); + assertEquals(0, firstPage.size()); + } + + @Test + public void getDomains_MultipleGroupsFullPage_ShouldContainAllDomainsInGroup() throws DiscoveryException { + DomainSearchCache cache = mock(DomainSearchCache.class); + + DummyKey groupOne = new DummyKey("1"); + DummyKey groupTwo = new DummyKey("2"); + DummyKey groupThree = new DummyKey("3"); + + Map> dummyData = new HashMap>() { + { + put(groupOne, Arrays.asList( + DomainSearchTestUtils.mockDomainResult("google.com"), + DomainSearchTestUtils.mockDomainResult("yahoo.com"), + DomainSearchTestUtils.mockDomainResult("bing.com"), + DomainSearchTestUtils.mockDomainResult("amazon.com")) + ); + put(groupTwo, Arrays.asList( + DomainSearchTestUtils.mockDomainResult("facebook.com"), + DomainSearchTestUtils.mockDomainResult("spotify.com"), + DomainSearchTestUtils.mockDomainResult("netbeans.com")) + ); + put(groupThree, Arrays.asList( + DomainSearchTestUtils.mockDomainResult("youtube.com")) + ); + } + }; + + when(cache.get(null, new ArrayList<>(), null, null, null, null, null)).thenReturn(dummyData); + + DomainSearch domainSearch = new DomainSearch(cache, null); + List firstPage = domainSearch.getDomainsInGroup(null, + new ArrayList<>(), null, null, null, groupOne, 0, 3, null, null); + assertEquals(3, firstPage.size()); + } + + @Test + public void getDomains_MultipleGroupsHalfPage_ShouldContainHalfDomainsInGroup() throws DiscoveryException { + DomainSearchCache cache = mock(DomainSearchCache.class); + + DummyKey groupOne = new DummyKey("1"); + DummyKey groupTwo = new DummyKey("2"); + DummyKey groupThree = new DummyKey("3"); + + Map> dummyData = new HashMap>() { + { + put(groupOne, Arrays.asList( + DomainSearchTestUtils.mockDomainResult("google.com"), + DomainSearchTestUtils.mockDomainResult("yahoo.com"), + DomainSearchTestUtils.mockDomainResult("bing.com"), + DomainSearchTestUtils.mockDomainResult("amazon.com")) + ); + put(groupTwo, Arrays.asList( + DomainSearchTestUtils.mockDomainResult("facebook.com"), + DomainSearchTestUtils.mockDomainResult("spotify.com"), + DomainSearchTestUtils.mockDomainResult("netbeans.com")) + ); + put(groupThree, Arrays.asList( + DomainSearchTestUtils.mockDomainResult("youtube.com")) + ); + } + }; + + when(cache.get(null, new ArrayList<>(), null, null, null, null, null)).thenReturn(dummyData); + + DomainSearch domainSearch = new DomainSearch(cache, null); + List firstPage = domainSearch.getDomainsInGroup(null, + new ArrayList<>(), null, null, null, groupTwo, 1, 2, null, null); + assertEquals(2, firstPage.size()); + for (int i = 0; i < firstPage.size(); i++) { + assertEquals(dummyData.get(groupTwo).get(i + 1), firstPage.get(i)); + } + } + + @Test + public void getDomains_SingleGroupSimulatedPaging_ShouldPageThroughAllDomains() throws DiscoveryException { + DomainSearchCache cache = mock(DomainSearchCache.class); + + DummyKey groupOne = new DummyKey("1"); + List domains = Arrays.asList( + DomainSearchTestUtils.mockDomainResult("google.com"), + DomainSearchTestUtils.mockDomainResult("yahoo.com"), + DomainSearchTestUtils.mockDomainResult("bing.com"), + DomainSearchTestUtils.mockDomainResult("amazon.com"), + DomainSearchTestUtils.mockDomainResult("facebook.com"), + DomainSearchTestUtils.mockDomainResult("capitalone.com"), + DomainSearchTestUtils.mockDomainResult("spotify.com"), + DomainSearchTestUtils.mockDomainResult("netsuite.com")); + + Map> dummyData = new HashMap>() { + { + put(groupOne, domains); + } + }; + + when(cache.get(null, new ArrayList<>(), null, null, null, null, null)).thenReturn(dummyData); + + DomainSearch domainSearch = new DomainSearch(cache, null); + + int start = 0; + int size = 2; + while (start + size <= domains.size()) { + List page = domainSearch.getDomainsInGroup(null, + new ArrayList<>(), null, null, null, groupOne, start, size, null, null); + assertEquals(2, page.size()); + for(int i = 0; i < page.size(); i++) { + assertEquals(domains.get(start + i), page.get(i)); + } + + start += size; + } + } + + private class DummyKey extends GroupKey { + + private final String name; + + public DummyKey(String name) { + this.name = name; + } + + @Override + String getDisplayName() { + return name; + } + + @Override + public boolean equals(Object otherKey) { + if (otherKey instanceof GroupKey) { + return this.getDisplayName().equals(((GroupKey) otherKey).getDisplayName()); + } else { + return false; + } + } + + @Override + public int hashCode() { + return this.name.hashCode(); + } + + @Override + public int compareTo(GroupKey o) { + return this.getDisplayName().compareTo(o.getDisplayName()); + } + } +} diff --git a/Core/test/unit/src/org/sleuthkit/autopsy/discovery/search/DomainSearchTestUtils.java b/Core/test/unit/src/org/sleuthkit/autopsy/discovery/search/DomainSearchTestUtils.java new file mode 100755 index 0000000000..8a884a2fac --- /dev/null +++ b/Core/test/unit/src/org/sleuthkit/autopsy/discovery/search/DomainSearchTestUtils.java @@ -0,0 +1,51 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.discovery.search; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import org.sleuthkit.datamodel.Content; + +/** + * Mock utility methods for DomainSearchTests + */ +public class DomainSearchTestUtils { + + private DomainSearchTestUtils() { + + } + + public static ResultDomain mockDomainResult(String domain, long start, long end, + long totalVisits, long visits, long filesDownloaded, long dataSourceId) { + Content dataSource = mockDataSource(dataSourceId); + return new ResultDomain(domain, start, end, totalVisits, + visits, filesDownloaded, dataSource); + } + + public static ResultDomain mockDomainResult(String domain) { + return DomainSearchTestUtils.mockDomainResult(domain, 0, 0, 0, 0, 0, 0); + } + + public static Content mockDataSource(long dataSourceId) { + Content dataSource = mock(Content.class); + when(dataSource.getName()).thenReturn(""); + when(dataSource.getId()).thenReturn(dataSourceId); + return dataSource; + } +} diff --git a/Experimental/nbproject/project.xml b/Experimental/nbproject/project.xml index ee67fbe787..e6cf918c75 100644 --- a/Experimental/nbproject/project.xml +++ b/Experimental/nbproject/project.xml @@ -143,7 +143,7 @@ 10 - 10.21 + 10.22
diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJobLogger.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJobLogger.java index 5bc3f46fac..4a7eb4d975 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJobLogger.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestJobLogger.java @@ -70,7 +70,7 @@ final class AutoIngestJobLogger { */ INFO, /** - * Qualifies a log message about an unexpected event or condtion during + * Qualifies a log message about an unexpected event or condition during * automated ingest processing. */ WARNING, @@ -208,6 +208,18 @@ final class AutoIngestJobLogger { void logDataSourceProcessorSelected(String dsp) throws AutoIngestJobLoggerException, InterruptedException { log(MessageCategory.INFO, "Using data source processor: " + dsp); } + + /** + * Log that a data source is being skipped. + * + * @param dataSourceName The name of the data source + * + * @throws AutoIngestJobLogger.AutoIngestJobLoggerException + * @throws InterruptedException + */ + void logSkippingDataSource(String dataSourceName) throws AutoIngestJobLoggerException, InterruptedException { + log(MessageCategory.INFO, "File type can not currently be processed"); + } /** * Logs the failure of the selected data source processor. diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java index 7c817e7739..a6ece45667 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java @@ -1040,7 +1040,7 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen } } - + /** * A task that submits an input directory scan task to the input directory * scan task executor. @@ -2440,6 +2440,15 @@ final class AutoIngestManager extends Observable implements PropertyChangeListen currentJob.setProcessingStage(AutoIngestJob.Stage.COMPLETED, Date.from(Instant.now())); return; } + + if (SupportedDataSources.shouldSkipFile(dataSource.getPath().toString())) { + Manifest manifest = currentJob.getManifest(); + AutoIngestJobLogger jobLogger = new AutoIngestJobLogger(manifest.getFilePath(), manifest.getDataSourceFileName(), currentJob.getCaseDirectoryPath()); + jobLogger.logSkippingDataSource(dataSource.getPath().toString()); + sysLogger.log(Level.INFO, "Skipping data source that can not be processed ({0})", dataSource.getPath().toString()); + currentJob.setProcessingStage(AutoIngestJob.Stage.COMPLETED, Date.from(Instant.now())); + return; + } if (currentJob.isCanceled() || jobProcessingTaskFuture.isCancelled()) { return; 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 823399e0d0..56a675e256 100755 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties-MERGED +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties-MERGED @@ -205,9 +205,7 @@ 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/SupportedDataSources.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/SupportedDataSources.java new file mode 100644 index 0000000000..9df1587215 --- /dev/null +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/SupportedDataSources.java @@ -0,0 +1,53 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.experimental.autoingest; + +import java.util.Arrays; +import java.util.List; +import org.apache.commons.io.FilenameUtils; + +/** + * Utility class for checking whether a data source/file should be processed + * in an automated setting. The goal is to not spend time analyzing large + * files that Autopsy can not handle yet. + */ +public final class SupportedDataSources { + + private static final List UNSUPPORTED_EXTENSIONS = Arrays.asList("xry", "dar"); + + /** + * Check whether a file should be added to a case, either as a data source or part of a + * logical file set. + * + * @param fileName The name of the file. + * + * @return true if the file is currently unsupported and should be skipped, false otherwise. + */ + public static boolean shouldSkipFile(String fileName) { + String ext = FilenameUtils.getExtension(fileName); + if (ext == null) { + return false; + } + return UNSUPPORTED_EXTENSIONS.contains(ext.toLowerCase()); + } + + private SupportedDataSources() { + // Static class + } +} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/VolatilityProcessor.java b/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/VolatilityProcessor.java index 9282362c1c..d68493e59a 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/VolatilityProcessor.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/volatilityDSP/VolatilityProcessor.java @@ -196,7 +196,7 @@ class VolatilityProcessor { List commandLine = new ArrayList<>(); commandLine.add("\"" + executableFile + "\""); //NON-NLS File memoryImage = new File(memoryImagePath); - commandLine.add("--filename=" + memoryImage.getName()); //NON-NLS + commandLine.add("--filename=" + String.format("\"%s\"",memoryImage.getName())); //NON-NLS if (!profile.isEmpty()) { commandLine.add("--profile=" + profile); //NON-NLS } @@ -213,7 +213,7 @@ class VolatilityProcessor { if (!directory.exists()) { directory.mkdirs(); } - commandLine.add("--dump-dir=" + outputDir); //NON-NLS + commandLine.add("--dump-dir=" + String.format("\"%s\"",outputDir)); //NON-NLS break; default: break; diff --git a/ImageGallery/nbproject/project.xml b/ImageGallery/nbproject/project.xml index b435b5888d..569fe45afc 100644 --- a/ImageGallery/nbproject/project.xml +++ b/ImageGallery/nbproject/project.xml @@ -127,7 +127,7 @@ 10 - 10.21 + 10.22 diff --git a/KeywordSearch/nbproject/project.xml b/KeywordSearch/nbproject/project.xml index 2df46265fa..aa8cb0cf8e 100644 --- a/KeywordSearch/nbproject/project.xml +++ b/KeywordSearch/nbproject/project.xml @@ -127,7 +127,7 @@ 10 - 10.21 + 10.22 diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle.properties-MERGED b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle.properties-MERGED index 44db83039a..95066b41d1 100755 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle.properties-MERGED +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle.properties-MERGED @@ -36,7 +36,7 @@ KeywordSearchResultFactory.createNodeForKey.noResultsFound.text=No results found KeywordSearchResultFactory.query.exception.msg=Could not perform the query OpenIDE-Module-Display-Category=Ingest Module -OpenIDE-Module-Long-Description=Keyword Search ingest module.\n\nThe module indexes files found in the disk image at ingest time.\nIt then periodically runs the search on the indexed files using one or more keyword lists (containing pure words and/or regular expressions) and posts results.\n\n\The module also contains additional tools integrated in the main GUI, such as keyword list configuration, keyword search bar in the top-right corner, extracted text viewer and search results viewer showing highlighted keywords found. +OpenIDE-Module-Long-Description=Keyword Search ingest module.\n\nThe module indexes files found in the disk image at ingest time.\nIt then periodically runs the search on the indexed files using one or more keyword lists (containing pure words and/or regular expressions) and posts results.\n\nThe module also contains additional tools integrated in the main GUI, such as keyword list configuration, keyword search bar in the top-right corner, extracted text viewer and search results viewer showing highlighted keywords found. OpenIDE-Module-Name=KeywordSearch OptionsCategory_Name_KeywordSearchOptions=Keyword Search OptionsCategory_Keywords_KeywordSearchOptions=Keyword Search diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/DropdownListSearchPanel.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/DropdownListSearchPanel.java index d12f6a0012..7b84080217 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/DropdownListSearchPanel.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/DropdownListSearchPanel.java @@ -42,6 +42,7 @@ import javax.swing.table.TableColumn; import org.openide.util.NbBundle; import org.openide.util.actions.SystemAction; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.guiutils.SimpleTableCellRenderer; import org.sleuthkit.autopsy.ingest.IngestManager; /** @@ -94,6 +95,7 @@ class DropdownListSearchPanel extends AdHocSearchPanel { column.setCellRenderer(new LeftCheckBoxRenderer()); } else { column.setPreferredWidth(((int) (leftWidth * 0.89))); + column.setCellRenderer(new SimpleTableCellRenderer()); } } final int rightWidth = rightPane.getPreferredSize().width; @@ -105,6 +107,7 @@ class DropdownListSearchPanel extends AdHocSearchPanel { column.setPreferredWidth(((int) (rightWidth * 0.38))); } } + keywordsTable.setDefaultRenderer(String.class, new SimpleTableCellRenderer()); loader = XmlKeywordSearchList.getCurrent(); listsTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() { diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalEditListPanel.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalEditListPanel.java index 8dbf3032a4..35b141f98c 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalEditListPanel.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalEditListPanel.java @@ -36,6 +36,7 @@ import org.netbeans.spi.options.OptionsPanelController; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.corecomponents.OptionsPanel; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.guiutils.SimpleTableCellRenderer; import org.sleuthkit.autopsy.ingest.IngestManager; /** @@ -76,6 +77,7 @@ class GlobalEditListPanel extends javax.swing.JPanel implements ListSelectionLis } keywordTable.setCellSelectionEnabled(false); keywordTable.setRowSelectionAllowed(true); + keywordTable.setDefaultRenderer(String.class, new SimpleTableCellRenderer()); final ListSelectionModel lsm = keywordTable.getSelectionModel(); lsm.addListSelectionListener(new ListSelectionListener() { diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalListsManagementPanel.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalListsManagementPanel.java index 554f791d05..b086ffc614 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalListsManagementPanel.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalListsManagementPanel.java @@ -38,6 +38,7 @@ import org.openide.util.NbBundle; import org.sleuthkit.autopsy.corecomponents.OptionsPanel; import org.sleuthkit.autopsy.coreutils.ModuleSettings; import org.sleuthkit.autopsy.coreutils.PlatformUtil; +import org.sleuthkit.autopsy.guiutils.SimpleTableCellRenderer; import org.sleuthkit.autopsy.ingest.IngestManager; /** @@ -63,6 +64,7 @@ class GlobalListsManagementPanel extends javax.swing.JPanel implements OptionsPa listsTable.setTableHeader(null); listsTable.setShowHorizontalLines(false); listsTable.setShowVerticalLines(false); + listsTable.setDefaultRenderer(String.class, new SimpleTableCellRenderer()); exportButton.setToolTipText(NbBundle.getMessage(this.getClass(), "KeywordSearchEditListPanel.customizeComponents.exportToFile")); copyListButton.setToolTipText(NbBundle.getMessage(this.getClass(), "KeywordSearchEditListPanel.customizeComponents.saveCurrentWIthNewNameToolTip")); listsTable.getParent().setBackground(listsTable.getBackground()); diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchJobSettingsPanel.form b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchJobSettingsPanel.form index ae97e69fb1..ac3cdd5782 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchJobSettingsPanel.form +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchJobSettingsPanel.form @@ -99,6 +99,9 @@ + + + diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchJobSettingsPanel.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchJobSettingsPanel.java index ae8797b992..a71485cbb1 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchJobSettingsPanel.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchJobSettingsPanel.java @@ -32,6 +32,7 @@ import javax.swing.table.AbstractTableModel; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.TableColumn; import org.sleuthkit.autopsy.coreutils.StringExtract.StringExtractUnicodeTable.SCRIPT; +import org.sleuthkit.autopsy.guiutils.SimpleTableCellRenderer; import org.sleuthkit.autopsy.ingest.IngestModuleIngestJobSettings; import org.sleuthkit.autopsy.ingest.IngestModuleIngestJobSettingsPanel; import org.sleuthkit.autopsy.keywordsearch.KeywordSearchIngestModule.StringsExtractOptions; @@ -84,7 +85,6 @@ public final class KeywordSearchJobSettingsPanel extends IngestModuleIngestJobSe if (i == 0) { column.setPreferredWidth(((int) (width * 0.07))); } else { - column.setCellRenderer(new KeywordTableCellRenderer()); column.setPreferredWidth(((int) (width * 0.92))); } } @@ -183,25 +183,6 @@ public final class KeywordSearchJobSettingsPanel extends IngestModuleIngestJobSe displayEncodings(); tableModel.fireTableDataChanged(); } - - /** - * Simple TableCellRenderer to add tool tips to cells. - */ - private static final class KeywordTableCellRenderer extends DefaultTableCellRenderer{ - - private static final long serialVersionUID = 1L; - - @Override - public Component getTableCellRendererComponent( - JTable table, Object value, - boolean isSelected, boolean hasFocus, - int row, int column) { - JLabel label = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); - label.setToolTipText(label.getText()); - return label; - } - - } private class KeywordListsTableModel extends AbstractTableModel { @@ -278,6 +259,7 @@ public final class KeywordSearchJobSettingsPanel extends IngestModuleIngestJobSe listsTable.setShowHorizontalLines(false); listsTable.setShowVerticalLines(false); listsScrollPane.setViewportView(listsTable); + listsTable.setDefaultRenderer(String.class, new SimpleTableCellRenderer()); titleLabel.setText(org.openide.util.NbBundle.getMessage(KeywordSearchJobSettingsPanel.class, "KeywordSearchJobSettingsPanel.titleLabel.text")); // NOI18N diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Server.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Server.java index 05a02bf11b..88c8a774f1 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Server.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Server.java @@ -18,6 +18,7 @@ */ package org.sleuthkit.autopsy.keywordsearch; +import com.google.common.util.concurrent.ThreadFactoryBuilder; import java.awt.event.ActionEvent; import java.beans.PropertyChangeListener; import java.io.BufferedReader; @@ -42,9 +43,11 @@ import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Random; +import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.logging.Level; +import static java.util.stream.Collectors.toList; import javax.swing.AbstractAction; import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.SolrRequest; @@ -73,6 +76,7 @@ import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.coreutils.ModuleSettings; import org.sleuthkit.autopsy.coreutils.PlatformUtil; +import org.sleuthkit.autopsy.coreutils.ThreadUtils; import org.sleuthkit.autopsy.healthmonitor.HealthMonitor; import org.sleuthkit.autopsy.healthmonitor.TimingMetric; import org.sleuthkit.autopsy.keywordsearchservice.KeywordSearchServiceException; @@ -1468,6 +1472,15 @@ public class Server { // the server to access a core needs to be built from a URL with the // core in it, and is only good for core-specific operations private final HttpSolrServer solrCore; + + private final int maxBufferSize; + private final List buffer; + private final Object bufferLock; + + private final ScheduledThreadPoolExecutor periodicTasksExecutor; + private static final long PERIODIC_BATCH_SEND_INTERVAL_MINUTES = 10; + private static final int NUM_BATCH_UPDATE_RETRIES = 10; + private static final long SLEEP_BETWEEN_RETRIES_MS = 10000; // 10 seconds private final int QUERY_TIMEOUT_MILLISECONDS = 86400000; // 24 Hours = 86,400,000 Milliseconds @@ -1475,6 +1488,7 @@ public class Server { this.name = name; this.caseType = caseType; this.textIndex = index; + bufferLock = new Object(); this.solrCore = new HttpSolrServer(currentSolrServer.getBaseURL() + "/" + name); //NON-NLS @@ -1490,7 +1504,45 @@ public class Server { solrCore.setAllowCompression(true); solrCore.setParser(new XMLResponseParser()); // binary parser is used by default + // document batching + maxBufferSize = org.sleuthkit.autopsy.keywordsearch.UserPreferences.getDocumentsQueueSize(); + logger.log(Level.INFO, "Using Solr document queue size = {0}", maxBufferSize); //NON-NLS + buffer = new ArrayList<>(maxBufferSize); + periodicTasksExecutor = new ScheduledThreadPoolExecutor(1, new ThreadFactoryBuilder().setNameFormat("periodic-batched-document-task-%d").build()); //NON-NLS + periodicTasksExecutor.scheduleWithFixedDelay(new SendBatchedDocumentsTask(), PERIODIC_BATCH_SEND_INTERVAL_MINUTES, PERIODIC_BATCH_SEND_INTERVAL_MINUTES, TimeUnit.MINUTES); } + + /** + * A task that periodically sends batched documents to Solr. Batched documents + * get sent automatically as soon as the batching buffer is gets full. However, + * if the buffer is not full, we want to periodically send the batched documents + * so that users are able to see them in their keyword searches. + */ + private final class SendBatchedDocumentsTask implements Runnable { + + @Override + public void run() { + List clone; + synchronized (bufferLock) { + + if (buffer.isEmpty()) { + return; + } + + // Buffer is full. Make a clone and release the lock, so that we don't + // hold other ingest threads + clone = buffer.stream().collect(toList()); + buffer.clear(); + } + + try { + // send the cloned list to Solr + sendBufferedDocs(clone); + } catch (KeywordSearchModuleException ex) { + logger.log(Level.SEVERE, "Periodic batched document update failed", ex); //NON-NLS + } + } + } /** * Get the name of the core @@ -1531,6 +1583,20 @@ public class Server { } private void commit() throws SolrServerException { + List clone; + synchronized (bufferLock) { + // Make a clone and release the lock, so that we don't + // hold other ingest threads + clone = buffer.stream().collect(toList()); + buffer.clear(); + } + + try { + sendBufferedDocs(clone); + } catch (KeywordSearchModuleException ex) { + throw new SolrServerException(NbBundle.getMessage(this.getClass(), "Server.commit.exception.msg"), ex); + } + try { //commit and block solrCore.commit(true, true); @@ -1548,14 +1614,77 @@ public class Server { solrCore.deleteByQuery(deleteQuery); } + /** + * Add a Solr document for indexing. Documents get batched instead of + * being immediately sent to Solr (unless batch size = 1). + * + * @param doc Solr document to be indexed. + * + * @throws KeywordSearchModuleException + */ void addDocument(SolrInputDocument doc) throws KeywordSearchModuleException { + + List clone; + synchronized (bufferLock) { + buffer.add(doc); + // buffer documents if the buffer is not full + if (buffer.size() < maxBufferSize) { + return; + } + + // Buffer is full. Make a clone and release the lock, so that we don't + // hold other ingest threads + clone = buffer.stream().collect(toList()); + buffer.clear(); + } + + // send the cloned list to Solr + sendBufferedDocs(clone); + } + + /** + * Send a list of buffered documents to Solr. + * + * @param docBuffer List of buffered Solr documents + * + * @throws KeywordSearchModuleException + */ + private void sendBufferedDocs(List docBuffer) throws KeywordSearchModuleException { + + if (docBuffer.isEmpty()) { + return; + } + try { - solrCore.add(doc); - } catch (Exception ex) { - // Solr throws a lot of unexpected exception types - logger.log(Level.SEVERE, "Could not add document to index via update handler: " + doc.getField("id"), ex); //NON-NLS - throw new KeywordSearchModuleException( - NbBundle.getMessage(this.getClass(), "Server.addDoc.exception.msg", doc.getField("id")), ex); //NON-NLS + boolean success = true; + for (int reTryAttempt = 0; reTryAttempt < NUM_BATCH_UPDATE_RETRIES; reTryAttempt++) { + try { + success = true; + solrCore.add(docBuffer); + } catch (Exception ex) { + success = false; + if (reTryAttempt < NUM_BATCH_UPDATE_RETRIES - 1) { + logger.log(Level.WARNING, "Unable to send document batch to Solr. Re-trying...", ex); //NON-NLS + try { + Thread.sleep(SLEEP_BETWEEN_RETRIES_MS); + } catch (InterruptedException ignore) { + throw new KeywordSearchModuleException( + NbBundle.getMessage(this.getClass(), "Server.addDocBatch.exception.msg"), ex); //NON-NLS + } + } + } + if (success) { + if (reTryAttempt > 0) { + logger.log(Level.INFO, "Batch update suceeded after {0} re-try", reTryAttempt); //NON-NLS + } + return; + } + } + // if we are here, it means all re-try attempts failed + logger.log(Level.SEVERE, "Unable to send document batch to Solr. All re-try attempts failed!"); //NON-NLS + throw new KeywordSearchModuleException(NbBundle.getMessage(this.getClass(), "Server.addDocBatch.exception.msg")); //NON-NLS + } finally { + docBuffer.clear(); } } @@ -1606,6 +1735,11 @@ public class Server { } synchronized void close() throws KeywordSearchModuleException { + + // stop the periodic batch update task. If the task is already running, + // allow it to finish. + ThreadUtils.shutDownTaskExecutor(periodicTasksExecutor); + // We only unload cores for "single-user" cases. if (this.caseType == CaseType.MULTI_USER_CASE) { return; diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/UserPreferences.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/UserPreferences.java new file mode 100755 index 0000000000..11f7654f8c --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/UserPreferences.java @@ -0,0 +1,76 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.keywordsearch; + +import java.util.prefs.BackingStoreException; +import java.util.prefs.PreferenceChangeListener; +import java.util.prefs.Preferences; +import org.openide.util.NbPreferences; + +/** + * Provides convenient access to a Preferences node for user preferences with + * default values. + */ +final class UserPreferences { + + private static final Preferences preferences = NbPreferences.forModule(UserPreferences.class); + private static final String INDEXING_DOC_QUEUE_SIZE = "IndexingDocumentQueueSize"; //NON-NLS + private static final int DEFAULT_INDEXING_DOC_QUEUE_SIZE = 30; //NON-NLS + + // Prevent instantiation. + private UserPreferences() { + } + + /** + * Reload all preferences from disk. This is only needed if the preferences + * file is being directly modified on disk while Autopsy is running. + * + * @throws BackingStoreException + */ + public static void reloadFromStorage() throws BackingStoreException { + preferences.sync(); + } + + /** + * Saves the current preferences to storage. This is only needed if the + * preferences files are going to be copied to another location while + * Autopsy is running. + * + * @throws BackingStoreException + */ + public static void saveToStorage() throws BackingStoreException { + preferences.flush(); + } + + public static void addChangeListener(PreferenceChangeListener listener) { + preferences.addPreferenceChangeListener(listener); + } + + public static void removeChangeListener(PreferenceChangeListener listener) { + preferences.removePreferenceChangeListener(listener); + } + + public static void setDocumentsQueueSize(int size) { + preferences.putInt(INDEXING_DOC_QUEUE_SIZE, size); + } + + public static int getDocumentsQueueSize() { + return preferences.getInt(INDEXING_DOC_QUEUE_SIZE, DEFAULT_INDEXING_DOC_QUEUE_SIZE); + } +} \ No newline at end of file diff --git a/NEWS.txt b/NEWS.txt index 67620c29a0..7f37faa9c8 100644 --- a/NEWS.txt +++ b/NEWS.txt @@ -1,3 +1,38 @@ +---------------- VERSION 4.17.0 -------------- +GUI: +Expanded the Data Source Summary panel to show recent activity, past cases, analysis results, etc. Also made this available from the main UI when a data source is selected. +Expanded Discovery UI to support searching for and basic display of web domains. It collapses the various web artifacts into a single view. + +Ingest Modules: +Added iOS Analyzer module based on iLEAPP and a subset of its artifacts. +New Picture Analyzer module that does EXIF extraction and HEIC conversion. HEIC/HEIF images are converted to JPEGs that retain EXIF using ImageMagick (replaces the previous EXIF ingest module). +Added support for the latest version of Edge browser that is based on Chromium into Recent Activity. Other Chromium-based browsers are also supported. +Updated the rules that search Web History artifacts for search queries. Expanded module to support multiple search engines for ambiguous URLs. +Bluetooth pairing artifacts are created based on RegRipper output. +Prefetch artifacts record the full path of exes. +PhotoRec module allows you to include or exclude specific file types. +Upgraded to Tika 1.23. + +Performance: +Documents are added to Solr in batches instead of one by one. +More efficient queries to find WAL files for SQLite databases. +Use a local drive for temp files for multi-user cases instead of the shared folder. + +Command Line +Command line support for report profiles. +Restored support for Windows file type association for opening a case in Autopsy by double clicking case metadata (.aut) file. +Better feedback for command line argument errors. + +Misc: +Updated versions of libvmdk, libvhdi, and libewf. +Persona UI fixes: Pre-populate account and changed order of New Persona dialog. +Streaming ingest support added to auto ingest. +Recent Activity module processes now use the global timeout. +Option to include Autopsy executable in portable case (Windows only.) +Upgraded to NetBeans 11 Rich Client Platform. +Added debug feature to save the stack trace on all threads. + + ---------------- VERSION 4.16.0 -------------- Ingest: - Added streaming ingest capability for disk images that allow files to be analyzed as soon as they are added to the database. diff --git a/RecentActivity/nbproject/project.xml b/RecentActivity/nbproject/project.xml index 6ecdb80735..5ebd78e0cd 100644 --- a/RecentActivity/nbproject/project.xml +++ b/RecentActivity/nbproject/project.xml @@ -60,7 +60,7 @@ 10 - 10.21 + 10.22 diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Bundle.properties b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Bundle.properties index 4a10106355..bf24ea35a3 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Bundle.properties +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Bundle.properties @@ -98,4 +98,6 @@ SearchEngineURLQueryAnalyzer.domainSubStr.none=NONE SearchEngineURLQueryAnalyzer.toString=Name: {0}\nDomain Substring: {1}\nCount: {2}\nSplit Tokens: \n{3} SearchEngineURLQueryAnalyzer.parentModuleName.noSpace=RecentActivity SearchEngineURLQueryAnalyzer.parentModuleName=Recent Activity +ExtractWebAccountType.moduleName.text=Web Account Type +ExtractWebAccountType.parentModuleName=Recent Activity UsbDeviceIdMapper.parseAndLookup.text=Product: {0} diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Bundle.properties-MERGED b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Bundle.properties-MERGED index 6a0dd03eef..a842d76f74 100755 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Bundle.properties-MERGED +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Bundle.properties-MERGED @@ -72,6 +72,9 @@ ExtractSru_process_errormsg_find_software_hive=Unable to find SOFTWARE HIVE file ExtractSru_process_errormsg_find_srudb_dat=Unable to find srudb.dat file ExtractSru_process_errormsg_write_software_hive=Unable to write SOFTWARE HIVE file ExtractSru_process_errormsg_write_srudb_dat=Unable to write srudb.dat file +ExtractWebAccountType.role.admin=Administrator role +ExtractWebAccountType.role.moderator=Moderator role +ExtractWebAccountType.role.user=User role ExtractZone_Internet=Internet Zone ExtractZone_Local_Intranet=Local Intranet Zone ExtractZone_Local_Machine=Local Machine Zone @@ -156,13 +159,19 @@ Firefox.getDlV24.errMsg.errAnalyzeFile={0}: Error while trying to analyze file:{ Firefox.getDlV24.errMsg.errParsingArtifacts={0}: Error parsing {1} Firefox web download artifacts. Progress_Message_Analyze_Registry=Analyzing Registry Files Progress_Message_Analyze_Usage=Data Sources Usage Analysis +# {0} - browserName Progress_Message_Chrome_AutoFill=Chrome Auto Fill Browser {0} +# {0} - browserName Progress_Message_Chrome_Bookmarks=Chrome Bookmarks Browser {0} Progress_Message_Chrome_Cache=Chrome Cache +# {0} - browserName Progress_Message_Chrome_Cookies=Chrome Cookies Browser {0} +# {0} - browserName Progress_Message_Chrome_Downloads=Chrome Downloads Browser {0} Progress_Message_Chrome_FormHistory=Chrome Form History +# {0} - browserName Progress_Message_Chrome_History=Chrome History Browser {0} +# {0} - browserName Progress_Message_Chrome_Logins=Chrome Logins Browser {0} Progress_Message_Edge_Bookmarks=Microsoft Edge Bookmarks Progress_Message_Edge_Cookies=Microsoft Edge Cookies @@ -225,6 +234,8 @@ SearchEngineURLQueryAnalyzer.domainSubStr.none=NONE SearchEngineURLQueryAnalyzer.toString=Name: {0}\nDomain Substring: {1}\nCount: {2}\nSplit Tokens: \n{3} SearchEngineURLQueryAnalyzer.parentModuleName.noSpace=RecentActivity SearchEngineURLQueryAnalyzer.parentModuleName=Recent Activity +ExtractWebAccountType.moduleName.text=Web Account Type +ExtractWebAccountType.parentModuleName=Recent Activity Shellbag_Artifact_Display_Name=Shell Bags Shellbag_Key_Attribute_Display_Name=Key Shellbag_Last_Write_Attribute_Display_Name=Last Write diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ChromeCacheExtractor.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ChromeCacheExtractor.java index ba34a98620..aedf34097f 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ChromeCacheExtractor.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ChromeCacheExtractor.java @@ -44,6 +44,7 @@ import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.casemodule.services.FileManager; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.NetworkUtils; import org.sleuthkit.autopsy.datamodel.ContentUtils; import org.sleuthkit.autopsy.ingest.DataSourceIngestModuleProgress; import org.sleuthkit.autopsy.ingest.IngestJobContext; @@ -519,9 +520,11 @@ final class ChromeCacheExtractor { BlackboardArtifact webCacheArtifact = cacheEntryFile.newArtifact(ARTIFACT_TYPE.TSK_WEB_CACHE); if (webCacheArtifact != null) { Collection webAttr = new ArrayList<>(); + String url = cacheEntry.getKey() != null ? cacheEntry.getKey() : ""; webAttr.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL, - moduleName, - ((cacheEntry.getKey() != null) ? cacheEntry.getKey() : ""))); + moduleName, url)); + webAttr.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN, + moduleName, NetworkUtils.extractDomain(url))); webAttr.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_CREATED, moduleName, cacheEntry.getCreationTime())); webAttr.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_HEADERS, diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Chromium.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Chromium.java index b63e33ecdf..9d2318278b 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Chromium.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Chromium.java @@ -96,13 +96,18 @@ class Chromium extends Extract { .build(); - @Messages({ + @Messages({"# {0} - browserName", "Progress_Message_Chrome_History=Chrome History Browser {0}", + "# {0} - browserName", "Progress_Message_Chrome_Bookmarks=Chrome Bookmarks Browser {0}", + "# {0} - browserName", "Progress_Message_Chrome_Cookies=Chrome Cookies Browser {0}", + "# {0} - browserName", "Progress_Message_Chrome_Downloads=Chrome Downloads Browser {0}", "Progress_Message_Chrome_FormHistory=Chrome Form History", + "# {0} - browserName", "Progress_Message_Chrome_AutoFill=Chrome Auto Fill Browser {0}", + "# {0} - browserName", "Progress_Message_Chrome_Logins=Chrome Logins Browser {0}", "Progress_Message_Chrome_Cache=Chrome Cache", }) diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractEdge.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractEdge.java index aeb2b27954..d9f8278a1b 100755 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractEdge.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractEdge.java @@ -575,10 +575,10 @@ final class ExtractEdge extends Extract { List commandLine = new ArrayList<>(); commandLine.add(dumperPath); commandLine.add("/table"); //NON-NLS - commandLine.add(inputFilePath); + commandLine.add(String.format("\"%s\"",inputFilePath)); commandLine.add("*"); //NON-NLS commandLine.add("/scomma"); //NON-NLS - commandLine.add(outputDir + "\\" + "*.csv"); //NON-NLS + commandLine.add(String.format("\"%s\"",outputDir + "\\" + "*.csv")); //NON-NLS ProcessBuilder processBuilder = new ProcessBuilder(commandLine); processBuilder.redirectOutput(outputFilePath.toFile()); diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractIE.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractIE.java index d128c09dc7..576a02a393 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractIE.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractIE.java @@ -400,7 +400,7 @@ class ExtractIE extends Extract { List commandLine = new ArrayList<>(); commandLine.add(JAVA_PATH); commandLine.add("-cp"); //NON-NLS - commandLine.add(PASCO_LIB_PATH); + commandLine.add(String.format("\"%s\"",PASCO_LIB_PATH)); commandLine.add("isi.pasco2.Main"); //NON-NLS commandLine.add("-T"); //NON-NLS commandLine.add("history"); //NON-NLS diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractPrefetch.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractPrefetch.java index 4771b76223..88f0019209 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractPrefetch.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractPrefetch.java @@ -177,9 +177,9 @@ final class ExtractPrefetch extends Extract { final Path errFilePath = Paths.get(tempOutPath, PREFETCH_ERROR_FILE_NAME); List commandLine = new ArrayList<>(); - commandLine.add(prefetchExePath); - commandLine.add(prefetchDir); //NON-NLS - commandLine.add(tempOutFile); + commandLine.add(String.format("\"%s\"",prefetchExePath)); + commandLine.add(String.format("\"%s\"",prefetchDir)); //NON-NLS + commandLine.add(String.format("\"%s\"",tempOutFile)); ProcessBuilder processBuilder = new ProcessBuilder(commandLine); processBuilder.redirectOutput(outputFilePath.toFile()); diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractRegistry.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractRegistry.java index 7ff07455cc..d4cf0e9c66 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractRegistry.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractRegistry.java @@ -472,7 +472,7 @@ class ExtractRegistry extends Extract { commandLine.add(cmd); } commandLine.add("-r"); //NON-NLS - commandLine.add(hiveFilePath); + commandLine.add(String.format("\"%s\"",hiveFilePath)); commandLine.add("-f"); //NON-NLS commandLine.add(hiveFileType); @@ -2044,7 +2044,7 @@ class ExtractRegistry extends Extract { if (line.contains("Group Name")) { String value = line.replaceAll("Group Name\\s*?:", "").trim(); groupName = (value.replaceAll("\\[\\d*?\\]", "")).trim(); - int startIndex = value.indexOf('['); + int startIndex = value.indexOf(" [") + 1; int endIndex = value.indexOf(']'); if (startIndex != -1 && endIndex != -1) { diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractSru.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractSru.java index fb85593953..2f6cf0a303 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractSru.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractSru.java @@ -247,10 +247,10 @@ final class ExtractSru extends Extract { final Path errFilePath = Paths.get(tempOutPath, SRU_ERROR_FILE_NAME); List commandLine = new ArrayList<>(); - commandLine.add(sruExePath); + commandLine.add(String.format("\"%s\"",sruExePath)); commandLine.add(sruFile); //NON-NLS commandLine.add(softwareHiveFile); - commandLine.add(tempOutFile); + commandLine.add(String.format("\"%s\"",tempOutFile)); ProcessBuilder processBuilder = new ProcessBuilder(commandLine); processBuilder.redirectOutput(outputFilePath.toFile()); diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractWebAccountType.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractWebAccountType.java new file mode 100644 index 0000000000..0c05907fb6 --- /dev/null +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractWebAccountType.java @@ -0,0 +1,374 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.recentactivity; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Collection; +import java.util.Objects; +import java.util.logging.Level; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.ingest.DataSourceIngestModuleProgress; +import org.sleuthkit.autopsy.ingest.IngestJobContext; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.TskCoreException; + + +/** + * Attempts to determine a user's role on a domain based on the URL. + */ +class ExtractWebAccountType extends Extract { + + private static final Logger logger = Logger.getLogger(ExtractWebAccountType.class.getName()); + + ExtractWebAccountType() { + moduleName = NbBundle.getMessage(ExtractWebAccountType.class, "ExtractWebAccountType.moduleName.text"); + } + + private void extractDomainRoles(Content dataSource, IngestJobContext context) { + try { + // Get web history blackboard artifacts + Collection listArtifacts = currentCase.getSleuthkitCase().getBlackboard().getArtifacts( + Arrays.asList(new BlackboardArtifact.Type(BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_HISTORY)), + Arrays.asList(dataSource.getId())); + logger.log(Level.INFO, "Processing {0} blackboard artifacts.", listArtifacts.size()); //NON-NLS + + // Set up collector for roles + RoleProcessor roleProcessor = new RoleProcessor(context); + + // Process each URL + for (BlackboardArtifact artifact : listArtifacts) { + if (context.dataSourceIngestIsCancelled()) { + return; + } + + findRolesForUrl(artifact, roleProcessor); + } + + // Create artifacts + roleProcessor.createArtifacts(); + + } catch (TskCoreException e) { + logger.log(Level.SEVERE, "Encountered error retrieving artifacts for domain role analysis", e); //NON-NLS + } + } + + /** + * Extract and store any role found in the given artifact. + * + * @param artifact The original artifact + * @param roleProcessor Object to collect and process domain roles. + * + * @throws TskCoreException + */ + private void findRolesForUrl(BlackboardArtifact artifact, RoleProcessor roleProcessor) throws TskCoreException { + + BlackboardAttribute urlAttr = artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL)); + if (urlAttr == null) { + return; + } + + BlackboardAttribute domainAttr = artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN)); + if (domainAttr == null) { + return; + } + + String url = urlAttr.getValueString().toLowerCase(); + String domain = domainAttr.getValueString().toLowerCase(); + + findMyBbRole(url, domain, artifact, roleProcessor); + findPhpBbRole(url, domain, artifact, roleProcessor); + findJoomlaRole(url, domain, artifact, roleProcessor); + findWordPressRole(url, domain, artifact, roleProcessor); + } + + /** + * Extract myBB role. + * + * @param url The full URL. + * @param domain The domain. + * @param artifact The original artifact. + * @param roleProcessor Object to collect and process domain roles. + */ + private void findMyBbRole(String url, String domain, BlackboardArtifact artifact, RoleProcessor roleProcessor) { + String platformName = "myBB platform"; // NON-NLS + + if (url.contains("/admin/index.php")) { + roleProcessor.addRole(domain, platformName, Role.ADMIN, url, artifact); + } else if (url.contains("/modcp.php")) { + roleProcessor.addRole(domain, platformName, Role.MOD, url, artifact); + } else if (url.contains("/usercp.php")) { + roleProcessor.addRole(domain, platformName, Role.USER, url, artifact); + } + } + + /** + * Extract phpBB role. + * + * @param url The full URL. + * @param domain The domain. + * @param artifact The original artifact. + * @param roleProcessor Object to collect and process domain roles. + */ + private void findPhpBbRole(String url, String domain, BlackboardArtifact artifact, RoleProcessor roleProcessor) { + String platformName = "phpBB platform"; // NON-NLS + + if (url.contains("/adm/index.php")) { + roleProcessor.addRole(domain, platformName, Role.ADMIN, url, artifact); + } else if (url.contains("/mcp.php")) { + roleProcessor.addRole(domain, platformName, Role.MOD, url, artifact); + } else if (url.contains("/ucp.php")) { + roleProcessor.addRole(domain, platformName, Role.USER, url, artifact); + } + } + + /** + * Extract Joomla role. + * + * @param url The full URL. + * @param domain The domain. + * @param artifact The original artifact. + * @param roleProcessor Object to collect and process domain roles. + */ + private void findJoomlaRole(String url, String domain, BlackboardArtifact artifact, RoleProcessor roleProcessor) { + String platformName = "Joomla platform"; // NON-NLS + + if (url.contains("/administrator/index.php")) { // NON-NLS + roleProcessor.addRole(domain, platformName, Role.ADMIN, url, artifact); + } + } + + /** + * Extract WordPress role. + * + * @param url The full URL. + * @param domain The domain. + * @param artifact The original artifact. + * @param roleProcessor Object to collect and process domain roles. + */ + private void findWordPressRole(String url, String domain, BlackboardArtifact artifact, RoleProcessor roleProcessor) { + String platformName = "WordPress platform"; // NON-NLS + + // For WordPress, any logged in user can get to /wp-admin/, /wp-admin/index.php and /wp-admin/profile.php, so + // assume that any other .php file will indicate an administrator + if (url.contains("/wp-admin/")) { + + if (url.endsWith("/wp-admin/") + || url.contains("/wp-admin/index.php") + || url.contains("/wp-admin/profile.php")) { + roleProcessor.addRole(domain, platformName, Role.USER, url, artifact); + } else { + roleProcessor.addRole(domain, platformName, Role.ADMIN, url, artifact); + } + } + } + + @Override + void process(Content dataSource, IngestJobContext context, DataSourceIngestModuleProgress progressBar) { + extractDomainRoles(dataSource, context); + } + + + /** + * Collects data for making blackboard artifacts. + * + * We only want a max of one role per domain, and the role should be the + * highest level found. The full URL and associated file will belong to the first + * artifact found with the recorded level. + */ + private class RoleProcessor { + private final IngestJobContext context; + private final Map roles = new HashMap<>(); + + RoleProcessor(IngestJobContext context) { + this.context = context; + } + + /** + * Add a role to the map if: + * - This is the first time we've seen this domain/platform + * - The level of the role is higher than previously seen for this domain/platform + * + * @param domain The domain. + * @param platform The probable platform for this role. + * @param role The role level. + * @param url The URL (stored for efficiency). + * @param artifact The original blackboard artifact the URL came from. + */ + void addRole(String domain, String platform, Role role, String url, BlackboardArtifact artifact) { + RoleKey key = new RoleKey(domain, platform); + if ((! roles.containsKey(key)) || + (roles.containsKey(key) && (role.getRank() > roles.get(key).getRole().getRank()))) { + roles.put(key, new DomainRole(domain, platform, role, url, artifact)); + } + } + + /** + * Create artifacts for the domain roles. + */ + void createArtifacts() { + + if (roles.isEmpty()) { + logger.log(Level.INFO, "Didn't find any web accounts."); + return; + } else { + logger.log(Level.INFO, "Found {0} web accounts.", roles.keySet().size()); + } + + try { + for (RoleKey key : roles.keySet()) { + if (context.dataSourceIngestIsCancelled()) { + return; + } + + DomainRole role = roles.get(key); + + AbstractFile file = tskCase.getAbstractFileById(role.getArtifact().getObjectID()); + if (file == null) { + continue; + } + + String desc = role.getRole().getDesc() + " (" + role.getPlatform() + ")"; // NON-NLS + + Collection bbattributes = new ArrayList<>(); + bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN, + NbBundle.getMessage(this.getClass(), + "ExtractWebAccountType.parentModuleName"), role.getDomain())); + bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_TEXT, + NbBundle.getMessage(this.getClass(), + "ExtractWebAccountType.parentModuleName"), desc)); + bbattributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL, + NbBundle.getMessage(this.getClass(), + "ExtractWebAccountType.parentModuleName"), role.getUrl())); + + postArtifact(createArtifactWithAttributes(BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_ACCOUNT_TYPE, file, bbattributes)); + } + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Error creating web accounts", ex); + } + } + } + + /** + * Possible roles with rank and display name. + */ + @NbBundle.Messages({ + "ExtractWebAccountType.role.user=User role", + "ExtractWebAccountType.role.moderator=Moderator role", + "ExtractWebAccountType.role.admin=Administrator role" + }) + private enum Role { + USER(Bundle.ExtractWebAccountType_role_user(), 0), + MOD(Bundle.ExtractWebAccountType_role_moderator(), 1), + ADMIN(Bundle.ExtractWebAccountType_role_admin(), 2); + + private final String desc; + private final int rank; + + Role(String desc, int rank) { + this.desc = desc; + this.rank = rank; + } + + String getDesc() { + return desc; + } + + int getRank() { + return rank; + } + } + + /** + * Holds key to retrieve data for a given domain/platform. + */ + private class RoleKey { + private final String domain; + private final String platform; + + RoleKey(String domain, String platform) { + this.domain = domain; + this.platform = platform; + } + + @Override + public boolean equals(Object other) { + if (! (other instanceof RoleKey)) { + return false; + } + + RoleKey otherKey = (RoleKey)other; + return (domain.equals(otherKey.domain) + && platform.equals(otherKey.platform)); + } + + @Override + public int hashCode() { + int hash = 7; + hash = 79 * hash + Objects.hashCode(this.domain); + hash = 79 * hash + Objects.hashCode(this.platform); + return hash; + } + } + + /** + * Holds full data for a domain role + */ + private class DomainRole { + final String domain; + final String platform; + final Role role; + final String url; + final BlackboardArtifact artifact; + + DomainRole(String domain, String platform, Role role, String url, BlackboardArtifact artifact) { + this.domain = domain; + this.role = role; + this.platform = platform; + this.url = url; + this.artifact = artifact; + } + + String getDomain() { + return domain; + } + + String getPlatform() { + return platform; + } + + Role getRole() { + return role; + } + + String getUrl() { + return url; + } + + BlackboardArtifact getArtifact() { + return artifact; + } + } +} diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/RAImageIngestModule.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/RAImageIngestModule.java index 488448d434..220317313b 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/RAImageIngestModule.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/RAImageIngestModule.java @@ -80,6 +80,7 @@ public final class RAImageIngestModule implements DataSourceIngestModule { Extract recycleBin = new ExtractRecycleBin(); Extract sru = new ExtractSru(); Extract prefetch = new ExtractPrefetch(); + Extract webAccountType = new ExtractWebAccountType(); extractors.add(chrome); extractors.add(firefox); @@ -88,6 +89,7 @@ public final class RAImageIngestModule implements DataSourceIngestModule { extractors.add(safari); extractors.add(recentDocuments); extractors.add(SEUQA); // this needs to run after the web browser modules + extractors.add(webAccountType); // this needs to run after the web browser modules extractors.add(registry); // this should run after quicker modules like the browser modules and needs to run before the DataSourceUsageAnalyzer extractors.add(osExtract); // this needs to run before the DataSourceUsageAnalyzer extractors.add(dataSourceAnalyzer); //this needs to run after ExtractRegistry and ExtractOs diff --git a/TSKVersion.xml b/TSKVersion.xml index 7da9869f04..106dd8f94f 100644 --- a/TSKVersion.xml +++ b/TSKVersion.xml @@ -1,3 +1,3 @@ - + diff --git a/Testing/nbproject/project.xml b/Testing/nbproject/project.xml index b2f142ed65..0e02fc1fba 100644 --- a/Testing/nbproject/project.xml +++ b/Testing/nbproject/project.xml @@ -47,7 +47,7 @@ 10 - 10.21 + 10.22 diff --git a/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties b/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties index 35138509d8..820ccd1cab 100644 --- a/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties +++ b/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties @@ -1,5 +1,5 @@ #Updated by build script -#Wed, 08 Jul 2020 15:15:46 -0400 +#Fri, 25 Sep 2020 17:10:05 -0400 LBL_splash_window_title=Starting Autopsy SPLASH_HEIGHT=314 SPLASH_WIDTH=538 @@ -8,4 +8,4 @@ SplashRunningTextBounds=0,289,538,18 SplashRunningTextColor=0x0 SplashRunningTextFontSize=19 -currentVersion=Autopsy 4.16.0 +currentVersion=Autopsy 4.17.0 diff --git a/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties b/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties index cf36e85b33..bdb8b1a0b8 100644 --- a/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties +++ b/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties @@ -1,4 +1,4 @@ #Updated by build script -#Wed, 08 Jul 2020 15:15:46 -0400 -CTL_MainWindow_Title=Autopsy 4.16.0 -CTL_MainWindow_Title_No_Project=Autopsy 4.16.0 +#Fri, 25 Sep 2020 17:10:05 -0400 +CTL_MainWindow_Title=Autopsy 4.17.0 +CTL_MainWindow_Title_No_Project=Autopsy 4.17.0 diff --git a/build-windows-installer.xml b/build-windows-installer.xml index 3973f3fe8c..830a3d226d 100644 --- a/build-windows-installer.xml +++ b/build-windows-installer.xml @@ -175,7 +175,7 @@ - + diff --git a/docs/doxygen-user/Doxyfile b/docs/doxygen-user/Doxyfile index eaa570e291..a87d92a966 100644 --- a/docs/doxygen-user/Doxyfile +++ b/docs/doxygen-user/Doxyfile @@ -38,7 +38,7 @@ PROJECT_NAME = "Autopsy User Documentation" # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 4.16.0 +PROJECT_NUMBER = 4.17.0 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a @@ -1025,7 +1025,7 @@ GENERATE_HTML = YES # The default directory is: html. # This tag requires that the tag GENERATE_HTML is set to YES. -HTML_OUTPUT = 4.16.0 +HTML_OUTPUT = 4.17.0 # The HTML_FILE_EXTENSION tag can be used to specify the file extension for each # generated HTML page (for example: .htm, .php, .asp). diff --git a/docs/doxygen-user/EXIF_parser_page.dox b/docs/doxygen-user/EXIF_parser_page.dox index 7837d9c2c2..2a58b8b5a8 100644 --- a/docs/doxygen-user/EXIF_parser_page.dox +++ b/docs/doxygen-user/EXIF_parser_page.dox @@ -1,29 +1,25 @@ -/*! \page EXIF_parser_page EXIF Parser Module +/*! \page EXIF_parser_page Picture Analyzer Module -What Does It Do -======== +\section exif_overview Overview -The EXIF Parser module extracts EXIF (Exchangeable Image File Format) information from ingested pictures. This information can contain geolocation data for the picture, time, date, camera model and settings (exposure values, resolution, etc) and other information. The discovered attributes are added to the BlackBoard. +The Picture Analyzer module extracts EXIF (Exchangeable Image File Format) information from ingested pictures. This information can contain geolocation data for the picture, time, date, camera model and settings (exposure values, resolution, etc) and other information. The discovered attributes are added to the Blackboard. This can tell you where and when a picture was taken, and give clues to the camera that took it. -This can tell you where and when a picture was taken, and give clues to the camera that took it. +The module also converts HEIC/HEIF images to JPG while maintaining their EXIF information, which will be processed and saved as it would for normal JPG images. -Configuration -======= -There is no configuration required. +\section exif_usage Using the Module -Using the Module -====== -Select the checkbox in the Ingest Modules settings screen to enable the EXIF Parser. +Select the checkbox in the Ingest Modules settings screen to enable the Picture Analyzer module. -Ingest Settings ------- -There are no runtime ingest settings required. -Seeing Results ------- +\section exif_results Seeing Results + Results are shown in the Results tree. \image html EXIF-tree.PNG +For HEIC files, the converted JPGs will appear in the data source tree as children of the original file. + +\image html EXIF-heic.png + */ diff --git a/docs/doxygen-user/command_line_ingest.dox b/docs/doxygen-user/command_line_ingest.dox index 97c521c685..d27566b738 100644 --- a/docs/doxygen-user/command_line_ingest.dox +++ b/docs/doxygen-user/command_line_ingest.dox @@ -6,15 +6,25 @@ The Command Line Ingest feature allows you to run many of Autopsy's functions fr \section command_line_ingest_config Configuration -Go to Tools->Options and then select the "Command Line Ingest" tab. +To configure command line ingest, go to Tools->Options and then select the "Command Line Ingest" tab. If you would like to create or open multi-user cases, you'll need to \ref install_multiuser_page "configure the multi-user settings". \image html command_line_ingest_options.png -Use the ingest module settings to configure how you want to run ingest. This is the same as normal \ref ingest_page "ingest module" configuration - choose a file filter then enable or disable the individual ingest modules, changing their settings if desired. Press "OK" to save your settings. +\subsection command_line_ingest_profile Configuring Ingest Profiles -Use the report module settings to choose and configure a report type. Only the selected report type will be generated. Configuration is generally the same as normal \ref reporting_page "report generation" with some slight differences. This is mainly seen in places where your options are dependent on the open case, such as choosing \ref tagging_page "tags" to report on or \ref interesting_files_identifier_page "interesting file" set names to include. For example, the HTML report normally allows you to choose specific tags to include but for command line ingest it will only have the option to include all tags. +From the options panel you can configure the default ingest profile. This is the same as normal \ref ingest_page "ingest module" configuration - choose a file filter then enable or disable the individual ingest modules, changing their settings if desired. Press "OK" to save your settings. -If you would like to create or open multi-user cases, you'll need to \ref install_multiuser_page "configure the multi-user settings". +Currently custom ingest profiles can not be configured on the command line ingest options panel but they can be created through the \ref ingest_page "ingest options panel" and then used on the command line. Here we've created an ingest profile that will only process image file types and will only run certain ingest modules. + +\image html command_line_ingest_profile.png + +See the section on \ref command_line_ds "running ingest" below for instructions on specifying an ingest profile on the command line. + +\subsection command_line_report_profile Configuring Report Profiles + +You can set up report profiles to use with command line ingest. You'll start with a "default" profile and can create additional profiles. Each profile will allow you to generate one type of report. Configuration is generally the same as normal \ref reporting_page "report generation" with some slight differences. This is mainly seen in places where your options are dependent on the open case, such as choosing \ref tagging_page "tags" to report on or \ref interesting_files_identifier_page "interesting file" set names to include. For example, the HTML report normally allows you to choose specific tags to include but for command line ingest it will only have the option to include all tags. + +If you wish to create additional report profiles, select "Make new profile" in the drop-down menu and then click the "Configure" button. You'll be prompted to name your new report profile and then will go through the normal report configuration. Having multiple report profiles will allow you to easily generate different report types from the command line. For example, you might have an "htmlReport" report profile that creates the HTML report and another report profile to generate KML reports. See the \ref command_line_report "report generation" section below for directions on how to specifiy a report profile on the command line. \section command_line_ingest_commands Command Options @@ -35,11 +45,16 @@ The table below shows a summary of the command line operations. You can run one Open Existing Case 
--caseDir
--caseDir="C:\work\Cases\test5_2019_09_20_11_01_29"
Add a Data Source
--addDataSource
---runIngest (optional)
--dataSourcePath
--addDataSource --dataSourcePath="R:\work\images\small2.img" --runIngest
+--runIngest (optional) +--runIngest=(ingest profile name) (optional)
--dataSourcePath
--addDataSource --dataSourcePath="R:\work\images\small2.img" --runIngest
-Run Ingest on Existing Data Source
--runIngest
--dataSourceObjectId
--runIngest --dataSourceObjectId=1
+Run Ingest on Existing Data Source
--runIngest
+--runIngest=(ingest profile name)
--dataSourceObjectId
--runIngest --dataSourceObjectId=1
+--runIngest="imageAnalysis" --dataSourceObjectId=1
-Generate Reports
--generateReports
 
--generateReports
+Generate Reports
--generateReports
+--generateReports=(report profile name)
 
--generateReports
+--generateReports="kmlReport"
Create List of Data Sources
--listAllDataSources
 
--listAllDataSources
@@ -92,6 +107,13 @@ autopsy64.exe --caseDir="C:\work\cases\test6_2019_09_20_13_00_51" --addDataSourc --dataSourcePath="R:\work\images\green_images.img" \endverbatim +Next we'll add a third data source ("red_images.img") to the case and run ingest using a custom ingest profile "imageAnalysis" created as described in the \ref command_line_ingest_profile "Configuring Ingest Profiles" section above. + +\verbatim +autopsy64.exe --caseDir="C:\work\cases\test6_2019_09_20_13_00_51" --addDataSource --runIngest="imageAnalysis" + --dataSourcePath="R:\work\images\red_images.img" +\endverbatim + Finally we'll add a folder ("Test files") as a logical file set to a new case ("test9"). \verbatim @@ -132,6 +154,12 @@ autopsy64.exe --caseDir="C:\work\cases\test6_2019_09_20_13_00_51" --addDataSourc --dataSourcePath="R:\work\images\small2.img" --runIngest --generateReports \endverbatim +The example above uses the default report profile. If you set up a custom report profile as described in the \ref command_line_report_profile "Configuring Ingest Profiles section" above, you can specify that profile after the --generateReports option. + +\verbatim +autopsy64.exe --caseDir="C:\work\cases\test6_2019_09_20_13_00_51" --generateReports="html" +\endverbatim + \subsection command_line_listds Listing All Data Sources You can add the --listAllDataSources at any time to output a list of all data sources currently in the case along with their object IDs, to be used when \ref command_line_existing_ds "running on an existing data source". This command can even be run alone with just the path to the case. @@ -151,7 +179,7 @@ If you've entered everything correctly, Autopsy will load and you'll see this di \image html command_line_ingest_dialog.png -If you instead see the normal case open dialog, it most likely means that your command line is malformed. Verify that there are no typos and that you have the appropriate parameters for the operation(s) you're attempting. +If you entered something incorrectly you will likely see an error in the output. You'll want to compare what you ran with the descriptions and examples above to try to fix the error. If everything works correctly, you'll see a log of the processing being done and Autopsy will close when finished. @@ -160,7 +188,13 @@ If everything works correctly, you'll see a log of the processing being done and \section command_line_ingest_results Viewing Results -You can open the case created on the command line like any other Autopsy case. Simply go to "Open Case" and then browse to the output folder you set up in the \ref command_line_ingest_config section and look for the folder starting with your case name. It will have a timestamp appended to the name you specified. +You can open the case you created directly from the command line by specifying either the case folder or the path to the ".aut" file. Remember that the folder name will have a timestamp appended to your case name. +\verbatim +autopsy64.exe "C:\work\cases\xpCase_2019_09_20_14_39_25" +autopsy64.exe "C:\work\cases\xpCase_2019_09_20_14_39_25\xpCase.aut" +\endverbatim + +You can also open the case normally through Autopsy. Simply go to "Open Case" and then browse to the output folder you set up in the \ref command_line_ingest_config section and look for the folder starting with your case name. It will have a timestamp appended to the name you specified. \image html command_line_ingest_open_case.png diff --git a/docs/doxygen-user/configuration.dox b/docs/doxygen-user/configuration.dox new file mode 100644 index 0000000000..e3ef07a2ac --- /dev/null +++ b/docs/doxygen-user/configuration.dox @@ -0,0 +1,54 @@ +/*! \page config_page General Configuration + +[TOC] + +\section config_overview Overview + +You can configure Autopsy through the main options panel. This is accessed by going to Tools->Options. Here you can change how cases are displayed, configure how Autopsy runs, and create hash sets, keyword lists, etc. + +The options panel has different tabs for each feature. These options are documented on the main pages for each feature. For example, the \ref keyword_search_configuration_dialog "keyword search options panel" is described on the main \ref keyword_search_page page. The tabs that do not go with a feature documented elsewhere will be described here. + +\section config_app Application Options + +The first tab on the options panel is for general application settings. + +\image html options_application.png + +The top section lets you adjust how much memory is used by Autopsy and how many log files to keep. Generally each Autopsy session generates one log file, though it can generate more if the log file becomes too large. + +The next section lets you specify where Autopsy should store temporary files. These files will be deleted when a case is closed. + +The final section lets you set a custom logo. + +\image html options_logo.png + +This logo will be displayed in any generated \ref report_html "HTML reports". + +\image html options_logo_report.jpg + +\section config_view View Options + +See the \ref view_options_page page for a description of how you can customize what data is displayed in Autopsy. + +\section config_ext_viewer External Viewers + +The External Viewers tab allows you to add file associations and select a hex editor. + +\image html options_ext_viewer.jpg + +The top section allows you to enter custom file associations by either extension or MIME type. In the image above we've associated .xml files with the Notepad application. Now it is possible to right click on a .xml file in the results viewer and choose "Open in external viewer" to open that .xml file in Notepad. + +\image html options_ext_viewer_context_menu.jpg + +The bottom section allows you to specify a hex editor. HxD has been tested and is the default, but other hex editors will work as well. The selected hex editor will be launched when clicking the "Launch in HxD" button in the "Hex" tab of the \ref content_viewer_page. + +\section config_general General Options + +The general options tab allows you to configure proxy settings. + +\image html proxySettings.PNG + + + + +*/ \ No newline at end of file diff --git a/docs/doxygen-user/content_viewer.dox b/docs/doxygen-user/content_viewer.dox index 0b74b142d6..ff1d720127 100644 --- a/docs/doxygen-user/content_viewer.dox +++ b/docs/doxygen-user/content_viewer.dox @@ -14,9 +14,7 @@ The Hex tab is nearly always available and shows you the raw and exact contents \image html content_viewer_hex.png -If desired, you can open the file in an external hex editor. This is configured through the "External Viewer" tab on the options panel. HxD has been verified to work with Autopsy, but alternate hex editors may also be compatible. - -\image html content_viewer_hex_editor_setup.png +If desired, you can open the file in an external hex editor. This is configured through the \ref config_ext_viewer tab on the options panel. HxD has been verified to work with Autopsy, but alternate hex editors may also be compatible. Note that this process saves the file to disk before launching the hex editor. A progress indicator will be displayed in the lower right corner of the application. If you wish to cancel the file export, click the 'X' to the right of the progress bar. diff --git a/docs/doxygen-user/data_sources.dox b/docs/doxygen-user/data_sources.dox index 9a5dcd3692..316a355879 100644 --- a/docs/doxygen-user/data_sources.dox +++ b/docs/doxygen-user/data_sources.dox @@ -21,26 +21,25 @@ You can add a data source in several ways: The data source must remain accessible for the duration of the analysis because the case contains a reference to the data source. It does not copy the data source into the case folder. Regardless of the type of data source, there are some common steps in the process: - -1) You will select the type of data source. +
    +
  1. You will select the type of data source. \image html select-data-source-type.PNG -2) You will be prompted to specify the data source to add. This screen varies based on the data source type. Details on adding each type of data source are provided below. +
  2. You will be prompted to specify the data source to add. This screen varies based on the data source type. Details on adding each type of data source are provided below. NOTE: If you are adding a data source to a multi-user case, ensure that all Autopsy clients will have access to the data source at the same path. We recommend using UNC paths to ensure this consistent mapping. -3) Autopsy will perform a basic examination of the data source and populate an embedded database with an entry for each file in the data source. No content is analyzed in the process, only the files are enumerated. - -4) While it is examining the data source, you will be prompted with a list of ingest modules to enable. If one or more ingest profiles have been saved, there will be a screen before this asking whether to use one of the saved profiles or do a custom setup. See \ref ingest_page for more information on setting up ingest profiles. +
  3. Next you will be prompted with a list of ingest modules to enable. If one or more ingest profiles have been saved, there will be a screen before this asking whether to use one of the saved profiles or do a custom setup. See \ref ingest_page for more information on setting up ingest profiles. \image html select-ingest-modules.PNG -5) After you configure the ingest modules, you may need to wait for Autopsy to finish its basic examination of the data source. +
  4. You will need to wait while Autopsy performs a basic examination of the data source and populates an embedded database with an entry for each file in the data source. \image html data-source-progress-bar.PNG -6) After the ingest modules have been configured and the basic examination of the data source is complete, the ingest modules will begin to analyze the file contents. +
  5. After the basic examination of the data source is complete, the ingest modules will likely still be running but you can start browsing through the files in your data source. +
Data sources can be removed from cases created with Autopsy 4.14.0 and later. See the section \ref data_source_deletion "below". diff --git a/docs/doxygen-user/file_discovery.dox b/docs/doxygen-user/file_discovery.dox index 92f2e5a4e9..db7b872a28 100644 --- a/docs/doxygen-user/file_discovery.dox +++ b/docs/doxygen-user/file_discovery.dox @@ -75,7 +75,7 @@ The possibly user created filter restricts the results to files that suspected t \image html FileDiscovery/fd_userCreatedFilter.png -This means the file must have a "User Content Suspected" result associated with it. These primarily come from the \ref EXIF_parser_page "Exif parser module". +This means the file must have a "User Content Suspected" result associated with it. These primarily come from the \ref EXIF_parser_page. \image html FileDiscovery/fd_userContentArtifact.png diff --git a/docs/doxygen-user/ileapp.dox b/docs/doxygen-user/ileapp.dox new file mode 100644 index 0000000000..21a314e0ac --- /dev/null +++ b/docs/doxygen-user/ileapp.dox @@ -0,0 +1,20 @@ +/*! \page ileapp_page iOS Analyzer (iLEAPP) + +[TOC] + +\section ileapp_overview Overview + +The iOS Analyzer ingest module runs iLEAPP (https://github.com/abrignoni/iLEAPP) and converts the results into results that can be viewed in Autopsy. + +\section ileapp_config Using the Module + +Select the checkbox in the Ingest Modules settings screen to enable the IOS Analzyer (iLEAPP) module. In Autopsy 4.17.0 the module only runs on .tar/.zip files found in a \ref ds_log "logical files data source". + +\section ileapp_results Seeing Results + +Results from the iOS Analyzer module will appear in the \ref tree_viewer_page under Results->Extracted Content. + +\image html ileapp_main.jpg + + +*/ diff --git a/docs/doxygen-user/image_gallery.dox b/docs/doxygen-user/image_gallery.dox index bd0a7befbd..39b11a93fb 100644 --- a/docs/doxygen-user/image_gallery.dox +++ b/docs/doxygen-user/image_gallery.dox @@ -11,7 +11,7 @@ This document assumes basic familiarity with Autopsy. Quick Start =========== 1. The Image Gallery tool can be configured to collect data about images/videos as ingest runs or all at once after ingest. To change this setting go to "Tools", "Options", "Image /Video Gallery". This setting is saved per case, but cannot be changed during ingest. See the Options window for more details -2. Create a case as normal and add a disk image (or folder of files) as a data source. Ensure that you have the hash lookup module enabled with NSRL and known bad hashsets, the EXIF module enabled, and the File Type module enabled. +2. Create a case as normal and add a disk image (or folder of files) as a data source. Ensure that you have the hash lookup module enabled with NSRL and known bad hashsets, the Picture Analyzer module enabled, and the File Type module enabled. 3. Click the "View Images/Videos" button or select "View Images/Videos" in the "Tools" menu. This will open the Autopsy Image/Video Analysis tool in a new window. 4. Groups of images will be presented as they are analyzed by the background ingest modules. You can later resort and regroup, but it is required to keep it grouped by folder while ingest is still ongoing. 5. As each group is reviewed, the next highest priority group is presented, according to a sorting criteria (the default is the density of hash set hits). diff --git a/docs/doxygen-user/images/EXIF-heic.png b/docs/doxygen-user/images/EXIF-heic.png new file mode 100644 index 0000000000..b3f1014326 Binary files /dev/null and b/docs/doxygen-user/images/EXIF-heic.png differ diff --git a/docs/doxygen-user/images/command_line_ingest_dialog.png b/docs/doxygen-user/images/command_line_ingest_dialog.png index 51caccb308..5b4a050140 100644 Binary files a/docs/doxygen-user/images/command_line_ingest_dialog.png and b/docs/doxygen-user/images/command_line_ingest_dialog.png differ diff --git a/docs/doxygen-user/images/command_line_ingest_options.png b/docs/doxygen-user/images/command_line_ingest_options.png index 015c2dee45..4e42611368 100644 Binary files a/docs/doxygen-user/images/command_line_ingest_options.png and b/docs/doxygen-user/images/command_line_ingest_options.png differ diff --git a/docs/doxygen-user/images/command_line_ingest_profile.png b/docs/doxygen-user/images/command_line_ingest_profile.png new file mode 100644 index 0000000000..8d417c837c Binary files /dev/null and b/docs/doxygen-user/images/command_line_ingest_profile.png differ diff --git a/docs/doxygen-user/images/ileapp_main.jpg b/docs/doxygen-user/images/ileapp_main.jpg new file mode 100644 index 0000000000..8a08b8cba7 Binary files /dev/null and b/docs/doxygen-user/images/ileapp_main.jpg differ diff --git a/docs/doxygen-user/images/options_application.png b/docs/doxygen-user/images/options_application.png new file mode 100644 index 0000000000..977c63e77a Binary files /dev/null and b/docs/doxygen-user/images/options_application.png differ diff --git a/docs/doxygen-user/images/options_ext_viewer.jpg b/docs/doxygen-user/images/options_ext_viewer.jpg new file mode 100644 index 0000000000..fcb9fa75de Binary files /dev/null and b/docs/doxygen-user/images/options_ext_viewer.jpg differ diff --git a/docs/doxygen-user/images/options_ext_viewer_context_menu.jpg b/docs/doxygen-user/images/options_ext_viewer_context_menu.jpg new file mode 100644 index 0000000000..d6d53cb2f0 Binary files /dev/null and b/docs/doxygen-user/images/options_ext_viewer_context_menu.jpg differ diff --git a/docs/doxygen-user/images/options_logo.png b/docs/doxygen-user/images/options_logo.png new file mode 100644 index 0000000000..99e04f80b4 Binary files /dev/null and b/docs/doxygen-user/images/options_logo.png differ diff --git a/docs/doxygen-user/images/options_logo_report.jpg b/docs/doxygen-user/images/options_logo_report.jpg new file mode 100644 index 0000000000..43266b8d6f Binary files /dev/null and b/docs/doxygen-user/images/options_logo_report.jpg differ diff --git a/docs/doxygen-user/images/portable_case_open_bat.png b/docs/doxygen-user/images/portable_case_open_bat.png new file mode 100644 index 0000000000..57e0985562 Binary files /dev/null and b/docs/doxygen-user/images/portable_case_open_bat.png differ diff --git a/docs/doxygen-user/images/portable_case_report_panel.png b/docs/doxygen-user/images/portable_case_report_panel.png index e0a8a00513..0919d68d62 100644 Binary files a/docs/doxygen-user/images/portable_case_report_panel.png and b/docs/doxygen-user/images/portable_case_report_panel.png differ diff --git a/docs/doxygen-user/images/troubleshooting_log_menu.png b/docs/doxygen-user/images/troubleshooting_log_menu.png new file mode 100644 index 0000000000..f6f4689021 Binary files /dev/null and b/docs/doxygen-user/images/troubleshooting_log_menu.png differ diff --git a/docs/doxygen-user/images/troubleshooting_thread.png b/docs/doxygen-user/images/troubleshooting_thread.png new file mode 100644 index 0000000000..a384b81156 Binary files /dev/null and b/docs/doxygen-user/images/troubleshooting_thread.png differ diff --git a/docs/doxygen-user/installation.dox b/docs/doxygen-user/installation.dox index 1577806f54..86845cc25d 100644 --- a/docs/doxygen-user/installation.dox +++ b/docs/doxygen-user/installation.dox @@ -51,8 +51,10 @@ To install Autopsy, perform the following steps: If you are behind a proxy and need access to a network with Autopsy or one of the modules, you may set your proxy information in the _Tools_, _Options_, _General_ tab as shown in the screenshot below. -

\image html proxySettings.PNG -

+ +\section install_file_association Windows File Association + +If desired, you can set up a Windows file association for .aut files and the Autopsy executable (normally C:\\Program Files\\Autopsy-(current version)\\bin\\autopsy64.exe). This will allow you to double click on the .aut file for a case to launch Autopsy with the selected case open. Instructions for setting up file associations can be found by doing a web search for "how to set Windows file associations". */ diff --git a/docs/doxygen-user/keyword_search.dox b/docs/doxygen-user/keyword_search.dox index 0625459cf3..c950659bc7 100644 --- a/docs/doxygen-user/keyword_search.dox +++ b/docs/doxygen-user/keyword_search.dox @@ -59,7 +59,7 @@ that exist in a location that Autopsy can understand. To add support for more la and move them to the right location. The following steps breakdown this process for you:
    -
  1. Navigate to https://github.com/tesseract-ocr/tesseract/wiki/Data-Files. +
  2. Navigate to https://tesseract-ocr.github.io/tessdoc/Data-Files.
  3. Under the section titled "Data Files for Version 4.00 (November 29, 2016)" you will find a table containing files that represent each language. These files have the extension ".traineddata".
  4. To download the desired language, click on the links in the far right-hand column of the table. You may download as many as you like. Note that you must only choose from this table. Language files under any other sections are not guaranteed to work in Autopsy.
  5. Once you've downloaded your language files, simply drag and drop them into the "AppData\Roaming\autopsy\ocr_language_packs" folder under your user folder. diff --git a/docs/doxygen-user/main.dox b/docs/doxygen-user/main.dox index 7180130389..cbefad8249 100644 --- a/docs/doxygen-user/main.dox +++ b/docs/doxygen-user/main.dox @@ -12,13 +12,14 @@ Help Topics ------- The following topics are available here: -- Installation - - \subpage installation_page - - \subpage performance_page +- \subpage installation_page +- Configuration + - \subpage config_page + - \subpage performance_page - Multi-user Cluster - \subpage install_multiuser_page - \subpage multiuser_sec_page - - \subpage multiuser_page + - \subpage multiuser_page - \subpage quick_start_guide "Quick Start Guide" @@ -49,6 +50,7 @@ The following topics are available here: - \subpage plaso_page - \subpage drone_page - \subpage gpx_page + - \subpage ileapp_page - Reviewing the Results - \subpage uilayout_page @@ -98,6 +100,7 @@ The following topics are available here: - \subpage translations_page - \subpage update_interesting_files_page +- \subpage troubleshooting_page If the topic you need is not listed, then you can: - Refer to the Autopsy Wiki diff --git a/docs/doxygen-user/personas.dox b/docs/doxygen-user/personas.dox index a443955445..b227ab508c 100644 --- a/docs/doxygen-user/personas.dox +++ b/docs/doxygen-user/personas.dox @@ -1,5 +1,7 @@ /*! \page personas_page Personas +[TOC] + \section personas_overview Overview Autopsy can store and organize account information based on personas, which represent an online identity. A person may have several online identities and therefore several personas. As an example, a single person may have a set of accounts that post online about loving cats and another set of accounts that appear unrelated that post about hating cats. @@ -20,13 +22,13 @@ Here are some basic concepts about persona: Personas are stored in the Central Repository based on accounts that were found in results. These results are generated by various ingest modules such as the \ref recent_activity_page and \ref android_analyzer_page. -Autopsy provides a dedicated tool, \ref personas_editor "Personas Editor", to create, view, edit, and delete personas. +Autopsy provides a dedicated tool, the \ref personas_editor "Personas Editor", to create, view, edit, and delete personas. \section personas_editor Personas Editor The Personas Editor is loaded through the Tools -> Personas menu item. -The left panel in the Personas Editor is a table that lists personas, based on the selected criteria. The right panel displays the details of selected persona. +The left panel in the Personas Editor is a table that lists personas, based on the selected criteria. The right panel displays the details of the persona currently selected in the left panel. By default, when the Personas Editor is launched, all the personas in the Central Repository are listed in the table. You may filter this list by checking the "Filter personas by Keyword" checkbox. Type in either a persona name or an account identifier in the textbox and select the "Name" or "Account" radio button appropriately. Then click the "Show" button to show only the personas that match the filtering criteria. @@ -78,13 +80,13 @@ All personas must be associated with at least one account. Normally these accoun \section personas_artifact_viewers Persona integration in Content Viewers -Autopsy shows persona associated with accounts, where applicable. When viewing contact, call log and message results, Autopsy shows the personas associated with accounts in these panels. If no persona exists for an account, Autopsy provides a button for the user to create one. +Autopsy shows personas associated with accounts, where applicable. When viewing contact, call log and message results, Autopsy shows the personas associated with accounts in these panels. If no persona exists for an account, Autopsy provides a button for the user to create one. As shown below, when viewing a contact result you may see persona data. When one or more personas are found associated with the accounts in the result then the Persona name is shown in the contact content viewer. There will be a "View" button to see the details of the persona. \image html Personas/personas_contact_found.png -If no matching persona is found, a "Create" button is shown to create a persona for the account(s). This will bring you to the \ref personas_create "Create Personas" panel with the account(s) already added. +If no matching persona is found, a "Create" button is shown to create a persona for the account(s). This will bring you to the \ref personas_create "Create Personas" panel with the account(s) already added, assuming the accounts exist in the central repository. If the desired account does not appear, close the personas panel and then copy the account to the clipboard either by highlighting the account and hitting control+c or by right clicking on it and selecting copy. Next go to Tools->Personas to open the Personas Editor, and click "Create Account". Paste in the account and select the type, then OK. Now close the editor and click "Create" again. The account should now be auto-populated. \image html Personas/personas_contact_not_found.png diff --git a/docs/doxygen-user/portable_case.dox b/docs/doxygen-user/portable_case.dox index de90443026..02a0fc81b9 100644 --- a/docs/doxygen-user/portable_case.dox +++ b/docs/doxygen-user/portable_case.dox @@ -33,7 +33,9 @@ You can tag any additional files you want to include in the portable case. See t \image html portable_case_tags.png -Portable cases are created through the \ref reporting_page feature. The Generate Report dialog will display a list of all tags and interesting file sets that are in use in the current case and you can choose which ones you would like to include. At the bottom you can choose to optionally package the case. Choosing to package the case without chunking will simply compress the portable case in a single archive that can be extracted with common compression programs. If you choose split the packaged case into multiple files, you will need to use the "Unpack and Open Portable Case" option to open it. This will be discussed in the next section. +Portable cases are created through the \ref reporting_page feature. The Generate Report dialog will display a list of all tags and interesting file sets that are in use in the current case and you can choose which ones you would like to include. At the bottom you can choose to optionally package the case or to include the Autopsy application. Choosing to package the case without chunking will simply compress the portable case in a single archive that can be extracted with common compression programs. If you choose split the packaged case into multiple files, you will need to use the "Unpack and Open Portable Case" option to open it. You can not include the application if you use this option. Unpacking a portable case will be discussed in the next section. + +If the recipient of the portable case does not have Autopsy, you can choose to include the Autopsy application in the portable case. This will allow the recipient to open the portable case without installing any other software. You may choose to package the case without chunking. If you do, the recipient will have to decompress it before opening Autopsy. The portable case will be placed in the "Reports" folder in the current case. @@ -49,7 +51,11 @@ If you packaged the portable case but did not choose to split it into chunks, yo \section portable_case_usage Using a Portable Case -Unpackaged portable cases can be opened like any other case through Case->Open Case. If your portable case was packaged, you'll need to use the unpack option to open it. Open the "Case" menu and then select "Unpack and Open Portable Case" option. This will bring up a dialog where you can browse to your packaged case and select where to extract it to. The case will also open. Note that any changes made to the case at this point will be saved to the unpacked location, and next time you open it you will need to browse to the unpacked folder. +If the Autopsy application was included in the portable case, it can be opened by double-clicking on the "open.bat" file. + +\image html portable_case_open_bat.png + +Otherwise, you'll start by opening the Autopsy application. Unpackaged portable cases can be opened like any other case through Case->Open Case. If your portable case was packaged, you'll need to use the unpack option to open it. Open the "Case" menu and then select "Unpack and Open Portable Case" option. This will bring up a dialog where you can browse to your packaged case and select where to extract it to. The case will also open. Note that any changes made to the case at this point will be saved to the unpacked location, and next time you open it you will need to browse to the unpacked folder. \image html portable_case_unpackage.png diff --git a/docs/doxygen-user/quick_start_guide.dox b/docs/doxygen-user/quick_start_guide.dox index 19b3a44c1d..6eec36c2a2 100644 --- a/docs/doxygen-user/quick_start_guide.dox +++ b/docs/doxygen-user/quick_start_guide.dox @@ -36,7 +36,7 @@ The standard ingest modules included with Autopsy are: - \subpage file_type_identification_page determines file types based on signatures and reports them based on MIME type. It stores the results in the Blackboard and many modules depend on this. It uses the Tika open source library. You can define your own custom file types in Tools, Options, File Types. - \subpage extension_mismatch_detector_page uses the results from the File Type Identification and flags files that have an extension not traditionally associated with the file's detected type. Ignores 'known' (NSRL) files. You can customize the MIME types and file extensions per MIME type in Tools, Options, File Extension Mismatch. - \subpage embedded_file_extractor_page opens ZIP, RAR, other archive formats, Doc, Docx, PPT, PPTX, XLS, and XLSX and sends the derived files from those files back through the ingest pipeline for analysis. -- \subpage EXIF_parser_page extracts EXIF information from JPEG files and posts the results into the tree in the main UI. +- \subpage EXIF_parser_page extracts EXIF information from JPEG files and posts the results into the tree in the main UI. Also converts HEIC/HEIF files to JPEG format and extracts EXIF data from those JPEGs. - \subpage keyword_search_page uses keyword lists to identify files with specific words in them. You can select the keyword lists to search for automatically and you can create new lists using the "Advanced" button. Note that with keyword search, you can always conduct searches after ingest has finished. The keyword lists that you select during ingest will be searched for at periodic intervals and you will get the results in real-time. You do not need to wait for all files to be indexed before performing a keyword search, however you will only get results from files that have already been indexed when you perform your search. - \subpage email_parser_page identifies Thunderbird MBOX files and PST format files based on file signatures, extracting the e-mails from them, adding the results to the Blackboard. - \subpage encryption_page looks for encrypted files. @@ -49,6 +49,7 @@ The standard ingest modules included with Autopsy are: - \subpage plaso_page uses Plaso to create \ref timeline_page "timeline" events. - \subpage android_analyzer_page allows you to parse common items from Android devices. Places artifacts into the BlackBoard. - \subpage gpx_page extracts geolocation data from .gpx files. +- \subpage ileapp_page extracts data from iOS data sources. When you select a module, you will have the option to change its settings. For example, you can configure which keyword search lists to use during ingest and which hash sets to use. Refer to the individual module help for details on configuring each module. @@ -85,17 +86,28 @@ In addition to the 3-panel UI with the tree on the left, there are other interfa \subsection s3a Timeline -The timeline feature can be opened from the "Tools" menu or the toolbar. This will show you file system and other events organized by time using various display techniques. See the \subpage timeline_page section for more details. +The Timeline feature can be opened from the "Tools" menu or the toolbar or though the "Timeline" button. This will show you file system and other events organized by time using various display techniques. See the \subpage timeline_page section for more details. \subsection s3b Image Gallery -The Image Gallery focuses on showing the pictures and videos from the data source organized by folder. It will show you files as soon as they have been hashed and EXIF data extracted. You can open it from the "Tools" menu. See the \subpage image_gallery_page section for more details. +The Image Gallery focuses on showing the pictures and videos from the data source organized by folder. It will show you files as soon as they have been hashed and EXIF data extracted. You can open it from the "Tools" menu or through the "Image Gallery" button. See the \subpage image_gallery_page section for more details. \subsection s3c Communications -The Communications interface focuses on showing which accounts were communicated with the most and what messages were sent. It allows you to focus on certain relationships or communications within a certain date rage. You can open it from the "Tools" menu. See the \subpage communications_page section for more details. +The Communications interface focuses on showing which accounts were communicated with the most and what messages were sent. It allows you to focus on certain relationships or communications within a certain date rage. You can open it from the "Tools" menu or through the "Communications" button. See the \subpage communications_page section for more details. +\subsection s3d Geolocation + +The Geolocation panel shows a map with markers for all geolocation results found in the case. You can open it from the "Tools" menu or through the "Geolocation" button. See the \subpage geolocation_page section for more details. + +\subsection s3e Discovery + +The Discovery panel allows you to search for different types of data within a case and display it in an easily reviewable form. You can open it from the "Tools" menu or through the "Discovery" button. See the \subpage discovery_page section for more details. + +\subsection s3f Personas + +The Personas panel is for creating and managing personas. Creating a persona allows you to associate one or more accounts with a name and other data. You can open it from the "Tools" menu or through the "Personas" button. See the \subpage personas_page section for more details. \section s5 Example Use Cases In this section, we will provide examples of how to do common analysis tasks. diff --git a/docs/doxygen-user/timeline.dox b/docs/doxygen-user/timeline.dox index 3ad6de8d86..9e82265570 100644 --- a/docs/doxygen-user/timeline.dox +++ b/docs/doxygen-user/timeline.dox @@ -18,7 +18,7 @@ Use this section to learn the basics of timeline. More details on the display op First you'll need to have a case open in Autopsy. To get the most out of timeline, you'll want to do the following during ingest: - Enable the \ref hash_db_page "hash lookup module" and use the NSRL to ignore known files - Enable the \ref recent_activity_page "recent activity module" to generate web-related events and other miscellaneous event types -- Enable the \ref EXIF_parser_page "EXIF parser module" to generate events on when images were +- Enable the \ref EXIF_parser_page to generate events on when images were - Enable other ingest modules that apply to your data. If you have email data, ensure the \ref email_parser_page "email parser module" is enabled. If you are analyzing mobile devices, ensure the \ref android_analyzer_page "Android analyzer module" and any other relevant modules are enabled. To open timeline, either use the "Timeline" button or navigate to "Tools" then "Timeline" in the menu. You can open timeline while an image is processing but the data will not be complete until it finishes. Timeline will start in \ref timeline_counts_view "counts view" with a chart showing the number of events in each time period. diff --git a/docs/doxygen-user/triage.dox b/docs/doxygen-user/triage.dox index 47ac724105..f17059e32a 100644 --- a/docs/doxygen-user/triage.dox +++ b/docs/doxygen-user/triage.dox @@ -92,7 +92,7 @@ In this scenario, you are trying to answer whether child exploitation images exi
    • Choose View->File Types->Images in the \ref tree_viewer_page "tree viewer" and review the thumbnails
    • Wait for hash set hits -
    • Review EXIF fles +
    • Review EXIF files
    • \ref tagging_page "Tag" any notable files found
  6. You can stop the analysis at any time. All data read so far will be in the VHD file. diff --git a/docs/doxygen-user/troubleshooting.dox b/docs/doxygen-user/troubleshooting.dox new file mode 100644 index 0000000000..d0a7d14e76 --- /dev/null +++ b/docs/doxygen-user/troubleshooting.dox @@ -0,0 +1,104 @@ +/*! \page troubleshooting_page Troubleshooting + +If you are experiencing an error, we encourage you to post on the forum (https://sleuthkit.discourse.group/), including as much information as possible: +
      +
    • Your operating system and version of Autopsy +
    • What led to the error. For example: +
        +
      • What type of data source was being processed? +
      • Which ingest modules were running? +
      • Which specialized viewer were you using? +
      +
    • The error being displayed on screen (if applicable) +
    • If there were any errors in the \ref troubleshooting_logs "logs" +
    + +\section troubleshooting_user_folder Deleting the Autopsy User Folder + +If Autopsy starts behaving strangely, stops loading entirely, or menu items go missing, you probably need to delete your user folder. Doing so essenitally gives you a fresh installation. On Windows the user folder is located in "C:\Users\(user name)\AppData\Roaming\autopsy". + +Note that if you delete this folder you will lose all your Autopsy settings including keyword lists, interesting file sets, and general configuration. If you have settings you want to keep you can try the following: +
      +
    • Make a copy of the autopsy folder. +
    • Delete the autopsy folder. +
    • Open Autopsy to regenerate the folder with default settings. +
    • Close Autopsy and copy over the old configuration files that look relevant. For example, if you're trying to restore your hash sets, you will want to copy back the "HashDatabases" folder and the "hashLookup.settings" file. +
    + +Alternately, you could copy the fresh user folder somewhere, move your old version back, and replace folders until it works again. + +\section troubleshooting_logs Viewing the Logs + +The logs are generally the most helpful in figuring out why an error is occurring. There are two sets of logs - the system logs and the case logs. There is an option in the UI to open the log folder: + +\image html troubleshooting_log_menu.png + +If you have a case open, clicking this will open the folder containing the case logs. If not, it will open the system log folder. You can also browse to these folders normally: +
      +
    • Case logs: (case folder)\\Logs +
    • System logs: C:\\Users\\(user name)\\AppData\\Roaming\\autopsy\\var\\log (on Windows) +
    + +In either case, the log of interest is most likely "autopsy.log.0", although it could be one of the older versions if you've closed and reopened Autopsy since the error occurred. You're looking for entries starting with "SEVERE" and possibly "WARNING" if there are no severe errors. Note that it's not unusual to have many warnings in the log. This is an example of a severe error with a stack trace: + +\verbatim +Sep 23, 2020 9:48:24 AM org.sleuthkit.autopsy.casemodule.services.TagNameDefinition saveToCase +SEVERE: Error saving tag name definition +org.sleuthkit.datamodel.TskCoreException: Error adding row for Follow Up tag name to tag_names table + at org.sleuthkit.datamodel.SleuthkitCase.addOrUpdateTagName(SleuthkitCase.java:9846) + at org.sleuthkit.autopsy.casemodule.services.TagNameDefinition.saveToCase(TagNameDefinition.java:239) + at org.sleuthkit.autopsy.casemodule.services.TagsManager.(TagsManager.java:288) + at org.sleuthkit.autopsy.casemodule.services.Services.(Services.java:50) + at org.sleuthkit.autopsy.casemodule.Case.openCaseLevelServices(Case.java:2480) + at org.sleuthkit.autopsy.casemodule.Case.open(Case.java:1993) + at org.sleuthkit.autopsy.casemodule.Case.lambda$doOpenCaseAction$6(Case.java:1863) + at java.util.concurrent.FutureTask.run(FutureTask.java:266) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) + at java.lang.Thread.run(Thread.java:748) +Caused by: java.sql.SQLException: ResultSet closed + at org.sqlite.core.CoreResultSet.checkOpen(CoreResultSet.java:76) + at org.sqlite.jdbc3.JDBC3ResultSet.findColumn(JDBC3ResultSet.java:39) + at org.sqlite.jdbc3.JDBC3ResultSet.getLong(JDBC3ResultSet.java:422) + at com.mchange.v2.c3p0.impl.NewProxyResultSet.getLong(NewProxyResultSet.java:424) + at org.sleuthkit.datamodel.SleuthkitCase.addOrUpdateTagName(SleuthkitCase.java:9843) + ... 10 more +\endverbatim + +If the error message doesn't help you solve the problem yourself, please post to the forum including the full stack trace (if available). + +\section troubleshooting_stack Creating a Thread Dump + +You can also generate a thread dump of the current state. This is useful if an ingest module or other process seems to be stuck. To generate a thread dump, go to "Help" then "Thread Dump" in the UI. + +\image html troubleshooting_thread.png + +You'll then see a file similar to this in a text viewer: + +\verbatim"Module-Actions" Id=222 RUNNABLE + at sun.management.ThreadImpl.getThreadInfo1(Native Method) + at sun.management.ThreadImpl.getThreadInfo(ThreadImpl.java:178) + at org.sleuthkit.autopsy.actions.ThreadDumpAction$ThreadDumper.createThreadDump(ThreadDumpAction.java:120) + at org.sleuthkit.autopsy.actions.ThreadDumpAction$ThreadDumper.doInBackground(ThreadDumpAction.java:91) + at org.sleuthkit.autopsy.actions.ThreadDumpAction$ThreadDumper.doInBackground(ThreadDumpAction.java:87) + at javax.swing.SwingWorker$1.call(SwingWorker.java:295) + at java.util.concurrent.FutureTask.run(FutureTask.java:266) + at javax.swing.SwingWorker.run(SwingWorker.java:334) + ... + + +"IM-start-ingest-jobs-0" Id=218 WAITING on java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject@7ceb341e + at sun.misc.Unsafe.park(Native Method) + - waiting on java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject@7ceb341e + at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) + at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039) + at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442) + at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074) + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134) + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) + at java.lang.Thread.run(Thread.java:748) +\endverbatim + +If the thread dump says anything about a deadlock then that's the problem. Please make sure to post any deadlocks to the forum. Even if not the thread dump could help diagnose your problem so consider including in your post. + +*/ \ No newline at end of file diff --git a/docs/doxygen/Doxyfile b/docs/doxygen/Doxyfile index 261517d081..722e81fb14 100644 --- a/docs/doxygen/Doxyfile +++ b/docs/doxygen/Doxyfile @@ -38,7 +38,7 @@ PROJECT_NAME = "Autopsy" # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 4.16.0 +PROJECT_NUMBER = 4.17.0 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears a the top of each page and should give viewer a @@ -1066,7 +1066,7 @@ GENERATE_HTML = YES # The default directory is: html. # This tag requires that the tag GENERATE_HTML is set to YES. -HTML_OUTPUT = api-docs/4.16.0/ +HTML_OUTPUT = api-docs/4.17.0/ # The HTML_FILE_EXTENSION tag can be used to specify the file extension for each # generated HTML page (for example: .htm, .php, .asp). diff --git a/nbproject/project.properties b/nbproject/project.properties index f4e09e3743..2460fd3765 100644 --- a/nbproject/project.properties +++ b/nbproject/project.properties @@ -4,7 +4,7 @@ app.title=Autopsy ### lowercase version of above app.name=${branding.token} ### if left unset, version will default to today's date -app.version=4.16.0 +app.version=4.17.0 ### build.type must be one of: DEVELOPMENT, RELEASE #build.type=RELEASE build.type=DEVELOPMENT diff --git a/release_scripts/localization_scripts/README.md b/release_scripts/localization_scripts/README.md index 8d6ef1e5c2..cdef6d238d 100644 --- a/release_scripts/localization_scripts/README.md +++ b/release_scripts/localization_scripts/README.md @@ -1,20 +1,20 @@ ## Description This folder provides tools to handle updates of bundle files for language localization. There are three main scripts: -- `allbundlesscript.py` - generates a csv file containing the relative path of the bundle file, the key, and the value for each property. -- `diffscript.py` - determines the property values that have changed between two commits and generates a csv file containing the relative path, the key, the previous value, the new value, and the change type (addition, deletion, change). -- `updatepropsscript.py` - Given a csv file containing the relative path of the bundle, the key, and the new value, will update the property values for a given language within the project. +- `allbundlesscript.py` - generates a file containing the relative path of the bundle file, the key, and the value for each property. +- `diffscript.py` - determines the property values that have changed between two commits and generates a file containing the relative path, the key, the previous value, the new value, and the change type (addition, deletion, change). +- `updatepropsscript.py` - Given a csv or xlsx file containing the relative path of the bundle, the key, and the new value, will update the property values for a given language within the project. All of these scripts provide more details on usage by calling the script with `-h`. ## Basic Localization Update Workflow -1. Call `python3 diffscript.py -l ` to generate a csv file containing differences in properties file values from the language's previous commit to the `HEAD` commit. The language identifier should be the abbreviated identifier used for the bundle (i.e. 'ja' for Japanese). The output path should be specified as a relative path with the dot slash notation (i.e. `./outputpath.csv`) or an absolute path. +1. Call `python3 diffscript.py -l ` to generate a file containing differences in properties file values from the language's previous commit to the `HEAD` commit. The language identifier should be the abbreviated identifier used for the bundle (i.e. 'ja' for Japanese). The output path should be specified as a relative path with the dot slash notation (i.e. `./outputpath.xlsx`) or an absolute path. 2. Update csv file with translations -3. Call `python3 updatepropsscript.py -l ` to update properties files based on the newly generated csv file. The csv file should be formatted such that the columns are bundle relative path, property files key, translated value and commit id for the latest commit id for which these changes represent. The commit id only needs to be in the header row. The output path should be specified as a relative path with the dot slash notation (i.e. `./outputpath.csv`) or an absolute path. +3. Call `python3 updatepropsscript.py -l ` to update properties files based on the newly generated file. The file should be formatted such that the columns are bundle relative path, property files key, original value (or empty column), translated value and commit id for the latest commit id for which these changes represent. The commit id only needs to be in the header row. The output path should be specified as a relative path with the dot slash notation (i.e. `./outputpath.xlsx`) or an absolute path. ## Localization Generation for the First Time -First-time updates should follow a similar procedure except that instead of calling `diffscript.py`, call `python3 allbundlesscript ` to generate a csv file with relative paths of bundle files, property file keys, property file values. The output path should be specified as a relative path with the dot slash notation (i.e. `./inputpath.csv`) or an absolute path. +First-time updates should follow a similar procedure except that instead of calling `diffscript.py`, call `python3 allbundlesscript ` to generate a file with relative paths of bundle files, property file keys, property file values. The output path should be specified as a relative path with the dot slash notation (i.e. `./inputpath.xlsx`) or an absolute path. ##Unit Tests Unit tests can be run from this directory using `python3 -m unittest`. \ No newline at end of file diff --git a/release_scripts/localization_scripts/allbundlesscript.py b/release_scripts/localization_scripts/allbundlesscript.py index 139dec15a5..3304620cf1 100644 --- a/release_scripts/localization_scripts/allbundlesscript.py +++ b/release_scripts/localization_scripts/allbundlesscript.py @@ -1,70 +1,52 @@ """This script finds all '.properties-MERGED' files and writes relative path, key, and value to a CSV file. -This script requires the python libraries: gitpython and jproperties. As a consequence, it also requires -git >= 1.7.0 and python >= 3.4. This script relies on fetching 'HEAD' from current branch. So make sure -repo is on correct branch (i.e. develop). +This script requires the python libraries: gitpython, jproperties, pyexcel-xlsx, xlsxwriter and pyexcel. As a +consequence, it also requires git >= 1.7.0 and python >= 3.4. This script relies on fetching 'HEAD' from current +branch. So make sure repo is on correct branch (i.e. develop). """ - import sys from envutil import get_proj_dir -from fileutil import get_filename_addition, OMITTED_ADDITION +from excelutil import write_results_to_xlsx from gitutil import get_property_file_entries, get_commit_id, get_git_root -from csvutil import records_to_csv -from typing import Union -import re +from csvutil import write_results_to_csv import argparse - -def write_items_to_csv(repo_path: str, output_path: str, show_commit: bool, value_regex: Union[str, None] = None): - """Determines the contents of '.properties-MERGED' files and writes to a csv file. - - Args: - repo_path (str): The local path to the git repo. - output_path (str): The output path for the csv file. - show_commit (bool): Whether or not to include the commit id in the header - value_regex (Union[str, None]): If non-none, only key value pairs where the value is a regex match with this - value will be included. - """ - - row_header = ['Relative path', 'Key', 'Value'] - if show_commit: - row_header.append(get_commit_id(repo_path, 'HEAD')) - - rows = [] - omitted = [] - - for entry in get_property_file_entries(repo_path): - new_entry = [entry.rel_path, entry.key, entry.value] - if value_regex is None or re.match(value_regex, entry.value): - rows.append(new_entry) - else: - omitted.append(new_entry) - - records_to_csv(output_path, [row_header] + rows) - - if len(omitted) > 0: - records_to_csv(get_filename_addition(output_path, OMITTED_ADDITION), [row_header] + omitted) +from outputtype import OutputType +from propentry import convert_to_output def main(): # noinspection PyTypeChecker parser = argparse.ArgumentParser(description='Gathers all key-value pairs within .properties-MERGED files into ' - 'one csv file.', + 'one file.', formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument(dest='output_path', type=str, help='The path to the output csv file. The output path should be' + parser.add_argument(dest='output_path', type=str, help='The path to the output file. The output path should be' ' specified as a relative path with the dot slash notation ' - '(i.e. \'./outputpath.csv\') or an absolute path.') + '(i.e. \'./outputpath.xlsx\') or an absolute path.') parser.add_argument('-r', '--repo', dest='repo_path', type=str, required=False, help='The path to the repo. If not specified, path of script is used.') - parser.add_argument('-nc', '--no_commit', dest='no_commit', action='store_true', default=False, - required=False, help="Suppresses adding commits to the generated csv header.") + parser.add_argument('-o', '--output-type', dest='output_type', type=OutputType, choices=list(OutputType), + required=False, help="The output type. Currently supports 'csv' or 'xlsx'.", default='xlsx') + parser.add_argument('-nc', '--no-commit', dest='no_commit', action='store_true', default=False, + required=False, help="Suppresses adding commits to the generated header.") + parser.add_argument('-nt', '--no-translated-col', dest='no_translated_col', action='store_true', default=False, + required=False, help="Don't include a column for translation.") args = parser.parse_args() repo_path = args.repo_path if args.repo_path is not None else get_git_root(get_proj_dir()) output_path = args.output_path show_commit = not args.no_commit + output_type = args.output_type + translated_col = not args.no_translated_col + commit_id = get_commit_id(repo_path, 'HEAD') if show_commit else None - write_items_to_csv(repo_path, output_path, show_commit) + processing_result = convert_to_output(get_property_file_entries(repo_path), commit_id, translated_col) + + # based on https://stackoverflow.com/questions/60208/replacements-for-switch-statement-in-python + { + OutputType.csv: write_results_to_csv, + OutputType.xlsx: write_results_to_xlsx + }[output_type](processing_result, output_path) sys.exit(0) diff --git a/release_scripts/localization_scripts/csvutil.py b/release_scripts/localization_scripts/csvutil.py index daa66f5396..f0886b91fa 100644 --- a/release_scripts/localization_scripts/csvutil.py +++ b/release_scripts/localization_scripts/csvutil.py @@ -4,6 +4,9 @@ from typing import List, Iterable, Tuple import csv import os +from fileutil import OMITTED_ADDITION, get_filename_addition, DELETED_ADDITION +from outputresult import OutputResult + def records_to_csv(output_path: str, rows: Iterable[List[str]]): """Writes rows to a csv file at the specified path. @@ -26,10 +29,10 @@ def records_to_csv(output_path: str, rows: Iterable[List[str]]): def csv_to_records(input_path: str, header_row: bool) -> Tuple[List[List[str]], List[str]]: - """Writes rows to a csv file at the specified path. + """Reads rows from a csv file at the specified path. Args: - input_path (str): The path where the csv file will be written. + input_path (str): The path where the csv file will be read. header_row (bool): Whether or not there is a header row to be skipped. """ @@ -49,3 +52,20 @@ def csv_to_records(input_path: str, header_row: bool) -> Tuple[List[List[str]], raise Exception("There was an error parsing csv {path}".format(path=input_path), e) return results, header + + +def write_results_to_csv(results: OutputResult, output_path: str): + """ + Writes the result of processing to the output path as a csv file. If omitted values are present, for output_path of + /dir/file.csv, the omitted will be written to /dir/file-omitted.csv. + + Args: + results: The results to be written. + output_path: The output path. + """ + + records_to_csv(output_path, results.results) + if results.omitted: + records_to_csv(get_filename_addition(output_path, OMITTED_ADDITION), results.omitted) + if results.deleted: + records_to_csv(get_filename_addition(output_path, DELETED_ADDITION), results.deleted) diff --git a/release_scripts/localization_scripts/diffscript.py b/release_scripts/localization_scripts/diffscript.py index 2713fef518..c2dc154b65 100644 --- a/release_scripts/localization_scripts/diffscript.py +++ b/release_scripts/localization_scripts/diffscript.py @@ -1,51 +1,16 @@ """This script determines the updated, added, and deleted properties from the '.properties-MERGED' files -and generates a csv file containing the items changed. This script requires the python libraries: -gitpython and jproperties. As a consequence, it also requires git >= 1.7.0 and python >= 3.4. +and generates a csv file containing the items changed. : gitpython, jproperties, pyexcel-xlsx, xlsxwriter and pyexcel. +As a consequence, it also requires git >= 1.7.0 and python >= 3.4. """ -import re import sys from envutil import get_proj_dir -from fileutil import get_filename_addition, OMITTED_ADDITION -from gitutil import get_property_files_diff, get_commit_id, get_git_root -from itemchange import ItemChange, ChangeType -from csvutil import records_to_csv +from excelutil import write_results_to_xlsx +from gitutil import get_property_files_diff, get_git_root, get_commit_id +from itemchange import convert_to_output +from csvutil import write_results_to_csv import argparse -from typing import Union from langpropsutil import get_commit_for_language, LANG_FILENAME - - -def write_diff_to_csv(repo_path: str, output_path: str, commit_1_id: str, commit_2_id: str, show_commits: bool, - value_regex: Union[str, None] = None): - """Determines the changes made in '.properties-MERGED' files from one commit to another commit. - - Args: - repo_path (str): The local path to the git repo. - output_path (str): The output path for the csv file. - commit_1_id (str): The initial commit for the diff. - commit_2_id (str): The latest commit for the diff. - show_commits (bool): Show commits in the header row. - value_regex (Union[str, None]): If non-none, only key value pairs where the value is a regex match with this - value will be included. - """ - - row_header = ItemChange.get_headers() - if show_commits: - row_header += [get_commit_id(repo_path, commit_1_id), get_commit_id(repo_path, commit_2_id)] - - rows = [] - omitted = [] - - for entry in get_property_files_diff(repo_path, commit_1_id, commit_2_id): - new_entry = entry.get_row() - if value_regex is not None and (entry.type == ChangeType.DELETION or not re.match(value_regex, entry.cur_val)): - omitted.append(new_entry) - else: - rows.append(new_entry) - - records_to_csv(output_path, [row_header] + rows) - - if len(omitted) > 0: - records_to_csv(get_filename_addition(output_path, OMITTED_ADDITION), [row_header] + omitted) +from outputtype import OutputType def main(): @@ -54,9 +19,9 @@ def main(): "'.properties-MERGED' files and generates a csv file containing " "the items changed.", formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument(dest='output_path', type=str, help='The path to the output csv file. The output path should ' + parser.add_argument(dest='output_path', type=str, help='The path to the output file. The output path should ' 'be specified as a relative path with the dot slash notation' - ' (i.e. \'./outputpath.csv\') or an absolute path.') + ' (i.e. \'./outputpath.xlsx\') or an absolute path.') parser.add_argument('-r', '--repo', dest='repo_path', type=str, required=False, help='The path to the repo. If not specified, path of script is used.') @@ -67,17 +32,29 @@ def main(): help='The commit for current release.') parser.add_argument('-nc', '--no-commits', dest='no_commits', action='store_true', default=False, required=False, help="Suppresses adding commits to the generated csv header.") + parser.add_argument('-o', '--output-type', dest='output_type', type=OutputType, choices=list(OutputType), + required=False, help="The output type. Currently supports 'csv' or 'xlsx'.", default='xlsx') parser.add_argument('-l', '--language', dest='language', type=str, default=None, required=False, help='Specify the language in order to determine the first commit to use (i.e. \'ja\' for ' 'Japanese. This flag overrides the first-commit flag.') + parser.add_argument('-lf', '--language-updates-file', dest='language_file', type=str, default=None, required=False, + help='Specify the path to the properties file containing key value pairs of language mapped to ' + 'the commit of when bundles for that language were most recently updated.') + + parser.add_argument('-nt', '--no-translated-col', dest='no_translated_col', action='store_true', default=False, + required=False, help="Don't include a column for translation.") args = parser.parse_args() repo_path = args.repo_path if args.repo_path is not None else get_git_root(get_proj_dir()) output_path = args.output_path commit_1_id = args.commit_1_id + output_type = args.output_type + show_translated_col = not args.no_translated_col + language_updates_file = args.language_file + lang = args.language if lang is not None: - commit_1_id = get_commit_for_language(lang) + commit_1_id = get_commit_for_language(lang, language_updates_file) if commit_1_id is None: print('Either the first commit or language flag need to be specified. If specified, the language file, ' + @@ -88,7 +65,18 @@ def main(): commit_2_id = args.commit_2_id show_commits = not args.no_commits - write_diff_to_csv(repo_path, output_path, commit_1_id, commit_2_id, show_commits) + changes = get_property_files_diff(repo_path, commit_1_id, commit_2_id) + processing_result = convert_to_output(changes, + commit1_id=get_commit_id(repo_path, commit_1_id) if show_commits else None, + commit2_id=get_commit_id(repo_path, commit_2_id) if show_commits else None, + show_translated_col=show_translated_col, + separate_deleted=True) + + # based on https://stackoverflow.com/questions/60208/replacements-for-switch-statement-in-python + { + OutputType.csv: write_results_to_csv, + OutputType.xlsx: write_results_to_xlsx + }[output_type](processing_result, output_path) sys.exit(0) diff --git a/release_scripts/localization_scripts/excelutil.py b/release_scripts/localization_scripts/excelutil.py new file mode 100644 index 0000000000..460006ec16 --- /dev/null +++ b/release_scripts/localization_scripts/excelutil.py @@ -0,0 +1,84 @@ +"""Provides tools for parsing and writing to a csv file. +""" +import collections +from typing import List, OrderedDict, Tuple, Union +import xlsxwriter +import pyexcel +from xlsxwriter.format import Format + +from outputresult import OutputResult, ColumnStyle + +Workbook = OrderedDict[str, List[List[str]]] + +# The name for the results sheet +RESULTS_SHEET_NAME = 'results' + +# The name for the sheet of deleted items +DELETED_SHEET_NAME = 'deleted' + +# The name for the sheet of omitted items +OMITTED_SHEET_NAME = 'omitted' + + +def excel_to_records(input_path: str) -> Workbook: + """Reads rows to a excel file at the specified path. + + Args: + input_path (str): The path where the excel file will be read. + """ + + return pyexcel.get_book_dict( + file_name=input_path + ) + + +def get_writer_format(workbook: xlsxwriter.Workbook, style: ColumnStyle) -> Union[Tuple[Format, int], None]: + if style: + wb_format = workbook.add_format({'text_wrap': 1, 'valign': 'top'}) if style['wrap_text'] else None + return wb_format, style['width'] + else: + return None + + +def write_results_to_xlsx(results: OutputResult, output_path: str): + """ + Writes the result of processing to the output path as a xlsx file. Results will be written to a 'results' sheet. + Omitted results will be written to an 'omitted' sheet. Deleted results will be written to a 'deleted' sheet. + + Args: + results: The results to be written. + output_path: The output path. + """ + + workbook = collections.OrderedDict([(RESULTS_SHEET_NAME, results.results)]) + if results.omitted: + workbook[OMITTED_SHEET_NAME] = results.omitted + if results.deleted: + workbook[DELETED_SHEET_NAME] = results.deleted + + wb_file = xlsxwriter.Workbook(output_path) + styles = [] + if results.column_styles: + styles = list(map(lambda style: get_writer_format(wb_file, style), results.column_styles)) + + for sheet_name, values in workbook.items(): + sheet = wb_file.add_worksheet(name=sheet_name) + if results.freeze_first_row: + sheet.freeze_panes(1, 0) + + for col_idx in range(0, len(styles)): + if styles[col_idx]: + col_format, width = styles[col_idx] + sheet.set_column(col_idx, col_idx, width) + + for row_idx in range(0, len(values)): + row = values[row_idx] + for col_idx in range(0, len(row)): + cell_format = None + if len(styles) > col_idx and styles[col_idx] and styles[col_idx][0]: + cell_format = styles[col_idx][0] + + cell_value = row[col_idx] + sheet.write(row_idx, col_idx, cell_value, cell_format) + + wb_file.close() diff --git a/release_scripts/localization_scripts/fileutil.py b/release_scripts/localization_scripts/fileutil.py index 5139812db2..22b1f5ef97 100644 --- a/release_scripts/localization_scripts/fileutil.py +++ b/release_scripts/localization_scripts/fileutil.py @@ -43,6 +43,9 @@ def get_new_path(orig_path: str, new_filename: str) -> str: # For use with creating csv filenames for entries that have been omitted. OMITTED_ADDITION = '-omitted' +# For use with creating csv filenames for entries that have been deleted. +DELETED_ADDITION = '-deleted' + def get_filename_addition(orig_path: str, filename_addition: str) -> str: """Gets filename with addition. So if item is '/path/name.ext' and the filename_addition is '-add', the new result diff --git a/release_scripts/localization_scripts/itemchange.py b/release_scripts/localization_scripts/itemchange.py index 27448cb529..61a6cab941 100644 --- a/release_scripts/localization_scripts/itemchange.py +++ b/release_scripts/localization_scripts/itemchange.py @@ -1,7 +1,13 @@ from typing import Iterator, List, Union + +from outputresult import OutputResult from propsutil import get_entry_dict from enum import Enum +from tabularutil import WITH_TRANSLATED_COLS, RELATIVE_PATH_COL, KEY_COL, create_output_result, WITH_TRANSLATED_STYLE, \ + VALUE_STYLE +import re + class ChangeType(Enum): """Describes the nature of a change in the properties file.""" @@ -49,18 +55,72 @@ class ItemChange: """ return ['Relative Path', 'Key', 'Change Type', 'Previous Value', 'Current Value'] - def get_row(self) -> List[str]: + def get_row(self, show_translated_col: bool) -> List[str]: """Returns the list of values to be entered as a row in csv serialization. + Args: + show_translated_col (bool): Whether or not the translated columns are showing; otherwise use default. Returns: List[str]: The list of values to be entered as a row in csv serialization. """ - return [ - self.rel_path, - self.key, - self.type, - self.prev_val, - self.cur_val] + + if show_translated_col: + return [ + self.rel_path, + self.key, + self.cur_val + ] + else: + return [ + self.rel_path, + self.key, + str(self.type) if self.type else None, + self.prev_val, + self.cur_val] + + +ITEMCHANGE_DEFAULT_COLS = [RELATIVE_PATH_COL, KEY_COL, 'Change Type', 'Previous Value', 'Current Value'] + + +def convert_to_output(items: Iterator[ItemChange], commit1_id: Union[str, None] = None, + commit2_id: Union[str, None] = None, show_translated_col: bool = True, + value_regex: Union[str, None] = None, separate_deleted: bool = True) -> OutputResult: + """ + Converts PropEntry objects to an output result to be written to a tabular datasource. + Args: + items: The PropEntry items. + commit1_id: The first commit id to be shown in the header or None. + commit2_id: The second commit id to be shown in the header or None. + show_translated_col: Whether or not to show an empty translated column. + value_regex: Regex to determine if a value should be omitted. + separate_deleted: Deleted items should not be included in regular results. + + Returns: An OutputResult to be written. + + """ + header = WITH_TRANSLATED_COLS if show_translated_col else ITEMCHANGE_DEFAULT_COLS + style = WITH_TRANSLATED_STYLE if show_translated_col else [None, None, None, VALUE_STYLE, VALUE_STYLE] + + if commit1_id: + header = header + [commit1_id] + + if commit2_id: + header = header + [commit2_id] + + results = [] + omitted = [] + deleted = [] + + for item in items: + item_row = item.get_row(show_translated_col) + if separate_deleted and item.type == ChangeType.DELETION: + deleted.append(item_row) + elif value_regex is not None and re.match(value_regex, item.cur_val): + omitted.append(item_row) + else: + results.append(item_row) + + return create_output_result(header, results, omitted=omitted, deleted=deleted, style=style) def get_item_change(rel_path: str, key: str, prev_val: str, cur_val: str) -> Union[ItemChange, None]: diff --git a/release_scripts/localization_scripts/langpropsutil.py b/release_scripts/localization_scripts/langpropsutil.py index 841574c9e9..463f570cc0 100644 --- a/release_scripts/localization_scripts/langpropsutil.py +++ b/release_scripts/localization_scripts/langpropsutil.py @@ -13,12 +13,25 @@ def _get_last_update_key(language: str) -> str: return "bundles.{lang}.lastupdated".format(lang=language) -def _get_props_path(): - return path.join(get_proj_dir(), LANG_FILENAME) +def _get_props_path(language_updates_file: Union[str, None]): + if language_updates_file: + return language_updates_file + else: + return path.join(get_proj_dir(), LANG_FILENAME) -def get_commit_for_language(language: str) -> Union[str, None]: - lang_dict = get_entry_dict_from_path(_get_props_path()) +def get_commit_for_language(language: str, language_updates_file: Union[str, None] = None) -> Union[str, None]: + """ + Retrieves the latest commit for a particular language. + Args: + language: The language key. + language_updates_file: The file containing the most recent updates. If not provided, the default file located + in the same directory as the running script is used. + + Returns: The most recent commit that the particular language has been updated or None if no key exists. + + """ + lang_dict = get_entry_dict_from_path(_get_props_path(language_updates_file)) if lang_dict is None: return None @@ -29,6 +42,15 @@ def get_commit_for_language(language: str) -> Union[str, None]: return lang_dict[key] -def set_commit_for_language(language: str, latest_commit: str): +def set_commit_for_language(language: str, latest_commit: str, language_updates_file: Union[str, None] = None): + """ + Sets the most recent update for a language within the language updates file. + Args: + language: The language key. + latest_commit: The commit for how recent the language is. + language_updates_file: The file containing the most recent updates. If not provided, the default file located + in the same directory as the running script is used. + + """ key = _get_last_update_key(language) - update_entry_dict({key: latest_commit}, _get_props_path()) + update_entry_dict({key: latest_commit}, _get_props_path(language_updates_file)) diff --git a/release_scripts/localization_scripts/outputresult.py b/release_scripts/localization_scripts/outputresult.py new file mode 100644 index 0000000000..a5169eede1 --- /dev/null +++ b/release_scripts/localization_scripts/outputresult.py @@ -0,0 +1,42 @@ +from typing import List, Union, TypedDict + + +class ColumnStyle(TypedDict): + """ + Describes style for each cell in a column. + """ + width: int + wrap_text: bool + + +class OutputResult: + """ + Describes a result that is ready to be written to file(s). + """ + column_styles: List[ColumnStyle] + freeze_first_row: bool + results: List[List[str]] + omitted: Union[List[List[str]], None] + deleted: Union[List[List[str]], None] + + def __init__(self, results: List[List[str]], omitted: Union[List[List[str]], None] = None, + deleted: Union[List[List[str]], None] = None, style: Union[List[ColumnStyle], None] = None, + freeze_first_row: bool = True): + """ + Constructs a ProcessingResult. + Args: + results: Items to be written as results. Data will be written such that the item at row,cell will be + located within result at results[row][col]. + omitted: Items to be written as omitted. Data will be written such that the item at row,cell will be + located within result at results[row][col]. + deleted: Items to be written as omitted. Data will be written such that the item at row,cell will be + located within result at results[row][col]. + style: Style for each column. No column formatting will happen for null. + freeze_first_row: Whether or not first row should be frozen. + """ + + self.results = results + self.omitted = omitted + self.deleted = deleted + self.column_styles = style + self.freeze_first_row = freeze_first_row diff --git a/release_scripts/localization_scripts/outputtype.py b/release_scripts/localization_scripts/outputtype.py new file mode 100644 index 0000000000..8b488df506 --- /dev/null +++ b/release_scripts/localization_scripts/outputtype.py @@ -0,0 +1,10 @@ +from enum import Enum + + +class OutputType(Enum): + """Describes the output file type.""" + xlsx = 'xlsx' + csv = 'csv' + + def __str__(self): + return str(self.value) \ No newline at end of file diff --git a/release_scripts/localization_scripts/propentry.py b/release_scripts/localization_scripts/propentry.py index 99c00f749e..feb0896cd5 100644 --- a/release_scripts/localization_scripts/propentry.py +++ b/release_scripts/localization_scripts/propentry.py @@ -1,3 +1,9 @@ +from typing import List, Union, Iterator +from outputresult import OutputResult +from tabularutil import WITH_TRANSLATED_COLS, DEFAULT_COLS, create_output_result, DEFAULT_STYLES, WITH_TRANSLATED_STYLE +import re + + class PropEntry: rel_path: str key: str @@ -17,3 +23,46 @@ class PropEntry: self.key = key self.value = value self.should_delete = should_delete + + def get_row(self) -> List[str]: + """Returns the list of values to be entered as a row in serialization. + + Returns: + List[str]: The list of values to be entered as a row in serialization. + """ + return [ + self.rel_path, + self.key, + self.value] + + +def convert_to_output(items: Iterator[PropEntry], commit_id: Union[str, None] = None, + show_translated_col: bool = True, value_regex: Union[str, None] = None) -> OutputResult: + """ + Converts PropEntry objects to an output result to be written to a tabular datasource. + Args: + items: The PropEntry items. + commit_id: The commit id to be shown in the header or None. + show_translated_col: Whether or not to show an empty translated column. + value_regex: Regex to determine if a value should be omitted. + + Returns: An OutputResult to be written. + + """ + header = WITH_TRANSLATED_COLS if show_translated_col else DEFAULT_COLS + style = WITH_TRANSLATED_STYLE if show_translated_col else DEFAULT_STYLES + + if commit_id: + header = header + ['', commit_id] + + results = [] + omitted = [] + + for item in items: + new_entry = item.get_row() + if value_regex is None or re.match(value_regex, item.value): + results.append(new_entry) + else: + omitted.append(new_entry) + + return create_output_result(header, results, omitted=omitted, style=style) diff --git a/release_scripts/localization_scripts/tabularutil.py b/release_scripts/localization_scripts/tabularutil.py new file mode 100644 index 0000000000..3a05f1ee91 --- /dev/null +++ b/release_scripts/localization_scripts/tabularutil.py @@ -0,0 +1,47 @@ +from typing import TypeVar, List, Union + +from outputresult import OutputResult, ColumnStyle + +T = TypeVar('T') + + +RELATIVE_PATH_COL = 'Relative path' +KEY_COL = 'Key' +VALUE_COL = 'Value' + +ENGLISH_VALUE_COL = 'English Value' +TRANSLATED_VALUE_COL = 'Translated Value' + +VALUE_STYLE: ColumnStyle = { + 'width': 100, + 'wrap_text': True +} + +DEFAULT_COLS = [RELATIVE_PATH_COL, KEY_COL, VALUE_COL] +DEFAULT_STYLES = [None, None, VALUE_STYLE] + +WITH_TRANSLATED_COLS = [RELATIVE_PATH_COL, KEY_COL, ENGLISH_VALUE_COL, TRANSLATED_VALUE_COL] +WITH_TRANSLATED_STYLE = [None, None, VALUE_STYLE, VALUE_STYLE] + + +def create_output_result(row_header: List[str], results: List[List[str]], + omitted: Union[List[List[str]], None] = None, + deleted: Union[List[List[str]], None] = None, + style: Union[List[ColumnStyle], None] = None) -> OutputResult: + + """ + Creates OutputResult from components. + Args: + row_header: The row header. + results: The results. + omitted: The omitted items if any. + deleted: The deleted items if any. + style: Style of columns if any. + + Returns: The generated OutputResult. + + """ + omitted_result = [row_header] + omitted if omitted else None + deleted_result = [row_header] + deleted if deleted else None + + return OutputResult([row_header] + results, omitted_result, deleted_result, style) diff --git a/release_scripts/localization_scripts/updatepropsscript.py b/release_scripts/localization_scripts/updatepropsscript.py index 8ba02d4664..0ee1fcc172 100644 --- a/release_scripts/localization_scripts/updatepropsscript.py +++ b/release_scripts/localization_scripts/updatepropsscript.py @@ -1,13 +1,15 @@ """This script finds all '.properties-MERGED' files and writes relative path, key, and value to a CSV file. -This script requires the python libraries: jproperties. It also requires Python 3.x. +This script requires the python libraries: jproperties, pyexcel-xlsx, xlsxwriter and pyexcel. +It also requires Python 3.x. """ -from typing import List, Dict, Tuple, Callable, Iterator +from typing import List, Dict, Tuple, Callable, Iterator, Union import sys import os from envutil import get_proj_dir -from fileutil import get_new_path +from excelutil import excel_to_records +from fileutil import get_new_path, get_path_pieces from gitutil import get_git_root from langpropsutil import set_commit_for_language from propsutil import set_entry_dict, get_entry_dict_from_path, get_lang_bundle_name @@ -44,18 +46,23 @@ def update_prop_entries(entries: Iterator[PropEntry], repo_path: str): for rel_path, (entries, to_delete) in items_by_file.items(): abs_path = os.path.join(repo_path, rel_path) + changed = False prop_items = get_entry_dict_from_path(abs_path) if prop_items is None: prop_items = {} for key_to_delete in to_delete: if key_to_delete in prop_items: + changed = True del prop_items[key_to_delete] for key, val in entries.items(): + changed = True prop_items[key] = val - set_entry_dict(prop_items, abs_path) + # only write to disk if a change was made + if changed: + set_entry_dict(prop_items, abs_path) def get_by_file(entries: Iterator[PropEntry]) -> Dict[str, Tuple[Dict[str, str], List[str]]]: @@ -95,7 +102,7 @@ def idx_bounded(num: int, max_exclusive: int) -> bool: def get_prop_entry(row: List[str], path_idx: int = 0, key_idx: int = 1, - value_idx: int = 2, + value_idx: int = 3, should_delete_converter: Callable[[List[str]], bool] = None, path_converter: Callable[[str], str] = None) -> PropEntry: """Parses a PropEntry object from a row of values in a csv. @@ -121,6 +128,11 @@ def get_prop_entry(row: List[str], key = row[key_idx] if idx_bounded(key_idx, len(row)) else None value = row[value_idx] if idx_bounded(value_idx, len(row)) else None should_delete = False if should_delete_converter is None else should_delete_converter(row) + + # delete this key if no value provided + if not value or not value.strip(): + should_delete = True + return PropEntry(path, key, value, should_delete) @@ -147,11 +159,11 @@ def get_prop_entries(rows: List[List[str]], Returns: List[PropEntry]: The generated prop entry objects. """ - propentry_iter = map(lambda row: get_prop_entry(row, path_idx, key_idx, value_idx, should_delete_converter, - path_converter), rows) + prop_entries = map(lambda row: get_prop_entry(row, path_idx, key_idx, value_idx, should_delete_converter, + path_converter), rows) - # filter rows that have no value - return filter(lambda entry: entry and entry.value.strip(), propentry_iter) + # ensure a value is present + return filter(lambda prop_entry: prop_entry and prop_entry.key.strip() and prop_entry.rel_path.strip(), prop_entries) def get_should_deleted(row_items: List[str], requested_idx: int) -> bool: @@ -170,62 +182,168 @@ def get_should_deleted(row_items: List[str], requested_idx: int) -> bool: return False +class DataRows: + """ + Defines pieces of an intermediate parsed result from a data source including the header row (if present), results + as a 2d list, and deleted results as a 2d list. + """ + header: Union[List[str], None] + results: List[List[str]] + deleted_results: Union[List[List[str]], None] + + def __init__(self, results: List[List[str]], header: Union[List[str], None] = None, + deleted_results: Union[List[List[str]], None] = None): + """ + Creates a DataRows object. + Args: + results: The 2d list of strings representing cells. + header: The header row if present. + deleted_results: The 2d list of strings representing cells or None. + """ + self.header = header + self.results = results + self.deleted_results = deleted_results + + +def get_csv_rows(input_path: str, has_header: bool) -> DataRows: + """ + Gets rows of a csv file in a DataRows format. + Args: + input_path: The input path of the file. + has_header: Whether or not it has a header. + + Returns: An intermediate result DataRows object for further parsing. + + """ + all_items, header = csv_to_records(input_path, has_header) + return DataRows(header=header, results=all_items) + + +def get_xlsx_rows(input_path: str, has_header: bool, results_sheet: str, deleted_sheet: str) -> DataRows: + """ + Gets worksheets of an excel workbook in a DataRows format. + Args: + input_path: The input path of the file. + has_header: Whether or not is has a header. + results_sheet: The name of the results sheet. + deleted_sheet: The name of the sheet containing deleted items. + + Returns: An intermediate result DataRows object for further parsing. + + """ + workbook = excel_to_records(input_path) + results_items = workbook[results_sheet] + header = None + if has_header and len(results_items) > 0: + header = results_items[0] + results_items = results_items[1:len(results_items)] + + deleted_items = workbook[deleted_sheet] if deleted_sheet and deleted_sheet in workbook else None + if has_header and deleted_items and len(deleted_items) > 0: + deleted_items = deleted_items[1:len(deleted_items)] + + return DataRows(header=header, results=results_items, deleted_results=deleted_items) + + +def get_prop_entries_from_data(datarows: DataRows, path_idx: int, key_idx: int, value_idx: int, + should_delete_converter: Union[Callable[[List[str]], bool], None], + path_converter: Callable) -> List[PropEntry]: + """ + Converts a DataRows object into PropEntry objects. + Args: + datarows: The DataRows object. + path_idx: The index of the column containing the path. + key_idx: The index of the column containing the key. + value_idx: The index of the column containing the value. + should_delete_converter: Given a list of strings representing a row, returns true if the entry should be + deleted. + path_converter: Converts the path to the proper format. + + Returns: A list of PropEntry items. + + """ + + prop_entries = list(get_prop_entries(datarows.results, path_idx, key_idx, value_idx, should_delete_converter, + path_converter)) + + if datarows.deleted_results and len(datarows.deleted_results) > 0: + prop_entries += list(get_prop_entries(datarows.deleted_results, path_idx, key_idx, value_idx, lambda row: True, + path_converter)) + + return prop_entries + + def main(): # noinspection PyTypeChecker parser = argparse.ArgumentParser(description='Updates properties files in the autopsy git repo.', formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument(dest='csv_file', type=str, help='The path to the csv file. The default format for the csv ' - 'file has columns of relative path, properties file key, ' - 'properties file value, whether or not the key should be ' - 'deleted, and commit id for how recent these updates are. ' - 'If the key should be deleted, the deletion row should be ' - '\'DELETION.\' A header row is expected by default and the ' - 'commit id, if specified, should only be in the first row. The' - ' input path should be specified as a relative path with the ' - 'dot slash notation (i.e. `./inputpath.csv`) or an absolute ' - 'path.') + parser.add_argument(dest='file', type=str, help='The path to the file (ending in either .xlsx or .csv). ' + 'The default format for the file has columns of relative path, ' + 'properties file key, properties file value, translated value, ' + 'first commit, and commit id for how recent these updates ' + 'are. If the key should be deleted, the deletion row should be ' + '\'DELETION.\' A header row is expected by default and the ' + 'commit id, if specified, should only be in the first row. The' + ' input path should be specified as a relative path with the ' + 'dot slash notation (i.e. `./inputpath.csv`) or an absolute ' + 'path.') parser.add_argument('-r', '--repo', dest='repo_path', type=str, required=False, help='The path to the repo. If not specified, parent repo of path of script is used.') + parser.add_argument('-p', '--path-idx', dest='path_idx', action='store', type=int, default=0, required=False, help='The column index in the csv file providing the relative path to the properties file.') parser.add_argument('-k', '--key-idx', dest='key_idx', action='store', type=int, default=1, required=False, help='The column index in the csv file providing the key within the properties file.') - parser.add_argument('-v', '--value-idx', dest='value_idx', action='store', type=int, default=2, required=False, + parser.add_argument('-v', '--value-idx', dest='value_idx', action='store', type=int, default=3, required=False, help='The column index in the csv file providing the value within the properties file.') - parser.add_argument('-d', '--should-delete-idx', dest='should_delete_idx', action='store', type=int, default=3, - required=False, help='The column index in the csv file providing whether or not the file ' - 'should be deleted. Any non-blank content will be treated as True.') - parser.add_argument('-c', '--commit-idx', dest='latest_commit_idx', action='store', type=int, default=4, + parser.add_argument('-c', '--commit-idx', dest='latest_commit_idx', action='store', type=int, default=5, required=False, help='The column index in the csv file providing the commit for which this ' 'update applies. The commit should be located in the header row. ') + parser.add_argument('-rs', '--results-sheet', dest='results_sheet', action='store', type=str, + default='results', required=False, help='In an excel workbook, the sheet that indicates ' + 'results items. This is only used for xlsx files.') + parser.add_argument('-ds', '--deleted-sheet', dest='deleted_sheet', action='store', type=str, + default='deleted', required=False, help='In an excel workbook, the sheet that indicates ' + 'deleted items. This is only used for xlsx files.') + parser.add_argument('-di', '--should-delete-idx', dest='should_delete_idx', action='store', type=int, default=-1, + required=False, help='The column index in the csv file providing whether or not the file ' + 'should be deleted. Any non-blank content will be treated as True.') + + parser.add_argument('-z', '--has-no-header', dest='has_no_header', action='store_true', default=False, + required=False, help='Specify whether or not there is a header within the csv file.') parser.add_argument('-f', '--file-rename', dest='file_rename', action='store', type=str, default=None, required=False, help='If specified, the properties file will be renamed to the argument' ' preserving the specified relative path.') - parser.add_argument('-z', '--has-no-header', dest='has_no_header', action='store_true', default=False, - required=False, help='Specify whether or not there is a header within the csv file.') + parser.add_argument('-o', '--should-overwrite', dest='should_overwrite', action='store_true', default=False, required=False, help="Whether or not to overwrite the previously existing properties files" " ignoring previously existing values.") - parser.add_argument('-l', '--language', dest='language', type=str, default='HEAD', required=False, + parser.add_argument('-l', '--language', dest='language', type=str, default=None, required=False, help='Specify the language in order to update the last updated properties file and rename ' 'files within directories. This flag overrides the file-rename flag.') + parser.add_argument('-lf', '--language-updates-file', dest='language_file', type=str, default=None, required=False, + help='Specify the path to the properties file containing key value pairs of language mapped to ' + 'the commit of when bundles for that language were most recently updated.') args = parser.parse_args() repo_path = args.repo_path if args.repo_path is not None else get_git_root(get_proj_dir()) - input_path = args.csv_file + + input_path = args.file path_idx = args.path_idx key_idx = args.key_idx value_idx = args.value_idx has_header = not args.has_no_header overwrite = args.should_overwrite + deleted_sheet = args.deleted_sheet + results_sheet = args.results_sheet # means of determining if a key should be deleted from a file - if args.should_delete_idx is None: + if args.should_delete_idx < 0: should_delete_converter = None else: def should_delete_converter(row_items: List[str]): @@ -241,9 +359,19 @@ def main(): else: path_converter = None - # retrieve records from csv - all_items, header = list(csv_to_records(input_path, has_header)) - prop_entries = get_prop_entries(all_items, path_idx, key_idx, value_idx, should_delete_converter, path_converter) + # retrieve records from file + ext = get_path_pieces(input_path)[2] + if ext == 'xlsx': + data_rows = get_xlsx_rows(input_path, has_header, results_sheet, deleted_sheet) + elif ext == 'csv': + data_rows = get_csv_rows(input_path, has_header) + else: + raise ValueError('Expected either a csv file or xlsx file for input.') + + # convert to PropEntry objects + prop_entries = get_prop_entries_from_data(data_rows, path_idx, key_idx, value_idx, + should_delete_converter, path_converter) + header = data_rows.header # write to files if overwrite: @@ -252,8 +380,8 @@ def main(): update_prop_entries(prop_entries, repo_path) # update the language last update if applicable - if args.language is not None and header is not None and len(header) > args.latest_commit_idx >= 0: - set_commit_for_language(args.language, header[args.latest_commit_idx]) + if args.language and header is not None and len(header) > args.latest_commit_idx >= 0: + set_commit_for_language(args.language, header[args.latest_commit_idx], args.language_file) sys.exit(0) diff --git a/ruleset.xml b/ruleset.xml index 468f95087e..24f285b959 100644 --- a/ruleset.xml +++ b/ruleset.xml @@ -1,298 +1,308 @@ - - Ruleset used by Autopsy - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/thirdparty/iLeapp/LICENSE b/thirdparty/iLeapp/LICENSE new file mode 100644 index 0000000000..ae8fc549fc --- /dev/null +++ b/thirdparty/iLeapp/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Brigs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/thirdparty/iLeapp/ileapp.exe b/thirdparty/iLeapp/ileapp.exe new file mode 100644 index 0000000000..8176b4f679 Binary files /dev/null and b/thirdparty/iLeapp/ileapp.exe differ diff --git a/thunderbirdparser/nbproject/project.xml b/thunderbirdparser/nbproject/project.xml index 0dc27e4902..6643e72e0a 100644 --- a/thunderbirdparser/nbproject/project.xml +++ b/thunderbirdparser/nbproject/project.xml @@ -53,7 +53,7 @@ 10 - 10.21 + 10.22 diff --git a/unix_setup.sh b/unix_setup.sh index a9d01739f6..63e2daee9a 100644 --- a/unix_setup.sh +++ b/unix_setup.sh @@ -5,7 +5,7 @@ # NOTE: update_sleuthkit_version.pl updates this value and relies # on it keeping the same name and whitespace. Don't change it. -TSK_VERSION=4.10.0 +TSK_VERSION=4.10.1 # In the beginning...