diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/application/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/centralrepository/application/Bundle.properties-MERGED new file mode 100755 index 0000000000..458d4b520b --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/application/Bundle.properties-MERGED @@ -0,0 +1,8 @@ +OtherOccurrences.csvHeader.attribute=Matched Attribute +OtherOccurrences.csvHeader.case=Case +OtherOccurrences.csvHeader.comment=Comment +OtherOccurrences.csvHeader.dataSource=Data Source +OtherOccurrences.csvHeader.device=Device +OtherOccurrences.csvHeader.known=Known +OtherOccurrences.csvHeader.path=Path +OtherOccurrences.csvHeader.value=Attribute Value diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrenceNodeInstanceData.java b/Core/src/org/sleuthkit/autopsy/centralrepository/application/NodeData.java similarity index 88% rename from Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrenceNodeInstanceData.java rename to Core/src/org/sleuthkit/autopsy/centralrepository/application/NodeData.java index 92ebc821f8..bae27ab680 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrenceNodeInstanceData.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/application/NodeData.java @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.centralrepository.contentviewer; +package org.sleuthkit.autopsy.centralrepository.application; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; @@ -30,7 +30,7 @@ import org.sleuthkit.datamodel.TskDataException; /** * Class for populating the Other Occurrences tab */ -class OtherOccurrenceNodeInstanceData implements OtherOccurrenceNodeData { +public class NodeData { // For now hard code the string for the central repo files type, since // getting it dynamically can fail. @@ -56,7 +56,7 @@ class OtherOccurrenceNodeInstanceData implements OtherOccurrenceNodeData { * @param type The type of the instance * @param value The value of the instance */ - OtherOccurrenceNodeInstanceData(CorrelationAttributeInstance instance, CorrelationAttributeInstance.Type type, String value) { + public NodeData(CorrelationAttributeInstance instance, CorrelationAttributeInstance.Type type, String value) { caseName = instance.getCorrelationCase().getDisplayName(); deviceID = instance.getCorrelationDataSource().getDeviceID(); dataSourceName = instance.getCorrelationDataSource().getName(); @@ -77,7 +77,7 @@ class OtherOccurrenceNodeInstanceData implements OtherOccurrenceNodeData { * * @throws CentralRepoException */ - OtherOccurrenceNodeInstanceData(AbstractFile newFile, Case autopsyCase) throws CentralRepoException { + NodeData(AbstractFile newFile, Case autopsyCase) throws CentralRepoException { caseName = autopsyCase.getDisplayName(); try { DataSource dataSource = autopsyCase.getSleuthkitCase().getDataSource(newFile.getDataSource().getId()); @@ -119,7 +119,7 @@ class OtherOccurrenceNodeInstanceData implements OtherOccurrenceNodeData { * * @param newComment The new comment */ - void updateComment(String newComment) { + public void updateComment(String newComment) { comment = newComment; } @@ -129,7 +129,7 @@ class OtherOccurrenceNodeInstanceData implements OtherOccurrenceNodeData { * @return true if this node was created from a central repo instance, false * otherwise */ - boolean isCentralRepoNode() { + public boolean isCentralRepoNode() { return (originalCorrelationInstance != null); } @@ -138,7 +138,7 @@ class OtherOccurrenceNodeInstanceData implements OtherOccurrenceNodeData { * * @return the case name */ - String getCaseName() { + public String getCaseName() { return caseName; } @@ -147,7 +147,7 @@ class OtherOccurrenceNodeInstanceData implements OtherOccurrenceNodeData { * * @return the device ID */ - String getDeviceID() { + public String getDeviceID() { return deviceID; } @@ -156,7 +156,7 @@ class OtherOccurrenceNodeInstanceData implements OtherOccurrenceNodeData { * * @return the data source name */ - String getDataSourceName() { + public String getDataSourceName() { return dataSourceName; } @@ -165,7 +165,7 @@ class OtherOccurrenceNodeInstanceData implements OtherOccurrenceNodeData { * * @return the file path */ - String getFilePath() { + public String getFilePath() { return filePath; } @@ -174,7 +174,7 @@ class OtherOccurrenceNodeInstanceData implements OtherOccurrenceNodeData { * * @return the type */ - String getType() { + public String getType() { return typeStr; } @@ -183,7 +183,7 @@ class OtherOccurrenceNodeInstanceData implements OtherOccurrenceNodeData { * * @return the value */ - String getValue() { + public String getValue() { return value; } @@ -192,7 +192,7 @@ class OtherOccurrenceNodeInstanceData implements OtherOccurrenceNodeData { * * @return the known status */ - TskData.FileKnown getKnown() { + public TskData.FileKnown getKnown() { return known; } @@ -201,7 +201,7 @@ class OtherOccurrenceNodeInstanceData implements OtherOccurrenceNodeData { * * @return the comment */ - String getComment() { + public String getComment() { return comment; } @@ -211,7 +211,7 @@ class OtherOccurrenceNodeInstanceData implements OtherOccurrenceNodeData { * * @return the original abstract file */ - AbstractFile getAbstractFile() throws CentralRepoException { + public AbstractFile getAbstractFile() throws CentralRepoException { if (originalAbstractFile == null) { throw new CentralRepoException("AbstractFile is null"); } @@ -226,7 +226,7 @@ class OtherOccurrenceNodeInstanceData implements OtherOccurrenceNodeData { * * @throws CentralRepoException */ - CorrelationAttributeInstance getCorrelationAttributeInstance() throws CentralRepoException { + public CorrelationAttributeInstance getCorrelationAttributeInstance() throws CentralRepoException { if (originalCorrelationInstance == null) { throw new CentralRepoException("CorrelationAttributeInstance is null"); } @@ -239,7 +239,7 @@ class OtherOccurrenceNodeInstanceData implements OtherOccurrenceNodeData { * * @return the CSV_ITEM_SEPARATOR string */ - static String getCsvItemSeparator() { + public static String getCsvItemSeparator() { return CSV_ITEM_SEPARATOR; } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/application/OtherOccurrences.java b/Core/src/org/sleuthkit/autopsy/centralrepository/application/OtherOccurrences.java new file mode 100755 index 0000000000..934c8015a4 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/application/OtherOccurrences.java @@ -0,0 +1,438 @@ +/* + * Central Repository + * + * Copyright 2021 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.centralrepository.application; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.logging.Level; +import org.apache.commons.lang3.StringUtils; +import org.joda.time.DateTimeZone; +import org.joda.time.LocalDateTime; +import org.openide.nodes.Node; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +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.CorrelationAttributeUtil; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationCase; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationDataSource; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardArtifactTag; +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; + +/** + * Contains most of the methods for gathering data from the DB and CR for the + * OtherOccurrencesPanel. + */ +public final class OtherOccurrences { + + private static final Logger logger = Logger.getLogger(OtherOccurrences.class.getName()); + + private static final String UUID_PLACEHOLDER_STRING = "NoCorrelationAttributeInstance"; + + private OtherOccurrences() { + } + + /** + * Determine what attributes can be used for correlation based on the node. + * If EamDB is not enabled, get the default Files correlation. + * + * @param node The node to correlate + * + * @return A list of attributes that can be used for correlation + */ + public static Collection getCorrelationAttributesFromNode(Node node, AbstractFile file) { + Collection ret = new ArrayList<>(); + + // correlate on blackboard artifact attributes if they exist and supported + BlackboardArtifact bbArtifact = getBlackboardArtifactFromNode(node); + if (bbArtifact != null && CentralRepository.isEnabled()) { + ret.addAll(CorrelationAttributeUtil.makeCorrAttrsForCorrelation(bbArtifact)); + } + + // we can correlate based on the MD5 if it is enabled + if (file != null && CentralRepository.isEnabled() && file.getSize() > 0) { + try { + + List artifactTypes = CentralRepository.getInstance().getDefinedCorrelationTypes(); + String md5 = file.getMd5Hash(); + if (md5 != null && !md5.isEmpty() && null != artifactTypes && !artifactTypes.isEmpty()) { + for (CorrelationAttributeInstance.Type aType : artifactTypes) { + if (aType.getId() == CorrelationAttributeInstance.FILES_TYPE_ID) { + CorrelationCase corCase = CentralRepository.getInstance().getCase(Case.getCurrentCase()); + try { + ret.add(new CorrelationAttributeInstance( + aType, + md5, + corCase, + CorrelationDataSource.fromTSKDataSource(corCase, file.getDataSource()), + file.getParentPath() + file.getName(), + "", + file.getKnown(), + file.getId())); + } catch (CorrelationAttributeNormalizationException ex) { + logger.log(Level.INFO, String.format("Unable to check create CorrelationAttribtueInstance for value %s and type %s.", md5, aType.toString()), ex); + } + break; + } + } + } + } catch (CentralRepoException | TskCoreException ex) { + logger.log(Level.SEVERE, "Error connecting to DB", ex); // NON-NLS + } + // If EamDb not enabled, get the Files default correlation type to allow Other Occurances to be enabled. + } else if (file != null && file.getSize() > 0) { + String md5 = file.getMd5Hash(); + if (md5 != null && !md5.isEmpty()) { + try { + final CorrelationAttributeInstance.Type fileAttributeType + = CorrelationAttributeInstance.getDefaultCorrelationTypes() + .stream() + .filter(attrType -> attrType.getId() == CorrelationAttributeInstance.FILES_TYPE_ID) + .findAny() + .get(); + //The Central Repository is not enabled + ret.add(new CorrelationAttributeInstance(fileAttributeType, md5, null, null, "", "", TskData.FileKnown.UNKNOWN, file.getId())); + } catch (CentralRepoException ex) { + logger.log(Level.SEVERE, "Error connecting to DB", ex); // NON-NLS + } catch (CorrelationAttributeNormalizationException ex) { + logger.log(Level.INFO, String.format("Unable to create CorrelationAttributeInstance for value %s", md5), ex); // NON-NLS + } + } + } + return ret; + } + + /** + * Get the associated BlackboardArtifact from a node, if it exists. + * + * @param node The node + * + * @return The associated BlackboardArtifact, or null + */ + public static BlackboardArtifact getBlackboardArtifactFromNode(Node node) { + BlackboardArtifactTag nodeBbArtifactTag = node.getLookup().lookup(BlackboardArtifactTag.class); + BlackboardArtifact nodeBbArtifact = node.getLookup().lookup(BlackboardArtifact.class); + + if (nodeBbArtifactTag != null) { + return nodeBbArtifactTag.getArtifact(); + } else if (nodeBbArtifact != null) { + return nodeBbArtifact; + } + + return null; + + } + + /** + * Get the associated AbstractFile from a node, if it exists. + * + * @param node The node + * + * @return The associated AbstractFile, or null + */ + public static AbstractFile getAbstractFileFromNode(Node node) { + BlackboardArtifactTag nodeBbArtifactTag = node.getLookup().lookup(BlackboardArtifactTag.class); + ContentTag nodeContentTag = node.getLookup().lookup(ContentTag.class); + BlackboardArtifact nodeBbArtifact = node.getLookup().lookup(BlackboardArtifact.class); + AbstractFile nodeAbstractFile = node.getLookup().lookup(AbstractFile.class); + + if (nodeBbArtifactTag != null) { + Content content = nodeBbArtifactTag.getContent(); + if (content instanceof AbstractFile) { + return (AbstractFile) content; + } + } else if (nodeContentTag != null) { + Content content = nodeContentTag.getContent(); + if (content instanceof AbstractFile) { + return (AbstractFile) content; + } + } else if (nodeBbArtifact != null) { + Content content; + try { + content = nodeBbArtifact.getSleuthkitCase().getContentById(nodeBbArtifact.getObjectID()); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Error retrieving blackboard artifact", ex); // NON-NLS + return null; + } + + if (content instanceof AbstractFile) { + return (AbstractFile) content; + } + } else if (nodeAbstractFile != null) { + return nodeAbstractFile; + } + + return null; + } + + /** + * Query the central repo database (if enabled) and the case database to + * find all artifact instances correlated to the given central repository + * artifact. If the central repo is not enabled, this will only return files + * from the current case with matching MD5 hashes. + * + * @param corAttr CorrelationAttribute to query for + * + * @return A collection of correlated artifact instances + */ + public static Map getCorrelatedInstances(AbstractFile file, String deviceId, String dataSourceName, CorrelationAttributeInstance corAttr) { + // @@@ Check exception + try { + final Case openCase = Case.getCurrentCaseThrows(); + String caseUUID = openCase.getName(); + HashMap nodeDataMap = new HashMap<>(); + + if (CentralRepository.isEnabled()) { + List instances = CentralRepository.getInstance().getArtifactInstancesByTypeValue(corAttr.getCorrelationType(), corAttr.getCorrelationValue()); + + for (CorrelationAttributeInstance artifactInstance : instances) { + + // Only add the attribute if it isn't the object the user selected. + // We consider it to be a different object if at least one of the following is true: + // - the case UUID is different + // - the data source name is different + // - the data source device ID is different + // - the file path is different + if (artifactInstance.getCorrelationCase().getCaseUUID().equals(caseUUID) + && (!StringUtils.isBlank(dataSourceName) && artifactInstance.getCorrelationDataSource().getName().equals(dataSourceName)) + && (!StringUtils.isBlank(deviceId) && artifactInstance.getCorrelationDataSource().getDeviceID().equals(deviceId)) + && (file != null && artifactInstance.getFilePath().equalsIgnoreCase(file.getParentPath() + file.getName()))) { + continue; + } + NodeData newNode = new NodeData(artifactInstance, corAttr.getCorrelationType(), corAttr.getCorrelationValue()); + UniquePathKey uniquePathKey = new UniquePathKey(newNode); + nodeDataMap.put(uniquePathKey, newNode); + } + if (file != null && corAttr.getCorrelationType().getDisplayName().equals("Files")) { + List caseDbFiles = getCaseDbMatches(corAttr, openCase, file); + + for (AbstractFile caseDbFile : caseDbFiles) { + addOrUpdateNodeData(openCase, nodeDataMap, caseDbFile); + } + } + } + return nodeDataMap; + } catch (CentralRepoException ex) { + logger.log(Level.SEVERE, "Error getting artifact instances from database.", ex); // NON-NLS + } catch (CorrelationAttributeNormalizationException ex) { + logger.log(Level.INFO, "Error getting artifact instances from database.", ex); // NON-NLS + } catch (NoCurrentCaseException ex) { + logger.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS + } catch (TskCoreException ex) { + // do nothing. + // @@@ Review this behavior + logger.log(Level.SEVERE, "Exception while querying open case.", ex); // NON-NLS + } + + return new HashMap<>( + 0); + } + + /** + * Get all other abstract files in the current case with the same MD5 as the + * selected node. + * + * @param corAttr The CorrelationAttribute containing the MD5 to search for + * @param openCase The current case + * @param file The current file. + * + * @return List of matching AbstractFile objects + * + * @throws NoCurrentCaseException + * @throws TskCoreException + * @throws CentralRepoException + */ + public static List getCaseDbMatches(CorrelationAttributeInstance corAttr, Case openCase, AbstractFile file) throws NoCurrentCaseException, TskCoreException, CentralRepoException { + List caseDbArtifactInstances = new ArrayList<>(); + if (file != null) { + String md5 = corAttr.getCorrelationValue(); + SleuthkitCase tsk = openCase.getSleuthkitCase(); + List matches = tsk.findAllFilesWhere(String.format("md5 = '%s'", new Object[]{md5})); + + for (AbstractFile fileMatch : matches) { + if (file.equals(fileMatch)) { + continue; // If this is the file the user clicked on + } + caseDbArtifactInstances.add(fileMatch); + } + } + return caseDbArtifactInstances; + + } + + /** + * Adds the file to the nodeDataMap map if it does not already exist + * + * @param autopsyCase + * @param nodeDataMap + * @param newFile + * + * @throws TskCoreException + * @throws CentralRepoException + */ + public static void addOrUpdateNodeData(final Case autopsyCase, Map nodeDataMap, AbstractFile newFile) throws TskCoreException, CentralRepoException { + + NodeData newNode = new NodeData(newFile, autopsyCase); + + // If the caseDB object has a notable tag associated with it, update + // the known status to BAD + if (newNode.getKnown() != TskData.FileKnown.BAD) { + List fileMatchTags = autopsyCase.getServices().getTagsManager().getContentTagsByContent(newFile); + for (ContentTag tag : fileMatchTags) { + TskData.FileKnown tagKnownStatus = tag.getName().getKnownStatus(); + if (tagKnownStatus.equals(TskData.FileKnown.BAD)) { + newNode.updateKnown(TskData.FileKnown.BAD); + break; + } + } + } + + // Make a key to see if the file is already in the map + UniquePathKey uniquePathKey = new UniquePathKey(newNode); + + // If this node is already in the list, the only thing we need to do is + // update the known status to BAD if the caseDB version had known status BAD. + // Otherwise this is a new node so add the new node to the map. + if (nodeDataMap.containsKey(uniquePathKey)) { + if (newNode.getKnown() == TskData.FileKnown.BAD) { + NodeData prevInstance = nodeDataMap.get(uniquePathKey); + prevInstance.updateKnown(newNode.getKnown()); + } + } else { + nodeDataMap.put(uniquePathKey, newNode); + } + } + + /** + * Create a unique string to be used as a key for deduping data sources as + * best as possible + */ + public static String makeDataSourceString(String caseUUID, String deviceId, String dataSourceName) { + return caseUUID + deviceId + dataSourceName; + } + + /** + * Gets the list of Eam Cases and determines the earliest case creation + * date. Sets the label to display the earliest date string to the user. + */ + public static String getEarliestCaseDate() throws CentralRepoException { + String dateStringDisplay = ""; + + if (CentralRepository.isEnabled()) { + LocalDateTime earliestDate = LocalDateTime.now(DateTimeZone.UTC); + DateFormat datetimeFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.US); + CentralRepository dbManager = CentralRepository.getInstance(); + List cases = dbManager.getCases(); + for (CorrelationCase aCase : cases) { + LocalDateTime caseDate; + try { + caseDate = LocalDateTime.fromDateFields(datetimeFormat.parse(aCase.getCreationDate())); + + if (caseDate.isBefore(earliestDate)) { + earliestDate = caseDate; + dateStringDisplay = aCase.getCreationDate(); + } + } catch (ParseException ex) { + throw new CentralRepoException("Failed to format case creation date " + aCase.getCreationDate(), ex); + } + } + } + + return dateStringDisplay; + } + + @NbBundle.Messages({ + "OtherOccurrences.csvHeader.case=Case", + "OtherOccurrences.csvHeader.device=Device", + "OtherOccurrences.csvHeader.dataSource=Data Source", + "OtherOccurrences.csvHeader.attribute=Matched Attribute", + "OtherOccurrences.csvHeader.value=Attribute Value", + "OtherOccurrences.csvHeader.known=Known", + "OtherOccurrences.csvHeader.path=Path", + "OtherOccurrences.csvHeader.comment=Comment" + }) + + /** + * Create a cvs file of occurrences for the given parameters. + * + * @param destFile Output file for the csv data. + * @param abstractFile Source file. + * @param correlationAttList List of correclationAttributeInstances, should + * not be null. + * @param dataSourceName Name of the data source. + * @param deviceId Device id. + * + * @throws IOException + */ + public static void writeOtherOccurrencesToFileAsCSV(File destFile, AbstractFile abstractFile, Collection correlationAttList, String dataSourceName, String deviceId) throws IOException { + try (BufferedWriter writer = Files.newBufferedWriter(destFile.toPath())) { + //write headers + StringBuilder headers = new StringBuilder("\""); + headers.append(Bundle.OtherOccurrences_csvHeader_case()) + .append(NodeData.getCsvItemSeparator()).append(Bundle.OtherOccurrences_csvHeader_dataSource()) + .append(NodeData.getCsvItemSeparator()).append(Bundle.OtherOccurrences_csvHeader_attribute()) + .append(NodeData.getCsvItemSeparator()).append(Bundle.OtherOccurrences_csvHeader_value()) + .append(NodeData.getCsvItemSeparator()).append(Bundle.OtherOccurrences_csvHeader_known()) + .append(NodeData.getCsvItemSeparator()).append(Bundle.OtherOccurrences_csvHeader_path()) + .append(NodeData.getCsvItemSeparator()).append(Bundle.OtherOccurrences_csvHeader_comment()) + .append('"').append(System.getProperty("line.separator")); + writer.write(headers.toString()); + //write content + for (CorrelationAttributeInstance corAttr : correlationAttList) { + Map correlatedNodeDataMap = new HashMap<>(0); + // get correlation and reference set instances from DB + correlatedNodeDataMap.putAll(getCorrelatedInstances(abstractFile, deviceId, dataSourceName, corAttr)); + for (NodeData nodeData : correlatedNodeDataMap.values()) { + writer.write(nodeData.toCsvString()); + } + } + } + } + + /** + * Get a placeholder string to use in place of case uuid when it isn't + * available + * + * @return UUID_PLACEHOLDER_STRING + */ + public static String getPlaceholderUUID() { + return UUID_PLACEHOLDER_STRING; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/UniquePathKey.java b/Core/src/org/sleuthkit/autopsy/centralrepository/application/UniquePathKey.java similarity index 92% rename from Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/UniquePathKey.java rename to Core/src/org/sleuthkit/autopsy/centralrepository/application/UniquePathKey.java index fd11c4e70b..6de3ff2799 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/UniquePathKey.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/application/UniquePathKey.java @@ -16,12 +16,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.sleuthkit.autopsy.centralrepository.contentviewer; +package org.sleuthkit.autopsy.centralrepository.application; import java.util.Objects; import java.util.logging.Level; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.centralrepository.contentviewer.OtherOccurrencesPanel; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoException; import org.sleuthkit.autopsy.coreutils.Logger; @@ -29,7 +30,7 @@ import org.sleuthkit.autopsy.coreutils.Logger; * Used as a key to ensure we eliminate duplicates from the result set by not * overwriting CR correlation instances. */ -final class UniquePathKey { +public final class UniquePathKey { private static final Logger logger = Logger.getLogger(UniquePathKey.class.getName()); private final String dataSourceID; @@ -37,7 +38,7 @@ final class UniquePathKey { private final String type; private final String caseUUID; - UniquePathKey(OtherOccurrenceNodeInstanceData nodeData) { + public UniquePathKey(NodeData nodeData) { super(); dataSourceID = nodeData.getDeviceID(); if (nodeData.getFilePath() != null) { @@ -56,7 +57,7 @@ final class UniquePathKey { //place holder value will be used since correlation attribute was unavailble } catch (NoCurrentCaseException ex) { logger.log(Level.WARNING, "Unable to get current case", ex); - tempCaseUUID = OtherOccurrencesPanel.getPlaceholderUUID(); + tempCaseUUID = OtherOccurrences.getPlaceholderUUID(); } } caseUUID = tempCaseUUID; diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/Bundle.properties-MERGED index 1151be4fb3..e4507c7d3b 100755 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/Bundle.properties-MERGED @@ -31,18 +31,9 @@ OtherOccurrencesPanel.correlatedArtifacts.byType={0}% of data sources have {2} ( OtherOccurrencesPanel.correlatedArtifacts.failed=Failed to get frequency details. OtherOccurrencesPanel.correlatedArtifacts.isEmpty=There are no files or artifacts to correlate. OtherOccurrencesPanel.correlatedArtifacts.title=Attribute Frequency -OtherOccurrencesPanel.csvHeader.attribute=Matched Attribute -OtherOccurrencesPanel.csvHeader.case=Case -OtherOccurrencesPanel.csvHeader.comment=Comment -OtherOccurrencesPanel.csvHeader.dataSource=Data Source -OtherOccurrencesPanel.csvHeader.device=Device -OtherOccurrencesPanel.csvHeader.known=Known -OtherOccurrencesPanel.csvHeader.path=Path -OtherOccurrencesPanel.csvHeader.value=Attribute Value OtherOccurrencesPanel.earliestCaseLabel.toolTipText= OtherOccurrencesPanel.earliestCaseLabel.text=Central Repository Starting Date: OtherOccurrencesPanel.earliestCaseDate.text=Earliest Case Date -OtherOccurrencesPanel.earliestCaseNotAvailable=\ Not Enabled. OtherOccurrencesPanel.foundIn.text=Found %d instances in %d cases and %d data sources. OtherOccurrencesPanel.foundInLabel.text= OtherOccurrencesPanel.filesTable.toolTipText=Click column name to sort. Right-click on the table for more options. @@ -52,3 +43,5 @@ OtherOccurrencesPanel.showCommonalityMenuItem.text=Show Frequency OtherOccurrencesPanel.showCaseDetailsMenuItem.text=Show Case Details OtherOccurrencesPanel.table.noArtifacts=Item has no attributes with which to search. OtherOccurrencesPanel.table.noResultsFound=No results found. +OtherOccurrencesPanel_earliestCaseNotAvailable=Not Availble. +OtherOccurrencesPanel_table_loadingResults=Loading results diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.form b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.form index 60fb6c56df..ec0337e3b9 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.form +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.form @@ -20,6 +20,7 @@ + diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.java b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.java index cd901c0c1d..c44b4e2479 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.java @@ -19,33 +19,18 @@ package org.sleuthkit.autopsy.centralrepository.contentviewer; import java.awt.Component; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; +import java.awt.Cursor; +import java.util.concurrent.ExecutionException; import java.util.logging.Level; import org.sleuthkit.autopsy.coreutils.Logger; import javax.swing.JPanel; import org.openide.nodes.Node; import org.openide.util.NbBundle.Messages; import org.openide.util.lookup.ServiceProvider; -import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; -import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeNormalizationException; +import org.sleuthkit.autopsy.centralrepository.application.OtherOccurrences; import org.sleuthkit.autopsy.corecomponentinterfaces.DataContentViewer; -import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; -import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeUtil; -import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationCase; -import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationDataSource; -import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoException; import org.sleuthkit.datamodel.AbstractFile; -import org.sleuthkit.datamodel.BlackboardArtifact; -import org.sleuthkit.datamodel.BlackboardArtifactTag; -import org.sleuthkit.datamodel.Content; -import org.sleuthkit.datamodel.ContentTag; -import org.sleuthkit.datamodel.TskCoreException; -import org.sleuthkit.datamodel.TskData; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; -import org.sleuthkit.datamodel.TskException; /** * View correlation results from other cases @@ -57,13 +42,10 @@ import org.sleuthkit.datamodel.TskException; public final class DataContentViewerOtherCases extends JPanel implements DataContentViewer { private static final long serialVersionUID = -1L; - private static final Logger LOGGER = Logger.getLogger(DataContentViewerOtherCases.class.getName()); + private static final Logger logger = Logger.getLogger(DataContentViewerOtherCases.class.getName()); private final OtherOccurrencesPanel otherOccurrencesPanel = new OtherOccurrencesPanel(); - /** - * Could be null. - */ - private AbstractFile file; //the file which the content viewer is being populated for + private OtherOccurrencesNodeWorker worker = null; /** * Creates new form DataContentViewerOtherCases @@ -104,146 +86,6 @@ public final class DataContentViewerOtherCases extends JPanel implements DataCon } - /** - * Get the associated BlackboardArtifact from a node, if it exists. - * - * @param node The node - * - * @return The associated BlackboardArtifact, or null - */ - private BlackboardArtifact - getBlackboardArtifactFromNode(Node node) { - BlackboardArtifactTag nodeBbArtifactTag = node.getLookup().lookup(BlackboardArtifactTag.class - ); - BlackboardArtifact nodeBbArtifact = node.getLookup().lookup(BlackboardArtifact.class - ); - - if (nodeBbArtifactTag != null) { - return nodeBbArtifactTag.getArtifact(); - } else if (nodeBbArtifact != null) { - return nodeBbArtifact; - } - - return null; - - } - - /** - * Get the associated AbstractFile from a node, if it exists. - * - * @param node The node - * - * @return The associated AbstractFile, or null - */ - private AbstractFile getAbstractFileFromNode(Node node) { - BlackboardArtifactTag nodeBbArtifactTag = node.getLookup().lookup(BlackboardArtifactTag.class - ); - ContentTag nodeContentTag = node.getLookup().lookup(ContentTag.class - ); - BlackboardArtifact nodeBbArtifact = node.getLookup().lookup(BlackboardArtifact.class - ); - AbstractFile nodeAbstractFile = node.getLookup().lookup(AbstractFile.class - ); - - if (nodeBbArtifactTag != null) { - Content content = nodeBbArtifactTag.getContent(); - if (content instanceof AbstractFile) { - return (AbstractFile) content; - } - } else if (nodeContentTag != null) { - Content content = nodeContentTag.getContent(); - if (content instanceof AbstractFile) { - return (AbstractFile) content; - } - } else if (nodeBbArtifact != null) { - Content content; - try { - content = nodeBbArtifact.getSleuthkitCase().getContentById(nodeBbArtifact.getObjectID()); - } catch (TskCoreException ex) { - LOGGER.log(Level.SEVERE, "Error retrieving blackboard artifact", ex); // NON-NLS - return null; - } - - if (content instanceof AbstractFile) { - return (AbstractFile) content; - } - } else if (nodeAbstractFile != null) { - return nodeAbstractFile; - } - - return null; - } - - /** - * Determine what attributes can be used for correlation based on the node. - * If EamDB is not enabled, get the default Files correlation. - * - * @param node The node to correlate - * - * @return A list of attributes that can be used for correlation - */ - private Collection getCorrelationAttributesFromNode(Node node) { - Collection ret = new ArrayList<>(); - - // correlate on blackboard artifact attributes if they exist and supported - BlackboardArtifact bbArtifact = getBlackboardArtifactFromNode(node); - if (bbArtifact != null && CentralRepository.isEnabled()) { - ret.addAll(CorrelationAttributeUtil.makeCorrAttrsForCorrelation(bbArtifact)); - } - - // we can correlate based on the MD5 if it is enabled - if (this.file != null && CentralRepository.isEnabled() && this.file.getSize() > 0) { - try { - - List artifactTypes = CentralRepository.getInstance().getDefinedCorrelationTypes(); - String md5 = this.file.getMd5Hash(); - if (md5 != null && !md5.isEmpty() && null != artifactTypes && !artifactTypes.isEmpty()) { - for (CorrelationAttributeInstance.Type aType : artifactTypes) { - if (aType.getId() == CorrelationAttributeInstance.FILES_TYPE_ID) { - CorrelationCase corCase = CentralRepository.getInstance().getCase(Case.getCurrentCase()); - try { - ret.add(new CorrelationAttributeInstance( - aType, - md5, - corCase, - CorrelationDataSource.fromTSKDataSource(corCase, file.getDataSource()), - file.getParentPath() + file.getName(), - "", - file.getKnown(), - file.getId())); - } catch (CorrelationAttributeNormalizationException ex) { - LOGGER.log(Level.INFO, String.format("Unable to check create CorrelationAttribtueInstance for value %s and type %s.", md5, aType.toString()), ex); - } - break; - } - } - } - } catch (CentralRepoException | TskCoreException ex) { - LOGGER.log(Level.SEVERE, "Error connecting to DB", ex); // NON-NLS - } - // If EamDb not enabled, get the Files default correlation type to allow Other Occurances to be enabled. - } else if (this.file != null && this.file.getSize() > 0) { - String md5 = this.file.getMd5Hash(); - if (md5 != null && !md5.isEmpty()) { - try { - final CorrelationAttributeInstance.Type fileAttributeType - = CorrelationAttributeInstance.getDefaultCorrelationTypes() - .stream() - .filter(attrType -> attrType.getId() == CorrelationAttributeInstance.FILES_TYPE_ID) - .findAny() - .get(); - //The Central Repository is not enabled - ret.add(new CorrelationAttributeInstance(fileAttributeType, md5, null, null, "", "", TskData.FileKnown.UNKNOWN, this.file.getId())); - } catch (CentralRepoException ex) { - LOGGER.log(Level.SEVERE, "Error connecting to DB", ex); // NON-NLS - } catch (CorrelationAttributeNormalizationException ex) { - LOGGER.log(Level.INFO, String.format("Unable to create CorrelationAttributeInstance for value %s", md5), ex); // NON-NLS - } - } - } - return ret; - } - @Override public boolean isSupported(Node node) { @@ -251,39 +93,44 @@ public final class DataContentViewerOtherCases extends JPanel implements DataCon // - The central repo is enabled and the node has correlatable content // (either through the MD5 hash of the associated file or through a BlackboardArtifact) // - The central repo is disabled and the backing file has a valid MD5 hash - this.file = this.getAbstractFileFromNode(node); + AbstractFile file = OtherOccurrences.getAbstractFileFromNode(node); if (CentralRepository.isEnabled()) { - return !getCorrelationAttributesFromNode(node).isEmpty(); + return !OtherOccurrences.getCorrelationAttributesFromNode(node, file).isEmpty(); } else { - return this.file != null - && this.file.getSize() > 0 - && ((this.file.getMd5Hash() != null) && (!this.file.getMd5Hash().isEmpty())); + return file != null + && file.getSize() > 0 + && ((file.getMd5Hash() != null) && (!file.getMd5Hash().isEmpty())); } } @Override public void setNode(Node node) { - otherOccurrencesPanel.reset(); // reset the table to empty. + otherOccurrencesPanel.showPanelLoadingMessage(); + if (node == null) { return; } - //could be null - this.file = this.getAbstractFileFromNode(node); - String dataSourceName = ""; - String deviceId = ""; - try { - if (this.file != null) { - Content dataSource = this.file.getDataSource(); - dataSourceName = dataSource.getName(); - deviceId = Case.getCurrentCaseThrows().getSleuthkitCase().getDataSource(dataSource.getId()).getDeviceId(); - } - } catch (TskException | NoCurrentCaseException ex) { - // do nothing. - // @@@ Review this behavior - } - otherOccurrencesPanel.populateTable(getCorrelationAttributesFromNode(node), dataSourceName, deviceId, file); + if (worker != null) { + worker.cancel(true); + } + worker = new OtherOccurrencesNodeWorker(node) { + @Override + public void done() { + try { + if (!isCancelled()) { + OtherOccurrencesData data = get(); + otherOccurrencesPanel.populateTable(data); + otherOccurrencesPanel.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + } + } catch (InterruptedException | ExecutionException ex) { + DataContentViewerOtherCases.logger.log(Level.SEVERE, "Failed to update OtherOccurrencesPanel", ex); + } + } + }; + otherOccurrencesPanel.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + worker.execute(); } /** diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OccurrencePanel.java b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OccurrencePanel.java index c66d4ce085..4fb8ccff35 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OccurrencePanel.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OccurrencePanel.java @@ -18,6 +18,7 @@ */ package org.sleuthkit.autopsy.centralrepository.contentviewer; +import org.sleuthkit.autopsy.centralrepository.application.NodeData; import java.awt.Color; import java.awt.Font; import java.util.ArrayList; @@ -50,7 +51,7 @@ final class OccurrencePanel extends javax.swing.JPanel { private static final long serialVersionUID = 1L; private int gridY = 0; - private final List nodeDataList; + private final List nodeDataList; private final Map caseNamesAndDates = new HashMap<>(); private final Set dataSourceNames = new HashSet<>(); private final Set filePaths = new HashSet<>(); @@ -97,7 +98,7 @@ final class OccurrencePanel extends javax.swing.JPanel { * @param nodeDataList the list of OtherOccurrenceNodeData representing * common properties for the file */ - OccurrencePanel(List nodeDataList) { + OccurrencePanel(List nodeDataList) { this.nodeDataList = nodeDataList; customizeComponents(); } @@ -148,9 +149,9 @@ final class OccurrencePanel extends javax.swing.JPanel { addItemToBag(gridY, 0, TOP_INSET, 0, commonPropertiesLabel); gridY++; //for each other occurrence - for (OtherOccurrenceNodeData occurrence : nodeDataList) { - if (occurrence instanceof OtherOccurrenceNodeInstanceData) { - String type = ((OtherOccurrenceNodeInstanceData) occurrence).getType(); + for (NodeData occurrence : nodeDataList) { + if (occurrence instanceof NodeData) { + String type = occurrence.getType(); if (!type.isEmpty()) { javax.swing.JLabel typeLabel = new javax.swing.JLabel(); org.openide.awt.Mnemonics.setLocalizedText(typeLabel, Bundle.OccurrencePanel_commonPropertyTypeLabel_text()); @@ -160,7 +161,7 @@ final class OccurrencePanel extends javax.swing.JPanel { addItemToBag(gridY, 1, VERTICAL_GAP, 0, typeFieldValue); gridY++; } - String value = ((OtherOccurrenceNodeInstanceData) occurrence).getValue(); + String value = occurrence.getValue(); if (!value.isEmpty()) { javax.swing.JLabel valueLabel = new javax.swing.JLabel(); org.openide.awt.Mnemonics.setLocalizedText(valueLabel, Bundle.OccurrencePanel_commonPropertyValueLabel_text()); @@ -170,7 +171,7 @@ final class OccurrencePanel extends javax.swing.JPanel { addItemToBag(gridY, 1, 0, 0, valueFieldValue); gridY++; } - TskData.FileKnown knownStatus = ((OtherOccurrenceNodeInstanceData) occurrence).getKnown(); + TskData.FileKnown knownStatus = occurrence.getKnown(); javax.swing.JLabel knownStatusLabel = new javax.swing.JLabel(); org.openide.awt.Mnemonics.setLocalizedText(knownStatusLabel, Bundle.OccurrencePanel_commonPropertyKnownStatusLabel_text()); addItemToBag(gridY, 0, 0, 0, knownStatusLabel); @@ -181,7 +182,7 @@ final class OccurrencePanel extends javax.swing.JPanel { } addItemToBag(gridY, 1, 0, 0, knownStatusValue); gridY++; - String comment = ((OtherOccurrenceNodeInstanceData) occurrence).getComment(); + String comment = occurrence.getComment(); if (!comment.isEmpty()) { javax.swing.JLabel commentLabel = new javax.swing.JLabel(); org.openide.awt.Mnemonics.setLocalizedText(commentLabel, Bundle.OccurrencePanel_commonPropertyCommentLabel_text()); @@ -201,10 +202,9 @@ final class OccurrencePanel extends javax.swing.JPanel { } String caseDate = ""; try { - OtherOccurrenceNodeInstanceData nodeData = ((OtherOccurrenceNodeInstanceData) occurrence); - if (nodeData.isCentralRepoNode()) { + if (occurrence.isCentralRepoNode()) { if (CentralRepository.isEnabled()) { - CorrelationCase partialCase = nodeData.getCorrelationAttributeInstance().getCorrelationCase(); + CorrelationCase partialCase = occurrence.getCorrelationAttributeInstance().getCorrelationCase(); caseDate = CentralRepository.getInstance().getCaseByUUID(partialCase.getCaseUUID()).getCreationDate(); } } else { @@ -214,9 +214,9 @@ final class OccurrencePanel extends javax.swing.JPanel { LOGGER.log(Level.WARNING, "Error getting case created date for other occurrence content viewer", ex); } //Collect the data that is necessary for the other sections - caseNamesAndDates.put(((OtherOccurrenceNodeInstanceData) occurrence).getCaseName(), caseDate); - dataSourceNames.add(((OtherOccurrenceNodeInstanceData) occurrence).getDataSourceName()); - filePaths.add(((OtherOccurrenceNodeInstanceData) occurrence).getFilePath()); + caseNamesAndDates.put(occurrence.getCaseName(), caseDate); + dataSourceNames.add(occurrence.getDataSourceName()); + filePaths.add(occurrence.getFilePath()); } } //end for each diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrenceNodeData.java b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrenceNodeData.java deleted file mode 100644 index da4915e75f..0000000000 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrenceNodeData.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2018 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.centralrepository.contentviewer; -/** - * Marker interface for Other Occurrences nodes. - */ -interface OtherOccurrenceNodeData { - -} diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrenceNodeMessageData.java b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrenceNodeMessageData.java deleted file mode 100755 index 99e530349a..0000000000 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrenceNodeMessageData.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2018 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.centralrepository.contentviewer; - -/** - * Class for populating the Other Occurrences tab with a single message. - */ -final class OtherOccurrenceNodeMessageData implements OtherOccurrenceNodeData { - private final String displayMessage; - - OtherOccurrenceNodeMessageData(String displayMessage) { - this.displayMessage = displayMessage; - } - - String getDisplayMessage() { - return displayMessage; - } -} diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrenceOneTypeWorker.java b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrenceOneTypeWorker.java new file mode 100755 index 0000000000..9fd7b7fa51 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrenceOneTypeWorker.java @@ -0,0 +1,196 @@ +/* + * Central Repository + * + * Copyright 2021 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.centralrepository.contentviewer; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import javax.swing.SwingWorker; +import org.apache.commons.lang3.StringUtils; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.centralrepository.application.NodeData; +import org.sleuthkit.autopsy.centralrepository.application.OtherOccurrences; +import org.sleuthkit.autopsy.centralrepository.application.UniquePathKey; +import org.sleuthkit.autopsy.centralrepository.contentviewer.OtherOccurrenceOneTypeWorker.OneTypeData; +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.CorrelationCase; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.AbstractFile; + +/** + * Swing worker for getting the Other Occurrence data for the Domain Discovery + * window. + * + * This logic differs a bit from the OtherOcurrencesNodeWorker. + */ +class OtherOccurrenceOneTypeWorker extends SwingWorker { + + private static final Logger logger = Logger.getLogger(OtherOccurrenceOneTypeWorker.class.getName()); + + private final CorrelationAttributeInstance.Type aType; + private final String value; + private final AbstractFile file; + private final String deviceId; + private final String dataSourceName; + + /** + * Construct the worker. + * + * @param aType + * @param value + * @param file Source file, this maybe null. + * @param deviceId DeviceID string, this maybe an empty string. + * @param dataSourceName DataSourceName, this maybe an empty string. + */ + OtherOccurrenceOneTypeWorker(CorrelationAttributeInstance.Type aType, String value, AbstractFile file, String deviceId, String dataSourceName) { + this.aType = aType; + this.value = value; + this.file = file; + this.deviceId = deviceId; + this.dataSourceName = dataSourceName; + } + + @Override + protected OneTypeData doInBackground() throws Exception { + Map caseNames = new HashMap<>(); + int totalCount = 0; + Set dataSources = new HashSet<>(); + Collection correlationAttributesToAdd = new ArrayList<>(); + String earliestDate = OtherOccurrences.getEarliestCaseDate(); + OneTypeData results = null; + + if (CentralRepository.isEnabled()) { + List instances; + instances = CentralRepository.getInstance().getArtifactInstancesByTypeValue(aType, value); + HashMap nodeDataMap = new HashMap<>(); + String caseUUID = Case.getCurrentCase().getName(); + for (CorrelationAttributeInstance artifactInstance : instances) { + if (isCancelled()) { + break; + } + + // Only add the attribute if it isn't the object the user selected. + // We consider it to be a different object if at least one of the following is true: + // - the case UUID is different + // - the data source name is different + // - the data source device ID is different + // - the file path is different + if (artifactInstance.getCorrelationCase().getCaseUUID().equals(caseUUID) + && (!StringUtils.isBlank(dataSourceName) && artifactInstance.getCorrelationDataSource().getName().equals(dataSourceName)) + && (!StringUtils.isBlank(deviceId) && artifactInstance.getCorrelationDataSource().getDeviceID().equals(deviceId)) + && (file != null && artifactInstance.getFilePath().equalsIgnoreCase(file.getParentPath() + file.getName()))) { + + continue; + } + correlationAttributesToAdd.add(artifactInstance); + NodeData newNode = new NodeData(artifactInstance, aType, value); + UniquePathKey uniquePathKey = new UniquePathKey(newNode); + nodeDataMap.put(uniquePathKey, newNode); + } + + for (NodeData nodeData : nodeDataMap.values()) { + if (isCancelled()) { + break; + } + + if (nodeData.isCentralRepoNode()) { + try { + dataSources.add(OtherOccurrences.makeDataSourceString(nodeData.getCorrelationAttributeInstance().getCorrelationCase().getCaseUUID(), nodeData.getDeviceID(), nodeData.getDataSourceName())); + caseNames.put(nodeData.getCorrelationAttributeInstance().getCorrelationCase().getCaseUUID(), nodeData.getCorrelationAttributeInstance().getCorrelationCase()); + } catch (CentralRepoException ex) { + logger.log(Level.WARNING, "Unable to get correlation case for displaying other occurrence for case: " + nodeData.getCaseName(), ex); + } + } else { + try { + dataSources.add(OtherOccurrences.makeDataSourceString(Case.getCurrentCaseThrows().getName(), nodeData.getDeviceID(), nodeData.getDataSourceName())); + caseNames.put(Case.getCurrentCaseThrows().getName(), new CorrelationCase(Case.getCurrentCaseThrows().getName(), Case.getCurrentCaseThrows().getDisplayName())); + } catch (NoCurrentCaseException ex) { + logger.log(Level.WARNING, "No current case open for other occurrences", ex); + } + } + totalCount++; + } + } + + if (!isCancelled()) { + results = new OneTypeData(caseNames, totalCount, dataSources.size(), earliestDate, correlationAttributesToAdd); + } + + return results; + } + + /** + * Class to store the results of the worker thread. + */ + static final class OneTypeData { + + private final Map caseNames; + private final int totalCount; + private final int dataSourceCount; + private final Collection correlationAttributesToAdd; + private final String earliestCaseDate; + + /** + * Construct the results. + * + * @param caseNames Map of correlation cases. + * @param totalCount Total count of instances. + * @param dataSourceCount Data source count. + * @param earliestCaseDate Formatted string which contains the + * earliest case date. + * @param correlationAttributesToAdd The attributes to add to the main + * panel list. + */ + OneTypeData(Map caseNames, int totalCount, int dataSourceCount, String earliestCaseDate, Collection correlationAttributesToAdd) { + this.caseNames = caseNames; + this.totalCount = totalCount; + this.dataSourceCount = dataSourceCount; + this.correlationAttributesToAdd = correlationAttributesToAdd; + this.earliestCaseDate = earliestCaseDate; + } + + public Map getCaseNames() { + return caseNames; + } + + public int getTotalCount() { + return totalCount; + } + + public int getDataSourceCount() { + return dataSourceCount; + } + + public Collection getCorrelationAttributesToAdd() { + return correlationAttributesToAdd; + } + + public String getEarliestCaseDate() { + return earliestCaseDate; + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesDataSourcesTableModel.java b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesDataSourcesTableModel.java index c84b00fc80..55e1dea428 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesDataSourcesTableModel.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesDataSourcesTableModel.java @@ -18,6 +18,7 @@ */ package org.sleuthkit.autopsy.centralrepository.contentviewer; +import org.sleuthkit.autopsy.centralrepository.application.NodeData; import java.util.LinkedHashSet; import java.util.Objects; import java.util.Set; @@ -26,6 +27,7 @@ import javax.swing.table.AbstractTableModel; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.centralrepository.application.OtherOccurrences; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoException; import org.sleuthkit.autopsy.coreutils.Logger; @@ -139,11 +141,10 @@ final class OtherOccurrencesDataSourcesTableModel extends AbstractTableModel { * * @param newNodeData data to add to the table */ - void addNodeData(OtherOccurrenceNodeData newNodeData) { - OtherOccurrenceNodeInstanceData nodeData = (OtherOccurrenceNodeInstanceData) newNodeData; + void addNodeData(NodeData newNodeData) { String caseUUID; try { - caseUUID = nodeData.getCorrelationAttributeInstance().getCorrelationCase().getCaseUUID(); + caseUUID = newNodeData.getCorrelationAttributeInstance().getCorrelationCase().getCaseUUID(); } catch (CentralRepoException ignored) { //non central repo nodeData won't have a correlation case try { @@ -151,10 +152,10 @@ final class OtherOccurrencesDataSourcesTableModel extends AbstractTableModel { //place holder value will be used since correlation attribute was unavailble } catch (NoCurrentCaseException ex) { logger.log(Level.WARNING, "Unable to get current case", ex); - caseUUID = OtherOccurrencesPanel.getPlaceholderUUID(); + caseUUID = OtherOccurrences.getPlaceholderUUID(); } } - dataSourceSet.add(new DataSourceColumnItem(nodeData.getCaseName(), nodeData.getDeviceID(), nodeData.getDataSourceName(), caseUUID)); + dataSourceSet.add(new DataSourceColumnItem(newNodeData.getCaseName(), newNodeData.getDeviceID(), newNodeData.getDataSourceName(), caseUUID)); fireTableDataChanged(); } diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesFilesTableModel.java b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesFilesTableModel.java index 74a64c1e41..284255bfae 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesFilesTableModel.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesFilesTableModel.java @@ -18,6 +18,7 @@ */ package org.sleuthkit.autopsy.centralrepository.contentviewer; +import org.sleuthkit.autopsy.centralrepository.application.NodeData; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -28,6 +29,7 @@ import org.openide.util.NbBundle.Messages; import org.apache.commons.io.FilenameUtils; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.centralrepository.application.OtherOccurrences; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoException; import org.sleuthkit.autopsy.coreutils.Logger; @@ -40,7 +42,7 @@ public class OtherOccurrencesFilesTableModel extends AbstractTableModel { private static final long serialVersionUID = 1L; private static final Logger logger = Logger.getLogger(OtherOccurrencesFilesTableModel.class.getName()); private final List nodeKeys = new ArrayList<>(); - private final Map> nodeMap = new HashMap<>(); + private final Map> nodeMap = new HashMap<>(); /** * Create a table model for displaying file names @@ -75,7 +77,7 @@ public class OtherOccurrencesFilesTableModel extends AbstractTableModel { || nodeMap.get(nodeKeys.get(rowIdx)).isEmpty()) { return Bundle.OtherOccurrencesFilesTableModel_noData(); } - return FilenameUtils.getName(((OtherOccurrenceNodeInstanceData) nodeMap.get(nodeKeys.get(rowIdx)).get(0)).getFilePath()); + return FilenameUtils.getName( nodeMap.get(nodeKeys.get(rowIdx)).get(0).getFilePath()); } /** @@ -87,7 +89,7 @@ public class OtherOccurrencesFilesTableModel extends AbstractTableModel { * @return a list of OtherOccurrenceNodeData for the specified index or an * empty list if no data was found */ - List getListOfNodesForFile(int rowIdx) { + List getListOfNodesForFile(int rowIdx) { //if anything would prevent this from working return an empty list if (nodeMap.isEmpty() || nodeKeys.isEmpty() || rowIdx < 0 || rowIdx >= nodeKeys.size() || nodeKeys.get(rowIdx) == null @@ -107,9 +109,9 @@ public class OtherOccurrencesFilesTableModel extends AbstractTableModel { * * @param newNodeData data to add to the table */ - void addNodeData(OtherOccurrenceNodeData newNodeData) { - String newNodeKey = createNodeKey((OtherOccurrenceNodeInstanceData) newNodeData);//FilenameUtils.getName(((OtherOccurrenceNodeInstanceData)newNodeData).getFilePath()); - List nodeList = nodeMap.get(newNodeKey); + void addNodeData(NodeData newNodeData) { + String newNodeKey = createNodeKey(newNodeData); + List nodeList = nodeMap.get(newNodeKey); if (nodeList == null) { nodeKeys.add(newNodeKey); nodeList = new ArrayList<>(); @@ -119,7 +121,7 @@ public class OtherOccurrencesFilesTableModel extends AbstractTableModel { fireTableDataChanged(); } - private String createNodeKey(OtherOccurrenceNodeInstanceData nodeData) { + private String createNodeKey(NodeData nodeData) { String caseUUID; try { caseUUID = nodeData.getCorrelationAttributeInstance().getCorrelationCase().getCaseUUID(); @@ -130,7 +132,7 @@ public class OtherOccurrencesFilesTableModel extends AbstractTableModel { //place holder value will be used since correlation attribute was unavailble } catch (NoCurrentCaseException ex) { logger.log(Level.WARNING, "Unable to get current case", ex); - caseUUID = OtherOccurrencesPanel.getPlaceholderUUID(); + caseUUID = OtherOccurrences.getPlaceholderUUID(); } } return nodeData.getCaseName() + nodeData.getDataSourceName() + nodeData.getDeviceID() + nodeData.getFilePath() + caseUUID; diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesNodeWorker.java b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesNodeWorker.java new file mode 100755 index 0000000000..0654002f6f --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesNodeWorker.java @@ -0,0 +1,179 @@ +/* + * Central Repository + * + * Copyright 2021 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.centralrepository.contentviewer; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import javax.swing.SwingWorker; +import org.openide.nodes.Node; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.centralrepository.application.NodeData; +import org.sleuthkit.autopsy.centralrepository.application.OtherOccurrences; +import org.sleuthkit.autopsy.centralrepository.contentviewer.OtherOccurrencesNodeWorker.OtherOccurrencesData; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoException; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationCase; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.TskException; + +/** + * A SwingWorker that gathers data for the OtherOccurencesPanel which appears in + * the dataContentViewerOtherCases panel. + */ +class OtherOccurrencesNodeWorker extends SwingWorker { + + private static final Logger logger = Logger.getLogger(OtherOccurrencesNodeWorker.class.getName()); + + private final Node node; + + /** + * Constructs a new instance for the given node. + * + * @param node + */ + OtherOccurrencesNodeWorker(Node node) { + this.node = node; + } + + @Override + protected OtherOccurrencesData doInBackground() throws Exception { + AbstractFile file = OtherOccurrences.getAbstractFileFromNode(node); + String deviceId = ""; + String dataSourceName = ""; + Map caseNames = new HashMap<>(); + Case currentCase = Case.getCurrentCaseThrows(); + OtherOccurrencesData data = null; + try { + if (file != null) { + Content dataSource = file.getDataSource(); + deviceId = currentCase.getSleuthkitCase().getDataSource(dataSource.getId()).getDeviceId(); + dataSourceName = dataSource.getName(); + } + } catch (TskException ex) { + // do nothing. + // @@@ Review this behavior + return null; + } + Collection correlationAttributes = OtherOccurrences.getCorrelationAttributesFromNode(node, file); + + int totalCount = 0; + Set dataSources = new HashSet<>(); + for (CorrelationAttributeInstance corAttr : correlationAttributes) { + for (NodeData nodeData : OtherOccurrences.getCorrelatedInstances(file, deviceId, dataSourceName, corAttr).values()) { + if (nodeData.isCentralRepoNode()) { + try { + dataSources.add(OtherOccurrences.makeDataSourceString(nodeData.getCorrelationAttributeInstance().getCorrelationCase().getCaseUUID(), nodeData.getDeviceID(), nodeData.getDataSourceName())); + caseNames.put(nodeData.getCorrelationAttributeInstance().getCorrelationCase().getCaseUUID(), nodeData.getCorrelationAttributeInstance().getCorrelationCase()); + } catch (CentralRepoException ex) { + logger.log(Level.WARNING, "Unable to get correlation case for displaying other occurrence for case: " + nodeData.getCaseName(), ex); + } + } else { + try { + dataSources.add(OtherOccurrences.makeDataSourceString(Case.getCurrentCaseThrows().getName(), nodeData.getDeviceID(), nodeData.getDataSourceName())); + caseNames.put(Case.getCurrentCaseThrows().getName(), new CorrelationCase(Case.getCurrentCaseThrows().getName(), Case.getCurrentCaseThrows().getDisplayName())); + } catch (NoCurrentCaseException ex) { + logger.log(Level.WARNING, "No current case open for other occurrences", ex); + } + } + totalCount++; + + if (isCancelled()) { + break; + } + } + } + + if (!isCancelled()) { + data = new OtherOccurrencesData(correlationAttributes, file, dataSourceName, deviceId, caseNames, totalCount, dataSources.size(), OtherOccurrences.getEarliestCaseDate()); + } + + return data; + } + + /** + * Object to store all of the data gathered in the OtherOccurrencesWorker + * doInBackground method. + */ + static class OtherOccurrencesData { + + private final String deviceId; + private final AbstractFile file; + private final String dataSourceName; + private final Map caseMap; + private final int instanceDataCount; + private final int dataSourceCount; + private final String earliestCaseDate; + private final Collection correlationAttributes; + + private OtherOccurrencesData(Collection correlationAttributes, AbstractFile file, String dataSourceName, String deviceId, Map caseMap, int instanceCount, int dataSourceCount, String earliestCaseDate) { + this.file = file; + this.deviceId = deviceId; + this.dataSourceName = dataSourceName; + this.caseMap = caseMap; + this.instanceDataCount = instanceCount; + this.dataSourceCount = dataSourceCount; + this.earliestCaseDate = earliestCaseDate; + this.correlationAttributes = correlationAttributes; + } + + public String getDeviceId() { + return deviceId; + } + + public AbstractFile getFile() { + return file; + } + + public String getDataSourceName() { + return dataSourceName; + } + + public Map getCaseMap() { + return caseMap; + } + + public int getInstanceDataCount() { + return instanceDataCount; + } + + public int getDataSourceCount() { + return dataSourceCount; + } + + /** + * Returns the earliest date in the case. + * + * @return Formatted date string, or message that one was not found. + */ + public String getEarliestCaseDate() { + return earliestCaseDate; + } + + public Collection getCorrelationAttributes() { + return correlationAttributes; + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesPanel.java b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesPanel.java index 75ffb5c9ad..c4d0bf0e0a 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesPanel.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesPanel.java @@ -18,26 +18,23 @@ */ package org.sleuthkit.autopsy.centralrepository.contentviewer; +import org.sleuthkit.autopsy.centralrepository.application.NodeData; +import org.sleuthkit.autopsy.centralrepository.application.UniquePathKey; +import org.sleuthkit.autopsy.centralrepository.application.OtherOccurrences; +import java.awt.Cursor; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; -import java.io.BufferedWriter; import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.text.DateFormat; -import java.text.ParseException; -import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; -import java.util.Locale; import java.util.Map; -import java.util.Set; +import java.util.concurrent.ExecutionException; import java.util.logging.Level; import javax.swing.JFileChooser; import javax.swing.JMenuItem; @@ -45,15 +42,15 @@ import javax.swing.JOptionPane; import static javax.swing.JOptionPane.DEFAULT_OPTION; import static javax.swing.JOptionPane.ERROR_MESSAGE; import static javax.swing.JOptionPane.PLAIN_MESSAGE; +import javax.swing.SwingWorker; import javax.swing.filechooser.FileNameExtensionFilter; import javax.swing.table.TableModel; import javax.swing.table.TableRowSorter; -import org.apache.commons.lang3.StringUtils; -import org.joda.time.DateTimeZone; -import org.joda.time.LocalDateTime; +import org.openide.util.Exceptions; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.centralrepository.contentviewer.OtherOccurrencesNodeWorker.OtherOccurrencesData; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoException; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; @@ -61,22 +58,20 @@ import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeNor import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationCase; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.datamodel.AbstractFile; -import org.sleuthkit.datamodel.ContentTag; -import org.sleuthkit.datamodel.SleuthkitCase; -import org.sleuthkit.datamodel.TskCoreException; -import org.sleuthkit.datamodel.TskData; /** * Panel for displaying other occurrences results. */ @NbBundle.Messages({ "OtherOccurrencesPanel.table.noArtifacts=Item has no attributes with which to search.", - "OtherOccurrencesPanel.table.noResultsFound=No results found."}) + "OtherOccurrencesPanel.table.noResultsFound=No results found.", + "OtherOccurrencesPanel_table_loadingResults=Loading results" +}) public final class OtherOccurrencesPanel extends javax.swing.JPanel { private static final CorrelationCaseWrapper NO_ARTIFACTS_CASE = new CorrelationCaseWrapper(Bundle.OtherOccurrencesPanel_table_noArtifacts()); private static final CorrelationCaseWrapper NO_RESULTS_CASE = new CorrelationCaseWrapper(Bundle.OtherOccurrencesPanel_table_noResultsFound()); - private static final String UUID_PLACEHOLDER_STRING = "NoCorrelationAttributeInstance"; + private static final CorrelationCaseWrapper LOADING_CASE = new CorrelationCaseWrapper(Bundle.OtherOccurrencesPanel_table_loadingResults()); private static final Logger logger = Logger.getLogger(OtherOccurrencesPanel.class.getName()); private static final long serialVersionUID = 1L; private final OtherOccurrencesFilesTableModel filesTableModel; @@ -88,6 +83,8 @@ public final class OtherOccurrencesPanel extends javax.swing.JPanel { private String deviceId = ""; //the device id of the data source for the file which the content viewer is being populated for private AbstractFile file = null; + private SwingWorker worker; + /** * Creates new form OtherOccurrencesPanel */ @@ -101,16 +98,6 @@ public final class OtherOccurrencesPanel extends javax.swing.JPanel { customizeComponents(); } - /** - * Get a placeholder string to use in place of case uuid when it isn't - * available - * - * @return UUID_PLACEHOLDER_STRING - */ - static String getPlaceholderUUID() { - return UUID_PLACEHOLDER_STRING; - } - private void customizeComponents() { ActionListener actList = (ActionEvent e) -> { JMenuItem jmi = (JMenuItem) e.getSource(); @@ -182,14 +169,16 @@ public final class OtherOccurrencesPanel extends javax.swing.JPanel { */ private void showCommonalityDetails() { if (correlationAttributes.isEmpty()) { - JOptionPane.showConfirmDialog(showCommonalityMenuItem, + JOptionPane.showConfirmDialog(OtherOccurrencesPanel.this, Bundle.OtherOccurrencesPanel_correlatedArtifacts_isEmpty(), Bundle.OtherOccurrencesPanel_correlatedArtifacts_title(), DEFAULT_OPTION, PLAIN_MESSAGE); } else { StringBuilder msg = new StringBuilder(correlationAttributes.size()); int percentage; + this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); try { + // Leaving these calls on the EDT but adding wait cursor CentralRepository dbManager = CentralRepository.getInstance(); for (CorrelationAttributeInstance eamArtifact : correlationAttributes) { try { @@ -201,13 +190,15 @@ public final class OtherOccurrencesPanel extends javax.swing.JPanel { logger.log(Level.WARNING, String.format("Error getting commonality details for artifact with ID: %s.", eamArtifact.getID()), ex); } } - JOptionPane.showConfirmDialog(showCommonalityMenuItem, + this.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + JOptionPane.showConfirmDialog(OtherOccurrencesPanel.this, msg.toString(), Bundle.OtherOccurrencesPanel_correlatedArtifacts_title(), DEFAULT_OPTION, PLAIN_MESSAGE); } catch (CentralRepoException ex) { + this.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); logger.log(Level.SEVERE, "Error getting commonality details.", ex); - JOptionPane.showConfirmDialog(showCommonalityMenuItem, + JOptionPane.showConfirmDialog(OtherOccurrencesPanel.this, Bundle.OtherOccurrencesPanel_correlatedArtifacts_failed(), Bundle.OtherOccurrencesPanel_correlatedArtifacts_title(), DEFAULT_OPTION, ERROR_MESSAGE); @@ -227,20 +218,16 @@ public final class OtherOccurrencesPanel extends javax.swing.JPanel { if (-1 != selectedRowViewIdx) { CentralRepository dbManager = CentralRepository.getInstance(); int selectedRowModelIdx = filesTable.convertRowIndexToModel(selectedRowViewIdx); - List rowList = filesTableModel.getListOfNodesForFile(selectedRowModelIdx); + List rowList = filesTableModel.getListOfNodesForFile(selectedRowModelIdx); if (!rowList.isEmpty()) { - if (rowList.get(0) instanceof OtherOccurrenceNodeInstanceData) { - CorrelationCase eamCasePartial = ((OtherOccurrenceNodeInstanceData) rowList.get(0)).getCorrelationAttributeInstance().getCorrelationCase(); - caseDisplayName = eamCasePartial.getDisplayName(); - // query case details - CorrelationCase eamCase = dbManager.getCaseByUUID(eamCasePartial.getCaseUUID()); - if (eamCase != null) { - details = eamCase.getCaseDetailsOptionsPaneDialog(); - } else { - details = Bundle.OtherOccurrencesPanel_caseDetailsDialog_noDetails(); - } + CorrelationCase eamCasePartial = rowList.get(0).getCorrelationAttributeInstance().getCorrelationCase(); + caseDisplayName = eamCasePartial.getDisplayName(); + // query case details + CorrelationCase eamCase = dbManager.getCaseByUUID(eamCasePartial.getCaseUUID()); + if (eamCase != null) { + details = eamCase.getCaseDetailsOptionsPaneDialog(); } else { - details = Bundle.OtherOccurrencesPanel_caseDetailsDialog_notSelected(); + details = Bundle.OtherOccurrencesPanel_caseDetailsDialog_noDetails(); } } else { details = Bundle.OtherOccurrencesPanel_caseDetailsDialog_noDetailsReference(); @@ -249,7 +236,7 @@ public final class OtherOccurrencesPanel extends javax.swing.JPanel { } catch (CentralRepoException ex) { logger.log(Level.SEVERE, "Error loading case details", ex); } finally { - JOptionPane.showConfirmDialog(showCaseDetailsMenuItem, + JOptionPane.showConfirmDialog(OtherOccurrencesPanel.this, details, caseDisplayName, DEFAULT_OPTION, PLAIN_MESSAGE); @@ -271,85 +258,13 @@ public final class OtherOccurrencesPanel extends javax.swing.JPanel { if (!selectedFile.getName().endsWith(".csv")) { // NON-NLS selectedFile = new File(selectedFile.toString() + ".csv"); // NON-NLS } - writeOtherOccurrencesToFileAsCSV(selectedFile); + CSVWorker worker = new CSVWorker(selectedFile, file, dataSourceName, deviceId, Collections.unmodifiableCollection(correlationAttributes)); + worker.execute(); } } } - @NbBundle.Messages({ - "OtherOccurrencesPanel.csvHeader.case=Case", - "OtherOccurrencesPanel.csvHeader.device=Device", - "OtherOccurrencesPanel.csvHeader.dataSource=Data Source", - "OtherOccurrencesPanel.csvHeader.attribute=Matched Attribute", - "OtherOccurrencesPanel.csvHeader.value=Attribute Value", - "OtherOccurrencesPanel.csvHeader.known=Known", - "OtherOccurrencesPanel.csvHeader.path=Path", - "OtherOccurrencesPanel.csvHeader.comment=Comment" - }) - /** - * Write data for all cases in the content viewer to a CSV file - */ - private void writeOtherOccurrencesToFileAsCSV(File destFile) { - try (BufferedWriter writer = Files.newBufferedWriter(destFile.toPath())) { - //write headers - StringBuilder headers = new StringBuilder("\""); - headers.append(Bundle.OtherOccurrencesPanel_csvHeader_case()) - .append(OtherOccurrenceNodeInstanceData.getCsvItemSeparator()).append(Bundle.OtherOccurrencesPanel_csvHeader_dataSource()) - .append(OtherOccurrenceNodeInstanceData.getCsvItemSeparator()).append(Bundle.OtherOccurrencesPanel_csvHeader_attribute()) - .append(OtherOccurrenceNodeInstanceData.getCsvItemSeparator()).append(Bundle.OtherOccurrencesPanel_csvHeader_value()) - .append(OtherOccurrenceNodeInstanceData.getCsvItemSeparator()).append(Bundle.OtherOccurrencesPanel_csvHeader_known()) - .append(OtherOccurrenceNodeInstanceData.getCsvItemSeparator()).append(Bundle.OtherOccurrencesPanel_csvHeader_path()) - .append(OtherOccurrenceNodeInstanceData.getCsvItemSeparator()).append(Bundle.OtherOccurrencesPanel_csvHeader_comment()) - .append('"').append(System.getProperty("line.separator")); - writer.write(headers.toString()); - //write content - for (CorrelationAttributeInstance corAttr : correlationAttributes) { - Map correlatedNodeDataMap = new HashMap<>(0); - // get correlation and reference set instances from DB - correlatedNodeDataMap.putAll(getCorrelatedInstances(corAttr)); - for (OtherOccurrenceNodeInstanceData nodeData : correlatedNodeDataMap.values()) { - writer.write(nodeData.toCsvString()); - } - } - } catch (IOException ex) { - logger.log(Level.SEVERE, "Error writing selected rows to CSV.", ex); - } - } - - @NbBundle.Messages({"OtherOccurrencesPanel.earliestCaseNotAvailable= Not Enabled."}) - /** - * Gets the list of Eam Cases and determines the earliest case creation - * date. Sets the label to display the earliest date string to the user. - */ - private void setEarliestCaseDate() { - String dateStringDisplay = Bundle.OtherOccurrencesPanel_earliestCaseNotAvailable(); - - if (CentralRepository.isEnabled()) { - LocalDateTime earliestDate = LocalDateTime.now(DateTimeZone.UTC); - DateFormat datetimeFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.US); - try { - CentralRepository dbManager = CentralRepository.getInstance(); - List cases = dbManager.getCases(); - for (CorrelationCase aCase : cases) { - LocalDateTime caseDate = LocalDateTime.fromDateFields(datetimeFormat.parse(aCase.getCreationDate())); - - if (caseDate.isBefore(earliestDate)) { - earliestDate = caseDate; - dateStringDisplay = aCase.getCreationDate(); - } - - } - - } catch (CentralRepoException ex) { - logger.log(Level.SEVERE, "Error getting list of cases from database.", ex); // NON-NLS - } catch (ParseException ex) { - logger.log(Level.SEVERE, "Error parsing date of cases from database.", ex); // NON-NLS - } - - } - earliestCaseDate.setText(dateStringDisplay); - } - + @NbBundle.Messages({"OtherOccurrencesPanel_earliestCaseNotAvailable=Not Availble."}) /** * Reset the UI and clear cached data. */ @@ -371,123 +286,82 @@ public final class OtherOccurrencesPanel extends javax.swing.JPanel { * Populate the other occurrences table for one Correlation Attribute type * and value. * + * This method contains its own SwingWorker togather data. + * * @param aType The correlation attribute type to display other occurrences * for. * @param value The value being correlated on. */ - public void populateTableForOneType(CorrelationAttributeInstance.Type aType, String value) { - Map caseNames = new HashMap<>(); - int totalCount = 0; - Set dataSources = new HashSet<>(); - if (CentralRepository.isEnabled()) { - try { - List instances; - instances = CentralRepository.getInstance().getArtifactInstancesByTypeValue(aType, value); - HashMap nodeDataMap = new HashMap<>(); - String caseUUID = Case.getCurrentCase().getName(); - for (CorrelationAttributeInstance artifactInstance : instances) { + public void populateTableForOneType(CorrelationAttributeInstance.Type aType, String value) throws CentralRepoException { + if (worker != null) { + worker.cancel(true); + worker = null; + } - // Only add the attribute if it isn't the object the user selected. - // We consider it to be a different object if at least one of the following is true: - // - the case UUID is different - // - the data source name is different - // - the data source device ID is different - // - the file path is different - if (artifactInstance.getCorrelationCase().getCaseUUID().equals(caseUUID) - && (!StringUtils.isBlank(dataSourceName) && artifactInstance.getCorrelationDataSource().getName().equals(dataSourceName)) - && (!StringUtils.isBlank(deviceId) && artifactInstance.getCorrelationDataSource().getDeviceID().equals(deviceId)) - && (file != null && artifactInstance.getFilePath().equalsIgnoreCase(file.getParentPath() + file.getName()))) { - continue; + casesTableModel.addCorrelationCase(NO_ARTIFACTS_CASE); + + worker = new OtherOccurrenceOneTypeWorker(aType, value, file, deviceId, dataSourceName) { + @Override + public void done() { + try { + if (isCancelled()) { + return; } - correlationAttributes.add(artifactInstance); - OtherOccurrenceNodeInstanceData newNode = new OtherOccurrenceNodeInstanceData(artifactInstance, aType, value); - UniquePathKey uniquePathKey = new UniquePathKey(newNode); - nodeDataMap.put(uniquePathKey, newNode); - } - for (OtherOccurrenceNodeInstanceData nodeData : nodeDataMap.values()) { - if (nodeData.isCentralRepoNode()) { - try { - dataSources.add(makeDataSourceString(nodeData.getCorrelationAttributeInstance().getCorrelationCase().getCaseUUID(), nodeData.getDeviceID(), nodeData.getDataSourceName())); - caseNames.put(nodeData.getCorrelationAttributeInstance().getCorrelationCase().getCaseUUID(), nodeData.getCorrelationAttributeInstance().getCorrelationCase()); - } catch (CentralRepoException ex) { - logger.log(Level.WARNING, "Unable to get correlation case for displaying other occurrence for case: " + nodeData.getCaseName(), ex); - } - } else { - try { - dataSources.add(makeDataSourceString(Case.getCurrentCaseThrows().getName(), nodeData.getDeviceID(), nodeData.getDataSourceName())); - caseNames.put(Case.getCurrentCaseThrows().getName(), new CorrelationCase(Case.getCurrentCaseThrows().getName(), Case.getCurrentCaseThrows().getDisplayName())); - } catch (NoCurrentCaseException ex) { - logger.log(Level.WARNING, "No current case open for other occurrences", ex); - } + + casesTableModel.clearTable(); + + OtherOccurrenceOneTypeWorker.OneTypeData data = get(); + for (CorrelationCase corCase : data.getCaseNames().values()) { + casesTableModel.addCorrelationCase(new CorrelationCaseWrapper(corCase)); } - totalCount++; + int caseCount = casesTableModel.getRowCount(); + if (correlationAttributes.isEmpty()) { + casesTableModel.addCorrelationCase(NO_ARTIFACTS_CASE); + } else if (caseCount == 0) { + casesTableModel.addCorrelationCase(NO_RESULTS_CASE); + } + String earliestDate = data.getEarliestCaseDate(); + earliestCaseDate.setText(earliestDate.isEmpty() ? Bundle.OtherOccurrencesPanel_earliestCaseNotAvailable() : earliestDate); + foundInLabel.setText(String.format(Bundle.OtherOccurrencesPanel_foundIn_text(), data.getTotalCount(), caseCount, data.getDataSourceCount())); + if (caseCount > 0) { + casesTable.setRowSelectionInterval(0, 0); + } + + } catch (InterruptedException | ExecutionException ex) { + logger.log(Level.SEVERE, "Failed to update OtherOccurrence panel", ex); } - } catch (CorrelationAttributeNormalizationException | CentralRepoException ex) { - logger.log(Level.WARNING, "Error retrieving other occurrences for " + aType.getDisplayName() + ": " + value, ex); } - } - for (CorrelationCase corCase : caseNames.values()) { - casesTableModel.addCorrelationCase(new CorrelationCaseWrapper(corCase)); - } - int caseCount = casesTableModel.getRowCount(); - if (correlationAttributes.isEmpty()) { - casesTableModel.addCorrelationCase(NO_ARTIFACTS_CASE); - } else if (caseCount == 0) { - casesTableModel.addCorrelationCase(NO_RESULTS_CASE); - } - setEarliestCaseDate(); - foundInLabel.setText(String.format(Bundle.OtherOccurrencesPanel_foundIn_text(), totalCount, caseCount, dataSources.size())); - if (caseCount > 0) { - casesTable.setRowSelectionInterval(0, 0); - } + }; + + worker.execute(); + } + + /** + * Makes a loading message appear in the case table. + */ + void showPanelLoadingMessage() { + casesTableModel.addCorrelationCase(NO_ARTIFACTS_CASE); } /** * Load the correlatable data into the table model. If there is no data * available display the message on the status panel. * - * @param correlationAttrs The correlationAttributes to correlate on. - * @param dataSourceName The name of the dataSource to ignore results - * from. - * @param deviceId The deviceId of the device to ignore results - * from. - * @param abstractFile The abstract file to ignore files with the same - * location as. + * @param data A data wrapper object. */ @NbBundle.Messages({ "OtherOccurrencesPanel.foundIn.text=Found %d instances in %d cases and %d data sources." }) - void populateTable(Collection correlationAttrs, String dataSourceName, String deviceId, AbstractFile abstractFile) { - this.file = abstractFile; - this.dataSourceName = dataSourceName; - this.deviceId = deviceId; + void populateTable(OtherOccurrencesData data) { + this.file = data.getFile(); + this.dataSourceName = data.getDataSourceName(); + this.deviceId = data.getDeviceId(); - // get the attributes we can correlate on - correlationAttributes.addAll(correlationAttrs); - Map caseNames = new HashMap<>(); - int totalCount = 0; - Set dataSources = new HashSet<>(); - for (CorrelationAttributeInstance corAttr : correlationAttributes) { - for (OtherOccurrenceNodeInstanceData nodeData : getCorrelatedInstances(corAttr).values()) { - if (nodeData.isCentralRepoNode()) { - try { - dataSources.add(makeDataSourceString(nodeData.getCorrelationAttributeInstance().getCorrelationCase().getCaseUUID(), nodeData.getDeviceID(), nodeData.getDataSourceName())); - caseNames.put(nodeData.getCorrelationAttributeInstance().getCorrelationCase().getCaseUUID(), nodeData.getCorrelationAttributeInstance().getCorrelationCase()); - } catch (CentralRepoException ex) { - logger.log(Level.WARNING, "Unable to get correlation case for displaying other occurrence for case: " + nodeData.getCaseName(), ex); - } - } else { - try { - dataSources.add(makeDataSourceString(Case.getCurrentCaseThrows().getName(), nodeData.getDeviceID(), nodeData.getDataSourceName())); - caseNames.put(Case.getCurrentCaseThrows().getName(), new CorrelationCase(Case.getCurrentCaseThrows().getName(), Case.getCurrentCaseThrows().getDisplayName())); - } catch (NoCurrentCaseException ex) { - logger.log(Level.WARNING, "No current case open for other occurrences", ex); - } - } - totalCount++; - } - } - for (CorrelationCase corCase : caseNames.values()) { + casesTableModel.clearTable(); + + correlationAttributes.addAll(data.getCorrelationAttributes()); + + for (CorrelationCase corCase : data.getCaseMap().values()) { casesTableModel.addCorrelationCase(new CorrelationCaseWrapper(corCase)); } int caseCount = casesTableModel.getRowCount(); @@ -496,241 +370,142 @@ public final class OtherOccurrencesPanel extends javax.swing.JPanel { } else if (caseCount == 0) { casesTableModel.addCorrelationCase(NO_RESULTS_CASE); } - setEarliestCaseDate(); - foundInLabel.setText(String.format(Bundle.OtherOccurrencesPanel_foundIn_text(), totalCount, caseCount, dataSources.size())); + String earliestDate = data.getEarliestCaseDate(); + earliestCaseDate.setText(earliestDate.isEmpty() ? Bundle.OtherOccurrencesPanel_earliestCaseNotAvailable() : earliestDate); + foundInLabel.setText(String.format(Bundle.OtherOccurrencesPanel_foundIn_text(), data.getInstanceDataCount(), caseCount, data.getDataSourceCount())); if (caseCount > 0) { casesTable.setRowSelectionInterval(0, 0); } } /** - * Query the central repo database (if enabled) and the case database to - * find all artifact instances correlated to the given central repository - * artifact. If the central repo is not enabled, this will only return files - * from the current case with matching MD5 hashes. - * - * @param corAttr CorrelationAttribute to query for - * - * @return A collection of correlated artifact instances - */ - private Map getCorrelatedInstances(CorrelationAttributeInstance corAttr) { - // @@@ Check exception - try { - final Case openCase = Case.getCurrentCaseThrows(); - String caseUUID = openCase.getName(); - HashMap nodeDataMap = new HashMap<>(); - - if (CentralRepository.isEnabled()) { - List instances = CentralRepository.getInstance().getArtifactInstancesByTypeValue(corAttr.getCorrelationType(), corAttr.getCorrelationValue()); - - for (CorrelationAttributeInstance artifactInstance : instances) { - - // Only add the attribute if it isn't the object the user selected. - // We consider it to be a different object if at least one of the following is true: - // - the case UUID is different - // - the data source name is different - // - the data source device ID is different - // - the file path is different - if (artifactInstance.getCorrelationCase().getCaseUUID().equals(caseUUID) - && (!StringUtils.isBlank(dataSourceName) && artifactInstance.getCorrelationDataSource().getName().equals(dataSourceName)) - && (!StringUtils.isBlank(deviceId) && artifactInstance.getCorrelationDataSource().getDeviceID().equals(deviceId)) - && (file != null && artifactInstance.getFilePath().equalsIgnoreCase(file.getParentPath() + file.getName()))) { - continue; - } - OtherOccurrenceNodeInstanceData newNode = new OtherOccurrenceNodeInstanceData(artifactInstance, corAttr.getCorrelationType(), corAttr.getCorrelationValue()); - UniquePathKey uniquePathKey = new UniquePathKey(newNode); - nodeDataMap.put(uniquePathKey, newNode); - } - if (file != null && corAttr.getCorrelationType().getDisplayName().equals("Files")) { - List caseDbFiles = getCaseDbMatches(corAttr, openCase, file); - - for (AbstractFile caseDbFile : caseDbFiles) { - addOrUpdateNodeData(openCase, nodeDataMap, caseDbFile); - } - } - } - return nodeDataMap; - } catch (CentralRepoException ex) { - logger.log(Level.SEVERE, "Error getting artifact instances from database.", ex); // NON-NLS - } catch (CorrelationAttributeNormalizationException ex) { - logger.log(Level.INFO, "Error getting artifact instances from database.", ex); // NON-NLS - } catch (NoCurrentCaseException ex) { - logger.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS - } catch (TskCoreException ex) { - // do nothing. - // @@@ Review this behavior - logger.log(Level.SEVERE, "Exception while querying open case.", ex); // NON-NLS - } - - return new HashMap<>( - 0); - } - - /** - * Adds the file to the nodeDataMap map if it does not already exist - * - * @param autopsyCase - * @param nodeDataMap - * @param newFile - * - * @throws TskCoreException - * @throws CentralRepoException - */ - private void addOrUpdateNodeData(final Case autopsyCase, Map nodeDataMap, AbstractFile newFile) throws TskCoreException, CentralRepoException { - - OtherOccurrenceNodeInstanceData newNode = new OtherOccurrenceNodeInstanceData(newFile, autopsyCase); - - // If the caseDB object has a notable tag associated with it, update - // the known status to BAD - if (newNode.getKnown() != TskData.FileKnown.BAD) { - List fileMatchTags = autopsyCase.getServices().getTagsManager().getContentTagsByContent(newFile); - for (ContentTag tag : fileMatchTags) { - TskData.FileKnown tagKnownStatus = tag.getName().getKnownStatus(); - if (tagKnownStatus.equals(TskData.FileKnown.BAD)) { - newNode.updateKnown(TskData.FileKnown.BAD); - break; - } - } - } - - // Make a key to see if the file is already in the map - UniquePathKey uniquePathKey = new UniquePathKey(newNode); - - // If this node is already in the list, the only thing we need to do is - // update the known status to BAD if the caseDB version had known status BAD. - // Otherwise this is a new node so add the new node to the map. - if (nodeDataMap.containsKey(uniquePathKey)) { - if (newNode.getKnown() == TskData.FileKnown.BAD) { - OtherOccurrenceNodeInstanceData prevInstance = nodeDataMap.get(uniquePathKey); - prevInstance.updateKnown(newNode.getKnown()); - } - } else { - nodeDataMap.put(uniquePathKey, newNode); - } - } - - /** - * Get all other abstract files in the current case with the same MD5 as the - * selected node. - * - * @param corAttr The CorrelationAttribute containing the MD5 to search for - * @param openCase The current case - * @param file The current file. - * - * @return List of matching AbstractFile objects - * - * @throws NoCurrentCaseException - * @throws TskCoreException - * @throws CentralRepoException - */ - private List getCaseDbMatches(CorrelationAttributeInstance corAttr, Case openCase, AbstractFile file) throws NoCurrentCaseException, TskCoreException, CentralRepoException { - List caseDbArtifactInstances = new ArrayList<>(); - if (file != null) { - String md5 = corAttr.getCorrelationValue(); - SleuthkitCase tsk = openCase.getSleuthkitCase(); - List matches = tsk.findAllFilesWhere(String.format("md5 = '%s'", new Object[]{md5})); - - for (AbstractFile fileMatch : matches) { - if (file.equals(fileMatch)) { - continue; // If this is the file the user clicked on - } - caseDbArtifactInstances.add(fileMatch); - } - } - return caseDbArtifactInstances; - - } - - /** - * Create a unique string to be used as a key for deduping data sources as - * best as possible - */ - private String makeDataSourceString(String caseUUID, String deviceId, String dataSourceName) { - return caseUUID + deviceId + dataSourceName; - } - - /** - * Updates diplayed information to be correct for the current case selection + * Updates displayed information to be correct for the current case + * selection */ private void updateOnCaseSelection() { - int[] selectedCaseIndexes = casesTable.getSelectedRows(); + if (worker != null) { + worker.cancel(true); + worker = null; + } + + final int[] selectedCaseIndexes = casesTable.getSelectedRows(); dataSourcesTableModel.clearTable(); filesTableModel.clearTable(); + if (selectedCaseIndexes.length == 0) { //special case when no cases are selected occurrencePanel = new OccurrencePanel(); occurrencePanel.getPreferredSize(); detailsPanelScrollPane.setViewportView(occurrencePanel); - } else { - String currentCaseName; - try { - currentCaseName = Case.getCurrentCaseThrows().getName(); - } catch (NoCurrentCaseException ex) { - currentCaseName = null; - logger.log(Level.WARNING, "Unable to get current case for other occurrences content viewer", ex); - } - for (CorrelationAttributeInstance corAttr : correlationAttributes) { - Map correlatedNodeDataMap = new HashMap<>(0); - // get correlation and reference set instances from DB - correlatedNodeDataMap.putAll(getCorrelatedInstances(corAttr)); - for (OtherOccurrenceNodeInstanceData nodeData : correlatedNodeDataMap.values()) { - for (int selectedRow : selectedCaseIndexes) { - try { - if (nodeData.isCentralRepoNode()) { - if (casesTableModel.getCorrelationCase(casesTable.convertRowIndexToModel(selectedRow)) != null - && casesTableModel.getCorrelationCase(casesTable.convertRowIndexToModel(selectedRow)).getCaseUUID().equals(nodeData.getCorrelationAttributeInstance().getCorrelationCase().getCaseUUID())) { + return; + } + + setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + + worker = new SelectionWorker(correlationAttributes, file, deviceId, dataSourceName) { + @Override + public void done() { + if (isCancelled()) { + return; + } + + try { + Map correlatedNodeDataMap = get(); + + String currentCaseName; + try { + currentCaseName = Case.getCurrentCaseThrows().getName(); + } catch (NoCurrentCaseException ex) { + currentCaseName = null; + logger.log(Level.WARNING, "Unable to get current case for other occurrences content viewer", ex); + } + + for (NodeData nodeData : correlatedNodeDataMap.values()) { + for (int selectedRow : selectedCaseIndexes) { + try { + if (nodeData.isCentralRepoNode()) { + if (casesTableModel.getCorrelationCase(casesTable.convertRowIndexToModel(selectedRow)) != null + && casesTableModel.getCorrelationCase(casesTable.convertRowIndexToModel(selectedRow)).getCaseUUID().equals(nodeData.getCorrelationAttributeInstance().getCorrelationCase().getCaseUUID())) { + dataSourcesTableModel.addNodeData(nodeData); + } + } else if (currentCaseName != null && (casesTableModel.getCorrelationCase(casesTable.convertRowIndexToModel(selectedRow)).getCaseUUID().equals(currentCaseName))) { dataSourcesTableModel.addNodeData(nodeData); } - } else if (currentCaseName != null && (casesTableModel.getCorrelationCase(casesTable.convertRowIndexToModel(selectedRow)).getCaseUUID().equals(currentCaseName))) { - dataSourcesTableModel.addNodeData(nodeData); + } catch (CentralRepoException ex) { + logger.log(Level.WARNING, "Unable to get correlation attribute instance from OtherOccurrenceNodeInstanceData for case " + nodeData.getCaseName(), ex); } - } catch (CentralRepoException ex) { - logger.log(Level.WARNING, "Unable to get correlation attribute instance from OtherOccurrenceNodeInstanceData for case " + nodeData.getCaseName(), ex); } } + if (dataSourcesTable.getRowCount() > 0) { + dataSourcesTable.setRowSelectionInterval(0, 0); + } + + setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + + } catch (InterruptedException | ExecutionException ex) { + logger.log(Level.SEVERE, "Failed to update OtherOccurrencesPanel on data source selection", ex); } } - if (dataSourcesTable.getRowCount() > 0) { - dataSourcesTable.setRowSelectionInterval(0, 0); - } - } + }; + + worker.execute(); } /** - * Updates diplayed information to be correct for the current data source + * Updates displayed information to be correct for the current data source * selection */ private void updateOnDataSourceSelection() { - int[] selectedDataSources = dataSourcesTable.getSelectedRows(); - filesTableModel.clearTable(); - for (CorrelationAttributeInstance corAttr : correlationAttributes) { - Map correlatedNodeDataMap = new HashMap<>(0); + if (worker != null) { + worker.cancel(true); + worker = null; + } - // get correlation and reference set instances from DB - correlatedNodeDataMap.putAll(getCorrelatedInstances(corAttr)); - for (OtherOccurrenceNodeInstanceData nodeData : correlatedNodeDataMap.values()) { - for (int selectedDataSourceRow : selectedDataSources) { - try { - if (nodeData.isCentralRepoNode()) { - if (dataSourcesTableModel.getCaseUUIDForRow(dataSourcesTable.convertRowIndexToModel(selectedDataSourceRow)).equals(nodeData.getCorrelationAttributeInstance().getCorrelationCase().getCaseUUID()) - && dataSourcesTableModel.getDeviceIdForRow(dataSourcesTable.convertRowIndexToModel(selectedDataSourceRow)).equals(nodeData.getDeviceID())) { - filesTableModel.addNodeData(nodeData); - } - } else { - if (dataSourcesTableModel.getDeviceIdForRow(dataSourcesTable.convertRowIndexToModel(selectedDataSourceRow)).equals(nodeData.getDeviceID())) { - filesTableModel.addNodeData(nodeData); + setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + + final int[] selectedDataSources = dataSourcesTable.getSelectedRows(); + filesTableModel.clearTable(); + + worker = new SelectionWorker(correlationAttributes, file, deviceId, dataSourceName) { + @Override + public void done() { + if (isCancelled()) { + return; + } + + try { + Map correlatedNodeDataMap = get(); + for (NodeData nodeData : correlatedNodeDataMap.values()) { + for (int selectedDataSourceRow : selectedDataSources) { + try { + if (nodeData.isCentralRepoNode()) { + if (dataSourcesTableModel.getCaseUUIDForRow(dataSourcesTable.convertRowIndexToModel(selectedDataSourceRow)).equals(nodeData.getCorrelationAttributeInstance().getCorrelationCase().getCaseUUID()) + && dataSourcesTableModel.getDeviceIdForRow(dataSourcesTable.convertRowIndexToModel(selectedDataSourceRow)).equals(nodeData.getDeviceID())) { + filesTableModel.addNodeData(nodeData); + } + } else { + if (dataSourcesTableModel.getDeviceIdForRow(dataSourcesTable.convertRowIndexToModel(selectedDataSourceRow)).equals(nodeData.getDeviceID())) { + filesTableModel.addNodeData(nodeData); + } + } + } catch (CentralRepoException ex) { + logger.log(Level.WARNING, "Unable to get correlation attribute instance from OtherOccurrenceNodeInstanceData for case " + nodeData.getCaseName(), ex); } } - } catch (CentralRepoException ex) { - logger.log(Level.WARNING, "Unable to get correlation attribute instance from OtherOccurrenceNodeInstanceData for case " + nodeData.getCaseName(), ex); } + if (filesTable.getRowCount() > 0) { + filesTable.setRowSelectionInterval(0, 0); + } + } catch (InterruptedException | ExecutionException ex) { + logger.log(Level.SEVERE, "Failed to update OtherOccurrencesPanel on case selection", ex); + } finally { + setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); } } - } - if (filesTable.getRowCount() > 0) { - filesTable.setRowSelectionInterval(0, 0); - } + }; + + worker.execute(); } /** @@ -738,42 +513,47 @@ public final class OtherOccurrencesPanel extends javax.swing.JPanel { * currently selected File */ private void updateOnFileSelection() { - if (filesTable.getSelectedRowCount() == 1) { - //if there is one file selected update the deatils to show the data for that file - occurrencePanel = new OccurrencePanel(filesTableModel.getListOfNodesForFile(filesTable.convertRowIndexToModel(filesTable.getSelectedRow()))); - } else if (dataSourcesTable.getSelectedRowCount() == 1) { - //if no files were selected and only one data source is selected update the information to reflect the data source - String caseName = dataSourcesTableModel.getCaseNameForRow(dataSourcesTable.convertRowIndexToModel(dataSourcesTable.getSelectedRow())); - String dsName = dataSourcesTableModel.getValueAt(dataSourcesTable.convertRowIndexToModel(dataSourcesTable.getSelectedRow()), 0).toString(); - String caseCreatedDate = ""; - for (int row : casesTable.getSelectedRows()) { - if (casesTableModel.getValueAt(casesTable.convertRowIndexToModel(row), 0).toString().equals(caseName)) { - caseCreatedDate = getCaseCreatedDate(row); - break; + setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + try { + if (filesTable.getSelectedRowCount() == 1) { + //if there is one file selected update the deatils to show the data for that file + occurrencePanel = new OccurrencePanel(filesTableModel.getListOfNodesForFile(filesTable.convertRowIndexToModel(filesTable.getSelectedRow()))); + } else if (dataSourcesTable.getSelectedRowCount() == 1) { + //if no files were selected and only one data source is selected update the information to reflect the data source + String caseName = dataSourcesTableModel.getCaseNameForRow(dataSourcesTable.convertRowIndexToModel(dataSourcesTable.getSelectedRow())); + String dsName = dataSourcesTableModel.getValueAt(dataSourcesTable.convertRowIndexToModel(dataSourcesTable.getSelectedRow()), 0).toString(); + String caseCreatedDate = ""; + for (int row : casesTable.getSelectedRows()) { + if (casesTableModel.getValueAt(casesTable.convertRowIndexToModel(row), 0).toString().equals(caseName)) { + caseCreatedDate = getCaseCreatedDate(row); + break; + } + } + occurrencePanel = new OccurrencePanel(caseName, caseCreatedDate, dsName); + } else if (casesTable.getSelectedRowCount() == 1) { + //if no files were selected and a number of data source other than 1 are selected + //update the information to reflect the case + String createdDate; + String caseName = ""; + if (casesTable.getRowCount() > 0) { + caseName = casesTableModel.getValueAt(casesTable.convertRowIndexToModel(casesTable.getSelectedRow()), 0).toString(); + } + if (caseName.isEmpty()) { + occurrencePanel = new OccurrencePanel(); + } else { + createdDate = getCaseCreatedDate(casesTable.getSelectedRow()); + occurrencePanel = new OccurrencePanel(caseName, createdDate); } - } - occurrencePanel = new OccurrencePanel(caseName, caseCreatedDate, dsName); - } else if (casesTable.getSelectedRowCount() == 1) { - //if no files were selected and a number of data source other than 1 are selected - //update the information to reflect the case - String createdDate; - String caseName = ""; - if (casesTable.getRowCount() > 0) { - caseName = casesTableModel.getValueAt(casesTable.convertRowIndexToModel(casesTable.getSelectedRow()), 0).toString(); - } - if (caseName.isEmpty()) { - occurrencePanel = new OccurrencePanel(); } else { - createdDate = getCaseCreatedDate(casesTable.getSelectedRow()); - occurrencePanel = new OccurrencePanel(caseName, createdDate); + //else display an empty details area + occurrencePanel = new OccurrencePanel(); } - } else { - //else display an empty details area - occurrencePanel = new OccurrencePanel(); + //calling getPreferredSize has a side effect of ensuring it has a preferred size which reflects the contents which are visible + occurrencePanel.getPreferredSize(); + detailsPanelScrollPane.setViewportView(occurrencePanel); + } finally { + setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); } - //calling getPreferredSize has a side effect of ensuring it has a preferred size which reflects the contents which are visible - occurrencePanel.getPreferredSize(); - detailsPanelScrollPane.setViewportView(occurrencePanel); } /** @@ -802,6 +582,95 @@ public final class OtherOccurrencesPanel extends javax.swing.JPanel { return ""; } + /** + * SwingWorker used by the case and data source selection handler. + */ + private class SelectionWorker extends SwingWorker, Void> { + + private final Collection coAtInstances; + private final AbstractFile abstractFile; + private final String deviceIdStr; + private final String dataSourceNameStr; + + /** + * Construct a new SelectionWorker. + * + * @param coAtInstances + * @param abstractFile + * @param deviceIdStr + * @param dataSourceNameStr + */ + SelectionWorker(Collection coAtInstances, AbstractFile abstractFile, String deviceIdStr, String dataSourceNameStr) { + this.coAtInstances = coAtInstances; + this.abstractFile = abstractFile; + this.dataSourceNameStr = dataSourceNameStr; + this.deviceIdStr = deviceIdStr; + } + + @Override + protected Map doInBackground() throws Exception { + Map correlatedNodeDataMap = new HashMap<>(); + for (CorrelationAttributeInstance corAttr : coAtInstances) { + correlatedNodeDataMap.putAll(OtherOccurrences.getCorrelatedInstances(abstractFile, deviceIdStr, dataSourceNameStr, corAttr)); + + if(isCancelled()) { + return new HashMap<>(); + } + } + + return correlatedNodeDataMap; + } + } + + /** + * SwingWorker for creating the CSV dump file. + */ + private class CSVWorker extends SwingWorker { + + private final Collection correlationAttList; + private final String dataSourceName; + private final String deviceId; + private final File destFile; + private final AbstractFile abstractFile; + + /** + * Construct a CSVWorker + * + * @param destFile Output file. + * @param sourceFile Input file. + * @param dataSourceName Name of current dataSource. + * @param deviceId Id of the selected device. + * @param correlationAttList + */ + CSVWorker(File destFile, AbstractFile sourceFile, String dataSourceName, String deviceId, Collection correlationAttList) { + this.destFile = destFile; + this.abstractFile = sourceFile; + this.dataSourceName = dataSourceName; + this.deviceId = deviceId; + this.correlationAttList = correlationAttList; + } + + @Override + protected Void doInBackground() throws Exception { + OtherOccurrences.writeOtherOccurrencesToFileAsCSV(this.destFile, this.abstractFile, this.correlationAttList, this.dataSourceName, this.deviceId); + return null; + } + + @Override + public void done() { + try { + get(); + } catch (InterruptedException | ExecutionException ex) { + JOptionPane.showMessageDialog(OtherOccurrencesPanel.this, + "Failed to create csv file for Other Occurrences at\n" + destFile.getAbsolutePath(), + "Error Creating CSV", + JOptionPane.ERROR_MESSAGE); + + logger.log(Level.SEVERE, "Error writing selected rows to from OtherOccurrencePanel to " + destFile.getAbsolutePath(), ex); + } + } + } + /** * 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 @@ -976,9 +845,9 @@ public final class OtherOccurrencesPanel extends javax.swing.JPanel { boolean enableCentralRepoActions = false; if (CentralRepository.isEnabled() && filesTable.getSelectedRowCount() == 1) { int rowIndex = filesTable.getSelectedRow(); - List selectedFile = filesTableModel.getListOfNodesForFile(rowIndex); - if (!selectedFile.isEmpty() && selectedFile.get(0) instanceof OtherOccurrenceNodeInstanceData) { - OtherOccurrenceNodeInstanceData instanceData = (OtherOccurrenceNodeInstanceData) selectedFile.get(0); + List selectedFile = filesTableModel.getListOfNodesForFile(rowIndex); + if (!selectedFile.isEmpty() && selectedFile.get(0) instanceof NodeData) { + NodeData instanceData = selectedFile.get(0); enableCentralRepoActions = instanceData.isCentralRepoNode(); } } diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/AnnotationsContentViewer.form b/Core/src/org/sleuthkit/autopsy/contentviewers/AnnotationsContentViewer.form index 4fe61b2fb5..98fb50c89a 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/AnnotationsContentViewer.form +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/AnnotationsContentViewer.form @@ -21,21 +21,25 @@ - + - + - + + + + + - + diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/AnnotationsContentViewer.java b/Core/src/org/sleuthkit/autopsy/contentviewers/AnnotationsContentViewer.java index 5a4725a8d7..5d109438c3 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/AnnotationsContentViewer.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/AnnotationsContentViewer.java @@ -19,43 +19,23 @@ package org.sleuthkit.autopsy.contentviewers; import java.awt.Component; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.function.Function; +import java.util.concurrent.ExecutionException; import java.util.logging.Level; -import java.util.stream.Collectors; import javax.swing.JLabel; +import javax.swing.SwingWorker; import javax.swing.text.EditorKit; import javax.swing.text.html.HTMLEditorKit; -import org.apache.commons.lang.StringUtils; -import org.apache.commons.lang3.tuple.Pair; import static org.openide.util.NbBundle.Messages; -import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE; -import org.sleuthkit.autopsy.coreutils.Logger; import org.openide.nodes.Node; import org.openide.util.lookup.ServiceProvider; -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.CorrelationAttributeNormalizationException; -import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeUtil; -import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoException; import org.sleuthkit.autopsy.corecomponentinterfaces.DataContentViewer; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; -import org.sleuthkit.datamodel.BlackboardArtifactTag; -import org.sleuthkit.datamodel.Content; -import org.sleuthkit.datamodel.ContentTag; -import org.sleuthkit.datamodel.SleuthkitCase; -import org.sleuthkit.datamodel.Tag; import org.sleuthkit.datamodel.TskCoreException; -import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; -import org.sleuthkit.datamodel.BlackboardAttribute; -import org.jsoup.Jsoup; +import org.sleuthkit.autopsy.contentviewers.application.Annotations; +import org.sleuthkit.autopsy.coreutils.Logger; import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; /** * Annotations view of file contents. @@ -65,90 +45,10 @@ import org.jsoup.nodes.Element; @Messages({ "AnnotationsContentViewer.title=Annotations", "AnnotationsContentViewer.toolTip=Displays tags and comments associated with the selected content.", - "AnnotationsContentViewer.centralRepositoryEntry.title=Central Repository Comments", - "AnnotationsContentViewer.centralRepositoryEntryDataLabel.case=Case:", - "AnnotationsContentViewer.centralRepositoryEntryDataLabel.type=Type:", - "AnnotationsContentViewer.centralRepositoryEntryDataLabel.comment=Comment:", - "AnnotationsContentViewer.centralRepositoryEntryDataLabel.path=Path:", - "AnnotationsContentViewer.tagEntry.title=Tags", - "AnnotationsContentViewer.tagEntryDataLabel.tag=Tag:", - "AnnotationsContentViewer.tagEntryDataLabel.tagUser=Examiner:", - "AnnotationsContentViewer.tagEntryDataLabel.comment=Comment:", - "AnnotationsContentViewer.fileHitEntry.artifactCommentTitle=Artifact Comment", - "AnnotationsContentViewer.fileHitEntry.hashSetHitTitle=Hash Set Hit Comments", - "AnnotationsContentViewer.fileHitEntry.interestingFileHitTitle=Interesting File Hit Comments", - "AnnotationsContentViewer.fileHitEntry.setName=Set Name:", - "AnnotationsContentViewer.fileHitEntry.comment=Comment:", - "AnnotationsContentViewer.sourceFile.title=Source File", "AnnotationsContentViewer.onEmpty=No annotations were found for this particular item." }) public class AnnotationsContentViewer extends javax.swing.JPanel implements DataContentViewer { - /** - * Describes a key value pair for an item of type T where the key is the - * field name to display and the value is retrieved from item of type T - * using a provided Function. - * - * @param The item type. - */ - private static class ItemEntry { - - private final String itemName; - private final Function valueRetriever; - - ItemEntry(String itemName, Function valueRetriever) { - this.itemName = itemName; - this.valueRetriever = valueRetriever; - } - - String getItemName() { - return itemName; - } - - Function getValueRetriever() { - return valueRetriever; - } - - String retrieveValue(T object) { - return valueRetriever.apply(object); - } - } - - /** - * Describes a section that will be appended to the annotations view panel. - * - * @param The item type for items to display. - */ - private static class SectionConfig { - - private final String title; - private final List> attributes; - - SectionConfig(String title, List> attributes) { - this.title = title; - this.attributes = attributes; - } - - /** - * @return The title for the section. - */ - String getTitle() { - return title; - } - - /** - * @return Describes key-value pairs on the object to display to the - * user. - */ - List> getAttributes() { - return attributes; - } - } - - private static final Logger logger = Logger.getLogger(AnnotationsContentViewer.class.getName()); - - private static final String EMPTY_HTML = ""; - private static final int DEFAULT_FONT_SIZE = new JLabel().getFont().getSize(); // how big the subheader should be @@ -161,77 +61,34 @@ public class AnnotationsContentViewer extends javax.swing.JPanel implements Data private static final int DEFAULT_SUBSECTION_LEFT_PAD = DEFAULT_FONT_SIZE; // spacing occurring after an item - private static final int DEFAULT_TABLE_SPACING = DEFAULT_FONT_SIZE; private static final int DEFAULT_SECTION_SPACING = DEFAULT_FONT_SIZE * 2; private static final int DEFAULT_SUBSECTION_SPACING = DEFAULT_FONT_SIZE / 2; private static final int CELL_SPACING = DEFAULT_FONT_SIZE / 2; - // html stylesheet classnames for components - private static final String MESSAGE_CLASSNAME = "message"; - private static final String SUBSECTION_CLASSNAME = "subsection"; - private static final String SUBHEADER_CLASSNAME = "subheader"; - private static final String SECTION_CLASSNAME = "section"; - private static final String HEADER_CLASSNAME = "header"; - private static final String VERTICAL_TABLE_CLASSNAME = "vertical-table"; - // additional styling for components private static final String STYLE_SHEET_RULE - = String.format(" .%s { font-size: %dpx;font-style:italic; margin: 0px; padding: 0px; } ", MESSAGE_CLASSNAME, DEFAULT_FONT_SIZE) + = String.format(" .%s { font-size: %dpx;font-style:italic; margin: 0px; padding: 0px; } ", Annotations.MESSAGE_CLASSNAME, DEFAULT_FONT_SIZE) + String.format(" .%s {font-size:%dpx;font-weight:bold; margin: 0px; margin-top: %dpx; padding: 0px; } ", - SUBHEADER_CLASSNAME, SUBHEADER_FONT_SIZE, DEFAULT_SUBSECTION_SPACING) - + String.format(" .%s { font-size:%dpx;font-weight:bold; margin: 0px; padding: 0px; } ", HEADER_CLASSNAME, HEADER_FONT_SIZE) + Annotations.SUBHEADER_CLASSNAME, SUBHEADER_FONT_SIZE, DEFAULT_SUBSECTION_SPACING) + + String.format(" .%s { font-size:%dpx;font-weight:bold; margin: 0px; padding: 0px; } ", Annotations.HEADER_CLASSNAME, HEADER_FONT_SIZE) + String.format(" td { vertical-align: top; font-size:%dpx; text-align: left; margin: 0px; padding: 0px %dpx 0px 0px;} ", DEFAULT_FONT_SIZE, CELL_SPACING) + String.format(" th { vertical-align: top; text-align: left; margin: 0px; padding: 0px %dpx 0px 0px} ", DEFAULT_FONT_SIZE, CELL_SPACING) - + String.format(" .%s { margin: %dpx 0px; padding-left: %dpx; } ", SUBSECTION_CLASSNAME, DEFAULT_SUBSECTION_SPACING, DEFAULT_SUBSECTION_LEFT_PAD) - + String.format(" .%s { margin-bottom: %dpx; } ", SECTION_CLASSNAME, DEFAULT_SECTION_SPACING); - - // describing table values for a tag - private static final List> TAG_ENTRIES = Arrays.asList( - new ItemEntry<>(Bundle.AnnotationsContentViewer_tagEntryDataLabel_tag(), - (tag) -> (tag.getName() != null) ? tag.getName().getDisplayName() : null), - new ItemEntry<>(Bundle.AnnotationsContentViewer_tagEntryDataLabel_tagUser(), (tag) -> tag.getUserName()), - new ItemEntry<>(Bundle.AnnotationsContentViewer_tagEntryDataLabel_comment(), (tag) -> tag.getComment()) - ); - - private static final SectionConfig TAG_CONFIG - = new SectionConfig<>(Bundle.AnnotationsContentViewer_tagEntry_title(), TAG_ENTRIES); - - // file set attributes and table configurations - private static final List> FILESET_HIT_ENTRIES = Arrays.asList( - new ItemEntry<>(Bundle.AnnotationsContentViewer_fileHitEntry_setName(), - (bba) -> tryGetAttribute(bba, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME)), - new ItemEntry<>(Bundle.AnnotationsContentViewer_fileHitEntry_comment(), - (bba) -> tryGetAttribute(bba, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_COMMENT)) - ); - - private static final SectionConfig INTERESTING_FILE_CONFIG - = new SectionConfig<>(Bundle.AnnotationsContentViewer_fileHitEntry_interestingFileHitTitle(), FILESET_HIT_ENTRIES); - - private static final SectionConfig HASHSET_CONFIG - = new SectionConfig<>(Bundle.AnnotationsContentViewer_fileHitEntry_hashSetHitTitle(), FILESET_HIT_ENTRIES); - - private static final SectionConfig ARTIFACT_COMMENT_CONFIG - = new SectionConfig<>(Bundle.AnnotationsContentViewer_fileHitEntry_artifactCommentTitle(), FILESET_HIT_ENTRIES); - - // central repository attributes and table configuration - private static final List> CR_COMMENTS_ENTRIES = Arrays.asList( - new ItemEntry<>(Bundle.AnnotationsContentViewer_centralRepositoryEntryDataLabel_case(), - cai -> (cai.getCorrelationCase() != null) ? cai.getCorrelationCase().getDisplayName() : null), - new ItemEntry<>(Bundle.AnnotationsContentViewer_centralRepositoryEntryDataLabel_comment(), cai -> cai.getComment()), - new ItemEntry<>(Bundle.AnnotationsContentViewer_centralRepositoryEntryDataLabel_path(), cai -> cai.getFilePath()) - ); - - private static final SectionConfig CR_COMMENTS_CONFIG - = new SectionConfig<>(Bundle.AnnotationsContentViewer_centralRepositoryEntry_title(), CR_COMMENTS_ENTRIES); - + + String.format(" .%s { margin: %dpx 0px; padding-left: %dpx; } ", Annotations.SUBSECTION_CLASSNAME, DEFAULT_SUBSECTION_SPACING, DEFAULT_SUBSECTION_LEFT_PAD) + + String.format(" .%s { margin-bottom: %dpx; } ", Annotations.SECTION_CLASSNAME, DEFAULT_SECTION_SPACING); + + private static final long serialVersionUID = 1L; + private static final Logger logger = Logger.getLogger(AnnotationsContentViewer.class.getName()); + + private AnnotationWorker worker; + /** * Creates an instance of AnnotationsContentViewer. */ public AnnotationsContentViewer() { initComponents(); - Utilities.configureTextPaneAsHtml(jTextPane1); + Utilities.configureTextPaneAsHtml(textPanel); // get html editor kit and apply additional style rules - EditorKit editorKit = jTextPane1.getEditorKit(); + EditorKit editorKit = textPanel.getEditorKit(); if (editorKit instanceof HTMLEditorKit) { HTMLEditorKit htmlKit = (HTMLEditorKit) editorKit; htmlKit.getStyleSheet().addRule(STYLE_SHEET_RULE); @@ -240,472 +97,21 @@ public class AnnotationsContentViewer extends javax.swing.JPanel implements Data @Override public void setNode(Node node) { - if ((node == null) || (!isSupported(node))) { - resetComponent(); + resetComponent(); + + if(worker != null) { + worker.cancel(true); + worker = null; + } + + if(node == null) { return; } - Document html = Jsoup.parse(EMPTY_HTML); - Element body = html.getElementsByTag("body").first(); - - BlackboardArtifact artifact = node.getLookup().lookup(BlackboardArtifact.class); - Content sourceFile = null; - - try { - if (artifact != null) { - /* - * Get the source content based on the artifact to ensure we - * display the correct data instead of whatever was in the node. - */ - sourceFile = artifact.getSleuthkitCase().getAbstractFileById(artifact.getObjectID()); - } else { - /* - * No artifact is present, so get the content based on what's - * present in the node. In this case, the selected item IS the - * source file. - */ - sourceFile = node.getLookup().lookup(AbstractFile.class); - } - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, String.format( - "Exception while trying to retrieve a Content instance from the BlackboardArtifact '%s' (id=%d).", - artifact.getDisplayName(), artifact.getArtifactID()), ex); - } - - boolean somethingWasRendered = false; - if (artifact != null) { - somethingWasRendered = renderArtifact(body, artifact, sourceFile); - } else { - somethingWasRendered = renderContent(body, sourceFile, false); - } - - if (!somethingWasRendered) { - appendMessage(body, Bundle.AnnotationsContentViewer_onEmpty()); - } - - jTextPane1.setText(html.html()); - jTextPane1.setCaretPosition(0); + worker = new AnnotationWorker(node); + worker.execute(); } - /** - * Renders annotations for an artifact. - * - * @param parent The html element to render content int. - * @param bba The blackboard artifact to render. - * @param sourceContent The content from which the blackboard artifact - * comes. - * - * @return If any content was actually rendered. - */ - private static boolean renderArtifact(Element parent, BlackboardArtifact bba, Content sourceContent) { - boolean contentRendered = appendEntries(parent, TAG_CONFIG, getTags(bba), false); - - if (CentralRepository.isEnabled()) { - List centralRepoComments = getCentralRepositoryData(bba); - boolean crRendered = appendEntries(parent, CR_COMMENTS_CONFIG, centralRepoComments, false); - contentRendered = contentRendered || crRendered; - } - - // if artifact is a hashset hit or interesting file and has a non-blank comment - if ((ARTIFACT_TYPE.TSK_HASHSET_HIT.getTypeID() == bba.getArtifactTypeID() - || ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT.getTypeID() == bba.getArtifactTypeID()) - && (hasTskComment(bba))) { - - boolean filesetRendered = appendEntries(parent, ARTIFACT_COMMENT_CONFIG, Arrays.asList(bba), false); - contentRendered = contentRendered || filesetRendered; - } - - Element sourceFileSection = appendSection(parent, Bundle.AnnotationsContentViewer_sourceFile_title()); - boolean sourceFileRendered = renderContent(sourceFileSection, sourceContent, true); - - if (!sourceFileRendered) { - sourceFileSection.remove(); - } - - return contentRendered || sourceFileRendered; - } - - /** - * Renders annotations for a content item. - * - * @param parent The parent within which to render. - * @param sourceContent The content for which annotations will be gathered. - * @param isSubheader True if this section should be rendered as a - * subheader as opposed to a top-level header. - * - * @return If any content was actually rendered. - */ - private static boolean renderContent(Element parent, Content sourceContent, boolean isSubheader) { - boolean contentRendered = appendEntries(parent, TAG_CONFIG, getTags(sourceContent), isSubheader); - - if (sourceContent instanceof AbstractFile) { - AbstractFile sourceFile = (AbstractFile) sourceContent; - - if (CentralRepository.isEnabled()) { - List centralRepoComments = getCentralRepositoryData(sourceFile); - boolean crRendered = appendEntries(parent, CR_COMMENTS_CONFIG, centralRepoComments, isSubheader); - contentRendered = contentRendered || crRendered; - } - - boolean hashsetRendered = appendEntries(parent, HASHSET_CONFIG, - getFileSetHits(sourceFile, ARTIFACT_TYPE.TSK_HASHSET_HIT), - isSubheader); - - boolean interestingFileRendered = appendEntries(parent, INTERESTING_FILE_CONFIG, - getFileSetHits(sourceFile, ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT), - isSubheader); - - contentRendered = contentRendered || hashsetRendered || interestingFileRendered; - } - return contentRendered; - } - - /** - * Retrieves tags associated with a content item. - * - * @param sourceContent The content for which to gather content. - * - * @return The Tags associated with this item. - */ - private static List getTags(Content sourceContent) { - try { - SleuthkitCase tskCase = Case.getCurrentCaseThrows().getSleuthkitCase(); - return tskCase.getContentTagsByContent(sourceContent); - } catch (NoCurrentCaseException ex) { - logger.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Exception while getting tags from the case database.", ex); //NON-NLS - } - return new ArrayList<>(); - } - - /** - * Retrieves tags for blackboard artifact tags. - * - * @param bba The blackboard artifact for which to retrieve tags. - * - * @return The found tags. - */ - private static List getTags(BlackboardArtifact bba) { - try { - SleuthkitCase tskCase = Case.getCurrentCaseThrows().getSleuthkitCase(); - return tskCase.getBlackboardArtifactTagsByArtifact(bba); - } catch (NoCurrentCaseException ex) { - logger.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Exception while getting tags from the case database.", ex); //NON-NLS - } - return new ArrayList<>(); - } - - /** - * Retrieves the blackboard artifacts for a source file matching a certain - * type that have a non-blank TSK_COMMENT. - * - * @param sourceFile The source file for which to fetch artifacts. - * @param type The type of blackboard artifact to fetch. - * - * @return The artifacts found matching this type. - */ - private static List getFileSetHits(AbstractFile sourceFile, ARTIFACT_TYPE type) { - try { - SleuthkitCase tskCase = Case.getCurrentCaseThrows().getSleuthkitCase(); - return tskCase.getBlackboardArtifacts(type, sourceFile.getId()).stream() - .filter((bba) -> hasTskComment(bba)) - .collect(Collectors.toList()); - } catch (NoCurrentCaseException ex) { - logger.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Exception while getting file set hits from the case database.", ex); //NON-NLS - } - return new ArrayList<>(); - } - - /** - * Returns true if the artifact contains a non-blank TSK_COMMENT attribute. - * - * @param artifact The artifact to check. - * - * @return True if it has a non-blank TSK_COMMENT. - */ - private static boolean hasTskComment(BlackboardArtifact artifact) { - return StringUtils.isNotBlank(tryGetAttribute(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_COMMENT)); - } - - /** - * Attempts to retrieve the attribute of a particular type from a blackboard - * artifact. - * - * @param artifact The artifact from which to retrieve the information. - * @param attributeType The attribute type to retrieve from the artifact. - * - * @return The string value of the attribute or null if not found. - */ - private static String tryGetAttribute(BlackboardArtifact artifact, BlackboardAttribute.ATTRIBUTE_TYPE attributeType) { - if (artifact == null) { - return null; - } - - BlackboardAttribute attr = null; - try { - attr = artifact.getAttribute(new BlackboardAttribute.Type(attributeType)); - } catch (TskCoreException ex) { - logger.log(Level.WARNING, String.format("Unable to fetch attribute of type %s for artifact %s", attributeType, artifact), ex); - } - - if (attr == null) { - return null; - } - - return attr.getValueString(); - } - - /** - * Gets the "Central Repository Comments" section with data for the - * blackboard artifact. - * - * @param artifact The selected artifact. - * - * @return The Correlation Attribute Instances associated with the artifact - * that have comments. - */ - private static List getCentralRepositoryData(BlackboardArtifact artifact) { - if (artifact == null) { - return new ArrayList<>(); - } - - List> lookupKeys = CorrelationAttributeUtil.makeCorrAttrsForCorrelation(artifact) - .stream() - .map(cai -> Pair.of(cai.getCorrelationType(), cai.getCorrelationValue())) - .collect(Collectors.toList()); - - return getCorrelationAttributeComments(lookupKeys); - } - - /** - * Gets the "Central Repository Comments" section with data. - * - * @param sourceFile A selected file, or a source file of the selected - * artifact. - * - * @return The Correlation Attribute Instances associated with the - * sourcefile that have comments. - */ - private static List getCentralRepositoryData(AbstractFile sourceFile) { - if (sourceFile == null || StringUtils.isEmpty(sourceFile.getMd5Hash())) { - return new ArrayList<>(); - } - - List artifactTypes = null; - try { - artifactTypes = CentralRepository.getInstance().getDefinedCorrelationTypes(); - } catch (CentralRepoException ex) { - logger.log(Level.SEVERE, "Error connecting to the Central Repository database.", ex); // NON-NLS - } - - if (artifactTypes == null || artifactTypes.isEmpty()) { - return new ArrayList<>(); - } - - String md5 = sourceFile.getMd5Hash(); - - // get key lookups for a file attribute types and the md5 hash - List> lookupKeys = artifactTypes.stream() - .filter((attributeType) -> attributeType.getId() == CorrelationAttributeInstance.FILES_TYPE_ID) - .map((attributeType) -> Pair.of(attributeType, md5)) - .collect(Collectors.toList()); - - return getCorrelationAttributeComments(lookupKeys); - } - - /** - * Given a type and a value for that type, does a lookup in the Central - * Repository for matching values that have comments. - * - * @param lookupKeys The type and value to lookup. - * - * @return The found correlation attribute instances. - */ - private static List getCorrelationAttributeComments(List> lookupKeys) { - List instancesToRet = new ArrayList<>(); - - try { - // use lookup instances to find the actual correlation attributes for the items selected - for (Pair typeVal : lookupKeys) { - instancesToRet.addAll(CentralRepository.getInstance() - .getArtifactInstancesByTypeValue(typeVal.getKey(), typeVal.getValue()) - .stream() - // for each one found, if it has a comment, return - .filter((cai) -> StringUtils.isNotBlank(cai.getComment())) - .collect(Collectors.toList())); - } - - } catch (CentralRepoException ex) { - logger.log(Level.SEVERE, "Error connecting to the Central Repository database.", ex); // NON-NLS - } catch (CorrelationAttributeNormalizationException ex) { - logger.log(Level.SEVERE, "Error normalizing instance from Central Repository database.", ex); // NON-NLS - } - - return instancesToRet; - } - - /** - * Append entries to the parent element in the annotations viewer. Entries - * will be formatted as a table in the format specified in the - * SectionConfig. - * - * @param parent The parent element for which the entries will be - * attached. - * @param config The display configuration for this entry type (i.e. - * table type, name, if data is not present). - * @param items The items to display. - * @param isSubsection Whether or not this should be displayed as a - * subsection. If not displayed as a top-level section. - * - * @return If there was actual content rendered for this set of entries. - */ - private static boolean appendEntries(Element parent, SectionConfig config, List items, - boolean isSubsection) { - if (items == null || items.isEmpty()) { - return false; - } - - Element sectionDiv = (isSubsection) ? appendSubsection(parent, config.getTitle()) : appendSection(parent, config.getTitle()); - appendVerticalEntryTables(sectionDiv, items, config.getAttributes()); - return true; - } - - /** - * Appends a table where items are displayed in rows of key-value pairs. - * - * @param parent The parent to append the table. - * @param items The items to process into a series of tables. - * @param rowHeaders The keys and the means to process items in order to get - * key-value pairs. - * - * @return The parent element provided as parameter. - */ - private static Element appendVerticalEntryTables(Element parent, List items, List> rowHeaders) { - boolean isFirst = true; - for (T item : items) { - if (item == null) { - continue; - } - - List> tableData = rowHeaders.stream() - .map(row -> Arrays.asList(row.getItemName(), row.retrieveValue(item))) - .collect(Collectors.toList()); - - Element childTable = appendTable(parent, 2, tableData, null); - childTable.attr("class", VERTICAL_TABLE_CLASSNAME); - - if (isFirst) { - isFirst = false; - } else { - childTable.attr("style", String.format("margin-top: %dpx;", DEFAULT_TABLE_SPACING)); - } - } - - return parent; - } - - /** - * Appends a generic table to the parent element. - * - * @param parent The parent element that will have a table appended - * to it. - * @param columnNumber The number of columns to append. - * @param content The content in content.get(row).get(column) format. - * @param columnHeaders The column headers or null if no column headers - * should be created. - * - * @return The created table. - */ - private static Element appendTable(Element parent, int columnNumber, List> content, List columnHeaders) { - Element table = parent.appendElement("table"); - if (columnHeaders != null && !columnHeaders.isEmpty()) { - Element header = table.appendElement("thead"); - appendRow(header, columnHeaders, columnNumber, true); - } - Element tableBody = table.appendElement("tbody"); - - content.forEach((rowData) -> appendRow(tableBody, rowData, columnNumber, false)); - return table; - } - - /** - * Appends a row to the parent element (should be thead or tbody). - * - * @param rowParent The parent table element. - * @param data The data to place in columns within the table. - * @param columnNumber The number of columns to append. - * @param isHeader Whether or not this should have header cells ('th') - * instead of regular cells ('td'). - * - * @return The row created. - */ - private static Element appendRow(Element rowParent, List data, int columnNumber, boolean isHeader) { - String cellType = isHeader ? "th" : "td"; - Element row = rowParent.appendElement("tr"); - for (int i = 0; i < columnNumber; i++) { - Element cell = row.appendElement(cellType); - if (data != null && i < data.size()) { - cell.text(StringUtils.isEmpty(data.get(i)) ? "" : data.get(i)); - } - } - return row; - } - - /** - * Appends a new section with a section header to the parent element. - * - * @param parent The element to append this section to. - * @param headerText The text for the section. - * - * @return The div for the new section. - */ - private static Element appendSection(Element parent, String headerText) { - Element sectionDiv = parent.appendElement("div"); - sectionDiv.attr("class", SECTION_CLASSNAME); - Element header = sectionDiv.appendElement("h1"); - header.text(headerText); - header.attr("class", HEADER_CLASSNAME); - return sectionDiv; - } - - /** - * Appends a new subsection with a subsection header to the parent element. - * - * @param parent The element to append this subsection to. - * @param headerText The text for the subsection. - * - * @return The div for the new subsection. - */ - private static Element appendSubsection(Element parent, String headerText) { - Element subsectionDiv = parent.appendElement("div"); - subsectionDiv.attr("class", SUBSECTION_CLASSNAME); - Element header = subsectionDiv.appendElement("h2"); - header.text(headerText); - header.attr("class", SUBHEADER_CLASSNAME); - return subsectionDiv; - } - - /** - * Appends a message to the parent element. This is typically used in the - * event that no data exists for a certain type. - * - * @param parent The parent element that will have this message appended to - * it. - * @param message The message to append. - * - * @return The paragraph element for the new message. - */ - private static Element appendMessage(Element parent, String message) { - Element messageEl = parent.appendElement("p"); - messageEl.text(message); - messageEl.attr("class", MESSAGE_CLASSNAME); - return messageEl; - } /** * This method is called from within the constructor to initialize the form. @@ -716,31 +122,30 @@ public class AnnotationsContentViewer extends javax.swing.JPanel implements Data // //GEN-BEGIN:initComponents private void initComponents() { - jScrollPane5 = new javax.swing.JScrollPane(); - jTextPane1 = new javax.swing.JTextPane(); + javax.swing.JScrollPane scrollPane = new javax.swing.JScrollPane(); + textPanel = new javax.swing.JTextPane(); setPreferredSize(new java.awt.Dimension(100, 58)); - jTextPane1.setEditable(false); - jTextPane1.setName(""); // NOI18N - jTextPane1.setPreferredSize(new java.awt.Dimension(600, 52)); - jScrollPane5.setViewportView(jTextPane1); + textPanel.setEditable(false); + textPanel.setName(""); // NOI18N + textPanel.setPreferredSize(new java.awt.Dimension(600, 52)); + scrollPane.setViewportView(textPanel); javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(jScrollPane5, javax.swing.GroupLayout.DEFAULT_SIZE, 907, Short.MAX_VALUE) + .addComponent(scrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 907, Short.MAX_VALUE) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(jScrollPane5, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, 435, Short.MAX_VALUE) + .addComponent(scrollPane, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, 435, Short.MAX_VALUE) ); }// //GEN-END:initComponents // Variables declaration - do not modify//GEN-BEGIN:variables - private javax.swing.JScrollPane jScrollPane5; - private javax.swing.JTextPane jTextPane1; + private javax.swing.JTextPane textPanel; // End of variables declaration//GEN-END:variables @Override @@ -793,6 +198,49 @@ public class AnnotationsContentViewer extends javax.swing.JPanel implements Data @Override public void resetComponent() { - jTextPane1.setText(EMPTY_HTML); + textPanel.setText(""); + } + + /** + * A SwingWorker that will fetch the annotation information for the given + * node. + */ + private class AnnotationWorker extends SwingWorker { + private final Node node; + + AnnotationWorker(Node node) { + this.node = node; + } + + @Override + protected String doInBackground() throws Exception { + Document doc = Annotations.buildDocument(node); + + if(isCancelled()) { + return null; + } + + if(doc != null) { + return doc.html(); + } else { + return Bundle.AnnotationsContentViewer_onEmpty(); + } + } + + @Override + public void done() { + if (isCancelled()) { + return; + } + + try { + String text = get(); + textPanel.setText(text); + textPanel.setCaretPosition(0); + } catch (InterruptedException | ExecutionException ex) { + logger.log(Level.SEVERE, "Failed to get annotation information for node", ex); + } + } + } } diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED index 6df3399a0a..4714416d46 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED @@ -15,22 +15,7 @@ # governing permissions and limitations under the License. # -AnnotationsContentViewer.centralRepositoryEntry.title=Central Repository Comments -AnnotationsContentViewer.centralRepositoryEntryDataLabel.case=Case: -AnnotationsContentViewer.centralRepositoryEntryDataLabel.comment=Comment: -AnnotationsContentViewer.centralRepositoryEntryDataLabel.path=Path: -AnnotationsContentViewer.centralRepositoryEntryDataLabel.type=Type: -AnnotationsContentViewer.fileHitEntry.artifactCommentTitle=Artifact Comment -AnnotationsContentViewer.fileHitEntry.comment=Comment: -AnnotationsContentViewer.fileHitEntry.hashSetHitTitle=Hash Set Hit Comments -AnnotationsContentViewer.fileHitEntry.interestingFileHitTitle=Interesting File Hit Comments -AnnotationsContentViewer.fileHitEntry.setName=Set Name: AnnotationsContentViewer.onEmpty=No annotations were found for this particular item. -AnnotationsContentViewer.sourceFile.title=Source File -AnnotationsContentViewer.tagEntry.title=Tags -AnnotationsContentViewer.tagEntryDataLabel.comment=Comment: -AnnotationsContentViewer.tagEntryDataLabel.tag=Tag: -AnnotationsContentViewer.tagEntryDataLabel.tagUser=Examiner: AnnotationsContentViewer.title=Annotations AnnotationsContentViewer.toolTip=Displays tags and comments associated with the selected content. ApplicationContentViewer.title=Application @@ -85,6 +70,7 @@ MediaViewVideoPanel.progressLabel.text=00:00 MediaViewVideoPanel.infoLabel.text=info MediaViewImagePanel.imgFileTooLarge.msg=Could not load image file (too large): {0} +Metadata.nodeText.loading=Metadata loading... Metadata.nodeText.none=None Metadata.nodeText.truncated=(results truncated) Metadata.nodeText.unknown=Unknown diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/Metadata.java b/Core/src/org/sleuthkit/autopsy/contentviewers/Metadata.java index 45dae8e2e0..2c695dfcb4 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/Metadata.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/Metadata.java @@ -19,10 +19,14 @@ package org.sleuthkit.autopsy.contentviewers; import java.awt.Component; +import java.awt.Cursor; import java.util.List; +import java.util.concurrent.ExecutionException; import java.util.logging.Level; +import javax.swing.SwingWorker; import org.apache.commons.lang3.StringUtils; import org.openide.nodes.Node; +import org.openide.util.Exceptions; import org.openide.util.NbBundle; import org.openide.util.NbBundle.Messages; import org.openide.util.lookup.ServiceProvider; @@ -50,7 +54,9 @@ import org.sleuthkit.datamodel.TskData.TSK_DB_FILES_TYPE_ENUM; public class Metadata extends javax.swing.JPanel implements DataContentViewer { private static final Logger LOGGER = Logger.getLogger(Metadata.class.getName()); - + + private MetaDataWorker worker; + /** * Creates new form Metadata */ @@ -145,183 +151,64 @@ public class Metadata extends javax.swing.JPanel implements DataContentViewer { "Metadata.tableRowTitle.acquisitionDetails=Acquisition Details", "Metadata.tableRowTitle.downloadSource=Downloaded From", "Metadata.nodeText.unknown=Unknown", - "Metadata.nodeText.none=None"}) + "Metadata.nodeText.none=None", + "Metadata.nodeText.loading=Metadata loading..."}) @Override public void setNode(Node node) { - AbstractFile file = node.getLookup().lookup(AbstractFile.class); - Image image = node.getLookup().lookup(Image.class); - DataSource dataSource = node.getLookup().lookup(DataSource.class); - if (file == null && image == null) { - setText(NbBundle.getMessage(this.getClass(), "Metadata.nodeText.nonFilePassedIn")); - return; + + if (worker != null) { + worker.cancel(true); + worker = null; } - StringBuilder sb = new StringBuilder(); - startTable(sb); - - if (file != null) { - try { - addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.name"), file.getUniquePath()); - } catch (TskCoreException ex) { - addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.name"), file.getParentPath() + "/" + file.getName()); - } - - addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.type"), file.getType().getName()); - addRow(sb, Bundle.Metadata_tableRowTitle_mimeType(), file.getMIMEType()); - addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.size"), Long.toString(file.getSize())); - addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.fileNameAlloc"), file.getDirFlagAsString()); - addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.metadataAlloc"), file.getMetaFlagsAsString()); - addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.modified"), ContentUtils.getStringTime(file.getMtime(), file)); - addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.accessed"), ContentUtils.getStringTime(file.getAtime(), file)); - addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.created"), ContentUtils.getStringTime(file.getCrtime(), file)); - addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.changed"), ContentUtils.getStringTime(file.getCtime(), file)); - - - String md5 = file.getMd5Hash(); - if (md5 == null) { - md5 = NbBundle.getMessage(this.getClass(), "Metadata.tableRowContent.md5notCalc"); - } - addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.md5"), md5); - String sha256 = file.getSha256Hash(); - if (sha256 == null) { - sha256 = NbBundle.getMessage(this.getClass(), "Metadata.tableRowContent.md5notCalc"); - } - addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.sha256"), sha256); - addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.hashLookupResults"), file.getKnown().toString()); - addAcquisitionDetails(sb, dataSource); - - addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.internalid"), Long.toString(file.getId())); - if (file.getType().compareTo(TSK_DB_FILES_TYPE_ENUM.LOCAL) == 0) { - addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.localPath"), file.getLocalAbsPath()); - } - - try { - List associatedObjectArtifacts = file.getArtifacts(ARTIFACT_TYPE.TSK_ASSOCIATED_OBJECT); - if (!associatedObjectArtifacts.isEmpty()) { - BlackboardArtifact artifact = associatedObjectArtifacts.get(0); - BlackboardAttribute associatedArtifactAttribute = artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT)); - if (associatedArtifactAttribute != null) { - long artifactId = associatedArtifactAttribute.getValueLong(); - BlackboardArtifact associatedArtifact = artifact.getSleuthkitCase().getBlackboardArtifact(artifactId); - addDownloadSourceRow(sb, associatedArtifact); - } - } - } catch (TskCoreException ex) { - sb.append(NbBundle.getMessage(this.getClass(), "Metadata.nodeText.exceptionNotice.text")).append(ex.getLocalizedMessage()); - } - - endTable(sb); - - /* - * If we have a file system file, grab the more detailed metadata text - * too - */ - try { - if (file instanceof FsContent) { - FsContent fsFile = (FsContent) file; - - sb.append("
\n"); //NON-NLS
-                    sb.append(NbBundle.getMessage(this.getClass(), "Metadata.nodeText.text"));
-                    sb.append(" 

"); // NON-NLS - for (String str : fsFile.getMetaDataText()) { - sb.append(str).append("
"); //NON-NLS - - /* - * Very long results can cause the UI to hang before displaying, - * so truncate the results if necessary. - */ - if(sb.length() > 50000){ - sb.append(NbBundle.getMessage(this.getClass(), "Metadata.nodeText.truncated")); - break; + if (node != null) { + setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + setText(Bundle.Metadata_nodeText_loading()); + worker = new MetaDataWorker(node) { + @Override + public void done() { + try { + if (!isCancelled()) { + setText(get()); + jTextPane1.setCaretPosition(0); } + } catch (InterruptedException | ExecutionException ex) { + LOGGER.log(Level.SEVERE, "Failed to get metaData for node " + node.getName(), ex); } - sb.append("
\n"); //NON-NLS + + setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); } - } catch (TskCoreException ex) { - sb.append(NbBundle.getMessage(this.getClass(), "Metadata.nodeText.exceptionNotice.text")).append(ex.getLocalizedMessage()); - } + }; + + worker.execute(); } else { - try { - addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.name"), image.getUniquePath()); - } catch (TskCoreException ex) { - addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.name"), image.getName()); - } - addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.imageType"), image.getType().getName()); - addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.size"), Long.toString(image.getSize())); - - try { - String md5 = image.getMd5(); - if (md5 == null || md5.isEmpty()) { - md5 = NbBundle.getMessage(this.getClass(), "Metadata.tableRowContent.md5notCalc"); - } - addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.md5"), md5); - - String sha1 = image.getSha1(); - if (sha1 == null || sha1.isEmpty()) { - sha1 = NbBundle.getMessage(this.getClass(), "Metadata.tableRowContent.md5notCalc"); - } - addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.sha1"), sha1); - - String sha256 = image.getSha256(); - if (sha256 == null || sha256.isEmpty()) { - sha256 = NbBundle.getMessage(this.getClass(), "Metadata.tableRowContent.md5notCalc"); - } - addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.sha256"), sha256); - } catch (TskCoreException ex) { - sb.append(NbBundle.getMessage(this.getClass(), "Metadata.nodeText.exceptionNotice.text")).append(ex.getLocalizedMessage()); - } - addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.sectorSize"), Long.toString(image.getSsize())); - addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.timezone"), image.getTimeZone()); - addAcquisitionDetails(sb, dataSource); - addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.deviceId"), image.getDeviceId()); - addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.internalid"), Long.toString(image.getId())); - - // Add all the data source paths to the "Local Path" value cell. - String[] imagePaths = image.getPaths(); - if (imagePaths.length > 0) { - StringBuilder pathValues = new StringBuilder("
"); - pathValues.append(imagePaths[0]); - pathValues.append("
"); - for (int i=1; i < imagePaths.length; i++) { - pathValues.append("
"); - pathValues.append(imagePaths[i]); - pathValues.append("
"); - } - addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.localPath"), pathValues.toString()); - } else { - addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.localPath"), - NbBundle.getMessage(this.getClass(), "Metadata.nodeText.none")); - } + setText(""); } - - setText(sb.toString()); - jTextPane1.setCaretPosition(0); - this.setCursor(null); } - + /** - * Adds a row for download source from the given associated artifact, - * if the associated artifacts specifies a source. - * - * @param sb string builder. + * Adds a row for download source from the given associated artifact, if the + * associated artifacts specifies a source. + * + * @param sb string builder. * @param associatedArtifact - * + * * @throws TskCoreException if there is an error */ - private void addDownloadSourceRow(StringBuilder sb, BlackboardArtifact associatedArtifact ) throws TskCoreException { - if (associatedArtifact != null && - ((associatedArtifact.getArtifactTypeID() == ARTIFACT_TYPE.TSK_WEB_DOWNLOAD.getTypeID()) || - (associatedArtifact.getArtifactTypeID() == ARTIFACT_TYPE.TSK_WEB_CACHE.getTypeID())) ) { + private void addDownloadSourceRow(StringBuilder sb, BlackboardArtifact associatedArtifact) throws TskCoreException { + if (associatedArtifact != null + && ((associatedArtifact.getArtifactTypeID() == ARTIFACT_TYPE.TSK_WEB_DOWNLOAD.getTypeID()) + || (associatedArtifact.getArtifactTypeID() == ARTIFACT_TYPE.TSK_WEB_CACHE.getTypeID()))) { BlackboardAttribute urlAttr = associatedArtifact.getAttribute(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_URL)); if (urlAttr != null) { addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.downloadSource"), urlAttr.getValueString()); } } } - + /** * Add the acquisition details to the results (if applicable) - * + * * @param sb The output StringBuilder object * @param dataSource The data source (may be null) */ @@ -376,4 +263,169 @@ public class Metadata extends javax.swing.JPanel implements DataContentViewer { public int isPreferred(Node node) { return 1; } + + /** + * SwingWorker for gathering the file metadata. + */ + private class MetaDataWorker extends SwingWorker { + + private final Node node; + + MetaDataWorker(Node node) { + this.node = node; + } + + @Override + protected String doInBackground() throws Exception { + AbstractFile file = node.getLookup().lookup(AbstractFile.class); + Image image = node.getLookup().lookup(Image.class); + DataSource dataSource = node.getLookup().lookup(DataSource.class); + if (file == null && image == null) { + return NbBundle.getMessage(this.getClass(), "Metadata.nodeText.nonFilePassedIn"); + } + + StringBuilder sb = new StringBuilder(); + startTable(sb); + + if (file != null) { + try { + addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.name"), file.getUniquePath()); + } catch (TskCoreException ex) { + addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.name"), file.getParentPath() + "/" + file.getName()); + } + + addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.type"), file.getType().getName()); + addRow(sb, Bundle.Metadata_tableRowTitle_mimeType(), file.getMIMEType()); + addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.size"), Long.toString(file.getSize())); + addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.fileNameAlloc"), file.getDirFlagAsString()); + addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.metadataAlloc"), file.getMetaFlagsAsString()); + addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.modified"), ContentUtils.getStringTime(file.getMtime(), file)); + addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.accessed"), ContentUtils.getStringTime(file.getAtime(), file)); + addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.created"), ContentUtils.getStringTime(file.getCrtime(), file)); + addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.changed"), ContentUtils.getStringTime(file.getCtime(), file)); + + String md5 = file.getMd5Hash(); + if (md5 == null) { + md5 = NbBundle.getMessage(this.getClass(), "Metadata.tableRowContent.md5notCalc"); + } + addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.md5"), md5); + String sha256 = file.getSha256Hash(); + if (sha256 == null) { + sha256 = NbBundle.getMessage(this.getClass(), "Metadata.tableRowContent.md5notCalc"); + } + addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.sha256"), sha256); + addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.hashLookupResults"), file.getKnown().toString()); + addAcquisitionDetails(sb, dataSource); + + addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.internalid"), Long.toString(file.getId())); + if (file.getType().compareTo(TSK_DB_FILES_TYPE_ENUM.LOCAL) == 0) { + addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.localPath"), file.getLocalAbsPath()); + } + + try { + List associatedObjectArtifacts = file.getArtifacts(ARTIFACT_TYPE.TSK_ASSOCIATED_OBJECT); + if (!associatedObjectArtifacts.isEmpty()) { + BlackboardArtifact artifact = associatedObjectArtifacts.get(0); + BlackboardAttribute associatedArtifactAttribute = artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT)); + if (associatedArtifactAttribute != null) { + long artifactId = associatedArtifactAttribute.getValueLong(); + BlackboardArtifact associatedArtifact = artifact.getSleuthkitCase().getBlackboardArtifact(artifactId); + addDownloadSourceRow(sb, associatedArtifact); + } + } + } catch (TskCoreException ex) { + sb.append(NbBundle.getMessage(this.getClass(), "Metadata.nodeText.exceptionNotice.text")).append(ex.getLocalizedMessage()); + } + + endTable(sb); + + /* + * If we have a file system file, grab the more detailed + * metadata text too + */ + try { + if (file instanceof FsContent) { + FsContent fsFile = (FsContent) file; + + sb.append("
\n"); //NON-NLS
+                        sb.append(NbBundle.getMessage(this.getClass(), "Metadata.nodeText.text"));
+                        sb.append(" 

"); // NON-NLS + for (String str : fsFile.getMetaDataText()) { + sb.append(str).append("
"); //NON-NLS + + /* + * Very long results can cause the UI to hang before + * displaying, so truncate the results if necessary. + */ + if (sb.length() > 50000) { + sb.append(NbBundle.getMessage(this.getClass(), "Metadata.nodeText.truncated")); + break; + } + } + sb.append("
\n"); //NON-NLS + } + } catch (TskCoreException ex) { + sb.append(NbBundle.getMessage(this.getClass(), "Metadata.nodeText.exceptionNotice.text")).append(ex.getLocalizedMessage()); + } + } else { + try { + addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.name"), image.getUniquePath()); + } catch (TskCoreException ex) { + addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.name"), image.getName()); + } + addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.imageType"), image.getType().getName()); + addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.size"), Long.toString(image.getSize())); + + try { + String md5 = image.getMd5(); + if (md5 == null || md5.isEmpty()) { + md5 = NbBundle.getMessage(this.getClass(), "Metadata.tableRowContent.md5notCalc"); + } + addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.md5"), md5); + + String sha1 = image.getSha1(); + if (sha1 == null || sha1.isEmpty()) { + sha1 = NbBundle.getMessage(this.getClass(), "Metadata.tableRowContent.md5notCalc"); + } + addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.sha1"), sha1); + + String sha256 = image.getSha256(); + if (sha256 == null || sha256.isEmpty()) { + sha256 = NbBundle.getMessage(this.getClass(), "Metadata.tableRowContent.md5notCalc"); + } + addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.sha256"), sha256); + } catch (TskCoreException ex) { + sb.append(NbBundle.getMessage(this.getClass(), "Metadata.nodeText.exceptionNotice.text")).append(ex.getLocalizedMessage()); + } + addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.sectorSize"), Long.toString(image.getSsize())); + addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.timezone"), image.getTimeZone()); + addAcquisitionDetails(sb, dataSource); + addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.deviceId"), image.getDeviceId()); + addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.internalid"), Long.toString(image.getId())); + + // Add all the data source paths to the "Local Path" value cell. + String[] imagePaths = image.getPaths(); + if (imagePaths.length > 0) { + StringBuilder pathValues = new StringBuilder("
"); + pathValues.append(imagePaths[0]); + pathValues.append("
"); + for (int i = 1; i < imagePaths.length; i++) { + pathValues.append("
"); + pathValues.append(imagePaths[i]); + pathValues.append("
"); + } + addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.localPath"), pathValues.toString()); + } else { + addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.localPath"), + NbBundle.getMessage(this.getClass(), "Metadata.nodeText.none")); + } + } + + if (isCancelled()) { + return ""; + } + + return sb.toString(); + } + } } diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/application/Annotations.java b/Core/src/org/sleuthkit/autopsy/contentviewers/application/Annotations.java new file mode 100755 index 0000000000..feec2703dc --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/application/Annotations.java @@ -0,0 +1,673 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2021 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.contentviewers.application; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.stream.Collectors; +import javax.swing.JLabel; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.openide.nodes.Node; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +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.CorrelationAttributeUtil; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardArtifactTag; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.ContentTag; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.Tag; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * The business logic for the Annotations content panel. + */ +public class Annotations { + + @NbBundle.Messages({ + "Annotations.title=Annotations", + "Annotations.toolTip=Displays tags and comments associated with the selected content.", + "Annotations.centralRepositoryEntry.title=Central Repository Comments", + "Annotations.centralRepositoryEntryDataLabel.case=Case:", + "Annotations.centralRepositoryEntryDataLabel.type=Type:", + "Annotations.centralRepositoryEntryDataLabel.comment=Comment:", + "Annotations.centralRepositoryEntryDataLabel.path=Path:", + "Annotations.tagEntry.title=Tags", + "Annotations.tagEntryDataLabel.tag=Tag:", + "Annotations.tagEntryDataLabel.tagUser=Examiner:", + "Annotations.tagEntryDataLabel.comment=Comment:", + "Annotations.fileHitEntry.artifactCommentTitle=Artifact Comment", + "Annotations.fileHitEntry.hashSetHitTitle=Hash Set Hit Comments", + "Annotations.fileHitEntry.interestingFileHitTitle=Interesting File Hit Comments", + "Annotations.fileHitEntry.setName=Set Name:", + "Annotations.fileHitEntry.comment=Comment:", + "Annotations.sourceFile.title=Source File", + "Annotations.onEmpty=No annotations were found for this particular item." + }) + + private static final Logger logger = Logger.getLogger(Annotations.class.getName()); + + private static final String EMPTY_HTML = ""; + + private static final int DEFAULT_FONT_SIZE = new JLabel().getFont().getSize(); + // spacing occurring after an item + private static final int DEFAULT_TABLE_SPACING = DEFAULT_FONT_SIZE; + + // html stylesheet classnames for components + public static final String MESSAGE_CLASSNAME = "message"; + public static final String SUBSECTION_CLASSNAME = "subsection"; + public static final String SUBHEADER_CLASSNAME = "subheader"; + public static final String SECTION_CLASSNAME = "section"; + public static final String HEADER_CLASSNAME = "header"; + public static final String VERTICAL_TABLE_CLASSNAME = "vertical-table"; + + // describing table values for a tag + private static final List> TAG_ENTRIES = Arrays.asList( + new ItemEntry<>(Bundle.Annotations_tagEntryDataLabel_tag(), + (tag) -> (tag.getName() != null) ? tag.getName().getDisplayName() : null), + new ItemEntry<>(Bundle.Annotations_tagEntryDataLabel_tagUser(), (tag) -> tag.getUserName()), + new ItemEntry<>(Bundle.Annotations_tagEntryDataLabel_comment(), (tag) -> tag.getComment()) + ); + + private static final SectionConfig TAG_CONFIG + = new SectionConfig<>(Bundle.Annotations_tagEntry_title(), TAG_ENTRIES); + + // file set attributes and table configurations + private static final List> FILESET_HIT_ENTRIES = Arrays.asList( + new ItemEntry<>(Bundle.Annotations_fileHitEntry_setName(), + (bba) -> tryGetAttribute(bba, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME)), + new ItemEntry<>(Bundle.Annotations_fileHitEntry_comment(), + (bba) -> tryGetAttribute(bba, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_COMMENT)) + ); + + private static final SectionConfig INTERESTING_FILE_CONFIG + = new SectionConfig<>(Bundle.Annotations_fileHitEntry_interestingFileHitTitle(), FILESET_HIT_ENTRIES); + + private static final SectionConfig HASHSET_CONFIG + = new SectionConfig<>(Bundle.Annotations_fileHitEntry_hashSetHitTitle(), FILESET_HIT_ENTRIES); + + private static final SectionConfig ARTIFACT_COMMENT_CONFIG + = new SectionConfig<>(Bundle.Annotations_fileHitEntry_artifactCommentTitle(), FILESET_HIT_ENTRIES); + + // central repository attributes and table configuration + private static final List> CR_COMMENTS_ENTRIES = Arrays.asList( + new ItemEntry<>(Bundle.Annotations_centralRepositoryEntryDataLabel_case(), + cai -> (cai.getCorrelationCase() != null) ? cai.getCorrelationCase().getDisplayName() : null), + new ItemEntry<>(Bundle.Annotations_centralRepositoryEntryDataLabel_comment(), cai -> cai.getComment()), + new ItemEntry<>(Bundle.Annotations_centralRepositoryEntryDataLabel_path(), cai -> cai.getFilePath()) + ); + + private static final SectionConfig CR_COMMENTS_CONFIG + = new SectionConfig<>(Bundle.Annotations_centralRepositoryEntry_title(), CR_COMMENTS_ENTRIES); + + /* + * Private constructor for this utility class. + */ + private Annotations() { + + } + + /** + * Returns the formatted Annotation information for the given node. If no + * data was found the method will return null; + * + * @param node Node to get data for. + * + * @return A formatted document of annotation information for the given node + * or null. + */ + public static Document buildDocument(Node node) { + Document html = Jsoup.parse(EMPTY_HTML); + Element body = html.getElementsByTag("body").first(); + + BlackboardArtifact artifact = node.getLookup().lookup(BlackboardArtifact.class); + Content sourceFile = null; + + try { + if (artifact != null) { + /* + * Get the source content based on the artifact to ensure we + * display the correct data instead of whatever was in the node. + */ + sourceFile = artifact.getSleuthkitCase().getAbstractFileById(artifact.getObjectID()); + } else { + /* + * No artifact is present, so get the content based on what's + * present in the node. In this case, the selected item IS the + * source file. + */ + sourceFile = node.getLookup().lookup(AbstractFile.class); + } + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, String.format( + "Exception while trying to retrieve a Content instance from the BlackboardArtifact '%s' (id=%d).", + artifact.getDisplayName(), artifact.getArtifactID()), ex); + } + + boolean somethingWasRendered = false; + if (artifact != null) { + somethingWasRendered = renderArtifact(body, artifact, sourceFile); + } else { + somethingWasRendered = renderContent(body, sourceFile, false); + } + + if (!somethingWasRendered) { + return null; + } + + return html; + } + + /** + * Renders annotations for an artifact. + * + * @param parent The html element to render content int. + * @param bba The blackboard artifact to render. + * @param sourceContent The content from which the blackboard artifact + * comes. + * + * @return If any content was actually rendered. + */ + private static boolean renderArtifact(Element parent, BlackboardArtifact bba, Content sourceContent) { + boolean contentRendered = appendEntries(parent, TAG_CONFIG, getTags(bba), false); + + if (CentralRepository.isEnabled()) { + List centralRepoComments = getCentralRepositoryData(bba); + boolean crRendered = appendEntries(parent, CR_COMMENTS_CONFIG, centralRepoComments, false); + contentRendered = contentRendered || crRendered; + } + + // if artifact is a hashset hit or interesting file and has a non-blank comment + if ((BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT.getTypeID() == bba.getArtifactTypeID() + || BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT.getTypeID() == bba.getArtifactTypeID()) + && (hasTskComment(bba))) { + + boolean filesetRendered = appendEntries(parent, ARTIFACT_COMMENT_CONFIG, Arrays.asList(bba), false); + contentRendered = contentRendered || filesetRendered; + } + + Element sourceFileSection = appendSection(parent, Bundle.Annotations_sourceFile_title()); + boolean sourceFileRendered = renderContent(sourceFileSection, sourceContent, true); + + if (!sourceFileRendered) { + sourceFileSection.remove(); + } + + return contentRendered || sourceFileRendered; + } + + /** + * Renders annotations for a content item. + * + * @param parent The parent within which to render. + * @param sourceContent The content for which annotations will be gathered. + * @param isSubheader True if this section should be rendered as a + * subheader as opposed to a top-level header. + * + * @return If any content was actually rendered. + */ + private static boolean renderContent(Element parent, Content sourceContent, boolean isSubheader) { + boolean contentRendered = appendEntries(parent, TAG_CONFIG, getTags(sourceContent), isSubheader); + + if (sourceContent instanceof AbstractFile) { + AbstractFile sourceFile = (AbstractFile) sourceContent; + + if (CentralRepository.isEnabled()) { + List centralRepoComments = getCentralRepositoryData(sourceFile); + boolean crRendered = appendEntries(parent, CR_COMMENTS_CONFIG, centralRepoComments, isSubheader); + contentRendered = contentRendered || crRendered; + } + + boolean hashsetRendered = appendEntries(parent, HASHSET_CONFIG, + getFileSetHits(sourceFile, BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT), + isSubheader); + + boolean interestingFileRendered = appendEntries(parent, INTERESTING_FILE_CONFIG, + getFileSetHits(sourceFile, BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT), + isSubheader); + + contentRendered = contentRendered || hashsetRendered || interestingFileRendered; + } + return contentRendered; + } + + /** + * Retrieves tags associated with a content item. + * + * @param sourceContent The content for which to gather content. + * + * @return The Tags associated with this item. + */ + private static List getTags(Content sourceContent) { + try { + SleuthkitCase tskCase = Case.getCurrentCaseThrows().getSleuthkitCase(); + return tskCase.getContentTagsByContent(sourceContent); + } catch (NoCurrentCaseException ex) { + logger.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Exception while getting tags from the case database.", ex); //NON-NLS + } + return new ArrayList<>(); + } + + /** + * Retrieves tags for blackboard artifact tags. + * + * @param bba The blackboard artifact for which to retrieve tags. + * + * @return The found tags. + */ + private static List getTags(BlackboardArtifact bba) { + try { + SleuthkitCase tskCase = Case.getCurrentCaseThrows().getSleuthkitCase(); + return tskCase.getBlackboardArtifactTagsByArtifact(bba); + } catch (NoCurrentCaseException ex) { + logger.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Exception while getting tags from the case database.", ex); //NON-NLS + } + return new ArrayList<>(); + } + + /** + * Retrieves the blackboard artifacts for a source file matching a certain + * type that have a non-blank TSK_COMMENT. + * + * @param sourceFile The source file for which to fetch artifacts. + * @param type The type of blackboard artifact to fetch. + * + * @return The artifacts found matching this type. + */ + private static List getFileSetHits(AbstractFile sourceFile, BlackboardArtifact.ARTIFACT_TYPE type) { + try { + SleuthkitCase tskCase = Case.getCurrentCaseThrows().getSleuthkitCase(); + return tskCase.getBlackboardArtifacts(type, sourceFile.getId()).stream() + .filter((bba) -> hasTskComment(bba)) + .collect(Collectors.toList()); + } catch (NoCurrentCaseException ex) { + logger.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Exception while getting file set hits from the case database.", ex); //NON-NLS + } + return new ArrayList<>(); + } + + /** + * Returns true if the artifact contains a non-blank TSK_COMMENT attribute. + * + * @param artifact The artifact to check. + * + * @return True if it has a non-blank TSK_COMMENT. + */ + private static boolean hasTskComment(BlackboardArtifact artifact) { + return StringUtils.isNotBlank(tryGetAttribute(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_COMMENT)); + } + + /** + * Attempts to retrieve the attribute of a particular type from a blackboard + * artifact. + * + * @param artifact The artifact from which to retrieve the information. + * @param attributeType The attribute type to retrieve from the artifact. + * + * @return The string value of the attribute or null if not found. + */ + private static String tryGetAttribute(BlackboardArtifact artifact, BlackboardAttribute.ATTRIBUTE_TYPE attributeType) { + if (artifact == null) { + return null; + } + + BlackboardAttribute attr = null; + try { + attr = artifact.getAttribute(new BlackboardAttribute.Type(attributeType)); + } catch (TskCoreException ex) { + logger.log(Level.WARNING, String.format("Unable to fetch attribute of type %s for artifact %s", attributeType, artifact), ex); + } + + if (attr == null) { + return null; + } + + return attr.getValueString(); + } + + /** + * Gets the "Central Repository Comments" section with data for the + * blackboard artifact. + * + * @param artifact The selected artifact. + * + * @return The Correlation Attribute Instances associated with the artifact + * that have comments. + */ + private static List getCentralRepositoryData(BlackboardArtifact artifact) { + if (artifact == null) { + return new ArrayList<>(); + } + + List> lookupKeys = CorrelationAttributeUtil.makeCorrAttrsForCorrelation(artifact) + .stream() + .map(cai -> Pair.of(cai.getCorrelationType(), cai.getCorrelationValue())) + .collect(Collectors.toList()); + + return getCorrelationAttributeComments(lookupKeys); + } + + /** + * Gets the "Central Repository Comments" section with data. + * + * @param sourceFile A selected file, or a source file of the selected + * artifact. + * + * @return The Correlation Attribute Instances associated with the + * sourcefile that have comments. + */ + private static List getCentralRepositoryData(AbstractFile sourceFile) { + if (sourceFile == null || StringUtils.isEmpty(sourceFile.getMd5Hash())) { + return new ArrayList<>(); + } + + List artifactTypes = null; + try { + artifactTypes = CentralRepository.getInstance().getDefinedCorrelationTypes(); + } catch (CentralRepoException ex) { + logger.log(Level.SEVERE, "Error connecting to the Central Repository database.", ex); // NON-NLS + } + + if (artifactTypes == null || artifactTypes.isEmpty()) { + return new ArrayList<>(); + } + + String md5 = sourceFile.getMd5Hash(); + + // get key lookups for a file attribute types and the md5 hash + List> lookupKeys = artifactTypes.stream() + .filter((attributeType) -> attributeType.getId() == CorrelationAttributeInstance.FILES_TYPE_ID) + .map((attributeType) -> Pair.of(attributeType, md5)) + .collect(Collectors.toList()); + + return getCorrelationAttributeComments(lookupKeys); + } + + /** + * Given a type and a value for that type, does a lookup in the Central + * Repository for matching values that have comments. + * + * @param lookupKeys The type and value to lookup. + * + * @return The found correlation attribute instances. + */ + private static List getCorrelationAttributeComments(List> lookupKeys) { + List instancesToRet = new ArrayList<>(); + + try { + // use lookup instances to find the actual correlation attributes for the items selected + for (Pair typeVal : lookupKeys) { + instancesToRet.addAll(CentralRepository.getInstance() + .getArtifactInstancesByTypeValue(typeVal.getKey(), typeVal.getValue()) + .stream() + // for each one found, if it has a comment, return + .filter((cai) -> StringUtils.isNotBlank(cai.getComment())) + .collect(Collectors.toList())); + } + + } catch (CentralRepoException ex) { + logger.log(Level.SEVERE, "Error connecting to the Central Repository database.", ex); // NON-NLS + } catch (CorrelationAttributeNormalizationException ex) { + logger.log(Level.SEVERE, "Error normalizing instance from Central Repository database.", ex); // NON-NLS + } + + return instancesToRet; + } + + /** + * Append entries to the parent element in the annotations viewer. Entries + * will be formatted as a table in the format specified in the + * SectionConfig. + * + * @param parent The parent element for which the entries will be + * attached. + * @param config The display configuration for this entry type (i.e. + * table type, name, if data is not present). + * @param items The items to display. + * @param isSubsection Whether or not this should be displayed as a + * subsection. If not displayed as a top-level section. + * + * @return If there was actual content rendered for this set of entries. + */ + private static boolean appendEntries(Element parent, Annotations.SectionConfig config, List items, + boolean isSubsection) { + if (items == null || items.isEmpty()) { + return false; + } + + Element sectionDiv = (isSubsection) ? appendSubsection(parent, config.getTitle()) : appendSection(parent, config.getTitle()); + appendVerticalEntryTables(sectionDiv, items, config.getAttributes()); + return true; + } + + /** + * Appends a table where items are displayed in rows of key-value pairs. + * + * @param parent The parent to append the table. + * @param items The items to process into a series of tables. + * @param rowHeaders The keys and the means to process items in order to get + * key-value pairs. + * + * @return The parent element provided as parameter. + */ + private static Element appendVerticalEntryTables(Element parent, List items, List> rowHeaders) { + boolean isFirst = true; + for (T item : items) { + if (item == null) { + continue; + } + + List> tableData = rowHeaders.stream() + .map(row -> Arrays.asList(row.getItemName(), row.retrieveValue(item))) + .collect(Collectors.toList()); + + Element childTable = appendTable(parent, 2, tableData, null); + childTable.attr("class", VERTICAL_TABLE_CLASSNAME); + + if (isFirst) { + isFirst = false; + } else { + childTable.attr("style", String.format("margin-top: %dpx;", DEFAULT_TABLE_SPACING)); + } + } + + return parent; + } + + /** + * Appends a generic table to the parent element. + * + * @param parent The parent element that will have a table appended + * to it. + * @param columnNumber The number of columns to append. + * @param content The content in content.get(row).get(column) format. + * @param columnHeaders The column headers or null if no column headers + * should be created. + * + * @return The created table. + */ + private static Element appendTable(Element parent, int columnNumber, List> content, List columnHeaders) { + Element table = parent.appendElement("table"); + if (columnHeaders != null && !columnHeaders.isEmpty()) { + Element header = table.appendElement("thead"); + appendRow(header, columnHeaders, columnNumber, true); + } + Element tableBody = table.appendElement("tbody"); + + content.forEach((rowData) -> appendRow(tableBody, rowData, columnNumber, false)); + return table; + } + + /** + * Appends a row to the parent element (should be thead or tbody). + * + * @param rowParent The parent table element. + * @param data The data to place in columns within the table. + * @param columnNumber The number of columns to append. + * @param isHeader Whether or not this should have header cells ('th') + * instead of regular cells ('td'). + * + * @return The row created. + */ + private static Element appendRow(Element rowParent, List data, int columnNumber, boolean isHeader) { + String cellType = isHeader ? "th" : "td"; + Element row = rowParent.appendElement("tr"); + for (int i = 0; i < columnNumber; i++) { + Element cell = row.appendElement(cellType); + if (data != null && i < data.size()) { + cell.text(StringUtils.isEmpty(data.get(i)) ? "" : data.get(i)); + } + } + return row; + } + + /** + * Appends a new section with a section header to the parent element. + * + * @param parent The element to append this section to. + * @param headerText The text for the section. + * + * @return The div for the new section. + */ + private static Element appendSection(Element parent, String headerText) { + Element sectionDiv = parent.appendElement("div"); + sectionDiv.attr("class", SECTION_CLASSNAME); + Element header = sectionDiv.appendElement("h1"); + header.text(headerText); + header.attr("class", HEADER_CLASSNAME); + return sectionDiv; + } + + /** + * Appends a new subsection with a subsection header to the parent element. + * + * @param parent The element to append this subsection to. + * @param headerText The text for the subsection. + * + * @return The div for the new subsection. + */ + private static Element appendSubsection(Element parent, String headerText) { + Element subsectionDiv = parent.appendElement("div"); + subsectionDiv.attr("class", SUBSECTION_CLASSNAME); + Element header = subsectionDiv.appendElement("h2"); + header.text(headerText); + header.attr("class", SUBHEADER_CLASSNAME); + return subsectionDiv; + } + + /** + * Appends a message to the parent element. This is typically used in the + * event that no data exists for a certain type. + * + * @param parent The parent element that will have this message appended to + * it. + * @param message The message to append. + * + * @return The paragraph element for the new message. + */ + private static Element appendMessage(Element parent, String message) { + Element messageEl = parent.appendElement("p"); + messageEl.text(message); + messageEl.attr("class", MESSAGE_CLASSNAME); + return messageEl; + } + + /** + * Describes a key value pair for an item of type T where the key is the + * field name to display and the value is retrieved from item of type T + * using a provided Function. + * + * @param The item type. + */ + static class ItemEntry { + + private final String itemName; + private final Function valueRetriever; + + ItemEntry(String itemName, Function valueRetriever) { + this.itemName = itemName; + this.valueRetriever = valueRetriever; + } + + String getItemName() { + return itemName; + } + + Function getValueRetriever() { + return valueRetriever; + } + + String retrieveValue(T object) { + return valueRetriever.apply(object); + } + } + + /** + * Describes a section that will be appended to the annotations view panel. + * + * @param The item type for items to display. + */ + static class SectionConfig { + + private final String title; + private final List> attributes; + + SectionConfig(String title, List> attributes) { + this.title = title; + this.attributes = attributes; + } + + /** + * @return The title for the section. + */ + String getTitle() { + return title; + } + + /** + * @return Describes key-value pairs on the object to display to the + * user. + */ + List> getAttributes() { + return attributes; + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/application/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/contentviewers/application/Bundle.properties-MERGED new file mode 100755 index 0000000000..238576e0c7 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/application/Bundle.properties-MERGED @@ -0,0 +1,18 @@ +Annotations.centralRepositoryEntry.title=Central Repository Comments +Annotations.centralRepositoryEntryDataLabel.case=Case: +Annotations.centralRepositoryEntryDataLabel.comment=Comment: +Annotations.centralRepositoryEntryDataLabel.path=Path: +Annotations.centralRepositoryEntryDataLabel.type=Type: +Annotations.fileHitEntry.artifactCommentTitle=Artifact Comment +Annotations.fileHitEntry.comment=Comment: +Annotations.fileHitEntry.hashSetHitTitle=Hash Set Hit Comments +Annotations.fileHitEntry.interestingFileHitTitle=Interesting File Hit Comments +Annotations.fileHitEntry.setName=Set Name: +Annotations.onEmpty=No annotations were found for this particular item. +Annotations.sourceFile.title=Source File +Annotations.tagEntry.title=Tags +Annotations.tagEntryDataLabel.comment=Comment: +Annotations.tagEntryDataLabel.tag=Tag: +Annotations.tagEntryDataLabel.tagUser=Examiner: +Annotations.title=Annotations +Annotations.toolTip=Displays tags and comments associated with the selected content. diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/textcontentviewer/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/contentviewers/textcontentviewer/Bundle.properties-MERGED index 19b5e9ef6b..024103570d 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/textcontentviewer/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/textcontentviewer/Bundle.properties-MERGED @@ -1,3 +1,4 @@ +StringContentPanel_Loading_String=Loading text... StringsTextViewer.goToPageTextField.msgDlg=Please enter a valid page number between 1 and {0} StringsTextViewer.goToPageTextField.err=Invalid page number StringsTextViewer.setDataView.errorText=(offset {0}-{1} could not be read) diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/textcontentviewer/StringsContentPanel.java b/Core/src/org/sleuthkit/autopsy/contentviewers/textcontentviewer/StringsContentPanel.java index 95cbee43b7..750f143e0e 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/textcontentviewer/StringsContentPanel.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/textcontentviewer/StringsContentPanel.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2019 Basis Technology Corp. + * Copyright 2011-2021 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,12 +23,15 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.Arrays; import java.util.List; +import java.util.concurrent.ExecutionException; import java.util.logging.Level; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; import javax.swing.JMenuItem; import javax.swing.JOptionPane; +import javax.swing.SwingWorker; import org.apache.commons.lang3.StringUtils; +import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.coreutils.StringExtract; import org.sleuthkit.autopsy.coreutils.StringExtract.StringExtractResult; import org.sleuthkit.autopsy.coreutils.StringExtract.StringExtractUnicodeTable.SCRIPT; @@ -49,10 +52,10 @@ public class StringsContentPanel extends javax.swing.JPanel { private final byte[] data = new byte[(int) PAGE_LENGTH]; private static int currentPage = 1; private Content dataSource; - //string extract utility - private final StringExtract stringExtract = new StringExtract(); private static final Logger logger = Logger.getLogger(StringsContentPanel.class.getName()); + private SwingWorker worker; + /** * Creates new form StringsTextViewer */ @@ -81,10 +84,10 @@ public class StringsContentPanel extends javax.swing.JPanel { }); // use wrap layout for better component wrapping - WrapLayout layout = new WrapLayout(0,5); + WrapLayout layout = new WrapLayout(0, 5); layout.setOppositeAligned(Arrays.asList(panelScriptSelect)); controlPanel.setLayout(layout); - + } final void resetDisplay() { @@ -363,6 +366,10 @@ public class StringsContentPanel extends javax.swing.JPanel { private javax.swing.JLabel totalPageLabel; // End of variables declaration//GEN-END:variables + @Messages({ + "StringContentPanel_Loading_String=Loading text..." + }) + /** * Sets the DataView (The tabbed panel) * @@ -370,83 +377,36 @@ public class StringsContentPanel extends javax.swing.JPanel { * @param offset the starting offset */ void setDataView(Content dataSource, long offset) { + + if (worker != null) { + worker.cancel(true); + worker = null; + } + if (dataSource == null) { return; } - // change the cursor to "waiting cursor" for this operation - this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); - this.dataSource = dataSource; - int bytesRead = 0; - // set the data on the bottom and show it - if (dataSource.getSize() > 0) { - try { - bytesRead = dataSource.read(data, offset, PAGE_LENGTH); // read the data - } catch (TskCoreException ex) { - logger.log(Level.WARNING, "Error while trying to show the String content.", ex); //NON-NLS - } - } - String text; - if (bytesRead > 0) { - //text = DataConversion.getString(data, bytesRead, 4); - final SCRIPT selScript = (SCRIPT) languageCombo.getSelectedItem(); - stringExtract.setEnabledScript(selScript); - StringExtractResult res = stringExtract.extract(data, bytesRead, 0); - text = res.getText(); - if (StringUtils.isBlank(text)) { - text = NbBundle.getMessage(this.getClass(), - "StringsTextViewer.setDataView.errorNoText", currentOffset, - currentOffset + PAGE_LENGTH); - } - } else { - text = NbBundle.getMessage(this.getClass(), "StringsTextViewer.setDataView.errorText", currentOffset, - currentOffset + PAGE_LENGTH); - } - - // disable or enable the next button - if (offset + PAGE_LENGTH < dataSource.getSize()) { - nextPageButton.setEnabled(true); - } else { - nextPageButton.setEnabled(false); - } - - if (offset == 0) { - prevPageButton.setEnabled(false); - currentPage = 1; // reset the page number - } else { - prevPageButton.setEnabled(true); - } - - int totalPage = Math.round((dataSource.getSize() - 1) / PAGE_LENGTH) + 1; - totalPageLabel.setText(Integer.toString(totalPage)); - currentPageLabel.setText(Integer.toString(currentPage)); - outputViewPane.setText(text); // set the output view - setComponentsVisibility(true); // shows the components that not needed - outputViewPane.moveCaretPosition(0); - - this.setCursor(null); + worker = new ContentWorker(dataSource, offset); + outputViewPane.setText(Bundle.StringContentPanel_Loading_String()); + setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + worker.execute(); } void setDataView(StringContent dataSource) { - this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); - try { - this.dataSource = null; - - // set the data on the bottom and show it - String text = dataSource.getString(); - nextPageButton.setEnabled(false); - prevPageButton.setEnabled(false); - currentPage = 1; - - int totalPage = 1; - totalPageLabel.setText(Integer.toString(totalPage)); - currentPageLabel.setText(Integer.toString(currentPage)); - outputViewPane.setText(text); // set the output view - setComponentsVisibility(true); // shows the components that not needed - outputViewPane.moveCaretPosition(0); - } finally { - this.setCursor(null); + if (worker != null) { + worker.cancel(true); + worker = null; } + + if (dataSource == null) { + return; + } + + worker = new StringContentWorker(dataSource); + outputViewPane.setText(Bundle.StringContentPanel_Loading_String()); + setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + worker.execute(); } /** @@ -468,4 +428,145 @@ public class StringsContentPanel extends javax.swing.JPanel { languageLabel.setVisible(isVisible); } + /** + * Swingworker for getting the text from a content object. + */ + private final class ContentWorker extends SwingWorker { + + private final Content content; + private final long offset; + + /** + * ContentWorker constructor + * + * @param content Content to get text from. + * @param offset The starting offset. + */ + ContentWorker(Content content, long offset) { + this.content = content; + this.offset = offset; + } + + @Override + protected String doInBackground() throws Exception { + int bytesRead = 0; + // set the data on the bottom and show it + + if (content.getSize() > 0) { + try { + bytesRead = content.read(data, offset, PAGE_LENGTH); // read the data + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "Error while trying to show the String content.", ex); //NON-NLS + } + } + String text; + if (bytesRead > 0) { + //text = DataConversion.getString(data, bytesRead, 4); + final SCRIPT selScript = (SCRIPT) languageCombo.getSelectedItem(); + StringExtract stringExtract = new StringExtract(); + stringExtract.setEnabledScript(selScript); + StringExtractResult res = stringExtract.extract(data, bytesRead, 0); + text = res.getText(); + if (StringUtils.isBlank(text)) { + text = NbBundle.getMessage(this.getClass(), + "StringsTextViewer.setDataView.errorNoText", currentOffset, + currentOffset + PAGE_LENGTH); + } + } else { + text = NbBundle.getMessage(this.getClass(), "StringsTextViewer.setDataView.errorText", currentOffset, + currentOffset + PAGE_LENGTH); + } + + return text; + } + + @Override + public void done() { + if (isCancelled()) { + return; + } + + try { + if (isCancelled()) { + return; + } + String text = get(); + dataSource = content; + + // disable or enable the next button + if (offset + PAGE_LENGTH < dataSource.getSize()) { + nextPageButton.setEnabled(true); + } else { + nextPageButton.setEnabled(false); + } + + if (offset == 0) { + prevPageButton.setEnabled(false); + currentPage = 1; // reset the page number + } else { + prevPageButton.setEnabled(true); + } + + int totalPage = Math.round((dataSource.getSize() - 1) / PAGE_LENGTH) + 1; + totalPageLabel.setText(Integer.toString(totalPage)); + currentPageLabel.setText("1"); + outputViewPane.setText(text); // set the output view + setComponentsVisibility(true); // shows the components that not needed + outputViewPane.moveCaretPosition(0); + + setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + + } catch (InterruptedException | ExecutionException ex) { + logger.log(Level.SEVERE, String.format("Failed to get text from content (id=%d)", content.getId()), ex); + } + } + } + + /** + * SwingWorker for getting the text from a StringContent object. + */ + private final class StringContentWorker extends SwingWorker { + + private final StringContent content; + + /** + * Constructor to pulling the text out of a string content object. + * + * @param content + */ + StringContentWorker(StringContent content) { + this.content = content; + } + + @Override + protected String doInBackground() throws Exception { + return content.getString(); + } + + @Override + public void done() { + if (isCancelled()) { + return; + } + + try { + String text = get(); + + dataSource = null; + nextPageButton.setEnabled(false); + prevPageButton.setEnabled(false); + currentPage = 1; + + totalPageLabel.setText("1"); + currentPageLabel.setText("1"); + outputViewPane.setText(text); // set the output view + setComponentsVisibility(true); // shows the components that not needed + outputViewPane.moveCaretPosition(0); + setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + + } catch (InterruptedException | ExecutionException ex) { + logger.log(Level.SEVERE, String.format("Failed to get text from StringContent"), ex); + } + } + } } diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties-MERGED index dfe756c2fe..b39205f8e1 100755 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/Bundle.properties-MERGED @@ -34,6 +34,7 @@ DataContentViewerArtifact.failedToGetAttributes.message=Failed to get some or al DataContentViewerArtifact.failedToGetSourcePath.message=Failed to get source file path from case database DataContentViewerHex.copyingFile=Copying file to open in HxD... DataContentViewerHex.launchError=Unable to launch HxD Editor. Please specify the HxD install location in Tools -> Options -> External Viewer +DataContentViewerHex_loading_text=Loading hex from file... DataResultViewerTable.commentRender.name=C DataResultViewerTable.commentRender.toolTip=C(omments) indicates whether the item has a comment DataResultViewerTable.commentRenderer.crAndTagComment.toolTip=Comments exist both in Central Repository and on associated tag(s) diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerArtifact.java b/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerArtifact.java index b6a1373236..f27ac62a1a 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerArtifact.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerArtifact.java @@ -33,7 +33,6 @@ import org.openide.util.lookup.ServiceProvider; import org.sleuthkit.autopsy.corecomponentinterfaces.DataContentViewer; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.datamodel.BlackboardArtifact; -import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE; import org.sleuthkit.datamodel.BlackboardAttribute; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.TskCoreException; @@ -64,6 +63,11 @@ public class DataContentViewerArtifact extends javax.swing.JPanel implements Dat private final static String WAIT_TEXT = NbBundle.getMessage(DataContentViewerArtifact.class, "DataContentViewerArtifact.waitText"); private final static String ERROR_TEXT = NbBundle.getMessage(DataContentViewerArtifact.class, "DataContentViewerArtifact.errorText"); + // Value to return in isPreferred if this viewer is less preferred. + private static final int LESS_PREFERRED = 3; + // Value to return in isPreferred if this viewer is more preferred. + private static final int MORE_PREFERRED = 6; + private Node currentNode; // @@@ Remove this when the redundant setNode() calls problem is fixed. private int currentPage = 1; private final Object lock = new Object(); @@ -347,22 +351,38 @@ public class DataContentViewerArtifact extends javax.swing.JPanel implements Dat @Override public int isPreferred(Node node) { + // get the artifact from the lookup BlackboardArtifact artifact = node.getLookup().lookup(BlackboardArtifact.class); - // low priority if node doesn't have an artifact (meaning it was found from normal directory - // browsing, or if the artifact is something that means the user really wants to see the original - // file and not more details about the artifact - if ((artifact == null) - || (artifact.getArtifactTypeID() == ARTIFACT_TYPE.TSK_HASHSET_HIT.getTypeID()) - || (artifact.getArtifactTypeID() == ARTIFACT_TYPE.TSK_KEYWORD_HIT.getTypeID()) - || (artifact.getArtifactTypeID() == ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT.getTypeID()) - || (artifact.getArtifactTypeID() == ARTIFACT_TYPE.TSK_OBJECT_DETECTED.getTypeID()) - || (artifact.getArtifactTypeID() == ARTIFACT_TYPE.TSK_METADATA_EXIF.getTypeID()) - || (artifact.getArtifactTypeID() == ARTIFACT_TYPE.TSK_EXT_MISMATCH_DETECTED.getTypeID()) - || (artifact.getArtifactTypeID() == ARTIFACT_TYPE.TSK_WEB_DOWNLOAD.getTypeID()) - || (artifact.getArtifactTypeID() == ARTIFACT_TYPE.TSK_WEB_CACHE.getTypeID())) { - return 3; - } else { - return 6; + if (artifact == null) { + return LESS_PREFERRED; + } + + // get the type of the artifact + BlackboardArtifact.Type artifactType; + try { + artifactType = artifact.getType(); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, + String.format("There was an error getting the artifact type for artifact with id: %d", artifact.getId()), + ex); + return LESS_PREFERRED; + } + + // if web download or web cache, less preferred since the content is important and not the artifact itself. + if (artifactType.getTypeID() == BlackboardArtifact.Type.TSK_WEB_DOWNLOAD.getTypeID() + || artifactType.getTypeID() == BlackboardArtifact.Type.TSK_WEB_CACHE.getTypeID()) { + + return LESS_PREFERRED; + } + + switch (artifactType.getCategory()) { + // data artifacts should be more preferred + case DATA_ARTIFACT: + return MORE_PREFERRED; + // everything else is less preferred + case ANALYSIS_RESULT: + default: + return LESS_PREFERRED; } } diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerHex.java b/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerHex.java index 10188826b1..ffa5f073ff 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerHex.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerHex.java @@ -26,6 +26,7 @@ import java.awt.event.ActionListener; import java.io.File; import java.io.IOException; import java.nio.file.Paths; +import java.util.concurrent.ExecutionException; import java.util.logging.Level; import org.sleuthkit.autopsy.coreutils.PlatformUtil; import org.openide.util.NbBundle; @@ -37,6 +38,7 @@ import javax.swing.text.BadLocationException; import javax.swing.text.Utilities; import org.netbeans.api.progress.ProgressHandle; import org.openide.nodes.Node; +import org.openide.util.NbBundle.Messages; import org.openide.util.lookup.ServiceProvider; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; @@ -63,6 +65,8 @@ public class DataContentViewerHex extends javax.swing.JPanel implements DataCont private int totalPages; private Content dataSource; + private HexWorker worker; + private static final Logger logger = Logger.getLogger(DataContentViewerHex.class.getName()); /** @@ -455,16 +459,11 @@ public class DataContentViewerHex extends javax.swing.JPanel implements DataCont * @param page Page to display (1-based counting) */ private void setDataViewByPageNumber(int page) { - if (this.dataSource == null) { - return; - } if (page == 0) { return; } - currentPage = page; - long offset = (currentPage - 1) * PAGE_LENGTH; - setDataView(offset); - goToOffsetTextField.setText(Long.toString(offset)); + + launchWorker(dataSource, (page - 1) * PAGE_LENGTH, page); } /** @@ -473,75 +472,46 @@ public class DataContentViewerHex extends javax.swing.JPanel implements DataCont * @param offset Page to display (1-based counting) */ private void setDataViewByOffset(long offset) { + launchWorker(dataSource, offset, (int) (offset / PAGE_LENGTH) + 1); + } + + @Messages({ + "DataContentViewerHex_loading_text=Loading hex from file..." + }) + + /** + * Launches the worker thread to read the hex from the given source. + * + * @param source + * @param offset + * @param page + */ + private void launchWorker(Content source, long offset, int page) { if (this.dataSource == null) { return; } - currentPage = (int) (offset / PAGE_LENGTH) + 1; - setDataView(offset); - goToPageTextField.setText(Integer.toString(currentPage)); - } - private void setDataView(long offset) { - // change the cursor to "waiting cursor" for this operation - this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); - - String errorText = null; - - int bytesRead = 0; - if (dataSource.getSize() > 0) { - try { - bytesRead = dataSource.read(data, offset, PAGE_LENGTH); // read the data - } catch (TskCoreException ex) { - errorText = NbBundle.getMessage(this.getClass(), "DataContentViewerHex.setDataView.errorText", offset, - offset + PAGE_LENGTH); - logger.log(Level.WARNING, "Error while trying to show the hex content.", ex); //NON-NLS - } - } - - // set the data on the bottom and show it - if (bytesRead <= 0) { - errorText = NbBundle.getMessage(this.getClass(), "DataContentViewerHex.setDataView.errorText", offset, - offset + PAGE_LENGTH); - } - - // disable or enable the next button - if ((errorText == null) && (currentPage < totalPages)) { - nextPageButton.setEnabled(true); - } else { - nextPageButton.setEnabled(false); - } - - if ((errorText == null) && (currentPage > 1)) { - prevPageButton.setEnabled(true); - } else { - prevPageButton.setEnabled(false); - } - - currentPageLabel.setText(Integer.toString(currentPage)); - setComponentsVisibility(true); // shows the components that not needed - - // set the output view - if (errorText == null) { - int showLength = bytesRead < PAGE_LENGTH ? bytesRead : (int) PAGE_LENGTH; - outputTextArea.setText(DataConversion.byteArrayToHex(data, showLength, offset)); - } else { - outputTextArea.setText(errorText); - } - - outputTextArea.setCaretPosition(0); - this.setCursor(null); + worker = new HexWorker(source, offset, page); + setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + outputTextArea.setText(Bundle.DataContentViewerHex_loading_text()); + worker.execute(); } @Override public void setNode(Node selectedNode) { - if ((selectedNode == null) || (!isSupported(selectedNode))) { - resetComponent(); + if (worker != null) { + worker.cancel(true); + worker = null; + } + + resetComponent(); + + if ((selectedNode == null)) { return; } Content content = DataContentViewerUtility.getDefaultContent(selectedNode); if (content == null) { - resetComponent(); return; } @@ -607,7 +577,7 @@ public class DataContentViewerHex extends javax.swing.JPanel implements DataCont return false; } Content content = DataContentViewerUtility.getDefaultContent(node); - return content != null && !(content instanceof BlackboardArtifact) && content.getSize() > 0; + return content != null && !(content instanceof BlackboardArtifact) && content.getSize() > 0; } @Override @@ -619,4 +589,84 @@ public class DataContentViewerHex extends javax.swing.JPanel implements DataCont public Component getComponent() { return this; } + + /** + * SwingWorker to fetch hex from the given data source. + */ + private class HexWorker extends SwingWorker { + + private final byte[] data = new byte[(int) PAGE_LENGTH]; + private final long offset; + private final Content content; + private final int newCurrentPage; + private String errorText = ""; + + HexWorker(Content content, long offset, int newCurrentPage) { + this.content = content; + this.offset = offset; + this.newCurrentPage = newCurrentPage; + } + + @Override + protected String doInBackground() throws Exception { + int bytesRead = 0; + if (content.getSize() > 0) { + try { + bytesRead = content.read(data, offset, PAGE_LENGTH); // read the data + } catch (TskCoreException ex) { + errorText = NbBundle.getMessage(this.getClass(), "DataContentViewerHex.setDataView.errorText", offset, + offset + PAGE_LENGTH); + logger.log(Level.WARNING, "Error while trying to show the hex content.", ex); //NON-NLS + } + } + + // set the data on the bottom and show it + if (bytesRead <= 0) { + errorText = NbBundle.getMessage(this.getClass(), "DataContentViewerHex.setDataView.errorText", offset, + offset + PAGE_LENGTH); + } + + if (errorText.isEmpty()) { + int showLength = bytesRead < PAGE_LENGTH ? bytesRead : (int) PAGE_LENGTH; + return DataConversion.byteArrayToHex(data, showLength, offset); + } else { + return errorText; + } + } + + @Override + public void done() { + if (isCancelled()) { + return; + } + + try { + String text = get(); + outputTextArea.setText(text); + + // disable or enable the next button + if ((errorText.isEmpty()) && (newCurrentPage < totalPages)) { + nextPageButton.setEnabled(true); + } else { + nextPageButton.setEnabled(false); + } + + if ((errorText.isEmpty()) && (newCurrentPage > 1)) { + prevPageButton.setEnabled(true); + } else { + prevPageButton.setEnabled(false); + } + + currentPageLabel.setText(Integer.toString(newCurrentPage)); + setComponentsVisibility(true); // shows the components that not needed + outputTextArea.setCaretPosition(0); + goToPageTextField.setText(Integer.toString(newCurrentPage)); + currentPage = newCurrentPage; + setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + + } catch (InterruptedException | ExecutionException ex) { + logger.log(Level.SEVERE, String.format("Failed to get hex data from content (%d)", content.getId()), ex); + } + } + } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/AutopsyTreeChildFactory.java b/Core/src/org/sleuthkit/autopsy/datamodel/AutopsyTreeChildFactory.java index 845591e261..aa481b021e 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/AutopsyTreeChildFactory.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/AutopsyTreeChildFactory.java @@ -22,6 +22,7 @@ import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Objects; @@ -96,6 +97,7 @@ public final class AutopsyTreeChildFactory extends ChildFactory.Detachable list) { + List nodes = Collections.emptyList(); try { SleuthkitCase tskCase = Case.getCurrentCaseThrows().getSleuthkitCase(); if (Objects.equals(CasePreferences.getGroupItemsInTreeByDataSource(), true)) { @@ -103,27 +105,28 @@ public final class AutopsyTreeChildFactory extends ChildFactory.Detachable persons = personManager.getPersons(); // show persons level if there are persons to be shown if (!CollectionUtils.isEmpty(persons)) { - persons.stream() + nodes = persons.stream() .map(PersonGrouping::new) .sorted() - .forEach(list::add); + .collect(Collectors.toList()); if (CollectionUtils.isNotEmpty(personManager.getHostsForPerson(null))) { - list.add(new PersonGrouping(null)); + nodes.add(new PersonGrouping(null)); } } else { // otherwise, just show host level - tskCase.getHostManager().getAllHosts().stream() + nodes = tskCase.getHostManager().getAllHosts().stream() .map(HostGrouping::new) .sorted() - .forEach(list::add); + .collect(Collectors.toList()); } - list.add(new Reports()); - return true; + + // either way, add in reports node + nodes.add(new Reports()); } else { // data source by type view - List keys = new ArrayList<>(Arrays.asList( + nodes = Arrays.asList( new DataSourcesByType(), new Views(Case.getCurrentCaseThrows().getSleuthkitCase()), new DataArtifacts(), @@ -131,15 +134,16 @@ public final class AutopsyTreeChildFactory extends ChildFactory.Detachable ds != null) .map(DataSourceGrouping::new) .sorted((a, b) -> getNameOrEmpty(a).compareToIgnoreCase(getNameOrEmpty(b))) - .forEach(toPopulate::add); + .collect(Collectors.toList())); } return true; diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/PersonGroupingNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/PersonGroupingNode.java index 4f12779ee8..27c5b9f555 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/PersonGroupingNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/PersonGroupingNode.java @@ -126,10 +126,10 @@ public class PersonGroupingNode extends DisplayableItemNode { logger.log(Level.WARNING, String.format("Unable to get data sources for host: %s", personName), ex); } - hosts.stream() + toPopulate.addAll(hosts.stream() .map(HostGrouping::new) .sorted() - .forEach(toPopulate::add); + .collect(Collectors.toList())); return true; } diff --git a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.form b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.form index 5655a0ddd9..84c8a8ff33 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.form +++ b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.form @@ -675,7 +675,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.java b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.java index 7b518da9c4..525a072137 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetDefsPanel.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2020 Basis Technology Corp. + * Copyright 2011-2021 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -126,12 +126,8 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp this.exportSetButton.setVisible(false); this.mimeTypeComboBox.setVisible(false); this.mimeTypeLabel.setVisible(false); - this.fileSizeUnitComboBox.setVisible(false); - this.fileSizeSpinner.setVisible(false); this.filterDialogTitle = "FilesSetPanel.filter.title"; this.ruleDialogTitle = "FilesSetPanel.rule.title"; - this.fileSizeLabel.setVisible(false); - this.equalitySignComboBox.setVisible(false); this.ignoreKnownFilesCheckbox.setVisible(false); this.fileTypeLabel.setVisible(false); this.filesRadioButton.setVisible(false); @@ -192,7 +188,7 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp } this.fileSizeUnitComboBox.setSelectedIndex(1); - this.equalitySignComboBox.setSelectedIndex(2); + this.equalitySignComboBox.setSelectedIndex(0); } @Override @@ -217,7 +213,7 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp boolean ruleSelected = (FilesSetDefsPanel.this.rulesList.getSelectedValue() != null); - newRuleButton.setEnabled(canBeEnabled && !isStandardSet); + newRuleButton.setEnabled(canBeEnabled && setSelected && !isStandardSet); copySetButton.setEnabled(canBeEnabled && setSelected); newSetButton.setEnabled(canBeEnabled); editRuleButton.setEnabled(canBeEnabled && ruleSelected && !isStandardSet); @@ -292,7 +288,7 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp this.daysIncludedTextField.setText(""); this.rulePathConditionRegexCheckBox.setSelected(false); this.mimeTypeComboBox.setSelectedIndex(0); - this.equalitySignComboBox.setSelectedIndex(2); + this.equalitySignComboBox.setSelectedIndex(0); this.fileSizeUnitComboBox.setSelectedIndex(1); this.fileSizeSpinner.setValue(0); enableButtons(); @@ -405,7 +401,7 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp FilesSetDefsPanel.this.fileSizeSpinner.setValue(fileSizeCondition.getSizeValue()); } else { FilesSetDefsPanel.this.fileSizeUnitComboBox.setSelectedIndex(1); - FilesSetDefsPanel.this.equalitySignComboBox.setSelectedIndex(2); + FilesSetDefsPanel.this.equalitySignComboBox.setSelectedIndex(0); FilesSetDefsPanel.this.fileSizeSpinner.setValue(0); } if (dateCondition != null) { @@ -810,7 +806,7 @@ public final class FilesSetDefsPanel extends IngestModuleGlobalSettingsPanel imp org.openide.awt.Mnemonics.setLocalizedText(fileSizeLabel, org.openide.util.NbBundle.getMessage(FilesSetDefsPanel.class, "FilesSetDefsPanel.fileSizeLabel.text")); // NOI18N - equalitySignComboBox.setModel(new javax.swing.DefaultComboBoxModel(new String[] { "=", ">", "≥", "<", "≤" })); + equalitySignComboBox.setModel(new javax.swing.DefaultComboBoxModel(new String[] { ">", "<" })); equalitySignComboBox.setEnabled(false); fileSizeSpinner.setEnabled(false); diff --git a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetRulePanel.form b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetRulePanel.form index 1d75acf880..3d5993dd0b 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetRulePanel.form +++ b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetRulePanel.form @@ -282,7 +282,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetRulePanel.java b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetRulePanel.java index b30249bc69..541dccfa63 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetRulePanel.java +++ b/Core/src/org/sleuthkit/autopsy/modules/interestingitems/FilesSetRulePanel.java @@ -79,10 +79,6 @@ final class FilesSetRulePanel extends javax.swing.JPanel { if (panelType == FilesSetDefsPanel.PANEL_TYPE.FILE_INGEST_FILTERS) { //Hide the mimetype settings when this is displaying a FileSet rule instead of a interesting item rule mimeTypeComboBox.setVisible(false); mimeCheck.setVisible(false); - fileSizeComboBox.setVisible(false); - fileSizeCheck.setVisible(false); - equalitySymbolComboBox.setVisible(false); - fileSizeSpinner.setVisible(false); jLabel1.setVisible(false); filesRadioButton.setVisible(false); dirsRadioButton.setVisible(false); @@ -109,10 +105,6 @@ final class FilesSetRulePanel extends javax.swing.JPanel { if (panelType == FilesSetDefsPanel.PANEL_TYPE.FILE_INGEST_FILTERS) { //Hide the mimetype settings when this is displaying a FileSet rule instead of a interesting item rule mimeTypeComboBox.setVisible(false); mimeCheck.setVisible(false); - fileSizeComboBox.setVisible(false); - fileSizeCheck.setVisible(false); - equalitySymbolComboBox.setVisible(false); - fileSizeSpinner.setVisible(false); jLabel1.setVisible(false); filesRadioButton.setVisible(false); dirsRadioButton.setVisible(false); @@ -120,8 +112,6 @@ final class FilesSetRulePanel extends javax.swing.JPanel { } else { populateMimeTypesComboBox(); populateMimeConditionComponents(rule); - populateSizeConditionComponents(rule); - } populateMimeTypesComboBox(); populateRuleNameComponent(rule); @@ -129,6 +119,7 @@ final class FilesSetRulePanel extends javax.swing.JPanel { populateNameConditionComponents(rule); populatePathConditionComponents(rule); populateDateConditionComponents(rule); + populateSizeConditionComponents(rule); this.setButtons(okButton, cancelButton); updateNameTextFieldPrompt(); @@ -745,7 +736,7 @@ final class FilesSetRulePanel extends javax.swing.JPanel { mimeTypeComboBox.setModel(new javax.swing.DefaultComboBoxModel(new String[] {""})); mimeTypeComboBox.setEnabled(false); - equalitySymbolComboBox.setModel(new javax.swing.DefaultComboBoxModel(new String[] { "=", ">", "≥", "<", "≤" })); + equalitySymbolComboBox.setModel(new javax.swing.DefaultComboBoxModel(new String[] { ">", "<" })); equalitySymbolComboBox.setEnabled(false); fileSizeComboBox.setModel(new javax.swing.DefaultComboBoxModel(new String[] { Bundle.FilesSetRulePanel_bytes(), Bundle.FilesSetRulePanel_kiloBytes(), Bundle.FilesSetRulePanel_megaBytes(), Bundle.FilesSetRulePanel_gigaBytes() })); diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ChromeCacheExtractor.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ChromeCacheExtractor.java index 5dfb81431b..9435161a23 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ChromeCacheExtractor.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ChromeCacheExtractor.java @@ -33,6 +33,8 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -565,8 +567,13 @@ final class ChromeCacheExtractor { List effFiles = fileManager.findFiles(dataSource, "f_%", cachePath); //NON-NLS for (AbstractFile abstractFile : effFiles ) { + String cacheKey = cachePath + abstractFile.getName(); if (cachePath.equals(abstractFile.getParentPath()) && abstractFile.isFile()) { - this.externalFilesTable.put(cachePath + abstractFile.getName(), abstractFile); + // Don't overwrite an allocated version with an unallocated version + if (abstractFile.isMetaFlagSet(TskData.TSK_FS_META_FLAG_ENUM.ALLOC) + || !externalFilesTable.containsKey(cacheKey)) { + this.externalFilesTable.put(cacheKey, abstractFile); + } } } } @@ -590,20 +597,49 @@ final class ChromeCacheExtractor { return Optional.of(fileCopyCache.get(fileTableKey).getAbstractFile()); } - List cacheFiles = fileManager.findFiles(dataSource, cacheFileName, cacheFolderName); //NON-NLS if (!cacheFiles.isEmpty()) { - for (AbstractFile abstractFile: cacheFiles ) { - if (abstractFile.getUniquePath().trim().endsWith(DEFAULT_CACHE_PATH_STR)) { - return Optional.of(abstractFile); + // Sort the list for consistency. Preference is: + // - In correct subfolder and allocated + // - In correct subfolder and unallocated + // - In incorrect subfolder and allocated + Collections.sort(cacheFiles, new Comparator() { + @Override + public int compare(AbstractFile file1, AbstractFile file2) { + try { + if (file1.getUniquePath().trim().endsWith(DEFAULT_CACHE_PATH_STR) + && ! file2.getUniquePath().trim().endsWith(DEFAULT_CACHE_PATH_STR)) { + return -1; + } + + if (file2.getUniquePath().trim().endsWith(DEFAULT_CACHE_PATH_STR) + && ! file1.getUniquePath().trim().endsWith(DEFAULT_CACHE_PATH_STR)) { + return 1; + } + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "Error getting unique path for file with ID " + file1.getId() + " or " + file2.getId(), ex); + } + + if (file1.isMetaFlagSet(TskData.TSK_FS_META_FLAG_ENUM.ALLOC) + && ! file2.isMetaFlagSet(TskData.TSK_FS_META_FLAG_ENUM.ALLOC)) { + return -1; + } + if (file2.isMetaFlagSet(TskData.TSK_FS_META_FLAG_ENUM.ALLOC) + && ! file1.isMetaFlagSet(TskData.TSK_FS_META_FLAG_ENUM.ALLOC)) { + return 1; + } + + return Long.compare(file1.getId(), file2.getId()); } - } + }); + + // The best match will be the first element return Optional.of(cacheFiles.get(0)); } return Optional.empty(); } - + /** * Finds the "index" file that exists in each user's cache. This is used to * enumerate all of the caches on the system.