diff --git a/Core/ivy.xml b/Core/ivy.xml index 36245ce14d..92be3f86e6 100644 --- a/Core/ivy.xml +++ b/Core/ivy.xml @@ -46,6 +46,9 @@ + + + diff --git a/Core/nbproject/project.properties b/Core/nbproject/project.properties index 8d9c857fba..91f9aa13e0 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.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.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 @@ -121,4 +127,3 @@ 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 - diff --git a/Core/nbproject/project.xml b/Core/nbproject/project.xml index 19c863e6b7..ae69a4e5c6 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 @@ -553,20 +549,12 @@ ext/sleuthkit-4.10.0.jar - release\modules\ext\sleuthkit-4.10.0.jar + release/modules/ext/sleuthkit-4.10.0.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 @@ -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..3ad14f6137 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; 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/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/datamodel/utils/IconsUtil.java b/Core/src/org/sleuthkit/autopsy/datamodel/utils/IconsUtil.java index 3e4b1ff1c0..88dc335b5d 100755 --- a/Core/src/org/sleuthkit/autopsy/datamodel/utils/IconsUtil.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/utils/IconsUtil.java @@ -117,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/AnalysisSummary.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/AnalysisSummary.java index f3f436ae62..9d35e3d28d 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/AnalysisSummary.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/AnalysisSummary.java @@ -21,6 +21,7 @@ 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; @@ -146,6 +147,11 @@ public class AnalysisSummary implements DefaultArtifactUpdateGovernor { */ 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/IngestModuleCheckUtil.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/IngestModuleCheckUtil.java new file mode 100644 index 0000000000..86c731e78b --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/IngestModuleCheckUtil.java @@ -0,0 +1,146 @@ +/* + * 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. + * @param logger The logger to use. + */ + 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/PastCasesSummary.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/PastCasesSummary.java index 2bb1e9c37a..a304d367ff 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/PastCasesSummary.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/PastCasesSummary.java @@ -28,7 +28,6 @@ import java.util.Set; import java.util.logging.Level; import java.util.stream.Collectors; import java.util.stream.Stream; -import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.sleuthkit.autopsy.centralrepository.ingestmodule.CentralRepoIngestModuleFactory; import org.sleuthkit.autopsy.datasourcesummary.datamodel.SleuthkitCaseProvider.SleuthkitCaseProviderException; @@ -38,7 +37,6 @@ 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.IngestJobInfo; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TskCoreException; @@ -66,47 +64,6 @@ import org.sleuthkit.datamodel.TskCoreException; */ public class PastCasesSummary implements DefaultArtifactUpdateGovernor { - /** - * Exception that is thrown in the event that a data source has not been - * ingested with a particular ingest module. - */ - public static class NotIngestedWithModuleException extends Exception { - - private static final long serialVersionUID = 1L; - - private final String moduleDisplayName; - - /** - * Constructor. - * - * @param moduleName The module name. - * @param message The message for the exception. - */ - public NotIngestedWithModuleException(String moduleName, String message) { - super(message); - this.moduleDisplayName = moduleName; - } - - /** - * Constructor. - * - * @param moduleName The module name. - * @param message The message for the exception. - * @param thrwbl Inner exception if applicable. - */ - public NotIngestedWithModuleException(String moduleName, String message, Throwable thrwbl) { - super(message, thrwbl); - this.moduleDisplayName = moduleName; - } - - /** - * @return The module display name. - */ - public String getModuleDisplayName() { - return moduleDisplayName; - } - } - /** * Return type for results items in the past cases tab. */ @@ -335,16 +292,17 @@ public class PastCasesSummary implements DefaultArtifactUpdateGovernor { * * @param dataSource The data source. * - * @return The retrieved data. + * @return The retrieved data or null if null dataSource. * * @throws SleuthkitCaseProviderException * @throws TskCoreException - * @throws NotIngestedWithModuleException */ public PastCasesResult getPastCasesData(DataSource dataSource) - throws SleuthkitCaseProvider.SleuthkitCaseProviderException, TskCoreException, NotIngestedWithModuleException { + throws SleuthkitCaseProvider.SleuthkitCaseProviderException, TskCoreException { - throwOnNotCentralRepoIngested(dataSource); + if (dataSource == null) { + return null; + } SleuthkitCase skCase = caseProvider.get(); @@ -372,75 +330,4 @@ public class PastCasesSummary implements DefaultArtifactUpdateGovernor { getCaseCounts(Stream.concat(filesCases, nonDeviceArtifactCases.stream())) ); } - - /** - * Returns true if the ingest job info contains an ingest module that - * matches the Central Repo Module ingest display name. - * - * @param info The info. - * - * @return True if there is a central repo ingest match. - */ - private boolean hasCentralRepoIngest(IngestJobInfo info) { - if (info == null || info.getIngestModuleInfo() == null) { - return false; - } - - return info.getIngestModuleInfo().stream() - .anyMatch((moduleInfo) -> { - return StringUtils.isNotBlank(moduleInfo.getDisplayName()) - && moduleInfo.getDisplayName().trim().equalsIgnoreCase(CENTRAL_REPO_INGEST_NAME); - }); - } - - /** - * Returns true if the central repository ingest module has been run on the - * datasource. - * - * @param dataSource The data source. - * - * @return True if there is an ingest job pertaining to the data source - * where an ingest module matches the central repo ingest module - * display name. - * - * @throws SleuthkitCaseProviderException - * @throws TskCoreException - */ - public boolean isCentralRepoIngested(DataSource dataSource) - throws SleuthkitCaseProvider.SleuthkitCaseProviderException, TskCoreException { - if (dataSource == null) { - return false; - } - - long dataSourceId = dataSource.getId(); - - return this.caseProvider.get().getIngestJobs().stream() - .anyMatch((ingestJob) -> { - return ingestJob != null - && ingestJob.getObjectId() == dataSourceId - && hasCentralRepoIngest(ingestJob); - }); - - } - - /** - * Throws an exception if the current data source has not been ingested with - * the Central Repository Ingest Module. - * - * @param dataSource The data source to check if it has been ingested with - * the Central Repository Ingest Module. - * - * @throws SleuthkitCaseProviderException - * @throws TskCoreException - * @throws NotIngestedWithModuleException - */ - private void throwOnNotCentralRepoIngested(DataSource dataSource) - throws SleuthkitCaseProvider.SleuthkitCaseProviderException, TskCoreException, NotIngestedWithModuleException { - - if (!isCentralRepoIngested(dataSource)) { - String objectId = (dataSource == null) ? "" : String.valueOf(dataSource.getId()); - String message = String.format("Data source: %s has not been ingested with the Central Repository Ingest Module.", objectId); - throw new NotIngestedWithModuleException(CENTRAL_REPO_INGEST_NAME, message); - } - } } diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/RecentFilesSummary.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/RecentFilesSummary.java index 4dadb8e028..0a47e0ea6c 100755 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/RecentFilesSummary.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/RecentFilesSummary.java @@ -24,6 +24,7 @@ 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; @@ -108,7 +109,7 @@ public class RecentFilesSummary implements DefaultArtifactUpdateGovernor { */ 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 @@ -159,6 +160,10 @@ public class RecentFilesSummary implements DefaultArtifactUpdateGovernor { * @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(ARTIFACT_TYPE.TSK_WEB_DOWNLOAD), @@ -206,6 +211,10 @@ public class RecentFilesSummary implements DefaultArtifactUpdateGovernor { * @throws TskCoreException */ public List getRecentAttachments(DataSource dataSource, int maxCount) throws SleuthkitCaseProviderException, TskCoreException { + if (dataSource == null) { + return Collections.emptyList(); + } + return createListFromMap(buildAttachmentMap(dataSource), maxCount); } diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/UserActivitySummary.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/UserActivitySummary.java index 0f048a6ed0..2652a450ec 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/UserActivitySummary.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/UserActivitySummary.java @@ -149,6 +149,10 @@ public class UserActivitySummary implements DefaultArtifactUpdateGovernor { 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) { @@ -307,6 +311,10 @@ public class UserActivitySummary implements DefaultArtifactUpdateGovernor { 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()); @@ -391,6 +399,10 @@ public class UserActivitySummary implements DefaultArtifactUpdateGovernor { 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() @@ -476,6 +488,10 @@ public class UserActivitySummary implements DefaultArtifactUpdateGovernor { 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)); 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 3cb5a9b10d..34dfa97a13 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/AnalysisPanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/AnalysisPanel.java @@ -26,8 +26,11 @@ import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.datasourcesummary.datamodel.AnalysisSummary; import org.sleuthkit.autopsy.datasourcesummary.uiutils.CellModelTableCellRenderer.DefaultCellModel; 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; /** @@ -36,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 */ @@ -77,6 +90,9 @@ public class AnalysisPanel extends BaseDataSourceSummaryPanel { keywordHitsTable, interestingItemsTable ); + + private final IngestRunningLabel ingestRunningLabel = new IngestRunningLabel(); + /** * All of the components necessary for data fetch swing workers to load data @@ -99,20 +115,28 @@ public class AnalysisPanel extends BaseDataSourceSummaryPanel { // 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); @@ -134,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; @@ -152,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); diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/BaseDataSourceSummaryPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/BaseDataSourceSummaryPanel.java index 6b548e2b6e..77b4a4c688 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/BaseDataSourceSummaryPanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/BaseDataSourceSummaryPanel.java @@ -23,13 +23,18 @@ 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; @@ -55,6 +60,7 @@ abstract class BaseDataSourceSummaryPanel extends JPanel { 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; @@ -284,15 +290,20 @@ abstract class BaseDataSourceSummaryPanel extends JPanel { * @param dataSource The data source argument. */ protected void fetchInformation(List> dataFetchComponents, DataSource dataSource) { - // create swing workers to run for each loadable item - List> workers = dataFetchComponents - .stream() - .map((components) -> new DataFetchWorker<>(components, dataSource)) - .collect(Collectors.toList()); + 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); + // submit swing workers to run + if (!workers.isEmpty()) { + submit(workers); + } } } @@ -329,4 +340,78 @@ abstract class BaseDataSourceSummaryPanel extends JPanel { fetchInformation(dataSource); } } + + /** + * Get default message when there is a NotIngestedWithModuleException. + * + * @param exception 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-MERGED b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties-MERGED index c994603559..9a48089e5d 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties-MERGED @@ -1,7 +1,8 @@ AnalysisPanel_countColumn_title=Count AnalysisPanel_keyColumn_title=Name -ContainerPanel.getDataSources.error.text=Failed to get the list of datasources for the current case. -ContainerPanel.getDataSources.error.title=Load Failure +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 DataSourceSummaryDialog.closeButton.text=Close ContainerPanel.displayNameLabel.text=Display Name: @@ -54,6 +55,7 @@ PastCasesPanel_onNoCrIngest_message=No results will be shown because the Central 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 diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/ContainerPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/ContainerPanel.java index d350ee0fa7..8f11e0dcd3 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/ContainerPanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/ContainerPanel.java @@ -26,7 +26,6 @@ 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.ContainerSummary; import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchResult.ResultType; @@ -49,23 +48,16 @@ class ContainerPanel extends BaseDataSourceSummaryPanel { private final DataSource dataSource; private final Long unallocatedFilesSize; - private final String operatingSystem; - private final String dataSourceType; /** * Main constructor. * * @param dataSource The original datasource. * @param unallocatedFilesSize The unallocated file size. - * @param operatingSystem The string representing the operating - * system. - * @param dataSourceType The datasource type as a string. */ - ContainerPanelData(DataSource dataSource, Long unallocatedFilesSize, String operatingSystem, String dataSourceType) { + ContainerPanelData(DataSource dataSource, Long unallocatedFilesSize) { this.dataSource = dataSource; this.unallocatedFilesSize = unallocatedFilesSize; - this.operatingSystem = operatingSystem; - this.dataSourceType = dataSourceType; } /** @@ -81,21 +73,6 @@ class ContainerPanel extends BaseDataSourceSummaryPanel { Long getUnallocatedFilesSize() { return unallocatedFilesSize; } - - /** - * @return The string representing the operating system. - */ - String getOperatingSystem() { - return operatingSystem; - } - - /** - * @return The datasource type as a string. - */ - String getDataSourceType() { - return dataSourceType; - } - } // set of case events for which to call update (if the name changes, that will impact data shown) @@ -134,8 +111,6 @@ class ContainerPanel extends BaseDataSourceSummaryPanel { /** * Creates new form ContainerPanel. */ - @Messages({"ContainerPanel.getDataSources.error.text=Failed to get the list of datasources for the current case.", - "ContainerPanel.getDataSources.error.title=Load Failure"}) ContainerPanel(ContainerSummary containerSummary) { super(containerSummary, CONTAINER_UPDATES); @@ -144,20 +119,24 @@ class ContainerPanel extends BaseDataSourceSummaryPanel { (dataSource) -> { return new ContainerPanelData( dataSource, - containerSummary.getSizeOfUnallocatedFiles(dataSource), - containerSummary.getOperatingSystems(dataSource), - containerSummary.getDataSourceType(dataSource) + containerSummary.getSizeOfUnallocatedFiles(dataSource) ); }, (result) -> { - if (result.getResultType() == ResultType.SUCCESS) { + if (result != null && result.getResultType() == ResultType.SUCCESS) { ContainerPanelData data = result.getData(); - updateDetailsPanelData( - data.getDataSource(), - data.getUnallocatedFilesSize()); + DataSource dataSource = (data == null) ? null : data.getDataSource(); + Long unallocatedFileSize = (data == null) ? null : data.getUnallocatedFilesSize(); + + updateDetailsPanelData(dataSource, unallocatedFileSize); } else { - logger.log(Level.WARNING, "An exception occurred while attempting to fetch data for the ContainerPanel.", - result.getException()); + 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); } } diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/PastCasesPanel.form b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/PastCasesPanel.form index 13ddb081bd..7c84d44660 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/PastCasesPanel.form +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/PastCasesPanel.form @@ -51,6 +51,27 @@ + + + + + + + + + + + + + + + + + + + + + @@ -70,7 +91,7 @@ - + @@ -78,6 +99,7 @@ + @@ -109,7 +131,7 @@ - + @@ -117,6 +139,7 @@ + @@ -138,7 +161,7 @@ - + @@ -146,6 +169,7 @@ + @@ -179,6 +203,7 @@ + diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/PastCasesPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/PastCasesPanel.java index a530757276..76d36f4785 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/PastCasesPanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/PastCasesPanel.java @@ -21,18 +21,17 @@ package org.sleuthkit.autopsy.datasourcesummary.ui; import java.util.Arrays; import java.util.List; import java.util.function.Function; -import java.util.stream.Collectors; import org.apache.commons.lang3.tuple.Pair; import org.openide.util.NbBundle.Messages; -import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.centralrepository.ingestmodule.CentralRepoIngestModuleFactory; import org.sleuthkit.autopsy.datasourcesummary.datamodel.PastCasesSummary; -import org.sleuthkit.autopsy.datasourcesummary.datamodel.PastCasesSummary.NotIngestedWithModuleException; 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; @@ -45,11 +44,12 @@ import org.sleuthkit.datamodel.DataSource; "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(), @@ -76,6 +76,8 @@ public class PastCasesPanel extends BaseDataSourceSummaryPanel { private final List> dataFetchComponents; + private final IngestRunningLabel ingestRunningLabel = new IngestRunningLabel(); + public PastCasesPanel() { this(new PastCasesSummary()); } @@ -95,21 +97,14 @@ public class PastCasesPanel extends BaseDataSourceSummaryPanel { } /** - * handles displaying the result for the table. If a - * NotCentralRepoIngestedException is thrown, then an appropriate message is - * shown. Otherwise, this method uses the tables default showDataFetchResult - * method. + * 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) { - if (result.getResultType() == ResultType.ERROR && result.getException() instanceof NotIngestedWithModuleException) { - notableFileTable.showMessage(Bundle.PastCasesPanel_onNoCrIngest_message()); - sameIdTable.showMessage(Bundle.PastCasesPanel_onNoCrIngest_message()); - } else { - notableFileTable.showDataFetchResult(getSubResult(result, (res) -> (res == null) ? null : res.getTaggedNotable())); - sameIdTable.showDataFetchResult(getSubResult(result, (res) -> (res == null) ? null : res.getSameIdsResults())); - } + showResultWithModuleCheck(notableFileTable, getSubResult(result, (res) -> res.getTaggedNotable()), CR_FACTORY, CR_NAME); + showResultWithModuleCheck(sameIdTable, getSubResult(result, (res) -> res.getSameIdsResults()), CR_FACTORY, CR_NAME); } /** @@ -124,9 +119,12 @@ public class PastCasesPanel extends BaseDataSourceSummaryPanel { * @return The new result with the error of the original or the processed * data. */ - private DataFetchResult getSubResult(DataFetchResult inputResult, Function getSubResult) { - if (inputResult.getResultType() == ResultType.SUCCESS) { - return DataFetchResult.getSuccessResult(getSubResult.apply(inputResult.getData())); + 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()); } @@ -142,6 +140,12 @@ public class PastCasesPanel extends BaseDataSourceSummaryPanel { 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 @@ -153,22 +157,30 @@ public class PastCasesPanel 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 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(32767, 2)); + 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(32767, 20)); + 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(32767, 2)); + 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); @@ -176,10 +188,14 @@ public class PastCasesPanel extends BaseDataSourceSummaryPanel { 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); @@ -187,6 +203,8 @@ public class PastCasesPanel extends BaseDataSourceSummaryPanel { 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); 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 11e083c9ae..1b675032d0 100755 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/RecentFilesPanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/RecentFilesPanel.java @@ -22,12 +22,14 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.openide.util.NbBundle.Messages; +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.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; @@ -39,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" }) /** @@ -80,6 +87,12 @@ public final class RecentFilesPanel extends BaseDataSourceSummaryPanel { onNewDataSource(dataFetchComponents, tablePanelList, dataSource); } + @Override + public void close() { + ingestRunningLabel.unregister(); + super.close(); + } + /** * Setup the data model and columns for the panel tables. */ @@ -118,8 +131,11 @@ public final class RecentFilesPanel extends BaseDataSourceSummaryPanel { 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); } @@ -154,8 +170,11 @@ public final class RecentFilesPanel extends BaseDataSourceSummaryPanel { 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); } @@ -190,8 +209,8 @@ public final class RecentFilesPanel extends BaseDataSourceSummaryPanel { 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); } @@ -208,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(); @@ -217,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); diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TypesPanel.form b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TypesPanel.form index 2efe151eb5..efb1ca2aae 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TypesPanel.form +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TypesPanel.form @@ -52,6 +52,27 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TypesPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TypesPanel.java index e082543dee..4d9271d712 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TypesPanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TypesPanel.java @@ -26,22 +26,29 @@ 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 javax.swing.JLabel; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; 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; @@ -93,27 +100,64 @@ class TypesPanel extends BaseDataSourceSummaryPanel { this.showResults(null); } - private void setValue(String value, boolean italicize) { + private void setValue(String value) { String formattedKey = StringUtils.isBlank(key) ? "" : key; String formattedValue = StringUtils.isBlank(value) ? "" : value; - String htmlFormattedValue = (italicize) ? String.format("%s", formattedValue) : formattedValue; - label.setText(String.format("
%s: %s
", formattedKey, htmlFormattedValue)); + label.setText(String.format("%s: %s", formattedKey, formattedValue)); } @Override protected void setMessage(boolean visible, String message) { - setValue(message, true); + setValue(message); } @Override protected void setResults(String data) { - setValue(data, false); + 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; } } 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()); // All file type categories. private static final List>> FILE_MIME_TYPE_CATEGORIES = Arrays.asList( @@ -148,6 +192,8 @@ class TypesPanel extends BaseDataSourceSummaryPanel { directoriesLabel ); + private final IngestRunningLabel ingestRunningLabel = new IngestRunningLabel(); + // all of the means for obtaining data for the gui components. private final List> dataFetchComponents; @@ -158,6 +204,12 @@ class TypesPanel extends BaseDataSourceSummaryPanel { this(new MimeTypeSummary(), new TypesSummary(), new ContainerSummary()); } + @Override + public void close() { + ingestRunningLabel.unregister(); + super.close(); + } + /** * Creates a new TypesPanel. * @@ -176,11 +228,25 @@ class TypesPanel extends BaseDataSourceSummaryPanel { // usage label worker new DataFetchWorker.DataFetchComponents<>( containerData::getDataSourceType, - usageLabel::showDataFetchResult), + (result) -> { + showResultWithModuleCheck( + usageLabel, + result, + StringUtils::isNotBlank, + IngestModuleCheckUtil.RECENT_ACTIVITY_FACTORY, + IngestModuleCheckUtil.RECENT_ACTIVITY_MODULE_NAME); + }), // os label worker new DataFetchWorker.DataFetchComponents<>( containerData::getOperatingSystems, - osLabel::showDataFetchResult), + (result) -> { + showResultWithModuleCheck( + osLabel, + result, + StringUtils::isNotBlank, + IngestModuleCheckUtil.RECENT_ACTIVITY_FACTORY, + IngestModuleCheckUtil.RECENT_ACTIVITY_MODULE_NAME); + }), // size label worker new DataFetchWorker.DataFetchComponents<>( (dataSource) -> { @@ -191,7 +257,7 @@ class TypesPanel extends BaseDataSourceSummaryPanel { // file types worker new DataFetchWorker.DataFetchComponents<>( (dataSource) -> getMimeTypeCategoriesModel(mimeTypeData, dataSource), - fileMimeTypesChart::showDataFetchResult), + this::showMimeTypeCategories), // allocated files worker new DataFetchWorker.DataFetchComponents<>( (dataSource) -> getStringOrZero(typeData.getCountOfAllocatedFiles(dataSource)), @@ -230,7 +296,7 @@ class TypesPanel extends BaseDataSourceSummaryPanel { * * @return The pie chart items. */ - private List getMimeTypeCategoriesModel(MimeTypeSummary mimeTypeData, DataSource dataSource) + private TypesPieChartData getMimeTypeCategoriesModel(MimeTypeSummary mimeTypeData, DataSource dataSource) throws SQLException, SleuthkitCaseProviderException, TskCoreException { if (dataSource == null) { @@ -259,15 +325,76 @@ class TypesPanel extends BaseDataSourceSummaryPanel { fileCategoryItems.add(Pair.of(Bundle.TypesPanel_fileMimeTypesChart_other_title(), allRegularFiles - (categoryTotalCount + noMimeTypeCount))); + // check at this point to see if these are all 0; if so, we don't have useful content. + boolean usefulContent = fileCategoryItems.stream().anyMatch((pair) -> pair.getValue() != null && pair.getValue() > 0); + // create entry for not analyzed mime types category fileCategoryItems.add(Pair.of(Bundle.TypesPanel_fileMimeTypesChart_notAnalyzed_title(), noMimeTypeCount)); // create pie chart items to provide to pie chart - return fileCategoryItems.stream() + List items = fileCategoryItems.stream() .filter(keyCount -> keyCount.getRight() != null && keyCount.getRight() > 0) .map(keyCount -> new PieChartItem(keyCount.getLeft(), keyCount.getRight())) .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)); + } } /** @@ -304,6 +431,7 @@ class TypesPanel extends BaseDataSourceSummaryPanel { 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; @@ -322,6 +450,12 @@ class TypesPanel extends BaseDataSourceSummaryPanel { 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)); diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/UserActivityPanel.form b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/UserActivityPanel.form index a7a3d6e0b2..28560a3ec7 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/UserActivityPanel.form +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/UserActivityPanel.form @@ -60,6 +60,27 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/UserActivityPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/UserActivityPanel.java index 6437812296..476d8c5e6f 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/UserActivityPanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/UserActivityPanel.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.Locale; import org.apache.commons.lang.StringUtils; import org.openide.util.NbBundle.Messages; +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; @@ -36,6 +37,7 @@ import org.sleuthkit.autopsy.datasourcesummary.datamodel.TopProgramsSummary.TopP import org.sleuthkit.autopsy.datasourcesummary.datamodel.UserActivitySummary.TopDomainsResult; import org.sleuthkit.autopsy.datasourcesummary.uiutils.CellModelTableCellRenderer.DefaultCellModel; 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; @@ -70,6 +72,8 @@ public class UserActivityPanel extends BaseDataSourceSummaryPanel { 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. @@ -220,6 +224,8 @@ public class UserActivityPanel extends BaseDataSourceSummaryPanel { topAccountsTable ); + private final IngestRunningLabel ingestRunningLabel = new IngestRunningLabel(); + private final List> dataFetchComponents; private final TopProgramsSummary topProgramsData; @@ -250,28 +256,43 @@ public class UserActivityPanel extends BaseDataSourceSummaryPanel { // top programs query new DataFetchComponents>( (dataSource) -> topProgramsData.getTopPrograms(dataSource, TOP_PROGS_COUNT), - (result) -> topProgramsTable.showDataFetchResult(result, JTablePanel.getDefaultErrorMessage(), - Bundle.UserActivityPanel_noDataExists())), + (result) -> { + showResultWithModuleCheck(topProgramsTable, result, + IngestModuleCheckUtil.RECENT_ACTIVITY_FACTORY, + IngestModuleCheckUtil.RECENT_ACTIVITY_MODULE_NAME); + }), // top domains query new DataFetchComponents>( (dataSource) -> userActivityData.getRecentDomains(dataSource, TOP_DOMAINS_COUNT), - (result) -> recentDomainsTable.showDataFetchResult(result, JTablePanel.getDefaultErrorMessage(), - Bundle.UserActivityPanel_noDataExists())), + (result) -> { + showResultWithModuleCheck(recentDomainsTable, result, + IngestModuleCheckUtil.RECENT_ACTIVITY_FACTORY, + IngestModuleCheckUtil.RECENT_ACTIVITY_MODULE_NAME); + }), // top web searches query new DataFetchComponents>( (dataSource) -> userActivityData.getMostRecentWebSearches(dataSource, TOP_SEARCHES_COUNT), - (result) -> topWebSearchesTable.showDataFetchResult(result, JTablePanel.getDefaultErrorMessage(), - Bundle.UserActivityPanel_noDataExists())), + (result) -> { + showResultWithModuleCheck(topWebSearchesTable, result, + IngestModuleCheckUtil.RECENT_ACTIVITY_FACTORY, + IngestModuleCheckUtil.RECENT_ACTIVITY_MODULE_NAME); + }), // top devices query new DataFetchComponents>( (dataSource) -> userActivityData.getRecentDevices(dataSource, TOP_DEVICES_COUNT), - (result) -> topDevicesAttachedTable.showDataFetchResult(result, JTablePanel.getDefaultErrorMessage(), - Bundle.UserActivityPanel_noDataExists())), + (result) -> { + showResultWithModuleCheck(topDevicesAttachedTable, result, + IngestModuleCheckUtil.RECENT_ACTIVITY_FACTORY, + IngestModuleCheckUtil.RECENT_ACTIVITY_MODULE_NAME); + }), // top accounts query new DataFetchComponents>( (dataSource) -> userActivityData.getRecentAccounts(dataSource, TOP_ACCOUNTS_COUNT), - (result) -> topAccountsTable.showDataFetchResult(result, JTablePanel.getDefaultErrorMessage(), - Bundle.UserActivityPanel_noDataExists())) + (result) -> { + showResultWithModuleCheck(topAccountsTable, result, + ANDROID_FACTORY, + ANDROID_MODULE_NAME); + }) ); initComponents(); @@ -299,6 +320,12 @@ public class UserActivityPanel extends BaseDataSourceSummaryPanel { 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 @@ -310,6 +337,7 @@ public class UserActivityPanel extends BaseDataSourceSummaryPanel { 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; @@ -340,6 +368,12 @@ public class UserActivityPanel extends BaseDataSourceSummaryPanel { 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(UserActivityPanel.class, "UserActivityPanel.programsRunLabel.text")); // NOI18N programsRunLabel.setAlignmentX(Component.LEFT_ALIGNMENT); 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 491c3bfa56..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 @@ 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 a89b273fcc..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; @@ -140,9 +139,6 @@ public class DataFetchWorker extends SwingWorker { } } - // 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 index d664161696..390317a955 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DefaultArtifactUpdateGovernor.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DefaultArtifactUpdateGovernor.java @@ -36,6 +36,10 @@ public interface DefaultArtifactUpdateGovernor extends DefaultUpdateGovernor { @Override default boolean isRefreshRequired(ModuleDataEvent evt) { + if (evt == null || evt.getBlackboardArtifactType() == null) { + return false; + } + return getArtifactTypeIdsForRefresh().contains(evt.getBlackboardArtifactType().getTypeID()); } 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/PieChartPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/PieChartPanel.java index 8d6eab4a9b..466c578b60 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/PieChartPanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/PieChartPanel.java @@ -33,10 +33,14 @@ 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> { /** @@ -111,6 +115,13 @@ public class PieChartPanel extends AbstractLoadableComponent data) { this.dataset.clear(); - if (data != null) { + if (data != null && !data.isEmpty()) { for (PieChartPanel.PieChartItem slice : data) { this.dataset.setValue(slice.getLabel(), slice.getValue()); } + } 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); } } + + /** + * 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/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 c0d9ef6975..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=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 -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..637e982f98 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/DiscoveryAttributes.java @@ -0,0 +1,896 @@ +/* + * 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 files 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 (result.getKnown() == TskData.FileKnown.KNOWN) { + result.setFrequency(SearchData.Frequency.KNOWN); + } + + if (result.getType() != SearchData.Type.DOMAIN) { + ResultFile file = (ResultFile) result; + if (file.getFrequency() == SearchData.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(); + } + } else { + ResultDomain domainInstance = (ResultDomain) result; + if (domainInstance.getFrequency() != SearchData.Frequency.UNKNOWN) { + // Frequency already calculated, skipping... + continue; + } + domainsToQuery.add(domainInstance); + + 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 resultFiles 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 58% rename from Core/src/org/sleuthkit/autopsy/discovery/DiscoveryEventUtils.java rename to Core/src/org/sleuthkit/autopsy/discovery/search/DiscoveryEventUtils.java index ea5fca6676..67c69904fa 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,38 +52,39 @@ 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 } } @@ -91,21 +92,23 @@ final class DiscoveryEventUtils { /** * 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,13 +116,13 @@ 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, @@ -130,16 +133,16 @@ 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. */ - 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 +150,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 +168,7 @@ final class DiscoveryEventUtils { * * @return The grouping attribute used by the search. */ - FileSearch.AttributeType getGroupingAttr() { + public DiscoveryAttributes.AttributeType getGroupingAttr() { return groupingAttribute; } @@ -174,17 +177,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 +196,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 +229,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 +246,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 +273,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 +290,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 +326,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 +344,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 +371,7 @@ final class DiscoveryEventUtils { * * @return The grouping attribute used by the search. */ - FileSearch.AttributeType getGroupingAttr() { + public DiscoveryAttributes.AttributeType getGroupingAttr() { return groupingAttribute; } @@ -376,7 +380,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 +390,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 +399,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..ea177efcc9 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchArtifactsLoader.java @@ -0,0 +1,61 @@ +/* + * 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 { + 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) { + 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..99948592bf --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchCacheLoader.java @@ -0,0 +1,341 @@ +/* + * 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 { + + List domainResults = getResultDomainsFromDatabase(key); + + // Apply secondary in memory filters + for (AbstractFilter filter : key.getFilters()) { + if (filter.useAlternateFilter()) { + domainResults = filter.applyAlternateFilter(domainResults, key.getSleuthkitCase(), key.getCentralRepository()); + } + } + + // 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) { + attr.addAttributeToResults(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 { + + // 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(); + } + + 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 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()) { + 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; + } + } + + /** + * 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; + } + } +} 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..9dac2c2064 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/DomainSearchThumbnailLoader.java @@ -0,0 +1,163 @@ +/* + * 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 { + 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--) { + // 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--) { + // 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 { + final List jpegs = new ArrayList<>(); + for (BlackboardArtifact artifact : artifacts) { + 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 { + final BlackboardAttribute.Type TSK_PATH_ID = new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_PATH_ID); + final List jpegs = new ArrayList<>(); + for (BlackboardArtifact artifact : artifacts) { + 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..20a2ccfb62 --- /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 resultFiles 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..2b196aadcc --- /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; + 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..64a3c751c6 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/ResultDomain.java @@ -0,0 +1,141 @@ +/* + * 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; + + this.setFrequency(SearchData.Frequency.UNKNOWN); + } + + /** + * 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..c88726c998 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 93% rename from Core/src/org/sleuthkit/autopsy/discovery/DataSourceFilterPanel.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/DataSourceFilterPanel.java index a2a2cf4de9..ec4b819696 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/DataSourceFilterPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DataSourceFilterPanel.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.List; import java.util.logging.Level; import java.util.stream.Collectors; @@ -25,8 +26,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; @@ -140,6 +143,7 @@ final class DataSourceFilterPanel extends AbstractDiscoveryFilterPanel { dsListModel.removeAllElements(); for (DataSource ds : Case.getCurrentCase().getSleuthkitCase().getDataSources()) { dsListModel.add(count, new DataSourceItem(ds)); + count++; } } catch (TskCoreException ex) { logger.log(Level.SEVERE, "Error loading data sources", ex); @@ -185,19 +189,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 99% rename from Core/src/org/sleuthkit/autopsy/discovery/DataSourceModulesWrapper.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/DataSourceModulesWrapper.java index 2f0e30a6f0..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; 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 87% rename from Core/src/org/sleuthkit/autopsy/discovery/DiscoveryTopComponent.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryTopComponent.java index 633bf5c612..07465afcfd 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; @@ -182,7 +186,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 +261,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 +284,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 +305,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 84% rename from Core/src/org/sleuthkit/autopsy/discovery/ResultsPanel.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/ResultsPanel.java index fc86f7a963..2c9a4d4f41 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; } @@ -190,6 +218,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 +230,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 +252,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 +267,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 +315,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 +339,7 @@ final class ResultsPanel extends javax.swing.JPanel { videoThumbnailViewer.clearViewer(); imageThumbnailViewer.clearViewer(); documentPreviewViewer.clearViewer(); + domainSummaryViewer.clearViewer(); resultsViewerPanel.revalidate(); resultsViewerPanel.repaint(); }); @@ -646,7 +704,7 @@ final class ResultsPanel extends javax.swing.JPanel { @Override protected Void doInBackground() throws Exception { - FileSearch.getVideoThumbnails(thumbnailWrapper); + DiscoveryUiUtils.getVideoThumbnails(thumbnailWrapper); return null; } @@ -752,4 +810,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/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/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/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/modules/ileappanalyzer/ILeappFileProcessor.java b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ILeappFileProcessor.java index 1bc80c9019..8c9c8c340b 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ILeappFileProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ILeappFileProcessor.java @@ -202,6 +202,7 @@ public final class ILeappFileProcessor { * * @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 */ @@ -220,7 +221,7 @@ public final class ILeappFileProcessor { break; } String attrType = attributeType.getValueType().getLabel().toUpperCase(); - checkAttributeType(bbattributes, attrType, columnValues, columnNumber, attributeType); + 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 } @@ -234,7 +235,8 @@ public final class ILeappFileProcessor { } - private void checkAttributeType(Collection bbattributes, String attrType, String[] columnValues, Integer columnNumber, BlackboardAttribute.Type attributeType) { + 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")) { @@ -256,7 +258,7 @@ public final class ILeappFileProcessor { } 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.", columnValues[columnNumber]), ex); //NON-NLS + 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")) { 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..1033c60dda 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.java @@ -46,6 +46,7 @@ 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.guiutils.SimpleListCellRenderer; import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.autopsy.ingest.IngestModuleGlobalSettingsPanel; import org.sleuthkit.autopsy.ingest.IngestProfiles; @@ -648,6 +649,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 +678,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 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 b595742dd7..70bb171d5d 100644 --- a/Core/src/org/sleuthkit/autopsy/report/modules/html/HTMLReport.java +++ b/Core/src/org/sleuthkit/autopsy/report/modules/html/HTMLReport.java @@ -378,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/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/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..bad7df2423 --- /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 { + 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 { + 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 { + 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 { + 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/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/NEWS.txt b/NEWS.txt index 67620c29a0..d9c67e6a17 100644 --- a/NEWS.txt +++ b/NEWS.txt @@ -1,3 +1,39 @@ +---------------- 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. +Support for HEIC/HEIF images by converting them to JPEGs using ImageMagick (which retains EXIF). +New Picture Analyzer module that does HEIC conversion and EXIF extraction (replaces the previous EXIF 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/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..4456663bd8 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 @@ -225,6 +228,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/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/docs/doxygen-user/command_line_ingest.dox b/docs/doxygen-user/command_line_ingest.dox index 97c521c685..137005cdde 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,15 @@ 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)
--dataSourcePath
+--ingestProfile (optional)
--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
--dataSourceObjectId
+--ingestProfile (optional)
--runIngest --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 +106,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 + --dataSourcePath="R:\work\images\red_images.img" --ingestProfile="imageAnalysis" +\endverbatim + Finally we'll add a folder ("Test files") as a logical file set to a new case ("test9"). \verbatim @@ -132,6 +153,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 +178,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 may also see the normal case open dialog if your combination of commands and parameters was invalid. In either case you'll want to compare what you ran with the descriptions and examples above to try to find the error. If everything works correctly, you'll see a log of the processing being done and Autopsy will close when finished. @@ -160,7 +187,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/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