/*
* Autopsy Forensic Browser
*
* Copyright 2011-2016 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.datamodel;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.stream.Collectors;
import javax.swing.Action;
import org.apache.commons.lang3.StringUtils;
import org.openide.nodes.Children;
import org.openide.nodes.Sheet;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.lookup.Lookups;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
import org.sleuthkit.autopsy.casemodule.events.BlackBoardArtifactTagAddedEvent;
import org.sleuthkit.autopsy.casemodule.events.BlackBoardArtifactTagDeletedEvent;
import org.sleuthkit.autopsy.casemodule.events.ContentTagAddedEvent;
import org.sleuthkit.autopsy.casemodule.events.ContentTagDeletedEvent;
import org.sleuthkit.autopsy.timeline.actions.ViewArtifactInTimelineAction;
import org.sleuthkit.autopsy.timeline.actions.ViewFileInTimelineAction;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.BlackboardArtifact;
import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE;
import org.sleuthkit.datamodel.BlackboardAttribute;
import org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE;
import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.Tag;
import org.sleuthkit.datamodel.TskCoreException;
/**
* Node wrapping a blackboard artifact object. This is generated from several
* places in the tree.
*/
public class BlackboardArtifactNode extends DisplayableItemNode {
private final BlackboardArtifact artifact;
private final Content associated;
private List<NodeProperty<? extends Object>> customProperties;
private static final Logger LOGGER = Logger.getLogger(BlackboardArtifactNode.class.getName());
/*
* Artifact types which should have the full unique path of the associated
* content as a property.
*/
private static final Integer[] SHOW_UNIQUE_PATH = new Integer[]{
BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT.getTypeID(),
BlackboardArtifact.ARTIFACT_TYPE.TSK_KEYWORD_HIT.getTypeID(),
BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT.getTypeID(),};
// TODO (RC): This is an unattractive alternative to subclassing BlackboardArtifactNode,
// cut from the same cloth as the equally unattractive SHOW_UNIQUE_PATH array
// above. It should be removed when and if the subclassing is implemented.
private static final Integer[] SHOW_FILE_METADATA = new Integer[]{
BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT.getTypeID(),};
private final PropertyChangeListener pcl = new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
String eventType = evt.getPropertyName();
if (eventType.equals(Case.Events.BLACKBOARD_ARTIFACT_TAG_ADDED.toString())) {
BlackBoardArtifactTagAddedEvent event = (BlackBoardArtifactTagAddedEvent) evt;
if (event.getAddedTag().getArtifact().equals(artifact)) {
updateSheet();
}
} else if (eventType.equals(Case.Events.BLACKBOARD_ARTIFACT_TAG_DELETED.toString())) {
BlackBoardArtifactTagDeletedEvent event = (BlackBoardArtifactTagDeletedEvent) evt;
if (event.getDeletedTagInfo().getArtifactID() == artifact.getArtifactID()) {
updateSheet();
}
} else if (eventType.equals(Case.Events.CONTENT_TAG_ADDED.toString())) {
ContentTagAddedEvent event = (ContentTagAddedEvent) evt;
if (event.getAddedTag().getContent().equals(associated)) {
updateSheet();
}
} else if (eventType.equals(Case.Events.CONTENT_TAG_DELETED.toString())) {
ContentTagDeletedEvent event = (ContentTagDeletedEvent) evt;
if (event.getDeletedTagInfo().getContentID()== associated.getId()) {
updateSheet();
}
} else if (eventType.equals(Case.Events.CURRENT_CASE.toString())) {
if (evt.getNewValue() == null) {
// case was closed. Remove listeners so that we don't get called with a stale case handle
removeListeners();
}
}
}
};
/**
* Construct blackboard artifact node from an artifact and using provided
* icon
*
* @param artifact artifact to encapsulate
* @param iconPath icon to use for the artifact
*/
public BlackboardArtifactNode(BlackboardArtifact artifact, String iconPath) {
super(Children.LEAF, createLookup(artifact));
this.artifact = artifact;
//this.associated = getAssociatedContent(artifact);
this.associated = this.getLookup().lookup(Content.class);
this.setName(Long.toString(artifact.getArtifactID()));
this.setDisplayName();
this.setIconBaseWithExtension(iconPath);
Case.addPropertyChangeListener(pcl);
}
/**
* Construct blackboard artifact node from an artifact and using default
* icon for artifact type
*
* @param artifact artifact to encapsulate
*/
public BlackboardArtifactNode(BlackboardArtifact artifact) {
super(Children.LEAF, createLookup(artifact));
this.artifact = artifact;
//this.associated = getAssociatedContent(artifact);
this.associated = this.getLookup().lookup(Content.class);
this.setName(Long.toString(artifact.getArtifactID()));
this.setDisplayName();
this.setIconBaseWithExtension(ExtractedContent.getIconFilePath(artifact.getArtifactTypeID())); //NON-NLS
Case.addPropertyChangeListener(pcl);
}
private void removeListeners() {
Case.removePropertyChangeListener(pcl);
}
@Override
@NbBundle.Messages({
"BlackboardArtifactNode.getAction.errorTitle=Error getting actions",
"BlackboardArtifactNode.getAction.resultErrorMessage=There was a problem getting actions for the selected result."
+ " The 'View Result in Timeline' action will not be available.",
"BlackboardArtifactNode.getAction.linkedFileMessage=There was a problem getting actions for the selected result. "
+ " The 'View File in Timeline' action will not be available."})
public Action[] getActions(boolean context) {
List<Action> actionsList = new ArrayList<>();
actionsList.addAll(Arrays.asList(super.getActions(context)));
//if this artifact has a time stamp add the action to view it in the timeline
try {
if (ViewArtifactInTimelineAction.hasSupportedTimeStamp(artifact)) {
actionsList.add(new ViewArtifactInTimelineAction(artifact));
}
} catch (TskCoreException ex) {
LOGGER.log(Level.SEVERE, MessageFormat.format("Error getting arttribute(s) from blackboard artifact{0}.", artifact.getArtifactID()), ex); //NON-NLS
MessageNotifyUtil.Notify.error(Bundle.BlackboardArtifactNode_getAction_errorTitle(), Bundle.BlackboardArtifactNode_getAction_resultErrorMessage());
}
// if the artifact links to another file, add an action to go to that file
try {
AbstractFile c = findLinked(artifact);
if (c != null) {
actionsList.add(ViewFileInTimelineAction.createViewFileAction(c));
}
} catch (TskCoreException ex) {
LOGGER.log(Level.SEVERE, MessageFormat.format("Error getting linked file from blackboard artifact{0}.", artifact.getArtifactID()), ex); //NON-NLS
MessageNotifyUtil.Notify.error(Bundle.BlackboardArtifactNode_getAction_errorTitle(), Bundle.BlackboardArtifactNode_getAction_linkedFileMessage());
}
//if this artifact has associated content, add the action to view the content in the timeline
AbstractFile file = getLookup().lookup(AbstractFile.class);
if (null != file) {
actionsList.add(ViewFileInTimelineAction.createViewSourceFileAction(file));
}
return actionsList.toArray(new Action[actionsList.size()]);
}
/**
* Set the filter node display name. The value will either be the file name
* or something along the lines of e.g. "Messages Artifact" for keyword hits
* on artifacts.
*/
private void setDisplayName() {
String displayName = ""; //NON-NLS
if (associated != null) {
displayName = associated.getName();
}
// If this is a node for a keyword hit on an artifact, we set the
// display name to be the artifact type name followed by " Artifact"
// e.g. "Messages Artifact".
if (artifact != null && artifact.getArtifactTypeID() == ARTIFACT_TYPE.TSK_KEYWORD_HIT.getTypeID()) {
try {
for (BlackboardAttribute attribute : artifact.getAttributes()) {
if (attribute.getAttributeType().getTypeID() == ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT.getTypeID()) {
BlackboardArtifact associatedArtifact = Case.getCurrentCase().getSleuthkitCase().getBlackboardArtifact(attribute.getValueLong());
if (associatedArtifact != null) {
displayName = associatedArtifact.getDisplayName() + " Artifact";
}
}
}
} catch (TskCoreException ex) {
// Do nothing since the display name will be set to the file name.
}
}
this.setDisplayName(displayName);
}
@Override
protected Sheet createSheet() {
Sheet s = super.createSheet();
Sheet.Set ss = s.get(Sheet.PROPERTIES);
if (ss == null) {
ss = Sheet.createPropertiesSet();
s.put(ss);
}
final String NO_DESCR = NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.noDesc.text");
Map<String, Object> map = new LinkedHashMap<>();
fillPropertyMap(map, artifact);
ss.put(new NodeProperty<>(NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.createSheet.srcFile.name"),
NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.createSheet.srcFile.displayName"),
NO_DESCR,
this.getDisplayName()));
for (Map.Entry<String, Object> entry : map.entrySet()) {
ss.put(new NodeProperty<>(entry.getKey(),
entry.getKey(),
NO_DESCR,
entry.getValue()));
}
//append custom node properties
if (customProperties != null) {
for (NodeProperty<? extends Object> np : customProperties) {
ss.put(np);
}
}
final int artifactTypeId = artifact.getArtifactTypeID();
// If mismatch, add props for extension and file type
if (artifactTypeId == BlackboardArtifact.ARTIFACT_TYPE.TSK_EXT_MISMATCH_DETECTED.getTypeID()) {
String ext = ""; //NON-NLS
String actualMimeType = ""; //NON-NLS
if (associated instanceof AbstractFile) {
AbstractFile af = (AbstractFile) associated;
ext = af.getNameExtension();
actualMimeType = af.getMIMEType();
if (actualMimeType == null) {
actualMimeType = ""; //NON-NLS
}
}
ss.put(new NodeProperty<>(NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.createSheet.ext.name"),
NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.createSheet.ext.displayName"),
NO_DESCR,
ext));
ss.put(new NodeProperty<>(
NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.createSheet.mimeType.name"),
NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.createSheet.mimeType.displayName"),
NO_DESCR,
actualMimeType));
}
if (Arrays.asList(SHOW_UNIQUE_PATH).contains(artifactTypeId)) {
String sourcePath = ""; //NON-NLS
try {
sourcePath = associated.getUniquePath();
} catch (TskCoreException ex) {
LOGGER.log(Level.WARNING, "Failed to get unique path from: {0}", associated.getName()); //NON-NLS
}
if (sourcePath.isEmpty() == false) {
ss.put(new NodeProperty<>(
NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.createSheet.filePath.name"),
NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.createSheet.filePath.displayName"),
NO_DESCR,
sourcePath));
}
if (Arrays.asList(SHOW_FILE_METADATA).contains(artifactTypeId)) {
AbstractFile file = associated instanceof AbstractFile ? (AbstractFile) associated : null;
ss.put(new NodeProperty<>(NbBundle.getMessage(BlackboardArtifactNode.class, "ContentTagNode.createSheet.fileModifiedTime.name"),
NbBundle.getMessage(BlackboardArtifactNode.class, "ContentTagNode.createSheet.fileModifiedTime.displayName"),
"",
file != null ? ContentUtils.getStringTime(file.getMtime(), file) : ""));
ss.put(new NodeProperty<>(NbBundle.getMessage(BlackboardArtifactNode.class, "ContentTagNode.createSheet.fileChangedTime.name"),
NbBundle.getMessage(BlackboardArtifactNode.class, "ContentTagNode.createSheet.fileChangedTime.displayName"),
"",
file != null ? ContentUtils.getStringTime(file.getCtime(), file) : ""));
ss.put(new NodeProperty<>(NbBundle.getMessage(BlackboardArtifactNode.class, "ContentTagNode.createSheet.fileAccessedTime.name"),
NbBundle.getMessage(BlackboardArtifactNode.class, "ContentTagNode.createSheet.fileAccessedTime.displayName"),
"",
file != null ? ContentUtils.getStringTime(file.getAtime(), file) : ""));
ss.put(new NodeProperty<>(NbBundle.getMessage(BlackboardArtifactNode.class, "ContentTagNode.createSheet.fileCreatedTime.name"),
NbBundle.getMessage(BlackboardArtifactNode.class, "ContentTagNode.createSheet.fileCreatedTime.displayName"),
"",
file != null ? ContentUtils.getStringTime(file.getCrtime(), file) : ""));
ss.put(new NodeProperty<>(NbBundle.getMessage(BlackboardArtifactNode.class, "ContentTagNode.createSheet.fileSize.name"),
NbBundle.getMessage(BlackboardArtifactNode.class, "ContentTagNode.createSheet.fileSize.displayName"),
"",
associated.getSize()));
}
} else {
String dataSourceStr = "";
try {
Content dataSource = associated.getDataSource();
if (dataSource != null) {
dataSourceStr = dataSource.getName();
} else {
dataSourceStr = getRootParentName();
}
} catch (TskCoreException ex) {
LOGGER.log(Level.WARNING, "Failed to get image name from {0}", associated.getName()); //NON-NLS
}
if (dataSourceStr.isEmpty() == false) {
ss.put(new NodeProperty<>(
NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.createSheet.dataSrc.name"),
NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.createSheet.dataSrc.displayName"),
NO_DESCR,
dataSourceStr));
}
}
// add properties for tags
List<Tag> tags = new ArrayList<>();
try {
tags.addAll(Case.getCurrentCase().getServices().getTagsManager().getBlackboardArtifactTagsByArtifact(artifact));
tags.addAll(Case.getCurrentCase().getServices().getTagsManager().getContentTagsByContent(associated));
} catch (TskCoreException ex) {
LOGGER.log(Level.SEVERE, "Failed to get tags for artifact " + artifact.getDisplayName(), ex);
}
ss.put(new NodeProperty<>("Tags", NbBundle.getMessage(AbstractAbstractFileNode.class, "BlackboardArtifactNode.createSheet.tags.displayName"),
NO_DESCR, tags.stream().map(t -> t.getName().getDisplayName()).collect(Collectors.joining(", "))));
return s;
}
private void updateSheet() {
this.setSheet(createSheet());
}
private String getRootParentName() {
String parentName = associated.getName();
Content parent = associated;
try {
while ((parent = parent.getParent()) != null) {
parentName = parent.getName();
}
} catch (TskCoreException ex) {
LOGGER.log(Level.WARNING, "Failed to get parent name from {0}", associated.getName()); //NON-NLS
return "";
}
return parentName;
}
/**
* Add an additional custom node property to that node before it is
* displayed
*
* @param np NodeProperty to add
*/
public void addNodeProperty(NodeProperty<?> np) {
if (null == customProperties) {
//lazy create the list
customProperties = new ArrayList<>();
}
customProperties.add(np);
}
/**
* Fill map with Artifact properties
*
* @param map map with preserved ordering, where property names/values
* are put
* @param artifact to extract properties from
*/
@SuppressWarnings("deprecation")
private void fillPropertyMap(Map<String, Object> map, BlackboardArtifact artifact) {
try {
for (BlackboardAttribute attribute : artifact.getAttributes()) {
final int attributeTypeID = attribute.getAttributeType().getTypeID();
//skip some internal attributes that user shouldn't see
if (attributeTypeID == ATTRIBUTE_TYPE.TSK_PATH_ID.getTypeID()
|| attributeTypeID == ATTRIBUTE_TYPE.TSK_TAGGED_ARTIFACT.getTypeID()
|| attributeTypeID == ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT.getTypeID()
|| attributeTypeID == ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID()) {
} else if (attribute.getAttributeType().getValueType() == BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DATETIME) {
map.put(attribute.getAttributeType().getDisplayName(), ContentUtils.getStringTime(attribute.getValueLong(), associated));
} else if (artifact.getArtifactTypeID() == ARTIFACT_TYPE.TSK_TOOL_OUTPUT.getTypeID()
&& attributeTypeID == ATTRIBUTE_TYPE.TSK_TEXT.getTypeID()) {
/*
* This was added because the RegRipper output would often
* cause the UI to get a black line accross it and hang if
* you hovered over large output or selected it. This
* reduces the amount of data in the table. Could consider
* doing this for all fields in the UI.
*/
String value = attribute.getDisplayString();
if (value.length() > 512) {
value = value.substring(0, 512);
}
map.put(attribute.getAttributeType().getDisplayName(), value);
} else {
map.put(attribute.getAttributeType().getDisplayName(), attribute.getDisplayString());
}
}
} catch (TskCoreException ex) {
LOGGER.log(Level.SEVERE, "Getting attributes failed", ex); //NON-NLS
}
}
@Override
public <T> T accept(DisplayableItemNodeVisitor<T> v) {
return v.visit(this);
}
/**
* Create a Lookup based on what is in the passed in artifact.
*
* @param artifact
*
* @return
*/
private static Lookup createLookup(BlackboardArtifact artifact) {
List<Object> forLookup = new ArrayList<>();
forLookup.add(artifact);
// Add the content the artifact is associated with
Content content = getAssociatedContent(artifact);
if (content != null) {
forLookup.add(content);
}
// if there is a text highlighted version, of the content, add it too
// currently happens from keyword search module
TextMarkupLookup highlight = getHighlightLookup(artifact, content);
if (highlight != null) {
forLookup.add(highlight);
}
return Lookups.fixed(forLookup.toArray(new Object[forLookup.size()]));
}
private static Content getAssociatedContent(BlackboardArtifact artifact) {
try {
return artifact.getSleuthkitCase().getContentById(artifact.getObjectID());
} catch (TskCoreException ex) {
LOGGER.log(Level.WARNING, "Getting file failed", ex); //NON-NLS
}
throw new IllegalArgumentException(
NbBundle.getMessage(BlackboardArtifactNode.class, "BlackboardArtifactNode.getAssocCont.exception.msg"));
}
private static TextMarkupLookup getHighlightLookup(BlackboardArtifact artifact, Content content) {
if (artifact.getArtifactTypeID() != BlackboardArtifact.ARTIFACT_TYPE.TSK_KEYWORD_HIT.getTypeID()) {
return null;
}
long objectId = content.getId();
Lookup lookup = Lookup.getDefault();
TextMarkupLookup highlightFactory = lookup.lookup(TextMarkupLookup.class);
try {
List<BlackboardAttribute> attributes = artifact.getAttributes();
String keyword = null;
String regexp = null;
for (BlackboardAttribute att : attributes) {
final int attributeTypeID = att.getAttributeType().getTypeID();
if (attributeTypeID == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD.getTypeID()) {
keyword = att.getValueString();
} else if (attributeTypeID == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD_REGEXP.getTypeID()) {
regexp = att.getValueString();
} else if (attributeTypeID == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT.getTypeID()) {
objectId = att.getValueLong();
}
}
if (keyword != null) {
boolean isRegexp = StringUtils.isNotBlank(regexp);
String origQuery = isRegexp ? regexp : keyword;
return highlightFactory.createInstance(objectId, keyword, isRegexp, origQuery);
}
} catch (TskCoreException ex) {
LOGGER.log(Level.WARNING, "Failed to retrieve Blackboard Attributes", ex); //NON-NLS
}
return null;
}
@Override
public boolean isLeafTypeNode() {
return true;
}
@Override
public String getItemType() {
return getClass().getName();
}
}