diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/NodeProperty.java b/Core/src/org/sleuthkit/autopsy/datamodel/NodeProperty.java index 5aa0fb97d1..3c7e3e9afd 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/NodeProperty.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/NodeProperty.java @@ -30,7 +30,7 @@ public class NodeProperty extends PropertySupport.ReadOnly { private T value; @SuppressWarnings("unchecked") - public NodeProperty(String name, String displayName, String desc, T value) { + public NodeProperty(String name, String displayName, String desc, T value) { super(name, (Class) value.getClass(), displayName, desc); setValue("suppressCustomEditor", Boolean.TRUE); // remove the "..." (editing) button NON-NLS this.value = value; diff --git a/Core/src/org/sleuthkit/autopsy/mainui/nodes/AnalysisResultNode.java b/Core/src/org/sleuthkit/autopsy/mainui/nodes/AnalysisResultNode.java index b71f1f0e1c..59932f720e 100755 --- a/Core/src/org/sleuthkit/autopsy/mainui/nodes/AnalysisResultNode.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/nodes/AnalysisResultNode.java @@ -18,9 +18,13 @@ */ package org.sleuthkit.autopsy.mainui.nodes; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; +import java.util.logging.Level; import org.openide.util.Lookup; import org.openide.util.lookup.Lookups; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.datamodel.AnalysisResultItem; import org.sleuthkit.autopsy.datamodel.FileTypeExtensions; @@ -30,7 +34,10 @@ import org.sleuthkit.autopsy.mainui.datamodel.AnalysisResultTableSearchResultsDT import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.AnalysisResult; import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardArtifactTag; import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.ContentTag; +import org.sleuthkit.datamodel.Tag; import org.sleuthkit.datamodel.TskCoreException; /** @@ -58,7 +65,7 @@ public class AnalysisResultNode extends ArtifactNode> getAllTagsFromDatabase() { + List tags = new ArrayList<>(); + try { + List artifactTags = ContentNodeUtil.getArtifactTagsFromDatabase(getRowDTO().getArtifact()); + if(!artifactTags.isEmpty()) { + tags.addAll(artifactTags); + } + + List contentTags = ContentNodeUtil.getContentTagsFromDatabase(getRowDTO().getSrcContent()); + if(!contentTags.isEmpty()) { + tags.addAll(contentTags); + } + + } catch (TskCoreException | NoCurrentCaseException ex) { + logger.log(Level.SEVERE, "Failed to get content tags from database for Artifact id=" + getRowDTO().getArtifact().getId(), ex); + } + if(!tags.isEmpty()) { + return Optional.of(tags); + } + return Optional.empty(); + } + + @Override + public Logger getLogger() { + return logger; + } } diff --git a/Core/src/org/sleuthkit/autopsy/mainui/nodes/ArtifactNode.java b/Core/src/org/sleuthkit/autopsy/mainui/nodes/ArtifactNode.java index e86c576d13..6570627a38 100755 --- a/Core/src/org/sleuthkit/autopsy/mainui/nodes/ArtifactNode.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/nodes/ArtifactNode.java @@ -18,24 +18,40 @@ */ package org.sleuthkit.autopsy.mainui.nodes; +import java.lang.ref.WeakReference; +import java.text.MessageFormat; import java.util.List; import java.util.Optional; +import java.util.logging.Level; import javax.swing.Action; -import org.openide.nodes.AbstractNode; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; import org.openide.nodes.Children; import org.openide.nodes.Node; import org.openide.nodes.Sheet; import org.openide.util.Lookup; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoDbUtil; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoException; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeNormalizationException; +import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable; import org.sleuthkit.autopsy.datamodel.DirectoryNode; import org.sleuthkit.autopsy.datamodel.LayoutFileNode; import org.sleuthkit.autopsy.datamodel.LocalDirectoryNode; import org.sleuthkit.autopsy.datamodel.LocalFileNode; +import org.sleuthkit.autopsy.datamodel.NodeProperty; import org.sleuthkit.autopsy.datamodel.SlackFileNode; import org.sleuthkit.autopsy.datamodel.VirtualDirectoryNode; import org.sleuthkit.autopsy.mainui.datamodel.ArtifactRowDTO; import org.sleuthkit.autopsy.mainui.datamodel.ColumnKey; +import org.sleuthkit.autopsy.mainui.datamodel.SearchResultsDTO; +import static org.sleuthkit.autopsy.mainui.nodes.BaseNode.backgroundTasksPool; import org.sleuthkit.autopsy.mainui.nodes.actions.ActionContext; import org.sleuthkit.autopsy.mainui.nodes.actions.ActionsFactory; +import org.sleuthkit.autopsy.mainui.nodes.sco.SCOFetcher; +import org.sleuthkit.autopsy.mainui.nodes.sco.SCOSupporter; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.Content; @@ -48,23 +64,29 @@ import org.sleuthkit.datamodel.LocalDirectory; import org.sleuthkit.datamodel.LocalFile; import org.sleuthkit.datamodel.OsAccount; import org.sleuthkit.datamodel.SlackFile; +import org.sleuthkit.datamodel.Tag; import org.sleuthkit.datamodel.VirtualDirectory; -public abstract class ArtifactNode> extends AbstractNode implements ActionContext { +public abstract class ArtifactNode> extends BaseNode implements ActionContext, SCOSupporter { private final R rowData; - private final BlackboardArtifact.Type artifactType; private final List columns; private Node parentFileNode; - ArtifactNode(R rowData, List columns, BlackboardArtifact.Type artifactType, Lookup lookup, String iconPath) { - super(Children.LEAF, lookup); + ArtifactNode(SearchResultsDTO searchResults, R rowData, List columns, Lookup lookup, String iconPath) { + super(Children.LEAF, lookup, searchResults, rowData); this.rowData = rowData; - this.artifactType = artifactType; this.columns = columns; setupNodeDisplay(iconPath); } + @Override + protected Sheet createSheet() { + Sheet sheet = super.createSheet(); + backgroundTasksPool.submit(new SCOFetcher<>(new WeakReference<>(this))); + return sheet; + } + @Override public Optional getSourceContent() { return Optional.ofNullable(rowData.getSrcContent()); @@ -133,7 +155,7 @@ public abstract class ArtifactNode getContent() { + return Optional.of(rowData.getArtifact()); + } + + @Override + public void updateSheet(List> newProps) { + super.updateSheet(newProps); + } + + @Messages({ + "# {0} - occurrenceCount", + "# {1} - attributeType", + "ArtifactNode_createSheet_count_description=There were {0} datasource(s) found with occurrences of the correlation value of type {1}", + "ArtifactNode_createSheet_count_noCorrelationValues_description=Unable to find other occurrences because no value exists for the available correlation property" + }) + @Override + public Pair getCountPropertyAndDescription(CorrelationAttributeInstance attribute, String defaultDescription) { + Long count = -1L; + String description = defaultDescription; + try { + if (attribute != null && StringUtils.isNotBlank(attribute.getCorrelationValue())) { + count = CentralRepository.getInstance().getCountCasesWithOtherInstances(attribute); + description = Bundle.ArtifactNode_createSheet_count_description(count, attribute.getCorrelationType().getDisplayName()); + } else if (attribute != null) { + description = Bundle.ArtifactNode_createSheet_count_noCorrelationValues_description(); + } + } catch (CentralRepoException ex) { + getLogger().log(Level.SEVERE, MessageFormat.format("Error querying central repository for other occurences count (artifact objID={0}, corrAttrType={1}, corrAttrValue={2})", + getRowDTO().getArtifact().getId(), + attribute.getCorrelationType(), + attribute.getCorrelationValue()), ex); + } catch (CorrelationAttributeNormalizationException ex) { + getLogger().log(Level.SEVERE, MessageFormat.format("Error normalizing correlation attribute for central repository query (artifact objID={0}, corrAttrType={2}, corrAttrValue={3})", + getRowDTO().getArtifact().getId(), + attribute.getCorrelationType(), + attribute.getCorrelationValue()), ex); + } + return Pair.of(count, description); + } + + @Override + public DataResultViewerTable.HasCommentStatus getCommentProperty(List tags, List attributes) { + /* + * Has a tag with a comment been applied to the artifact or its source + * content? + */ + DataResultViewerTable.HasCommentStatus status = tags.size() > 0 ? DataResultViewerTable.HasCommentStatus.TAG_NO_COMMENT : DataResultViewerTable.HasCommentStatus.NO_COMMENT; + for (Tag tag : tags) { + if (!StringUtils.isBlank(tag.getComment())) { + status = DataResultViewerTable.HasCommentStatus.TAG_COMMENT; + break; + } + } + /* + * Is there a comment in the CR for anything that matches the value and + * type of the specified attributes. + */ + try { + if (CentralRepoDbUtil.commentExistsOnAttributes(attributes)) { + if (status == DataResultViewerTable.HasCommentStatus.TAG_COMMENT) { + status = DataResultViewerTable.HasCommentStatus.CR_AND_TAG_COMMENTS; + } else { + status = DataResultViewerTable.HasCommentStatus.CR_COMMENT; + } + } + } catch (CentralRepoException ex) { + getLogger().log(Level.SEVERE, "Attempted to Query CR for presence of comments in a Blackboard Artifact node and was unable to perform query, comment column will only reflect caseDB", ex); + } + return status; } /** diff --git a/Core/src/org/sleuthkit/autopsy/mainui/nodes/BaseNode.java b/Core/src/org/sleuthkit/autopsy/mainui/nodes/BaseNode.java index bb737e19ea..0819668813 100755 --- a/Core/src/org/sleuthkit/autopsy/mainui/nodes/BaseNode.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/nodes/BaseNode.java @@ -18,11 +18,18 @@ */ package org.sleuthkit.autopsy.mainui.nodes; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.logging.Logger; import javax.swing.Action; +import javax.swing.SwingUtilities; import org.openide.nodes.AbstractNode; import org.openide.nodes.Children; import org.openide.nodes.Sheet; import org.openide.util.Lookup; +import org.sleuthkit.autopsy.datamodel.NodeProperty; import org.sleuthkit.autopsy.mainui.datamodel.BaseRowDTO; import org.sleuthkit.autopsy.mainui.datamodel.SearchResultsDTO; import org.sleuthkit.autopsy.mainui.nodes.actions.ActionContext; @@ -31,10 +38,24 @@ import org.sleuthkit.autopsy.mainui.nodes.actions.ActionsFactory; /** * A a simple starting point for nodes. */ -abstract class BaseNode extends AbstractNode implements ActionContext { - +public abstract class BaseNode extends AbstractNode implements ActionContext { + private final S results; private final R rowData; + + /** + * A pool of background tasks to run any long computation needed to populate + * this node. + */ + static final ExecutorService backgroundTasksPool; + private static final Integer MAX_POOL_SIZE = 10; + + static { + //Initialize this pool only once! This will be used by every instance BaseNode + //to do their heavy duty SCO column and translation updates. + backgroundTasksPool = Executors.newFixedThreadPool(MAX_POOL_SIZE, + new ThreadFactoryBuilder().setNameFormat("BaseNode-background-task-%d").build()); + } BaseNode(Children children, Lookup lookup, S results, R rowData) { super(children, lookup); @@ -69,4 +90,41 @@ abstract class BaseNode extend public Action[] getActions(boolean context) { return ActionsFactory.getActions(this); } + + /** + * Updates the values of the properties in the current property sheet with + * the new properties being passed in. Only if that property exists in the + * current sheet will it be applied. That way, we allow for subclasses to + * add their own (or omit some!) properties and we will not accidentally + * disrupt their UI. + * + * Race condition if not synchronized. Only one update should be applied at + * a time. + * + * @param newProps New file property instances to be updated in the current + * sheet. + */ + protected synchronized void updateSheet(List> newProps) { + SwingUtilities.invokeLater(() -> { + /* + * Refresh ONLY those properties in the sheet currently. Subclasses + * may have only added a subset of our properties or their own + * properties. + */ + Sheet visibleSheet = this.getSheet(); + Sheet.Set visibleSheetSet = visibleSheet.get(Sheet.PROPERTIES); + Property[] visibleProps = visibleSheetSet.getProperties(); + for (NodeProperty newProp : newProps) { + for (int i = 0; i < visibleProps.length; i++) { + if (visibleProps[i].getName().equals(newProp.getName())) { + visibleProps[i] = newProp; + } + } + } + visibleSheetSet.put(visibleProps); + visibleSheet.put(visibleSheetSet); + //setSheet() will notify Netbeans to update this node in the UI. + this.setSheet(visibleSheet); + }); + } } diff --git a/Core/src/org/sleuthkit/autopsy/mainui/nodes/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/mainui/nodes/Bundle.properties-MERGED index 64865cab30..7105bae0db 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/nodes/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/mainui/nodes/Bundle.properties-MERGED @@ -1,4 +1,8 @@ AnalysisResultTypeFactory_adHocName=Adhoc Results +# {0} - occurrenceCount +# {1} - attributeType +ArtifactNode_createSheet_count_description=There were {0} datasource(s) found with occurrences of the correlation value of type {1} +ArtifactNode_createSheet_count_noCorrelationValues_description=Unable to find other occurrences because no value exists for the available correlation property ImageNode_ExtractUnallocAction_text=Extract Unallocated Space to Single Files SearchResultRootNode_createSheet_childCount_displayName=Child Count SearchResultRootNode_createSheet_childCount_name=Child Count diff --git a/Core/src/org/sleuthkit/autopsy/mainui/nodes/ContentNodeUtil.java b/Core/src/org/sleuthkit/autopsy/mainui/nodes/ContentNodeUtil.java index c5cbdd580b..707b2c3fb5 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/nodes/ContentNodeUtil.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/nodes/ContentNodeUtil.java @@ -18,17 +18,24 @@ */ package org.sleuthkit.autopsy.mainui.nodes; +import java.util.ArrayList; import java.util.Date; import java.util.List; import org.openide.nodes.Sheet; import org.openide.util.Lookup; import org.openide.util.lookup.Lookups; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.coreutils.TimeZoneUtils; import org.sleuthkit.autopsy.datamodel.DirectoryNode; import org.sleuthkit.autopsy.datamodel.NodeProperty; import org.sleuthkit.autopsy.datamodel.TskContentItem; import org.sleuthkit.autopsy.mainui.datamodel.ColumnKey; +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; /** * Utilities for setting up nodes that handle content. @@ -68,6 +75,12 @@ public class ContentNodeUtil { Object cellValue = values.get(i); if (cellValue == null) { + sheetSet.put(new NodeProperty<>( + columnKey.getFieldName(), + columnKey.getDisplayName(), + columnKey.getDescription(), + "" + )); continue; } @@ -85,4 +98,22 @@ public class ContentNodeUtil { return sheet; } + + /** + * Get all tags from the case database that are associated with the file + * + * @return a list of tags that are associated with the file + */ + public static List getContentTagsFromDatabase(Content content) throws TskCoreException, NoCurrentCaseException{ + List tags = new ArrayList<>(); + tags.addAll(Case.getCurrentCaseThrows().getServices().getTagsManager().getContentTagsByContent(content)); + + return tags; + } + + public static List getArtifactTagsFromDatabase(BlackboardArtifact artifact) throws TskCoreException, NoCurrentCaseException{ + List tags = new ArrayList<>(); + tags.addAll(Case.getCurrentCaseThrows().getServices().getTagsManager().getBlackboardArtifactTagsByArtifact(artifact)); + return tags; + } } diff --git a/Core/src/org/sleuthkit/autopsy/mainui/nodes/DataArtifactNode.java b/Core/src/org/sleuthkit/autopsy/mainui/nodes/DataArtifactNode.java index 1527e654c5..f6d9984408 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/nodes/DataArtifactNode.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/nodes/DataArtifactNode.java @@ -18,14 +18,23 @@ */ package org.sleuthkit.autopsy.mainui.nodes; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.logging.Level; import org.openide.util.Lookup; import org.openide.util.lookup.Lookups; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.datamodel.utils.IconsUtil; import org.sleuthkit.autopsy.datamodel.DataArtifactItem; import org.sleuthkit.autopsy.mainui.datamodel.DataArtifactRowDTO; import org.sleuthkit.autopsy.mainui.datamodel.DataArtifactTableSearchResultsDTO; +import org.sleuthkit.datamodel.BlackboardArtifactTag; +import org.sleuthkit.datamodel.ContentTag; import org.sleuthkit.datamodel.DataArtifact; +import org.sleuthkit.datamodel.Tag; +import org.sleuthkit.datamodel.TskCoreException; /** * node to display a data artifact. @@ -48,6 +57,27 @@ public class DataArtifactNode extends ArtifactNode> getAllTagsFromDatabase() { + try { + List artifactTags = ContentNodeUtil.getArtifactTagsFromDatabase(getRowDTO().getArtifact()); + if(!artifactTags.isEmpty()) { + List tags = new ArrayList<>(); + tags.addAll(artifactTags); + return Optional.of(tags); + } + + } catch (TskCoreException | NoCurrentCaseException ex) { + logger.log(Level.SEVERE, "Failed to get content tags from database for Artifact id=" + getRowDTO().getArtifact().getId(), ex); + } + return Optional.empty(); } } diff --git a/Core/src/org/sleuthkit/autopsy/mainui/nodes/FileNode.java b/Core/src/org/sleuthkit/autopsy/mainui/nodes/FileNode.java index 292a86a381..ed050b1f6b 100644 --- a/Core/src/org/sleuthkit/autopsy/mainui/nodes/FileNode.java +++ b/Core/src/org/sleuthkit/autopsy/mainui/nodes/FileNode.java @@ -18,25 +18,34 @@ */ package org.sleuthkit.autopsy.mainui.nodes; +import java.lang.ref.WeakReference; +import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.swing.Action; -import org.openide.nodes.AbstractNode; import org.openide.nodes.Children; import org.openide.nodes.Node; import org.openide.nodes.Sheet; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.datamodel.FileTypeExtensions; +import org.sleuthkit.autopsy.datamodel.NodeProperty; import org.sleuthkit.autopsy.mainui.datamodel.SearchResultsDTO; import org.sleuthkit.autopsy.mainui.datamodel.FileRowDTO; import org.sleuthkit.autopsy.mainui.datamodel.ColumnKey; import org.sleuthkit.autopsy.mainui.datamodel.FileRowDTO.ExtensionMediaType; import org.sleuthkit.autopsy.mainui.datamodel.FileRowDTO.LayoutFileRowDTO; import org.sleuthkit.autopsy.mainui.datamodel.FileRowDTO.SlackFileRowDTO; -import org.sleuthkit.autopsy.mainui.nodes.actions.ActionContext; import org.sleuthkit.autopsy.mainui.nodes.actions.ActionsFactory; +import org.sleuthkit.autopsy.mainui.nodes.sco.SCOFetcher; +import org.sleuthkit.autopsy.mainui.nodes.sco.SCOSupporter; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.ContentTag; import org.sleuthkit.datamodel.LayoutFile; +import org.sleuthkit.datamodel.Tag; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskData; import org.sleuthkit.datamodel.TskData.TSK_DB_FILES_TYPE_ENUM; @@ -45,7 +54,9 @@ import org.sleuthkit.datamodel.TskData.TSK_FS_NAME_FLAG_ENUM; /** * A node for representing an AbstractFile. */ -public class FileNode extends AbstractNode implements ActionContext { +public class FileNode extends BaseNode implements SCOSupporter { + + private static final Logger logger = Logger.getLogger(FileNode.class.getName()); /** * Gets the path to the icon file that should be used to visually represent @@ -95,7 +106,7 @@ public class FileNode extends AbstractNode implements ActionContext { public FileNode(SearchResultsDTO results, FileRowDTO file, boolean directoryBrowseMode) { // GVDTODO: at some point, this leaf will need to allow for children - super(Children.LEAF, ContentNodeUtil.getLookup(file.getAbstractFile())); + super(Children.LEAF, ContentNodeUtil.getLookup(file.getAbstractFile()), results, file); setIcon(file); setDisplayName(ContentNodeUtil.getContentDisplayName(file.getFileName())); setName(ContentNodeUtil.getContentName(file.getId())); @@ -187,7 +198,40 @@ public class FileNode extends AbstractNode implements ActionContext { @Override protected Sheet createSheet() { - return ContentNodeUtil.setSheet(super.createSheet(), this.columns, this.fileData.getCellValues()); + Sheet sheet = super.createSheet(); + backgroundTasksPool.submit(new SCOFetcher<>(new WeakReference<>(this))); + return sheet; + } + + @Override + public Logger getLogger() { + return logger; + } + + @Override + public Optional getContent() { + return Optional.ofNullable(fileData.getAbstractFile()); + } + + @Override + public void updateSheet(List> newProps) { + super.updateSheet(newProps); + } + + @Override + public Optional> getAllTagsFromDatabase() { + try { + List contentTags = ContentNodeUtil.getContentTagsFromDatabase(fileData.getAbstractFile()); + if(!contentTags.isEmpty()) { + List tags = new ArrayList<>(); + tags.addAll(contentTags); + return Optional.of(tags); + } + + } catch (TskCoreException | NoCurrentCaseException ex) { + logger.log(Level.SEVERE, "Failed to get content tags from database for AbstractFile id=" + fileData.getAbstractFile().getId(), ex); + } + return Optional.empty(); } /** diff --git a/Core/src/org/sleuthkit/autopsy/mainui/nodes/sco/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/mainui/nodes/sco/Bundle.properties-MERGED new file mode 100755 index 0000000000..55c1a0d6fe --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/mainui/nodes/sco/Bundle.properties-MERGED @@ -0,0 +1,9 @@ +SCOFetcher_comment_display_name=C +SCOFetcher_count_display_name=O +SCOFetcher_occurrences_defaultDescription=No correlation properties found +SCOFetcher_occurrences_multipleProperties=Multiple different correlation properties exist for this result +SCOFetcher_score_display_name=S +SCOSupporter.valueLoading=value loading +# {0} - significanceDisplayName +SCOSupporter_getScorePropertyAndDescription_description=Has an {0} analysis result score +SCOSupporter_nodescription_text=no description diff --git a/Core/src/org/sleuthkit/autopsy/mainui/nodes/sco/SCOFetcher.java b/Core/src/org/sleuthkit/autopsy/mainui/nodes/sco/SCOFetcher.java new file mode 100755 index 0000000000..cbb056ba43 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/mainui/nodes/sco/SCOFetcher.java @@ -0,0 +1,239 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.sleuthkit.autopsy.mainui.nodes.sco; + +import java.beans.PropertyChangeListener; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.swing.SwingWorker; +import org.apache.commons.lang3.tuple.Pair; +import org.openide.util.Exceptions; +import org.openide.util.NbBundle; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeUtil; +import org.sleuthkit.autopsy.core.UserPreferences; +import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable; +import org.sleuthkit.autopsy.datamodel.NodeProperty; +import org.sleuthkit.autopsy.mainui.nodes.sco.SCOFetcher.SCOData; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.AnalysisResult; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.DataArtifact; +import org.sleuthkit.datamodel.OsAccount; +import org.sleuthkit.datamodel.OsAccountInstance; +import org.sleuthkit.datamodel.Score; +import org.sleuthkit.datamodel.Tag; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * + * @author kelly + */ +public class SCOFetcher extends SwingWorker { + + private final WeakReference weakSupporterRef; + private static final Logger logger = Logger.getLogger(SCOFetcher.class.getName()); + + public SCOFetcher(WeakReference weakSupporterRef) { + this.weakSupporterRef = weakSupporterRef; + } + + @NbBundle.Messages({"SCOFetcher_occurrences_defaultDescription=No correlation properties found", + "SCOFetcher_occurrences_multipleProperties=Multiple different correlation properties exist for this result"}) + @Override + protected SCOData doInBackground() throws Exception { + SCOSupporter scoSupporter = weakSupporterRef.get(); + Content content = scoSupporter.getContent().get(); + //Check for stale reference or if columns are disabled + if (content == null || UserPreferences.getHideSCOColumns()) { + return null; + } + // get the SCO column values + Pair scoreAndDescription; + Pair countAndDescription = null; + scoreAndDescription = scoSupporter.getScorePropertyAndDescription(); + + String description = Bundle.SCOFetcher_occurrences_defaultDescription(); + List listOfPossibleAttributes = new ArrayList<>(); + //the lists returned will be empty if the CR is not enabled + if (content instanceof AbstractFile) { + listOfPossibleAttributes.addAll(CorrelationAttributeUtil.makeCorrAttrsForSearch((AbstractFile) content)); + } else if (content instanceof AnalysisResult) { + listOfPossibleAttributes.addAll(CorrelationAttributeUtil.makeCorrAttrsForSearch((AnalysisResult) content)); + } else if (content instanceof DataArtifact) { + listOfPossibleAttributes.addAll(CorrelationAttributeUtil.makeCorrAttrsForSearch((DataArtifact) content)); + } else if (content instanceof OsAccount) { + try { + List osAccountInstances = ((OsAccount) content).getOsAccountInstances(); + + /* + * In the most common use cases it will not matter which + * OsAccountInstance is selected, so choosing the first one is + * the most efficient solution. + */ + OsAccountInstance osAccountInstance = osAccountInstances.isEmpty() ? null : osAccountInstances.get(0); + /* + * If we have a Case whith both data sources in the CR and data + * sources not in the CR, some of the OsAccountInstances for + * this OsAccount have not been processed into the CR. In this + * situation the counts may not always be accurate or + * consistent. + * + * In order to ensure conistency in all use cases we would need + * to ensure we always had an OsAccountInstance whose data + * source was in the CR when such an OsAccountInstance was + * available. + * + * The following block of code has been commented out because it + * reduces efficiency in what are believed to be the most common + * use cases. It would serve the purpose of providing + * consistency in edge cases where users are putting some but + * not all the data concerning OS Accounts, which is present in + * a single Case, into the CR. See TODO-JIRA-8031 for a similar + * issue in the OO viewer. + */ + +// if (CentralRepository.isEnabled() && !osAccountInstances.isEmpty()) { +// try { +// CentralRepository centralRepo = CentralRepository.getInstance(); +// //Correlation Cases are cached when we get them so this shouldn't involve a round trip for every node. +// CorrelationCase crCase = centralRepo.getCase(Case.getCurrentCaseThrows()); +// for (OsAccountInstance caseOsAccountInstance : osAccountInstances) { +// //correlation data sources are also cached so once should not involve round trips every time. +// CorrelationDataSource correlationDataSource = centralRepo.getDataSource(crCase, caseOsAccountInstance.getDataSource().getId()); +// if (correlationDataSource != null) { +// //we have found a data source which exists in the CR we will use it instead of the arbitrary first instance +// osAccountInstance = caseOsAccountInstance; +// break; +// } +// } +// } catch (CentralRepoException ex) { +// logger.log(Level.SEVERE, "Error checking CR for data sources which exist in it", ex); +// } catch (NoCurrentCaseException ex) { +// logger.log(Level.WARNING, "The current case was closed while attempting to find a data source in the central repository", ex); +// } +// } + listOfPossibleAttributes.addAll(CorrelationAttributeUtil.makeCorrAttrsForSearch(osAccountInstance)); + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Unable to get the DataSource or OsAccountInstances from an OsAccount with ID: " + content.getId(), ex); + } + } + + Optional> optionalList = scoSupporter.getAllTagsFromDatabase(); + + DataResultViewerTable.HasCommentStatus commentStatus = DataResultViewerTable.HasCommentStatus.NO_COMMENT; + + if(optionalList.isPresent()) { + commentStatus = scoSupporter.getCommentProperty(optionalList.get(), listOfPossibleAttributes); + } + + CorrelationAttributeInstance corInstance = null; + if (CentralRepository.isEnabled()) { + if (listOfPossibleAttributes.size() > 1) { + //Don't display anything if there is more than 1 correlation property for an artifact but let the user know + description = Bundle.SCOFetcher_occurrences_multipleProperties(); + } else if (!listOfPossibleAttributes.isEmpty()) { + //there should only be one item in the list + corInstance = listOfPossibleAttributes.get(0); + } + countAndDescription = scoSupporter.getCountPropertyAndDescription(corInstance, description); + } + if (isCancelled()) { + return null; + } + + return new SCOData(scoreAndDescription, commentStatus, countAndDescription); + } + + @Messages({ + "SCOFetcher_score_display_name=S", + "SCOFetcher_comment_display_name=C", + "SCOFetcher_count_display_name=O" + + }) + @Override + public void done() { + if (isCancelled() || UserPreferences.getHideSCOColumns()) { + return; + } + + try { + SCOData data = get(); + + if(data == null) { + return; + } + + List> props = new ArrayList<>(); + + if(data.getScoreAndDescription() != null) { + props.add(new NodeProperty<>( + Bundle.SCOFetcher_score_display_name(), + Bundle.SCOFetcher_score_display_name(), + data.getScoreAndDescription().getRight(), + data.getScoreAndDescription().getLeft())); + } + + if(data.getComment() != null) { + props.add(new NodeProperty<>( + Bundle.SCOFetcher_comment_display_name(), + Bundle.SCOFetcher_comment_display_name(), + "", + data.getComment())); + } + + if(data.getCountAndDescription() != null) { + props.add(new NodeProperty<>( + Bundle.SCOFetcher_count_display_name(), + Bundle.SCOFetcher_count_display_name(), + data.getCountAndDescription().getRight(), + data.getCountAndDescription().getLeft())); + } + + SCOSupporter scoSupporter = weakSupporterRef.get(); + + if(!props.isEmpty() && scoSupporter != null) { + scoSupporter.updateSheet(props); + } + + } catch (InterruptedException | ExecutionException ex) { + Exceptions.printStackTrace(ex); + } + } + + public static class SCOData { + + private final Pair scoreAndDescription; + private final DataResultViewerTable.HasCommentStatus comment; + private final Pair countAndDescription; + + SCOData(Pair scoreAndDescription, DataResultViewerTable.HasCommentStatus comment, Pair countAndDescription) { + this.scoreAndDescription = scoreAndDescription; + this.comment = comment; + this.countAndDescription = countAndDescription; + } + + Pair getScoreAndDescription() { + return scoreAndDescription; + } + + DataResultViewerTable.HasCommentStatus getComment() { + return comment; + } + + Pair getCountAndDescription() { + return countAndDescription; + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/mainui/nodes/sco/SCOSupporter.java b/Core/src/org/sleuthkit/autopsy/mainui/nodes/sco/SCOSupporter.java new file mode 100755 index 0000000000..e2509e4fd6 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/mainui/nodes/sco/SCOSupporter.java @@ -0,0 +1,117 @@ +/* + * 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.mainui.nodes.sco; + +import java.util.List; +import java.util.Optional; +import java.util.logging.Logger; +import java.util.logging.Level; +import org.apache.commons.lang3.tuple.Pair; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; +import org.sleuthkit.autopsy.corecomponents.DataResultViewerTable; +import org.sleuthkit.autopsy.datamodel.NodeProperty; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.Score; +import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.Tag; + +/** + * + */ +public interface SCOSupporter { + + @NbBundle.Messages({"SCOSupporter_nodescription_text=no description", + "SCOSupporter.valueLoading=value loading"}) + static final String NO_DESCR = Bundle.SCOSupporter_nodescription_text(); + + default Optional getContent() { + return Optional.empty(); + } + + default Optional> getAllTagsFromDatabase() { + return Optional.empty(); + } + + default void updateSheet(List> newProps) { + + } + + /** + * + * @return + */ + Logger getLogger(); + + /** + * Returns Score property for the content. + * + * @return + */ + @NbBundle.Messages({ + "# {0} - significanceDisplayName", + "SCOSupporter_getScorePropertyAndDescription_description=Has an {0} analysis result score" + }) + default Pair getScorePropertyAndDescription() { + Score score = Score.SCORE_UNKNOWN; + Optional optional = getContent(); + if (optional.isPresent()) { + Content content = optional.get(); + try { + score = content.getAggregateScore(); + } catch (TskCoreException ex) { + getLogger().log(Level.WARNING, "Unable to get aggregate score for content with id: " + content.getId(), ex); + } + } + + String significanceDisplay = score.getSignificance().getDisplayName(); + String description = Bundle.SCOSupporter_getScorePropertyAndDescription_description(significanceDisplay); + return Pair.of(score, description); + } + + /** + * Returns comment property for the node. + * + * Default implementation is a null implementation. + * + * @param tags The list of tags. + * @param attributes The list of correlation attribute instances. + * + * @return Comment property for the underlying content of the node. + */ + default DataResultViewerTable.HasCommentStatus getCommentProperty(List tags, List attributes) { + return DataResultViewerTable.HasCommentStatus.NO_COMMENT; + } + + /** + * Returns occurrences/count property for the node. + * + * Default implementation is a null implementation. + * + * @param attribute The correlation attribute for which data will + * be retrieved. + * @param defaultDescription A description to use when none is determined by + * the getCountPropertyAndDescription method. + * + * @return count property for the underlying content of the node. + */ + default Pair getCountPropertyAndDescription(CorrelationAttributeInstance attribute, String defaultDescription) { + return Pair.of(-1L, NO_DESCR); + } +}