diff --git a/Core/src/org/sleuthkit/autopsy/core/Installer.java b/Core/src/org/sleuthkit/autopsy/core/Installer.java index 8809bb549f..bc14db3b7d 100644 --- a/Core/src/org/sleuthkit/autopsy/core/Installer.java +++ b/Core/src/org/sleuthkit/autopsy/core/Installer.java @@ -66,7 +66,7 @@ public class Installer extends ModuleInstall { //We should update this if we officially switch to a new version of CRT/compiler System.loadLibrary("msvcr100"); //NON-NLS System.loadLibrary("msvcp100"); //NON-NLS - + logger.log(Level.INFO, "MSVCR100 and MSVCP100 libraries loaded"); //NON-NLS } catch (UnsatisfiedLinkError e) { logger.log(Level.SEVERE, "Error loading MSVCR100 and MSVCP100 libraries, ", e); //NON-NLS @@ -85,14 +85,14 @@ public class Installer extends ModuleInstall { } catch (UnsatisfiedLinkError e) { logger.log(Level.SEVERE, "Error loading EWF library, ", e); //NON-NLS } - + try { System.loadLibrary("libvmdk"); //NON-NLS logger.log(Level.INFO, "VMDK library loaded"); //NON-NLS } catch (UnsatisfiedLinkError e) { logger.log(Level.SEVERE, "Error loading VMDK library, ", e); //NON-NLS } - + try { System.loadLibrary("libvhdi"); //NON-NLS logger.log(Level.INFO, "VHDI library loaded"); //NON-NLS @@ -107,7 +107,7 @@ public class Installer extends ModuleInstall { } catch (UnsatisfiedLinkError e) { logger.log(Level.SEVERE, "Error loading MSVCR120 library, ", e); //NON-NLS } - + try { System.loadLibrary("libeay32"); //NON-NLS logger.log(Level.INFO, "LIBEAY32 library loaded"); //NON-NLS @@ -122,18 +122,20 @@ public class Installer extends ModuleInstall { logger.log(Level.SEVERE, "Error loading SSLEAY32 library, ", e); //NON-NLS } - // This library name is different in 32-bit versus 64-bit - String libintlName = "libintl-8"; //NON-NLS - if (PlatformUtil.is64BitJVM() == false) { - libintlName = "intl"; //NON-NLS - } try { - System.loadLibrary(libintlName); //NON-NLS - logger.log(Level.INFO, libintlName + " library loaded"); //NON-NLS + System.loadLibrary("libiconv-2"); //NON-NLS + logger.log(Level.INFO, "libiconv-2 library loaded"); //NON-NLS } catch (UnsatisfiedLinkError e) { - logger.log(Level.SEVERE, "Error loading " + libintlName + " library, ", e); //NON-NLS + logger.log(Level.SEVERE, "Error loading libiconv-2 library, ", e); //NON-NLS } + try { + System.loadLibrary("libintl-8"); //NON-NLS + logger.log(Level.INFO, "libintl-8 library loaded"); //NON-NLS + } catch (UnsatisfiedLinkError e) { + logger.log(Level.SEVERE, "Error loading libintl-8 library, ", e); //NON-NLS + } + try { System.loadLibrary("libpq"); //NON-NLS logger.log(Level.INFO, "LIBPQ library loaded"); //NON-NLS @@ -156,7 +158,7 @@ public class Installer extends ModuleInstall { /** * Check if JavaFx initialized * - * @return false if java fx not initialized (classes coult not load), true + * @return false if java fx not initialized (classes could not load), true * if initialized */ public static boolean isJavaFxInited() { diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java index afeacb9a46..a1ca0a55c5 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java @@ -1,15 +1,15 @@ /* * Autopsy Forensic Browser - * - * Copyright 2011-2014 Basis Technology Corp. + * + * Copyright 2011-2016 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. @@ -18,12 +18,15 @@ */ package org.sleuthkit.autopsy.datamodel; +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 javax.swing.Action; +import org.apache.commons.lang3.StringUtils; import org.openide.nodes.Children; import org.openide.nodes.Sheet; import org.openide.util.Lookup; @@ -31,6 +34,9 @@ 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.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; @@ -38,7 +44,6 @@ import org.sleuthkit.datamodel.BlackboardAttribute; import org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.TskCoreException; -import org.sleuthkit.datamodel.TskException; /** * Node wrapping a blackboard artifact object. This is generated from several @@ -49,7 +54,7 @@ public class BlackboardArtifactNode extends DisplayableItemNode { private final BlackboardArtifact artifact; private final Content associated; private List> customProperties; - static final Logger logger = Logger.getLogger(BlackboardArtifactNode.class.getName()); + 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. @@ -100,13 +105,55 @@ public class BlackboardArtifactNode extends DisplayableItemNode { this.setIconBaseWithExtension(ExtractedContent.getIconFilePath(artifact.getArtifactTypeID())); //NON-NLS } + @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 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 = ""; + String displayName = ""; //NON-NLS if (associated != null) { displayName = associated.getName(); } @@ -120,7 +167,7 @@ public class BlackboardArtifactNode extends DisplayableItemNode { 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"; // NON-NLS + displayName = associatedArtifact.getDisplayName() + " Artifact"; } } } @@ -131,6 +178,7 @@ public class BlackboardArtifactNode extends DisplayableItemNode { this.setDisplayName(displayName); } + @Override protected Sheet createSheet() { Sheet s = super.createSheet(); Sheet.Set ss = s.get(Sheet.PROPERTIES); @@ -165,14 +213,14 @@ public class BlackboardArtifactNode extends DisplayableItemNode { // If mismatch, add props for extension and file type if (artifactTypeId == BlackboardArtifact.ARTIFACT_TYPE.TSK_EXT_MISMATCH_DETECTED.getTypeID()) { - String ext = ""; - String actualMimeType = ""; + 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 = ""; + actualMimeType = ""; //NON-NLS } } ss.put(new NodeProperty<>(NbBundle.getMessage(this.getClass(), "BlackboardArtifactNode.createSheet.ext.name"), @@ -187,11 +235,11 @@ public class BlackboardArtifactNode extends DisplayableItemNode { } if (Arrays.asList(SHOW_UNIQUE_PATH).contains(artifactTypeId)) { - String sourcePath = ""; + 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 + LOGGER.log(Level.WARNING, "Failed to get unique path from: {0}", associated.getName()); //NON-NLS } if (sourcePath.isEmpty() == false) { @@ -235,7 +283,7 @@ public class BlackboardArtifactNode extends DisplayableItemNode { dataSourceStr = getRootParentName(); } } catch (TskCoreException ex) { - logger.log(Level.WARNING, "Failed to get image name from {0}", associated.getName()); //NON-NLS + LOGGER.log(Level.WARNING, "Failed to get image name from {0}", associated.getName()); //NON-NLS } if (dataSourceStr.isEmpty() == false) { @@ -258,7 +306,7 @@ public class BlackboardArtifactNode extends DisplayableItemNode { parentName = parent.getName(); } } catch (TskCoreException ex) { - logger.log(Level.WARNING, "Failed to get parent name from {0}", associated.getName()); //NON-NLS + LOGGER.log(Level.WARNING, "Failed to get parent name from {0}", associated.getName()); //NON-NLS return ""; } return parentName; @@ -270,13 +318,12 @@ public class BlackboardArtifactNode extends DisplayableItemNode { * * @param np NodeProperty to add */ - public void addNodeProperty(NodeProperty np) { + public void addNodeProperty(NodeProperty np) { if (null == customProperties) { //lazy create the list customProperties = new ArrayList<>(); } customProperties.add(np); - } /** @@ -296,7 +343,6 @@ public class BlackboardArtifactNode extends DisplayableItemNode { || attributeTypeID == ATTRIBUTE_TYPE.TSK_TAGGED_ARTIFACT.getTypeID() || attributeTypeID == ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT.getTypeID() || attributeTypeID == ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID()) { - continue; } 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() @@ -317,8 +363,8 @@ public class BlackboardArtifactNode extends DisplayableItemNode { map.put(attribute.getAttributeType().getDisplayName(), attribute.getDisplayString()); } } - } catch (TskException ex) { - logger.log(Level.SEVERE, "Getting attributes failed", ex); //NON-NLS + } catch (TskCoreException ex) { + LOGGER.log(Level.SEVERE, "Getting attributes failed", ex); //NON-NLS } } @@ -357,13 +403,15 @@ public class BlackboardArtifactNode extends DisplayableItemNode { private static Content getAssociatedContent(BlackboardArtifact artifact) { try { return artifact.getSleuthkitCase().getContentById(artifact.getObjectID()); - } catch (TskException ex) { - logger.log(Level.WARNING, "Getting file failed", ex); //NON-NLS + } 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; @@ -388,7 +436,7 @@ public class BlackboardArtifactNode extends DisplayableItemNode { } } if (keyword != null) { - boolean isRegexp = (regexp != null && !regexp.equals("")); + boolean isRegexp = StringUtils.isNotBlank(regexp); String origQuery; if (isRegexp) { origQuery = regexp; @@ -397,8 +445,8 @@ public class BlackboardArtifactNode extends DisplayableItemNode { } return highlightFactory.createInstance(objectId, keyword, isRegexp, origQuery); } - } catch (TskException ex) { - logger.log(Level.WARNING, "Failed to retrieve Blackboard Attributes", ex); //NON-NLS + } catch (TskCoreException ex) { + LOGGER.log(Level.WARNING, "Failed to retrieve Blackboard Attributes", ex); //NON-NLS } return null; } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactTagNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactTagNode.java index c0e5ae555a..fad92a6403 100755 --- a/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactTagNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactTagNode.java @@ -1,15 +1,15 @@ /* * Autopsy Forensic Browser - * + * * Copyright 2013-2014 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. @@ -18,6 +18,8 @@ */ package org.sleuthkit.autopsy.datamodel; +import java.text.MessageFormat; +import java.util.Arrays; import java.util.List; import java.util.logging.Level; import javax.swing.Action; @@ -27,6 +29,11 @@ import org.openide.util.NbBundle; import org.openide.util.lookup.Lookups; import org.sleuthkit.autopsy.actions.DeleteBlackboardArtifactTagAction; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +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.BlackboardArtifactTag; import org.sleuthkit.datamodel.TskCoreException; @@ -38,7 +45,7 @@ import org.sleuthkit.datamodel.TskCoreException; * either content or blackboard artifact tag nodes. */ public class BlackboardArtifactTagNode extends DisplayableItemNode { - + private static final Logger LOGGER = Logger.getLogger(BlackboardArtifactTagNode.class.getName()); private static final String ICON_PATH = "org/sleuthkit/autopsy/images/green-tag-icon-16.png"; //NON-NLS private final BlackboardArtifactTag tag; @@ -93,11 +100,37 @@ public class BlackboardArtifactTagNode extends DisplayableItemNode { @Override public Action[] getActions(boolean context) { List actions = DataModelActionsFactory.getActions(tag.getContent(), true); - for (Action a : super.getActions(true)) { - actions.add(a); + actions.addAll(Arrays.asList(super.getActions(context))); + + BlackboardArtifact artifact = getLookup().lookup(BlackboardArtifact.class); + //if this artifact has a time stamp add the action to view it in the timeline + try { + if (ViewArtifactInTimelineAction.hasSupportedTimeStamp(artifact)) { + actions.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()); } - actions.add(null); // Adds a menu item separator. - + + // if the artifact links to another file, add an action to go to that file + try { + AbstractFile c = findLinked(artifact); + if (c != null) { + actions.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) { + + actions.add(ViewFileInTimelineAction.createViewSourceFileAction(file)); + } + actions.add(DeleteBlackboardArtifactTagAction.getInstance()); return actions.toArray(new Action[0]); } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties b/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties index b8e7fa071f..ffe0797410 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties @@ -109,10 +109,6 @@ ExtractedContentNode.createSheet.name.name=Name ExtractedContentNode.createSheet.name.displayName=Name ExtractedContentNode.createSheet.name.desc=no description LocalFileNode.viewFileInDir.text=View File in Directory -FileNode.viewFileInDir.text=View File in Directory -FileNode.getActions.viewInNewWin.text=View in New Window -FileNode.getActions.openInExtViewer.text=Open in External Viewer -FileNode.getActions.searchFilesSameMD5.text=Search for files with the same MD5 hash FileSize.fileSizeRootNode.name=File Size FileSize.createSheet.name.name=Name FileSize.createSheet.name.displayName=Name diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/Bundle_ja.properties b/Core/src/org/sleuthkit/autopsy/datamodel/Bundle_ja.properties index 6b27e2b4eb..036ff29159 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/Bundle_ja.properties +++ b/Core/src/org/sleuthkit/autopsy/datamodel/Bundle_ja.properties @@ -88,7 +88,7 @@ ExtractedContentNode.name.text=\u62bd\u51fa\u3055\u308c\u305f\u30b3\u30f3\u30c6\ ExtractedContentNode.createSheet.name.name=\u540d\u524d ExtractedContentNode.createSheet.name.displayName=\u540d\u524d ExtractedContentNode.createSheet.name.desc=\u8aac\u660e\u304c\u3042\u308a\u307e\u305b\u3093 -FileNode.viewFileInDir.text=\u30c7\u30a3\u30ec\u30af\u30c8\u30ea\u5185\u306e\u30d5\u30a1\u30a4\u30eb\u3092\u8868\u793a +FileNode.getActions.viewFileInDir.text=\u30c7\u30a3\u30ec\u30af\u30c8\u30ea\u5185\u306e\u30d5\u30a1\u30a4\u30eb\u3092\u8868\u793a FileNode.getActions.viewInNewWin.text=\u65b0\u898f\u30a6\u30a3\u30f3\u30c9\u30a6\u306b\u8868\u793a FileNode.getActions.openInExtViewer.text=\u5916\u90e8\u30d3\u30e5\u30fc\u30a2\u3067\u958b\u304f FileNode.getActions.searchFilesSameMD5.text=\u540c\u3058MD5\u30cf\u30c3\u30b7\u30e5\u3092\u6301\u3064\u30d5\u30a1\u30a4\u30eb\u3092\u691c\u7d22 diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/ContentTagNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/ContentTagNode.java index 890e33b773..b444551a58 100755 --- a/Core/src/org/sleuthkit/autopsy/datamodel/ContentTagNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/ContentTagNode.java @@ -1,15 +1,15 @@ /* * Autopsy Forensic Browser - * + * * Copyright 2013 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. @@ -18,6 +18,7 @@ */ package org.sleuthkit.autopsy.datamodel; +import java.util.Arrays; import java.util.List; import java.util.logging.Level; import javax.swing.Action; @@ -27,6 +28,7 @@ import org.openide.util.NbBundle; import org.openide.util.lookup.Lookups; import org.sleuthkit.autopsy.actions.DeleteContentTagAction; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.timeline.actions.ViewFileInTimelineAction; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.ContentTag; @@ -107,12 +109,15 @@ class ContentTagNode extends DisplayableItemNode { @Override public Action[] getActions(boolean context) { List actions = DataModelActionsFactory.getActions(tag.getContent(), false); - for (Action a : super.getActions(true)) { - actions.add(a); + actions.addAll(Arrays.asList(super.getActions(context))); + + AbstractFile file = getLookup().lookup(AbstractFile.class); + if (file != null) { + actions.add(ViewFileInTimelineAction.createViewFileAction(file)); } actions.add(null); // Adds a menu item separator. actions.add(DeleteContentTagAction.getInstance()); - return actions.toArray(new Action[0]); + return actions.toArray(new Action[actions.size()]); } @Override diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/DirectoryNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/DirectoryNode.java index a96a1ba662..f15eefd22c 100755 --- a/Core/src/org/sleuthkit/autopsy/datamodel/DirectoryNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/DirectoryNode.java @@ -27,6 +27,7 @@ import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; import org.sleuthkit.autopsy.directorytree.ExtractAction; import org.sleuthkit.autopsy.directorytree.NewWindowViewAction; import org.sleuthkit.autopsy.directorytree.ViewContextAction; +import org.sleuthkit.autopsy.timeline.actions.ViewFileInTimelineAction; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.Directory; import org.sleuthkit.datamodel.TskData.TSK_FS_NAME_FLAG_ENUM; @@ -80,12 +81,13 @@ public class DirectoryNode extends AbstractFsContentNode { actions.add(null); // creates a menu separator } actions.add(new NewWindowViewAction(NbBundle.getMessage(this.getClass(), "DirectoryNode.viewInNewWin.text"), this)); + actions.add(ViewFileInTimelineAction.createViewFileAction(getContent())); actions.add(null); // creates a menu separator actions.add(ExtractAction.getInstance()); actions.add(null); // creates a menu separator actions.add(AddContentTagAction.getInstance()); actions.addAll(ContextMenuExtensionPoint.getActions()); - return actions.toArray(new Action[0]); + return actions.toArray(new Action[actions.size()]); } @Override diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNode.java index 40f7d52b62..2bc0a5538a 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNode.java @@ -21,6 +21,10 @@ package org.sleuthkit.autopsy.datamodel; import org.openide.nodes.AbstractNode; import org.openide.nodes.Children; import org.openide.util.Lookup; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.TskCoreException; /** * A DisplayableItem is any node in the Autopsy directory tree. All of the nodes @@ -47,4 +51,27 @@ public abstract class DisplayableItemNode extends AbstractNode { * Added to support this feature. */ // public abstract String getItemType(); + /** + * this code started as a cut and past of + * DataResultFilterNode.GetPopupActionsDisplayableItemNodeVisitor.findLinked(BlackboardArtifactNode + * ba) + * + * + * @param artifact + * + * @return + */ + static AbstractFile findLinked(BlackboardArtifact artifact) throws TskCoreException { + + BlackboardAttribute pathIDAttribute = artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH_ID)); + + if (pathIDAttribute != null) { + long contentID = pathIDAttribute.getValueLong(); + if (contentID != -1) { + return artifact.getSleuthkitCase().getAbstractFileById(contentID); + } + } + + return null; + } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/EmailExtracted.java b/Core/src/org/sleuthkit/autopsy/datamodel/EmailExtracted.java index fecf47d7cd..35af75738b 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/EmailExtracted.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/EmailExtracted.java @@ -72,6 +72,7 @@ public class EmailExtracted implements AutopsyVisitableItem { private final class EmailResults extends Observable { + // NOTE: the map can be accessed by multiple worker threads and needs to be synchronized private final Map>> accounts = new LinkedHashMap<>(); EmailResults() { @@ -79,20 +80,28 @@ public class EmailExtracted implements AutopsyVisitableItem { } public Set getAccounts() { - return accounts.keySet(); + synchronized (accounts) { + return accounts.keySet(); + } } public Set getFolders(String account) { - return accounts.get(account).keySet(); + synchronized (accounts) { + return accounts.get(account).keySet(); + } } public List getArtifactIds(String account, String folder) { - return accounts.get(account).get(folder); + synchronized (accounts) { + return accounts.get(account).get(folder); + } } @SuppressWarnings("deprecation") public void update() { - accounts.clear(); + synchronized (accounts) { + accounts.clear(); + } if (skCase == null) { return; } @@ -107,24 +116,26 @@ public class EmailExtracted implements AutopsyVisitableItem { try (CaseDbQuery dbQuery = skCase.executeQuery(query)) { ResultSet resultSet = dbQuery.getResultSet(); - while (resultSet.next()) { - final String path = resultSet.getString("value_text"); //NON-NLS - final long artifactId = resultSet.getLong("artifact_id"); //NON-NLS - final Map parsedPath = parsePath(path); - final String account = parsedPath.get(MAIL_ACCOUNT); - final String folder = parsedPath.get(MAIL_FOLDER); + synchronized (accounts) { + while (resultSet.next()) { + final String path = resultSet.getString("value_text"); //NON-NLS + final long artifactId = resultSet.getLong("artifact_id"); //NON-NLS + final Map parsedPath = parsePath(path); + final String account = parsedPath.get(MAIL_ACCOUNT); + final String folder = parsedPath.get(MAIL_FOLDER); - Map> folders = accounts.get(account); - if (folders == null) { - folders = new LinkedHashMap<>(); - accounts.put(account, folders); + Map> folders = accounts.get(account); + if (folders == null) { + folders = new LinkedHashMap<>(); + accounts.put(account, folders); + } + List messages = folders.get(folder); + if (messages == null) { + messages = new ArrayList<>(); + folders.put(folder, messages); + } + messages.add(artifactId); } - List messages = folders.get(folder); - if (messages == null) { - messages = new ArrayList<>(); - folders.put(folder, messages); - } - messages.add(artifactId); } } catch (TskCoreException | SQLException ex) { logger.log(Level.WARNING, "Cannot initialize email extraction: ", ex); //NON-NLS diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/FileNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/FileNode.java index ff671a5dec..b722e33161 100755 --- a/Core/src/org/sleuthkit/autopsy/datamodel/FileNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/FileNode.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011 - 2013 Basis Technology Corp. + * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -31,17 +31,20 @@ import org.sleuthkit.autopsy.directorytree.ExtractAction; import org.sleuthkit.autopsy.directorytree.HashSearchAction; import org.sleuthkit.autopsy.directorytree.NewWindowViewAction; import org.sleuthkit.autopsy.directorytree.ViewContextAction; +import org.sleuthkit.autopsy.timeline.actions.ViewFileInTimelineAction; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.TskData.TSK_DB_FILES_TYPE_ENUM; import org.sleuthkit.datamodel.TskData.TSK_FS_NAME_FLAG_ENUM; /** - * This class is used to represent the "Node" for the file. It may have derived - * files children. + * This class is the Node for an AbstractFile. It may have derived files + * children. */ public class FileNode extends AbstractFsContentNode { /** + * Constructor + * * @param file underlying Content */ public FileNode(AbstractFile file) { @@ -69,44 +72,41 @@ public class FileNode extends AbstractFsContentNode { } } - /** - * Right click action for this node - * - * @param popup - * - * @return - */ @Override + @NbBundle.Messages({ + "FileNode.getActions.viewFileInDir.text=View File in Directory", + "FileNode.getActions.viewInNewWin.text=View in New Window", + "FileNode.getActions.openInExtViewer.text=Open in External Viewer", + "FileNode.getActions.searchFilesSameMD5.text=Search for files with the same MD5 hash"}) public Action[] getActions(boolean popup) { List actionsList = new ArrayList<>(); for (Action a : super.getActions(true)) { actionsList.add(a); } if (!this.getDirectoryBrowseMode()) { - actionsList.add(new ViewContextAction(NbBundle.getMessage(FileNode.class, "FileNode.viewFileInDir.text"), this)); + actionsList.add(new ViewContextAction(Bundle.FileNode_getActions_viewFileInDir_text(), this)); actionsList.add(null); // creates a menu separator } - actionsList.add(new NewWindowViewAction( - NbBundle.getMessage(FileNode.class, "FileNode.getActions.viewInNewWin.text"), this)); - actionsList.add(new ExternalViewerAction( - NbBundle.getMessage(FileNode.class, "FileNode.getActions.openInExtViewer.text"), this)); + actionsList.add(new NewWindowViewAction(Bundle.FileNode_getActions_viewInNewWin_text(), this)); + actionsList.add(new ExternalViewerAction(Bundle.FileNode_getActions_openInExtViewer_text(), this)); + actionsList.add(ViewFileInTimelineAction.createViewFileAction(getContent())); + actionsList.add(null); // creates a menu separator actionsList.add(ExtractAction.getInstance()); - actionsList.add(new HashSearchAction( - NbBundle.getMessage(FileNode.class, "FileNode.getActions.searchFilesSameMD5.text"), this)); + actionsList.add(new HashSearchAction(Bundle.FileNode_getActions_searchFilesSameMD5_text(), this)); actionsList.add(null); // creates a menu separator actionsList.add(AddContentTagAction.getInstance()); actionsList.addAll(ContextMenuExtensionPoint.getActions()); - return actionsList.toArray(new Action[0]); + return actionsList.toArray(new Action[actionsList.size()]); } @Override - public T accept(ContentNodeVisitor< T> v) { + public T accept(ContentNodeVisitor v) { return v.visit(this); } @Override - public T accept(DisplayableItemNodeVisitor< T> v) { + public T accept(DisplayableItemNodeVisitor v) { return v.visit(this); } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/HashsetHits.java b/Core/src/org/sleuthkit/autopsy/datamodel/HashsetHits.java index 6ecaec6258..2264b95ac6 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/HashsetHits.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/HashsetHits.java @@ -78,7 +78,7 @@ public class HashsetHits implements AutopsyVisitableItem { private class HashsetResults extends Observable { // maps hashset name to list of artifacts for that set - + // NOTE: the map can be accessed by multiple worker threads and needs to be synchronized private final Map> hashSetHitsMap = new LinkedHashMap<>(); HashsetResults() { @@ -86,18 +86,25 @@ public class HashsetHits implements AutopsyVisitableItem { } List getSetNames() { - List names = new ArrayList<>(hashSetHitsMap.keySet()); + List names; + synchronized (hashSetHitsMap) { + names = new ArrayList<>(hashSetHitsMap.keySet()); + } Collections.sort(names); return names; } Set getArtifactIds(String hashSetName) { - return hashSetHitsMap.get(hashSetName); + synchronized (hashSetHitsMap) { + return hashSetHitsMap.get(hashSetName); + } } @SuppressWarnings("deprecation") final void update() { - hashSetHitsMap.clear(); + synchronized (hashSetHitsMap) { + hashSetHitsMap.clear(); + } if (skCase == null) { return; @@ -113,13 +120,15 @@ public class HashsetHits implements AutopsyVisitableItem { try (CaseDbQuery dbQuery = skCase.executeQuery(query)) { ResultSet resultSet = dbQuery.getResultSet(); - while (resultSet.next()) { - String setName = resultSet.getString("value_text"); //NON-NLS - long artifactId = resultSet.getLong("artifact_id"); //NON-NLS - if (!hashSetHitsMap.containsKey(setName)) { - hashSetHitsMap.put(setName, new HashSet()); + synchronized (hashSetHitsMap) { + while (resultSet.next()) { + String setName = resultSet.getString("value_text"); //NON-NLS + long artifactId = resultSet.getLong("artifact_id"); //NON-NLS + if (!hashSetHitsMap.containsKey(setName)) { + hashSetHitsMap.put(setName, new HashSet()); + } + hashSetHitsMap.get(setName).add(artifactId); } - hashSetHitsMap.get(setName).add(artifactId); } } catch (TskCoreException | SQLException ex) { logger.log(Level.WARNING, "SQL Exception occurred: ", ex); //NON-NLS diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/InterestingHits.java b/Core/src/org/sleuthkit/autopsy/datamodel/InterestingHits.java index 8faec30ab7..b07f5568c0 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/InterestingHits.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/InterestingHits.java @@ -64,20 +64,28 @@ public class InterestingHits implements AutopsyVisitableItem { private class InterestingResults extends Observable { + // NOTE: the map can be accessed by multiple worker threads and needs to be synchronized private final Map> interestingItemsMap = new LinkedHashMap<>(); public List getSetNames() { - List setNames = new ArrayList<>(interestingItemsMap.keySet()); + List setNames; + synchronized (interestingItemsMap) { + setNames = new ArrayList<>(interestingItemsMap.keySet()); + } Collections.sort(setNames); return setNames; } public Set getArtifactIds(String setName) { - return interestingItemsMap.get(setName); + synchronized (interestingItemsMap) { + return interestingItemsMap.get(setName); + } } public void update() { - interestingItemsMap.clear(); + synchronized (interestingItemsMap) { + interestingItemsMap.clear(); + } loadArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT); loadArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_ARTIFACT_HIT); setChanged(); @@ -103,14 +111,16 @@ public class InterestingHits implements AutopsyVisitableItem { + " AND blackboard_artifacts.artifact_type_id=" + artId; //NON-NLS try (CaseDbQuery dbQuery = skCase.executeQuery(query)) { - ResultSet resultSet = dbQuery.getResultSet(); - while (resultSet.next()) { - String value = resultSet.getString("value_text"); //NON-NLS - long artifactId = resultSet.getLong("artifact_id"); //NON-NLS - if (!interestingItemsMap.containsKey(value)) { - interestingItemsMap.put(value, new HashSet<>()); + synchronized (interestingItemsMap) { + ResultSet resultSet = dbQuery.getResultSet(); + while (resultSet.next()) { + String value = resultSet.getString("value_text"); //NON-NLS + long artifactId = resultSet.getLong("artifact_id"); //NON-NLS + if (!interestingItemsMap.containsKey(value)) { + interestingItemsMap.put(value, new HashSet<>()); + } + interestingItemsMap.get(value).add(artifactId); } - interestingItemsMap.get(value).add(artifactId); } } catch (TskCoreException | SQLException ex) { logger.log(Level.WARNING, "SQL Exception occurred: ", ex); //NON-NLS diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/KeywordHits.java b/Core/src/org/sleuthkit/autopsy/datamodel/KeywordHits.java index 987cf72caa..319d9ecf9f 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/KeywordHits.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/KeywordHits.java @@ -73,85 +73,94 @@ public class KeywordHits implements AutopsyVisitableItem { private final class KeywordResults extends Observable { // Map from listName/Type to Map of keyword to set of artifact Ids - private final Map>> topLevelMap; + // NOTE: the map can be accessed by multiple worker threads and needs to be synchronized + private final Map>> topLevelMap = new LinkedHashMap<>(); KeywordResults() { - topLevelMap = new LinkedHashMap<>(); update(); } List getListNames() { - List names = new ArrayList<>(topLevelMap.keySet()); - // this causes the "Single ..." terms to be in the middle of the results, - // which is wierd. Make a custom comparator or do something else to maek them on top - //Collections.sort(names); - return names; + synchronized (topLevelMap) { + List names = new ArrayList<>(topLevelMap.keySet()); + // this causes the "Single ..." terms to be in the middle of the results, + // which is wierd. Make a custom comparator or do something else to maek them on top + //Collections.sort(names); + return names; + } } List getKeywords(String listName) { - List keywords = new ArrayList<>(topLevelMap.get(listName).keySet()); + List keywords; + synchronized (topLevelMap) { + keywords = new ArrayList<>(topLevelMap.get(listName).keySet()); + } Collections.sort(keywords); return keywords; } Set getArtifactIds(String listName, String keyword) { - return topLevelMap.get(listName).get(keyword); + synchronized (topLevelMap) { + return topLevelMap.get(listName).get(keyword); + } } // populate maps based on artifactIds void populateMaps(Map> artifactIds) { - topLevelMap.clear(); + synchronized (topLevelMap) { + topLevelMap.clear(); - // map of list name to keword to artifact IDs - Map>> listsMap = new LinkedHashMap<>(); + // map of list name to keword to artifact IDs + Map>> listsMap = new LinkedHashMap<>(); - // Map from from literal keyword to artifact IDs - Map> literalMap = new LinkedHashMap<>(); + // Map from from literal keyword to artifact IDs + Map> literalMap = new LinkedHashMap<>(); - // Map from regex keyword artifact IDs - Map> regexMap = new LinkedHashMap<>(); + // Map from regex keyword artifact IDs + Map> regexMap = new LinkedHashMap<>(); - // top-level nodes - topLevelMap.put(SIMPLE_LITERAL_SEARCH, literalMap); - topLevelMap.put(SIMPLE_REGEX_SEARCH, regexMap); + // top-level nodes + topLevelMap.put(SIMPLE_LITERAL_SEARCH, literalMap); + topLevelMap.put(SIMPLE_REGEX_SEARCH, regexMap); - for (Map.Entry> art : artifactIds.entrySet()) { - long id = art.getKey(); - Map attributes = art.getValue(); + for (Map.Entry> art : artifactIds.entrySet()) { + long id = art.getKey(); + Map attributes = art.getValue(); - // I think we can use attributes.remove(...) here? - String listName = attributes.get(Long.valueOf(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID())); - String word = attributes.get(Long.valueOf(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD.getTypeID())); - String reg = attributes.get(Long.valueOf(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD_REGEXP.getTypeID())); + // I think we can use attributes.remove(...) here? + String listName = attributes.get(Long.valueOf(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID())); + String word = attributes.get(Long.valueOf(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD.getTypeID())); + String reg = attributes.get(Long.valueOf(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD_REGEXP.getTypeID())); - // part of a list - if (listName != null) { - if (listsMap.containsKey(listName) == false) { - listsMap.put(listName, new LinkedHashMap>()); + // part of a list + if (listName != null) { + if (listsMap.containsKey(listName) == false) { + listsMap.put(listName, new LinkedHashMap>()); + } + + Map> listMap = listsMap.get(listName); + if (listMap.containsKey(word) == false) { + listMap.put(word, new HashSet()); + } + + listMap.get(word).add(id); + } // regular expression, single term + else if (reg != null) { + if (regexMap.containsKey(reg) == false) { + regexMap.put(reg, new HashSet()); + } + regexMap.get(reg).add(id); + } // literal, single term + else { + if (literalMap.containsKey(word) == false) { + literalMap.put(word, new HashSet()); + } + literalMap.get(word).add(id); } - - Map> listMap = listsMap.get(listName); - if (listMap.containsKey(word) == false) { - listMap.put(word, new HashSet()); - } - - listMap.get(word).add(id); - } // regular expression, single term - else if (reg != null) { - if (regexMap.containsKey(reg) == false) { - regexMap.put(reg, new HashSet()); - } - regexMap.get(reg).add(id); - } // literal, single term - else { - if (literalMap.containsKey(word) == false) { - literalMap.put(word, new HashSet()); - } - literalMap.get(word).add(id); + topLevelMap.putAll(listsMap); } - topLevelMap.putAll(listsMap); } - + setChanged(); notifyObservers(); } diff --git a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbIngestModule.java b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbIngestModule.java index 13f0d9914d..98baebadd5 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbIngestModule.java +++ b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbIngestModule.java @@ -93,6 +93,9 @@ public class HashDbIngestModule implements FileIngestModule { @Override public void startUp(org.sleuthkit.autopsy.ingest.IngestJobContext context) throws IngestModuleException { jobId = context.getJobId(); + if (!hashDbManager.verifyAllDatabasesLoadedCorrectly()) { + throw new IngestModuleException("Could not load all hash databases"); + } updateEnabledHashSets(hashDbManager.getKnownBadFileHashSets(), knownBadHashSets); updateEnabledHashSets(hashDbManager.getKnownFileHashSets(), knownHashSets); diff --git a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbManager.java b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbManager.java index f60aa406f3..aea5ae1ce2 100755 --- a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbManager.java +++ b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbManager.java @@ -38,6 +38,7 @@ import org.apache.commons.io.FilenameUtils; import org.netbeans.api.progress.ProgressHandle; import org.openide.util.NbBundle; import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.core.RuntimeProperties; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.ingest.IngestManager; @@ -63,6 +64,7 @@ public class HashDbManager implements PropertyChangeListener { private Set hashSetPaths = new HashSet<>(); PropertyChangeSupport changeSupport = new PropertyChangeSupport(HashDbManager.class); private static final Logger logger = Logger.getLogger(HashDbManager.class.getName()); + private boolean allDatabasesLoadedCorrectly = false; /** * Property change event support In events: For both of these enums, the old @@ -93,6 +95,10 @@ public class HashDbManager implements PropertyChangeListener { public synchronized void removePropertyChangeListener(PropertyChangeListener listener) { changeSupport.removePropertyChangeListener(listener); } + + synchronized boolean verifyAllDatabasesLoadedCorrectly(){ + return allDatabasesLoadedCorrectly; + } private HashDbManager() { loadHashsetsConfiguration(); @@ -457,7 +463,7 @@ public class HashDbManager implements PropertyChangeListener { */ @Messages({"# {0} - database name", "HashDbManager.noDbPath.message=Couldn't get valid database path for: {0}"}) private void configureSettings(HashLookupSettings settings) { - boolean dbInfoRemoved = false; + allDatabasesLoadedCorrectly = true; List hashDbInfoList = settings.getHashDbInfo(); for (HashDbInfo hashDb : hashDbInfoList) { try { @@ -466,7 +472,7 @@ public class HashDbManager implements PropertyChangeListener { addHashDatabase(SleuthkitJNI.openHashDatabase(dbPath), hashDb.getHashSetName(), hashDb.getSearchDuringIngest(), hashDb.getSendIngestMessages(), hashDb.getKnownFilesType()); } else { logger.log(Level.WARNING, Bundle.HashDbManager_noDbPath_message(hashDb.getHashSetName())); - dbInfoRemoved = true; + allDatabasesLoadedCorrectly = false; } } catch (TskCoreException ex) { Logger.getLogger(HashDbManager.class.getName()).log(Level.SEVERE, "Error opening hash database", ex); //NON-NLS @@ -475,13 +481,23 @@ public class HashDbManager implements PropertyChangeListener { "HashDbManager.unableToOpenHashDbMsg", hashDb.getHashSetName()), NbBundle.getMessage(this.getClass(), "HashDbManager.openHashDbErr"), JOptionPane.ERROR_MESSAGE); - dbInfoRemoved = true; + allDatabasesLoadedCorrectly = false; } } - if (dbInfoRemoved) { + + /* NOTE: When RuntimeProperties.coreComponentsAreActive() is "false", + I don't think we should overwrite hash db settings file because we + were unable to load a database. The user should have to fix the issue or + remove the database from settings. Overwiting the settings effectively removes + the database from HashLookupSettings and the user may not know about this + because the dialogs are not being displayed. The next time user starts Autopsy, HashDB + will load without errors and the user may think that the problem was solved.*/ + if (!allDatabasesLoadedCorrectly && RuntimeProperties.coreComponentsAreActive()) { try { HashLookupSettings.writeSettings(new HashLookupSettings(this.knownHashSets, this.knownBadHashSets)); + allDatabasesLoadedCorrectly = true; } catch (HashLookupSettings.HashLookupSettingsException ex) { + allDatabasesLoadedCorrectly = false; logger.log(Level.SEVERE, "Could not overwrite hash database settings.", ex); } } @@ -496,7 +512,8 @@ public class HashDbManager implements PropertyChangeListener { // Give the user an opportunity to find the desired file. String newPath = null; - if (JOptionPane.showConfirmDialog(null, + if (RuntimeProperties.coreComponentsAreActive() && + JOptionPane.showConfirmDialog(null, NbBundle.getMessage(this.getClass(), "HashDbManager.dlgMsg.dbNotFoundAtLoc", hashSetName, configuredPath), NbBundle.getMessage(this.getClass(), "HashDbManager.dlgTitle.MissingDb"), diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ChronoFieldListCell.java b/Core/src/org/sleuthkit/autopsy/timeline/ChronoFieldListCell.java new file mode 100644 index 0000000000..6309fec8a5 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ChronoFieldListCell.java @@ -0,0 +1,41 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2011-2016 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.timeline; + +import java.time.temporal.ChronoField; +import java.util.Locale; +import javafx.scene.control.ListCell; +import org.apache.commons.lang3.StringUtils; + +/** + * A ListCell for a ChronoField + */ +public class ChronoFieldListCell extends ListCell { + + @Override + protected void updateItem(ChronoField item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + } else { + String displayName = item.getDisplayName(Locale.getDefault()); + setText(StringUtils.splitByCharacterTypeCamelCase(displayName)[0]); + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/OpenTimelineAction.java b/Core/src/org/sleuthkit/autopsy/timeline/OpenTimelineAction.java index 54d68cd7d9..3c07e5df53 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/OpenTimelineAction.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/OpenTimelineAction.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2013-16 Basis Technology Corp. + * Copyright 2011-2016 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,8 +19,6 @@ package org.sleuthkit.autopsy.timeline; import java.awt.Component; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; import java.io.IOException; import java.util.logging.Level; import javax.swing.ImageIcon; @@ -38,51 +36,64 @@ import org.sleuthkit.autopsy.core.Installer; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.coreutils.ThreadConfined; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.BlackboardArtifact; +/** + * An Action that opens the Timeline window. Has methods to open the window in + * various specific states (e.g., showing a specific artifact in the List View) + */ @ActionID(category = "Tools", id = "org.sleuthkit.autopsy.timeline.Timeline") @ActionRegistration(displayName = "#CTL_MakeTimeline", lazy = false) @ActionReferences(value = { @ActionReference(path = "Menu/Tools", position = 100), @ActionReference(path = "Toolbars/Case", position = 102)}) -public class OpenTimelineAction extends CallableSystemAction implements Presenter.Toolbar { +public final class OpenTimelineAction extends CallableSystemAction implements Presenter.Toolbar { + private static final long serialVersionUID = 1L; private static final Logger LOGGER = Logger.getLogger(OpenTimelineAction.class.getName()); - private static final boolean fxInited = Installer.isJavaFxInited(); + private static final boolean FX_INITED = Installer.isJavaFxInited(); private static TimeLineController timeLineController = null; - private JButton toolbarButton = new JButton(); + private final JButton toolbarButton = new JButton(getName(), + new ImageIcon(getClass().getResource("images/btn_icon_timeline_colorized_26.png"))); //NON-NLS + + /** + * Invalidate the reference to the controller so that a new one will be + * instantiated the next time this action is invoked + */ synchronized static void invalidateController() { timeLineController = null; } public OpenTimelineAction() { - toolbarButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - performAction(); - } - }); + toolbarButton.addActionListener(actionEvent -> performAction()); this.setEnabled(false); } @Override public boolean isEnabled() { /** - * we disabled the check to hasData() because if it is executed while a - * data source is being added, it blocks the edt + * We used to also check if Case.getCurrentCase().hasData() was true. We + * disabled that check because if it is executed while a data source is + * being added, it blocks the edt */ - return Case.isCaseOpen() && fxInited;// && Case.getCurrentCase().hasData(); + return Case.isCaseOpen() && FX_INITED; + } + + @Override + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + public void performAction() { + showTimeline(); } @NbBundle.Messages({ "OpenTimelineAction.settingsErrorMessage=Failed to initialize timeline settings.", "OpenTimeLineAction.msgdlg.text=Could not create timeline, there are no data sources."}) - @Override - @ThreadConfined(type = ThreadConfined.ThreadType.AWT) - public void performAction() { + synchronized private void showTimeline(AbstractFile file, BlackboardArtifact artifact) { try { Case currentCase = Case.getCurrentCase(); if (currentCase.hasData() == false) { @@ -97,7 +108,9 @@ public class OpenTimelineAction extends CallableSystemAction implements Presente timeLineController.shutDownTimeLine(); timeLineController = new TimeLineController(currentCase); } - timeLineController.openTimeLine(); + + timeLineController.showTimeLine(file, artifact); + } catch (IOException iOException) { MessageNotifyUtil.Message.error(Bundle.OpenTimelineAction_settingsErrorMessage()); LOGGER.log(Level.SEVERE, "Failed to initialize per case timeline settings.", iOException); @@ -107,9 +120,41 @@ public class OpenTimelineAction extends CallableSystemAction implements Presente } } + /** + * Open the Timeline window with the default initial view. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + public void showTimeline() { + showTimeline(null, null); + } + + /** + * Open the Timeline window with the given file selected in ListView. The + * user will be prompted to choose which timestamp to use for the file, and + * how much time to show around it. + * + * @param file The AbstractFile to show in the Timeline. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + public void showFileInTimeline(AbstractFile file) { + showTimeline(file, null); + } + + /** + * Open the Timeline window with the given artifact selected in ListView. + * The how much time to show around it. + * + * @param artifact The BlackboardArtifact to show in the Timeline. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + public void showArtifactInTimeline(BlackboardArtifact artifact) { + showTimeline(null, artifact); + } + @Override + @NbBundle.Messages("OpenTimelineAction.displayName=Timeline") public String getName() { - return NbBundle.getMessage(OpenTimelineAction.class, "CTL_MakeTimeline"); + return Bundle.OpenTimelineAction_displayName(); } @Override @@ -140,10 +185,6 @@ public class OpenTimelineAction extends CallableSystemAction implements Presente */ @Override public Component getToolbarPresenter() { - ImageIcon icon = new ImageIcon(getClass().getResource("images/btn_icon_timeline_colorized_26.png")); //NON-NLS - toolbarButton.setIcon(icon); - toolbarButton.setText(this.getName()); - return toolbarButton; } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/PromptDialogManager.java b/Core/src/org/sleuthkit/autopsy/timeline/PromptDialogManager.java index 0e1996456e..45b4bfe1ff 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/PromptDialogManager.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/PromptDialogManager.java @@ -143,7 +143,7 @@ class PromptDialogManager { * @param dialog The dialog to set the title bar icon for. */ @ThreadConfined(type = ThreadConfined.ThreadType.JFX) - static private void setDialogIcons(Dialog dialog) { + static void setDialogIcons(Dialog dialog) { ((Stage) dialog.getDialogPane().getScene().getWindow()).getIcons().setAll(AUTOPSY_ICON); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ShowInTimelineDialog.fxml b/Core/src/org/sleuthkit/autopsy/timeline/ShowInTimelineDialog.fxml new file mode 100644 index 0000000000..df2088a935 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ShowInTimelineDialog.fxml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ShowInTimelineDialog.java b/Core/src/org/sleuthkit/autopsy/timeline/ShowInTimelineDialog.java new file mode 100644 index 0000000000..82e479c4ab --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/ShowInTimelineDialog.java @@ -0,0 +1,362 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2011-2016 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.timeline; + +import java.io.IOException; +import java.net.URL; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoField; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.logging.Level; +import java.util.stream.Collectors; +import javafx.beans.binding.Bindings; +import javafx.beans.property.SimpleObjectProperty; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.ButtonType; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Dialog; +import javafx.scene.control.DialogPane; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.Spinner; +import javafx.scene.control.SpinnerValueFactory; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.image.ImageView; +import javafx.scene.layout.VBox; +import javafx.stage.Modality; +import javafx.util.converter.IntegerStringConverter; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; +import org.apache.commons.lang3.text.WordUtils; +import org.controlsfx.validation.ValidationMessage; +import org.controlsfx.validation.ValidationSupport; +import org.controlsfx.validation.Validator; +import org.joda.time.Interval; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent; +import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; +import org.sleuthkit.autopsy.timeline.events.ViewInTimelineRequestedEvent; +import org.sleuthkit.autopsy.timeline.utils.IntervalUtils; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * A Dialog that, given an AbstractFile or BlackBoardArtifact, allows the user + * to choose a specific event and a time range around it to show in the Timeline + * List View. + */ +final class ShowInTimelineDialog extends Dialog { + + private static final Logger LOGGER = Logger.getLogger(ShowInTimelineDialog.class.getName()); + + @NbBundle.Messages({"ShowInTimelineDialog.showTimelineButtonType.text=Show Timeline"}) + private static final ButtonType SHOW = new ButtonType(Bundle.ShowInTimelineDialog_showTimelineButtonType_text(), ButtonBar.ButtonData.OK_DONE); + + /** + * List of ChronoUnits the user can select from when choosing a time range + * to show. + */ + private static final List SCROLL_BY_UNITS = Arrays.asList( + ChronoField.YEAR, + ChronoField.MONTH_OF_YEAR, + ChronoField.DAY_OF_MONTH, + ChronoField.HOUR_OF_DAY, + ChronoField.MINUTE_OF_HOUR, + ChronoField.SECOND_OF_MINUTE); + + @FXML + private TableView eventTable; + + @FXML + private TableColumn typeColumn; + + @FXML + private TableColumn dateTimeColumn; + + @FXML + private Spinner amountSpinner; + + @FXML + private ComboBox unitComboBox; + + @FXML + private Label chooseEventLabel; + + private final VBox contentRoot = new VBox(); + + private final TimeLineController controller; + + private final ValidationSupport validationSupport = new ValidationSupport(); + + /** + * Common Private Constructor + * + * @param controller The controller for this Dialog. + * @param eventIDS A List of eventIDs to present to the user to choose + * from. + */ + @NbBundle.Messages({ + "ShowInTimelineDialog.amountValidator.message=The entered amount must only contain digits." + }) + private ShowInTimelineDialog(TimeLineController controller, List eventIDS) { + this.controller = controller; + + //load dialog content fxml + final String name = "nbres:/" + StringUtils.replace(ShowInTimelineDialog.class.getPackage().getName(), ".", "/") + "/ShowInTimelineDialog.fxml"; // NON-NLS + try { + FXMLLoader fxmlLoader = new FXMLLoader(new URL(name)); + fxmlLoader.setRoot(contentRoot); + fxmlLoader.setController(this); + + fxmlLoader.load(); + } catch (IOException ex) { + LOGGER.log(Level.SEVERE, "Unable to load FXML, node initialization may not be complete.", ex); //NON-NLS + } + //assert that fxml loading happened correctly + assert eventTable != null : "fx:id=\"eventTable\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'."; + assert typeColumn != null : "fx:id=\"typeColumn\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'."; + assert dateTimeColumn != null : "fx:id=\"dateTimeColumn\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'."; + assert amountSpinner != null : "fx:id=\"amountsSpinner\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'."; + assert unitComboBox != null : "fx:id=\"unitChoiceBox\" was not injected: check your FXML file 'ShowInTimelineDialog.fxml'."; + + //validat that spinner has a integer in the text field. + validationSupport.registerValidator(amountSpinner.getEditor(), false, + Validator.createPredicateValidator(NumberUtils::isDigits, Bundle.ShowInTimelineDialog_amountValidator_message())); + + //configure dialog properties + PromptDialogManager.setDialogIcons(this); + initModality(Modality.APPLICATION_MODAL); + + //add scenegraph loaded from fxml to this dialog. + DialogPane dialogPane = getDialogPane(); + dialogPane.setContent(contentRoot); + //add buttons to dialog + dialogPane.getButtonTypes().setAll(SHOW, ButtonType.CANCEL); + + ///configure dialog controls + amountSpinner.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 1000)); + amountSpinner.getValueFactory().setConverter(new IntegerStringConverter() { + /** + * Convert the String to an Integer using Integer.valueOf, but if + * that throws a NumberFormatException, reset the spinner to the + * last valid value. + * + * @param string The String to convert + * + * @return The Integer value of string. + */ + @Override + public Integer fromString(String string) { + try { + return super.fromString(string); + } catch (NumberFormatException ex) { + return amountSpinner.getValue(); + } + } + }); + + unitComboBox.setButtonCell(new ChronoFieldListCell()); + unitComboBox.setCellFactory(comboBox -> new ChronoFieldListCell()); + unitComboBox.getItems().setAll(SCROLL_BY_UNITS); + unitComboBox.getSelectionModel().select(ChronoField.MINUTE_OF_HOUR); + + typeColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue().getEventType())); + typeColumn.setCellFactory(param -> new TypeTableCell<>()); + + dateTimeColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue().getStartMillis())); + dateTimeColumn.setCellFactory(param -> new DateTimeTableCell<>()); + + //add events to table + eventTable.getItems().setAll(eventIDS.stream().map(controller.getEventsModel()::getEventById).collect(Collectors.toSet())); + eventTable.setPrefHeight(Math.min(200, 24 * eventTable.getItems().size() + 28)); + } + + /** + * Constructor for artifact based dialog. suppressed the choosing event + * aspect as each artifact is assumed to have only one associated event. + * + * @param controller The controller for this Dialog + * @param artifact The BlackboardArtifact to configure this dialog for. + */ + @NbBundle.Messages({"ShowInTimelineDialog.artifactTitle=View Result in Timeline."}) + ShowInTimelineDialog(TimeLineController controller, BlackboardArtifact artifact) { + //get events IDs from artifact + this(controller, controller.getEventsModel().getEventIDsForArtifact(artifact)); + + //hide instructional label and autoselect first(and only) event. + chooseEventLabel.setVisible(false); + chooseEventLabel.setManaged(false); + eventTable.getSelectionModel().select(0); + + //require validation of ammount spinner to enable show button + getDialogPane().lookupButton(SHOW).disableProperty().bind(validationSupport.invalidProperty()); + + //set result converter that does not require selection. + setResultConverter(buttonType -> (buttonType == SHOW) + ? makeEventInTimeRange(eventTable.getItems().get(0)) + : null + ); + setTitle(Bundle.ShowInTimelineDialog_artifactTitle()); + } + + /** + * Constructor for file based dialog. Allows the user to choose an event + * (MAC time) derived from the given file + * + * @param controller The controller for this Dialog. + * @param file The AbstractFile to configure this dialog for. + */ + @NbBundle.Messages({"# {0} - file path", + "ShowInTimelineDialog.fileTitle=View {0} in timeline.", + "ShowInTimelineDialog.eventSelectionValidator.message=You must select an event."}) + ShowInTimelineDialog(TimeLineController controller, AbstractFile file) { + this(controller, controller.getEventsModel().getEventIDsForFile(file, false)); + + /* + * since ValidationSupport does not support list selection, we will + * manually apply and remove decoration in response to selection + * property changes. + */ + eventTable.getSelectionModel().selectedItemProperty().isNull().addListener((selectedItemNullProperty, wasNull, isNull) -> { + if (isNull) { + validationSupport.getValidationDecorator().applyValidationDecoration( + ValidationMessage.error(eventTable, Bundle.ShowInTimelineDialog_eventSelectionValidator_message())); + } else { + validationSupport.getValidationDecorator().removeDecorations(eventTable); + } + }); + + //require selection and validation of ammount spinner to enable show button + getDialogPane().lookupButton(SHOW).disableProperty().bind(Bindings.or( + validationSupport.invalidProperty(), + eventTable.getSelectionModel().selectedItemProperty().isNull() + )); + + //set result converter that uses selection. + setResultConverter(buttonType -> (buttonType == SHOW) + ? makeEventInTimeRange(eventTable.getSelectionModel().getSelectedItem()) + : null + ); + + setTitle(Bundle.ShowInTimelineDialog_fileTitle(StringUtils.abbreviateMiddle(getContentPathSafe(file), " ... ", 50))); + } + + /** + * Get the unique path for the content, or if that fails, just return the + * name. + * + * NOTE: This was copied from IamgeUtils and should be refactored to avoid + * duplication. + * + * @param content + * + * @return the unique path for the content, or if that fails, just the name. + */ + static String getContentPathSafe(Content content) { + try { + return content.getUniquePath(); + } catch (TskCoreException tskCoreException) { + String contentName = content.getName(); + LOGGER.log(Level.SEVERE, "Failed to get unique path for " + contentName, tskCoreException); //NON-NLS + return contentName; + } + } + + /** + * Construct this Dialog's "result" from the given event. + * + * @param selectedEvent The SingleEvent to include in the EventInTimeRange + * + * @return The EventInTimeRange that is the "result" of this dialog. + */ + private ViewInTimelineRequestedEvent makeEventInTimeRange(SingleEvent selectedEvent) { + Duration selectedDuration = unitComboBox.getSelectionModel().getSelectedItem().getBaseUnit().getDuration().multipliedBy(amountSpinner.getValue()); + Interval range = IntervalUtils.getIntervalAround(Instant.ofEpochMilli(selectedEvent.getStartMillis()), selectedDuration); + return new ViewInTimelineRequestedEvent(Collections.singleton(selectedEvent.getEventID()), range); + } + + /** + * ListCell that shows a ChronoUnit + */ + static private class ChronoUnitListCell extends ListCell { + + @Override + protected void updateItem(ChronoUnit item, boolean empty) { + super.updateItem(item, empty); + + if (empty || item == null) { + setText(null); + } else { + setText(WordUtils.capitalizeFully(item.toString())); + } + } + } + + /** + * TableCell that shows a formatted date/time for a given millisecond since + * the unix epoch + * + * @param Anything + */ + static private class DateTimeTableCell extends TableCell { + + @Override + protected void updateItem(Long item, boolean empty) { + super.updateItem(item, empty); + + if (item == null || empty) { + setText(null); + } else { + setText(TimeLineController.getZonedFormatter().print(item)); + } + } + } + + /** + * TableCell that shows a EventType including the associated icon. + * + * @param Anything + */ + static private class TypeTableCell extends TableCell { + + @Override + protected void updateItem(EventType item, boolean empty) { + super.updateItem(item, empty); + + if (item == null || empty) { + setText(null); + setGraphic(null); + } else { + setText(item.getDisplayName()); + setGraphic(new ImageView(item.getFXImage())); + } + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java index ed3f3b307a..11907374c6 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineController.java @@ -18,6 +18,7 @@ */ package org.sleuthkit.autopsy.timeline; +import com.google.common.eventbus.EventBus; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.IOException; @@ -26,6 +27,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.TimeZone; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -81,6 +83,7 @@ import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.TimeLineEvent; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; import org.sleuthkit.autopsy.timeline.db.EventsRepository; +import org.sleuthkit.autopsy.timeline.events.ViewInTimelineRequestedEvent; import org.sleuthkit.autopsy.timeline.filters.DescriptionFilter; import org.sleuthkit.autopsy.timeline.filters.RootFilter; import org.sleuthkit.autopsy.timeline.filters.TypeFilter; @@ -89,6 +92,8 @@ import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; import org.sleuthkit.autopsy.timeline.zooming.TimeUnits; import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.BlackboardArtifact; /** * Controller in the MVC design along with FilteredEventsModel TimeLineView. @@ -141,6 +146,7 @@ public class TimeLineController { private final ReadOnlyStringWrapper taskTitle = new ReadOnlyStringWrapper(); private final ReadOnlyStringWrapper statusMessage = new ReadOnlyStringWrapper(); + private EventBus eventbus = new EventBus("TimeLineController_EventBus"); /** * Status is a string that will be displayed in the status bar as a kind of @@ -218,7 +224,7 @@ public class TimeLineController { //selected events (ie shown in the result viewer) @GuardedBy("this") - private final ObservableList selectedEventIDs = FXCollections.synchronizedObservableList(FXCollections.observableArrayList()); + private final ObservableList selectedEventIDs = FXCollections.observableArrayList(); @GuardedBy("this") private final ReadOnlyObjectWrapper selectedTimeRange = new ReadOnlyObjectWrapper<>(); @@ -384,7 +390,9 @@ public class TimeLineController { /** * Rebuild the repo using the given repoBuilder (expected to be a member * reference to EventsRepository.rebuildRepository() or - * EventsRepository.rebuildTags()) and display the ui when it is done. + * EventsRepository.rebuildTags()) and display the UI when it is done. If + * either file or artifact is not null the user will be prompted to choose a + * derived event and time range to show in the Timeline List View. * * @param repoBuilder A Function from Consumer to * CancellationProgressTask. Ie a function that @@ -395,12 +403,16 @@ public class TimeLineController { * EventsRepository.rebuildTags() * @param markDBNotStale After the repo is rebuilt should it be marked not * stale + * @param file The AbstractFile from which to choose an event to + * show in the List View. + * @param artifact The BlackboardArtifact to show in the List View. */ @ThreadConfined(type = ThreadConfined.ThreadType.JFX) @NbBundle.Messages({ "TimeLineController.setIngestRunning.errMsgRunning=Failed to mark the timeline db as populated while ingest was running. Some results may be out of date or missing.", "TimeLinecontroller.setIngestRunning.errMsgNotRunning=Failed to mark the timeline db as populated while ingest was not running. Some results may be out of date or missing."}) - private void rebuildRepoHelper(Function, CancellationProgressTask> repoBuilder, Boolean markDBNotStale) { + private void rebuildRepoHelper(Function, CancellationProgressTask> repoBuilder, Boolean markDBNotStale, AbstractFile file, BlackboardArtifact artifact) { + boolean ingestRunning = IngestManager.getInstance().isIngestRunning(); //if there is an existing prompt or progressdialog, just show that if (promptDialogManager.bringCurrentDialogToFront()) { @@ -412,34 +424,51 @@ public class TimeLineController { return; //if they cancel, do nothing. } - //get a task that rebuilds the repo with the bellow state listener attached - final CancellationProgressTask rebuildRepositoryTask = repoBuilder.apply(newSate -> { - //this will be on JFX thread - switch (newSate) { - case SUCCEEDED: - /* - * Record if ingest was running the last time the db was - * rebuilt, and hence it might stale. - */ - try { - perCaseTimelineProperties.setIngestRunning(ingestRunning); - } catch (IOException ex) { - MessageNotifyUtil.Notify.error(Bundle.Timeline_dialogs_title(), - ingestRunning ? Bundle.TimeLineController_setIngestRunning_errMsgRunning() - : Bundle.TimeLinecontroller_setIngestRunning_errMsgNotRunning()); - LOGGER.log(Level.SEVERE, "Error marking the ingest state while the timeline db was populated.", ex); //NON-NLS - } - if (markDBNotStale) { - setEventsDBStale(false); - filteredEvents.postDBUpdated(); - } - SwingUtilities.invokeLater(this::showWindow); - break; - - case FAILED: - case CANCELLED: - setEventsDBStale(true); - break; + //get a task that rebuilds the repo with the below state listener attached + final CancellationProgressTask rebuildRepositoryTask; + rebuildRepositoryTask = repoBuilder.apply(new Consumer() { + @Override + public void accept(Worker.State newSate) { + //this will be on JFX thread + switch (newSate) { + case SUCCEEDED: + /* + * Record if ingest was running the last time the db was + * rebuilt, and hence it might stale. + */ + try { + perCaseTimelineProperties.setIngestRunning(ingestRunning); + } catch (IOException ex) { + MessageNotifyUtil.Notify.error(Bundle.Timeline_dialogs_title(), + ingestRunning ? Bundle.TimeLineController_setIngestRunning_errMsgRunning() + : Bundle.TimeLinecontroller_setIngestRunning_errMsgNotRunning()); + LOGGER.log(Level.SEVERE, "Error marking the ingest state while the timeline db was populated.", ex); //NON-NLS + } + if (markDBNotStale) { + setEventsDBStale(false); + filteredEvents.postDBUpdated(); + } + if (file == null && artifact == null) { + SwingUtilities.invokeLater(TimeLineController.this::showWindow); + TimeLineController.this.showFullRange(); + } else { + //prompt user to pick specific event and time range + ShowInTimelineDialog showInTimelineDilaog = + (file == null) + ? new ShowInTimelineDialog(TimeLineController.this, artifact) + : new ShowInTimelineDialog(TimeLineController.this, file); + Optional dialogResult = showInTimelineDilaog.showAndWait(); + dialogResult.ifPresent(viewInTimelineRequestedEvent -> { + SwingUtilities.invokeLater(TimeLineController.this::showWindow); + showInListView(viewInTimelineRequestedEvent); //show requested event in list view + }); + } + break; + case FAILED: + case CANCELLED: + setEventsDBStale(true); + break; + } } }); @@ -456,27 +485,62 @@ public class TimeLineController { */ @ThreadConfined(type = ThreadConfined.ThreadType.JFX) public void rebuildRepo() { - rebuildRepoHelper(eventsRepository::rebuildRepository, true); + rebuildRepo(null, null); + } + + /** + * Rebuild the entire repo in the background, and show the timeline when + * done. + * + * @param file The AbstractFile from which to choose an event to show in + * the List View. + * @param artifact The BlackboardArtifact to show in the List View. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + private void rebuildRepo(AbstractFile file, BlackboardArtifact artifact) { + rebuildRepoHelper(eventsRepository::rebuildRepository, true, file, artifact); } /** * Drop the tags table and rebuild it in the background, and show the * timeline when done. + * + * @param file The AbstractFile from which to choose an event to show in + * the List View. + * @param artifact The BlackboardArtifact to show in the List View. */ @ThreadConfined(type = ThreadConfined.ThreadType.JFX) - void rebuildTagsTable() { - rebuildRepoHelper(eventsRepository::rebuildTags, false); + private void rebuildTagsTable(AbstractFile file, BlackboardArtifact artifact) { + rebuildRepoHelper(eventsRepository::rebuildTags, false, file, artifact); } /** * Show the entire range of the timeline. */ - public boolean showFullRange() { + private boolean showFullRange() { synchronized (filteredEvents) { return pushTimeRange(filteredEvents.getSpanningInterval()); } } + /** + * Show the events and the amount of time indicated in the given + * ViewInTimelineRequestedEvent in the List View. + * + * @param requestEvent Contains the ID of the requested events and the + * timerange to show. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.JFX) + private void showInListView(ViewInTimelineRequestedEvent requestEvent) { + synchronized (filteredEvents) { + setViewMode(ViewMode.LIST); + selectEventIDs(requestEvent.getEventIDs()); + if (pushTimeRange(requestEvent.getInterval()) == false) { + eventbus.post(requestEvent); + } + } + } + /** * "Shut down" Timeline. Remove all the case and ingest listers. Close the * timeline window. @@ -497,9 +561,13 @@ public class TimeLineController { /** * Add the case and ingest listeners, prompt for rebuilding the database if * necessary, and show the timeline window. + * + * @param file The AbstractFile from which to choose an event to show in + * the List View. + * @param artifact The BlackboardArtifact to show in the List View. */ @ThreadConfined(type = ThreadConfined.ThreadType.AWT) - void openTimeLine() { + void showTimeLine(AbstractFile file, BlackboardArtifact artifact) { // listen for case changes (specifically images being added, and case changes). if (Case.isCaseOpen() && !listeningToAutopsy) { IngestManager.getInstance().addIngestModuleEventListener(ingestModuleListener); @@ -508,18 +576,21 @@ public class TimeLineController { listeningToAutopsy = true; } - Platform.runLater(this::promptForRebuild); + Platform.runLater(() -> promptForRebuild(file, artifact)); } /** * Prompt the user to confirm rebuilding the db. Checks if a database * rebuild is necessary and includes the reasons in the prompt. If the user * confirms, rebuilds the database. Shows the timeline window when the - * rebuild is done, or immediately if the rebuild is not confirmed. F + * rebuild is done, or immediately if the rebuild is not confirmed. + * + * @param file The AbstractFile from which to choose an event to show in + * the List View. + * @param artifact The BlackboardArtifact to show in the List View. */ @ThreadConfined(type = ThreadConfined.ThreadType.JFX) - private void promptForRebuild() { - + private void promptForRebuild(AbstractFile file, BlackboardArtifact artifact) { //if there is an existing prompt or progressdialog, just show that if (promptDialogManager.bringCurrentDialogToFront()) { return; @@ -527,7 +598,7 @@ public class TimeLineController { //if the repo is empty just (re)build it with out asking, the user can always cancel part way through if (eventsRepository.countAllEvents() == 0) { - rebuildRepo(); + rebuildRepo(file, artifact); return; } @@ -535,7 +606,7 @@ public class TimeLineController { List rebuildReasons = getRebuildReasons(); if (false == rebuildReasons.isEmpty()) { if (promptDialogManager.confirmRebuild(rebuildReasons)) { - rebuildRepo(); + rebuildRepo(file, artifact); return; } } @@ -547,7 +618,7 @@ public class TimeLineController { * * //TODO: can we check the tags to see if we need to do this? */ - rebuildTagsTable(); + rebuildTagsTable(file, artifact); } /** @@ -599,8 +670,7 @@ public class TimeLineController { */ synchronized public void pushPeriod(ReadablePeriod period) { synchronized (filteredEvents) { - final DateTime middleOf = IntervalUtils.middleOf(filteredEvents.timeRangeProperty().get()); - pushTimeRange(IntervalUtils.getIntervalAround(middleOf, period)); + pushTimeRange(IntervalUtils.getIntervalAroundMiddle(filteredEvents.getTimeRange(), period)); } } @@ -620,31 +690,6 @@ public class TimeLineController { pushTimeRange(new Interval(start, end)); } - public void selectEventIDs(Collection events) { - final LoggedTask selectEventIDsTask = new LoggedTask("Select Event IDs", true) { //NON-NLS - @Override - protected Interval call() throws Exception { - return filteredEvents.getSpanningInterval(events); - } - - @Override - protected void succeeded() { - super.succeeded(); - try { - synchronized (TimeLineController.this) { - selectedTimeRange.set(get()); - selectedEventIDs.setAll(events); - - } - } catch (InterruptedException | ExecutionException ex) { - LOGGER.log(Level.SEVERE, getTitle() + " Unexpected error", ex); //NON-NLS - } - } - }; - - monitorTask(selectEventIDsTask); - } - /** * Show the timeline TimeLineTopComponent. This method will construct a new * instance of TimeLineTopComponent if necessary. @@ -672,15 +717,35 @@ public class TimeLineController { } } - @SuppressWarnings("AssignmentToMethodParameter") //clamp timerange to case + /** + * Set the new interval to view, and record it in the history. The interval + * will be clamped to the span of events in the current case. + * + * @param timeRange The Interval to view. + * + * @return True if the interval was changed. False if the interval was the + * same as the existing one and no change happened. + */ synchronized public boolean pushTimeRange(Interval timeRange) { - timeRange = this.filteredEvents.getSpanningInterval().overlap(timeRange); + //clamp timerange to case + Interval clampedTimeRange; + if (timeRange == null) { + clampedTimeRange = this.filteredEvents.getSpanningInterval(); + } else { + Interval spanningInterval = this.filteredEvents.getSpanningInterval(); + if (spanningInterval.overlaps(timeRange)) { + clampedTimeRange = spanningInterval.overlap(timeRange); + } else { + clampedTimeRange = spanningInterval; + } + } + ZoomParams currentZoom = filteredEvents.zoomParametersProperty().get(); if (currentZoom == null) { - advance(InitialZoomState.withTimeRange(timeRange)); + advance(InitialZoomState.withTimeRange(clampedTimeRange)); return true; - } else if (currentZoom.hasTimeRange(timeRange) == false) { - advance(currentZoom.withTimeRange(timeRange)); + } else if (currentZoom.hasTimeRange(clampedTimeRange) == false) { + advance(currentZoom.withTimeRange(clampedTimeRange)); return true; } else { return false; @@ -748,6 +813,17 @@ public class TimeLineController { historyManager.advance(newState); } + /** + * Select the given event IDs and set their spanning interval as the + * selected time range. + * + * @param eventIDs The eventIDs to select + */ + synchronized public void selectEventIDs(Collection eventIDs) { + selectedTimeRange.set(filteredEvents.getSpanningInterval(eventIDs)); + selectedEventIDs.setAll(eventIDs); + } + public void selectTimeAndType(Interval interval, EventType type) { final Interval timeRange = filteredEvents.getSpanningInterval().overlap(interval); @@ -833,8 +909,28 @@ public class TimeLineController { } } + /** + * Register the given object to receive events. + * + * @param o The object to register. Must implement public methods annotated + * with Subscribe. + */ + synchronized public void registerForEvents(Object o) { + eventbus.register(o); + } + + /** + * Un-register the given object, so it no longer receives events. + * + * @param o The object to un-register. + */ + synchronized public void unRegisterForEvents(Object o) { + eventbus.unregister(0); + } + static synchronized public void setTimeZone(TimeZone timeZone) { TimeLineController.timeZone.set(timeZone); + } /** diff --git a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java index 1ca594fa14..bd72a08da1 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/TimeLineTopComponent.java @@ -25,7 +25,6 @@ import java.util.logging.Level; import javafx.application.Platform; import javafx.beans.InvalidationListener; import javafx.beans.Observable; -import javafx.collections.ObservableList; import javafx.scene.Scene; import javafx.scene.control.SplitPane; import javafx.scene.control.Tab; @@ -98,7 +97,7 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer private final InvalidationListener selectedEventsListener = new InvalidationListener() { @Override public void invalidated(Observable observable) { - ObservableList selectedEventIDs = controller.getSelectedEventIDs(); + List selectedEventIDs = controller.getSelectedEventIDs(); //depending on the active view mode, we either update the dataResultPanel, or update the contentViewerPanel directly. switch (controller.getViewMode()) { @@ -124,7 +123,7 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer LOGGER.log(Level.SEVERE, "Selecting the event node was vetoed.", ex); // NON-NLS } //if there is only one event selected push it into content viewer. - if (selectedEventIDs.size() == 1) { + if (childArray.length == 1) { contentViewerPanel.setNode(childArray[0]); } else { contentViewerPanel.setNode(null); @@ -137,7 +136,7 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer LOGGER.log(Level.SEVERE, "Failed to lookup Sleuthkit object backing a SingleEvent.", ex); // NON-NLS Platform.runLater(() -> { Notifications.create() - .owner(jFXViewPanel.getScene().getWindow()) + .owner(jFXViewPanel.getScene().getWindow()) .text(Bundle.TimelineTopComponent_selectedEventListener_errorMsg()) .showError(); }); @@ -159,6 +158,36 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer } }; + private void syncViewMode() { + switch (controller.getViewMode()) { + case COUNTS: + case DETAIL: + /* + * For counts and details mode, restore the result table at the + * bottom left. + */ + SwingUtilities.invokeLater(() -> { + splitYPane.remove(contentViewerPanel); + if ((horizontalSplitPane.getParent() == splitYPane) == false) { + splitYPane.setBottomComponent(horizontalSplitPane); + horizontalSplitPane.setRightComponent(contentViewerPanel); + } + }); + break; + case LIST: + /* + * For list mode, remove the result table, and let the content + * viewer expand across the bottom. + */ + SwingUtilities.invokeLater(() -> { + splitYPane.setBottomComponent(contentViewerPanel); + }); + break; + default: + throw new UnsupportedOperationException("Unknown ViewMode: " + controller.getViewMode()); + } + } + /** * Constructor * @@ -190,35 +219,8 @@ public final class TimeLineTopComponent extends TopComponent implements Explorer controller.getSelectedEventIDs().addListener(selectedEventsListener); //Listen to ViewMode and adjust GUI componenets as needed. - controller.viewModeProperty().addListener(viewMode -> { - switch (controller.getViewMode()) { - case COUNTS: - case DETAIL: - /* - * For counts and details mode, restore the result table at - * the bottom left. - */ - SwingUtilities.invokeLater(() -> { - splitYPane.remove(contentViewerPanel); - if ((horizontalSplitPane.getParent() == splitYPane) == false) { - splitYPane.setBottomComponent(horizontalSplitPane); - horizontalSplitPane.setRightComponent(contentViewerPanel); - } - }); - break; - case LIST: - /* - * For list mode, remove the result table, and let the - * content viewer expand across the bottom. - */ - SwingUtilities.invokeLater(() -> { - splitYPane.setBottomComponent(contentViewerPanel); - }); - break; - default: - throw new UnsupportedOperationException("Unknown ViewMode: " + controller.getViewMode()); - } - }); + controller.viewModeProperty().addListener(viewMode -> syncViewMode()); + syncViewMode(); } /** diff --git a/Core/src/org/sleuthkit/autopsy/timeline/actions/ViewArtifactInTimelineAction.java b/Core/src/org/sleuthkit/autopsy/timeline/actions/ViewArtifactInTimelineAction.java new file mode 100644 index 0000000000..4cd07f2597 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/actions/ViewArtifactInTimelineAction.java @@ -0,0 +1,81 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2011-2016 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.timeline.actions; + +import java.awt.event.ActionEvent; +import java.util.Set; +import java.util.stream.Collectors; +import javax.swing.AbstractAction; +import org.openide.util.NbBundle; +import org.openide.util.actions.SystemAction; +import org.sleuthkit.autopsy.timeline.OpenTimelineAction; +import org.sleuthkit.autopsy.timeline.datamodel.eventtype.ArtifactEventType; +import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * An action that shows the given artifact in the Timeline List View. + */ +public final class ViewArtifactInTimelineAction extends AbstractAction { + + private static final long serialVersionUID = 1L; + + private static final Set ARTIFACT_EVENT_TYPES = + EventType.allTypes.stream() + .filter((EventType t) -> t instanceof ArtifactEventType) + .map(ArtifactEventType.class::cast) + .collect(Collectors.toSet()); + + private final BlackboardArtifact artifact; + + @NbBundle.Messages({"ViewArtifactInTimelineAction.displayName=View Result in Timeline... "}) + public ViewArtifactInTimelineAction(BlackboardArtifact artifact) { + super(Bundle.ViewArtifactInTimelineAction_displayName()); + this.artifact = artifact; + } + + @Override + public void actionPerformed(ActionEvent e) { + SystemAction.get(OpenTimelineAction.class).showArtifactInTimeline(artifact); + } + + /** + * Does the given artifact have a type that Timeline supports, and does it + * have a positive timestamp in the supported attribute? + * + * @param artifact The artifact to test for a supported timestamp + * + * @return True if this artifact has a timestamp supported by Timeline. + */ + public static boolean hasSupportedTimeStamp(BlackboardArtifact artifact) throws TskCoreException { + //see if the given artifact is a supported type ... + for (ArtifactEventType artEventType : ARTIFACT_EVENT_TYPES) { + if (artEventType.getArtifactTypeID() == artifact.getArtifactTypeID()) { + //... and has a non-bogus timestamp in the supported attribute + BlackboardAttribute attribute = artifact.getAttribute(artEventType.getDateTimeAttributeType()); + if (null != attribute && attribute.getValueLong() > 0) { + return true; + } + } + } + return false; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/actions/ViewFileInTimelineAction.java b/Core/src/org/sleuthkit/autopsy/timeline/actions/ViewFileInTimelineAction.java new file mode 100644 index 0000000000..f90837e1f6 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/actions/ViewFileInTimelineAction.java @@ -0,0 +1,57 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2011-2016 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.timeline.actions; + +import java.awt.event.ActionEvent; +import javax.swing.AbstractAction; +import org.openide.util.NbBundle; +import org.openide.util.actions.SystemAction; +import org.sleuthkit.autopsy.timeline.OpenTimelineAction; +import org.sleuthkit.datamodel.AbstractFile; + +/** + * An action to prompt the user to pick an timestamp/event associated with the + * given file and show it in the Timeline List View + */ +public final class ViewFileInTimelineAction extends AbstractAction { + + private static final long serialVersionUID = 1L; + + private final AbstractFile file; + + private ViewFileInTimelineAction(AbstractFile file, String displayName) { + super(displayName); + this.file = file; + } + + @NbBundle.Messages({"ViewFileInTimelineAction.viewFile.displayName=View File in Timeline... "}) + public static ViewFileInTimelineAction createViewFileAction(AbstractFile file) { + return new ViewFileInTimelineAction(file, Bundle.ViewFileInTimelineAction_viewFile_displayName()); + } + + @NbBundle.Messages({"ViewFileInTimelineAction.viewSourceFile.displayName=View Source File in Timeline... "}) + public static ViewFileInTimelineAction createViewSourceFileAction(AbstractFile file) { + return new ViewFileInTimelineAction(file, Bundle.ViewFileInTimelineAction_viewSourceFile_displayName()); + } + + @Override + public void actionPerformed(ActionEvent e) { + SystemAction.get(OpenTimelineAction.class).showFileInTimeline(file); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/CombinedEvent.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/CombinedEvent.java index 71e022e65b..bdd1f1eaec 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/CombinedEvent.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/CombinedEvent.java @@ -18,11 +18,11 @@ */ package org.sleuthkit.autopsy.timeline.datamodel; -import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.Set; +import org.python.google.common.collect.ImmutableSet; import org.sleuthkit.autopsy.timeline.datamodel.eventtype.EventType; /** @@ -98,8 +98,8 @@ public class CombinedEvent { * * @return The event IDs of the combined events. */ - public Collection getEventIDs() { - return eventTypeMap.values(); + public ImmutableSet getEventIDs() { + return ImmutableSet.copyOf(eventTypeMap.values()); } /** diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java index 6227256130..b2a4704aa8 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/FilteredEventsModel.java @@ -62,6 +62,7 @@ import org.sleuthkit.autopsy.timeline.filters.TypeFilter; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; +import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardArtifactTag; import org.sleuthkit.datamodel.Content; @@ -113,7 +114,7 @@ public final class FilteredEventsModel { @GuardedBy("this") private final ReadOnlyObjectWrapper requestedZoomParamters = new ReadOnlyObjectWrapper<>(); - private final EventBus eventbus = new EventBus("Event_Repository_EventBus"); //NON-NLS + private final EventBus eventbus = new EventBus("FilteredEventsModel_EventBus"); //NON-NLS /** * The underlying repo for events. Atomic access to repo is synchronized @@ -429,6 +430,38 @@ public final class FilteredEventsModel { return false; } + /** + * Get a List of event IDs for the events that are derived from the given + * file. + * + * @param file The AbstractFile to get derived event IDs + * for. + * @param includeDerivedArtifacts If true, also get event IDs for events + * derived from artifacts derived form this + * file. If false, only gets events derived + * directly from this file (file system + * timestamps). + * + * @return A List of event IDs for the events that are derived from the + * given file. + */ + public List getEventIDsForFile(AbstractFile file, boolean includedDerivedArtifacts) { + return repo.getEventIDsForFile(file, includedDerivedArtifacts); + } + + /** + * Get a List of event IDs for the events that are derived from the given + * artifact. + * + * @param artifact The BlackboardArtifact to get derived event IDs for. + * + * @return A List of event IDs for the events that are derived from the + * given artifact. + */ + public List getEventIDsForArtifact(BlackboardArtifact artifact) { + return repo.getEventIDsForArtifact(artifact); + } + /** * Post a TagsAddedEvent to all registered subscribers, if the given set of * updated event IDs is not empty. diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/eventtype/ArtifactEventType.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/eventtype/ArtifactEventType.java index ccf482e7ec..90ed0148d3 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/eventtype/ArtifactEventType.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/eventtype/ArtifactEventType.java @@ -34,14 +34,30 @@ import org.sleuthkit.datamodel.TskCoreException; public interface ArtifactEventType extends EventType { public static final Logger LOGGER = Logger.getLogger(ArtifactEventType.class.getName()); - static final EmptyExtractor EMPTY_EXTRACTOR = new EmptyExtractor(); /** - * @return the Artifact type this event type is derived from + * Get the artifact type this event type is derived from. + * + * @return The artifact type this event type is derived from. */ public BlackboardArtifact.Type getArtifactType(); - public BlackboardAttribute.Type getDateTimeAttrubuteType(); + /** + * The attribute type this event type is derived from. + * + * @return The attribute type this event type is derived from. + */ + public BlackboardAttribute.Type getDateTimeAttributeType(); + + /** + * Get the ID of the the artifact type that this EventType is derived from. + * + * @return the ID of the the artifact type that this EventType is derived + * from. + */ + public default int getArtifactTypeID() { + return getArtifactType().getTypeID(); + } /** * given an artifact, pull out the time stamp, and compose the descriptions. @@ -57,7 +73,7 @@ public interface ArtifactEventType extends EventType { * @throws TskCoreException */ default AttributeEventDescription parseAttributesHelper(BlackboardArtifact artf) throws TskCoreException { - final BlackboardAttribute dateTimeAttr = artf.getAttribute(getDateTimeAttrubuteType()); + final BlackboardAttribute dateTimeAttr = artf.getAttribute(getDateTimeAttributeType()); long time = dateTimeAttr.getValueLong(); String shortDescription = getShortExtractor().apply(artf); @@ -144,10 +160,10 @@ public interface ArtifactEventType extends EventType { static public AttributeEventDescription buildEventDescription(ArtifactEventType type, BlackboardArtifact artf) throws TskCoreException { //if we got passed an artifact that doesn't correspond to the type of the event, //something went very wrong. throw an exception. - if (type.getArtifactType().getTypeID() != artf.getArtifactTypeID()) { + if (type.getArtifactTypeID() != artf.getArtifactTypeID()) { throw new IllegalArgumentException(); } - if (artf.getAttribute(type.getDateTimeAttrubuteType()) == null) { + if (artf.getAttribute(type.getDateTimeAttributeType()) == null) { LOGGER.log(Level.WARNING, "Artifact {0} has no date/time attribute, skipping it.", artf.getArtifactID()); // NON-NLS return null; } @@ -184,8 +200,10 @@ public interface ArtifactEventType extends EventType { try { return artf.getAttribute(attrType); } catch (TskCoreException ex) { - LOGGER.log(Level.SEVERE, MessageFormat.format("Error getting extracting attribute from artifact {0}.", artf.getArtifactID()), ex); // NON-NLS + LOGGER.log(Level.SEVERE, MessageFormat.format("Error getting attribute from artifact {0}.", artf.getArtifactID()), ex); // NON-NLS return null; } } + + } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/eventtype/MiscTypes.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/eventtype/MiscTypes.java index ce7f009ceb..4b099519a2 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/eventtype/MiscTypes.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/eventtype/MiscTypes.java @@ -76,7 +76,7 @@ public enum MiscTypes implements EventType, ArtifactEventType { final BlackboardAttribute latitude = getAttributeSafe(artf, new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_GEO_LATITUDE)); return stringValueOf(latitude) + " " + stringValueOf(longitude); // NON-NLS }, - EMPTY_EXTRACTOR), + new EmptyExtractor()), CALL_LOG(NbBundle.getMessage(MiscTypes.class, "MiscTypes.Calls.name"), "calllog.png", // NON-NLS new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_CALLLOG), new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DATETIME_START), @@ -104,7 +104,7 @@ public enum MiscTypes implements EventType, ArtifactEventType { @Override public AttributeEventDescription parseAttributesHelper(BlackboardArtifact artf) throws TskCoreException { - final BlackboardAttribute dateTimeAttr = artf.getAttribute(getDateTimeAttrubuteType()); + final BlackboardAttribute dateTimeAttr = artf.getAttribute(getDateTimeAttributeType()); long time = dateTimeAttr.getValueLong(); @@ -120,8 +120,8 @@ public enum MiscTypes implements EventType, ArtifactEventType { new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_INSTALLED_PROG), new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DATETIME), new AttributeExtractor(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_PROG_NAME)), - EMPTY_EXTRACTOR, - EMPTY_EXTRACTOR), + new EmptyExtractor(), + new EmptyExtractor()), EXIF(NbBundle.getMessage(MiscTypes.class, "MiscTypes.exif.name"), "camera-icon-16.png", // NON-NLS new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_METADATA_EXIF), new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DATETIME_CREATED), @@ -199,7 +199,7 @@ public enum MiscTypes implements EventType, ArtifactEventType { } @Override - public BlackboardAttribute.Type getDateTimeAttrubuteType() { + public BlackboardAttribute.Type getDateTimeAttributeType() { return dateTimeAttributeType; } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/eventtype/WebTypes.java b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/eventtype/WebTypes.java index 33cbe4836c..74c8193aef 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/datamodel/eventtype/WebTypes.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/datamodel/eventtype/WebTypes.java @@ -45,7 +45,7 @@ public enum WebTypes implements EventType, ArtifactEventType { @Override public AttributeEventDescription parseAttributesHelper(BlackboardArtifact artf) throws TskCoreException { - long time = artf.getAttribute(getDateTimeAttrubuteType()).getValueLong(); + long time = artf.getAttribute(getDateTimeAttributeType()).getValueLong(); String domain = getShortExtractor().apply(artf); String path = getMedExtractor().apply(artf); String fileName = StringUtils.substringAfterLast(path, "/"); @@ -103,7 +103,7 @@ public enum WebTypes implements EventType, ArtifactEventType { } @Override - public BlackboardAttribute.Type getDateTimeAttrubuteType() { + public BlackboardAttribute.Type getDateTimeAttributeType() { return dateTimeAttributeType; } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java b/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java index 8bbfc756af..00502b191e 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/db/EventDB.java @@ -71,6 +71,8 @@ import org.sleuthkit.autopsy.timeline.utils.RangeDivisionInfo; import org.sleuthkit.autopsy.timeline.zooming.DescriptionLoD; import org.sleuthkit.autopsy.timeline.zooming.EventTypeZoomLevel; import org.sleuthkit.autopsy.timeline.zooming.ZoomParams; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.Tag; import org.sleuthkit.datamodel.TskData; @@ -667,6 +669,69 @@ public class EventDB { } } + /** + * Get a List of event IDs for the events that are derived from the given + * artifact. + * + * @param artifact The BlackboardArtifact to get derived event IDs for. + * + * @return A List of event IDs for the events that are derived from the + * given artifact. + */ + List getEventIDsForArtifact(BlackboardArtifact artifact) { + DBLock.lock(); + + String query = "SELECT event_id FROM events WHERE artifact_id == " + artifact.getArtifactID(); + + ArrayList results = new ArrayList<>(); + try (Statement stmt = con.createStatement(); + ResultSet rs = stmt.executeQuery(query);) { + while (rs.next()) { + results.add(rs.getLong("event_id")); + } + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "Error executing getEventIDsForArtifact query.", ex); // NON-NLS + } finally { + DBLock.unlock(); + } + return results; + } + + /** + * Get a List of event IDs for the events that are derived from the given + * file. + * + * @param file The AbstractFile to get derived event IDs + * for. + * @param includeDerivedArtifacts If true, also get event IDs for events + * derived from artifacts derived form this + * file. If false, only gets events derived + * directly from this file (file system + * timestamps). + * + * @return A List of event IDs for the events that are derived from the + * given file. + */ + List getEventIDsForFile(AbstractFile file, boolean includeDerivedArtifacts) { + DBLock.lock(); + + String query = "SELECT event_id FROM events WHERE file_id == " + file.getId() + + (includeDerivedArtifacts ? "" : " AND artifact_id IS NULL"); + + ArrayList results = new ArrayList<>(); + try (Statement stmt = con.createStatement(); + ResultSet rs = stmt.executeQuery(query);) { + while (rs.next()) { + results.add(rs.getLong("event_id")); + } + } catch (SQLException ex) { + LOGGER.log(Level.SEVERE, "Error executing getEventIDsForFile query.", ex); // NON-NLS + } finally { + DBLock.unlock(); + } + return results; + } + /** * create the tags table if it doesn't already exist. This is broken out as * a separate method so it can be used by {@link #reInitializeTags() } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java b/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java index db6ce5949f..bb273d0b0e 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/db/EventsRepository.java @@ -207,6 +207,38 @@ public class EventsRepository { return eventDB.countAllEvents(); } + /** + * Get a List of event IDs for the events that are derived from the given + * file. + * + * @param file The AbstractFile to get derived event IDs + * for. + * @param includeDerivedArtifacts If true, also get event IDs for events + * derived from artifacts derived form this + * file. If false, only gets events derived + * directly from this file (file system + * timestamps). + * + * @return A List of event IDs for the events that are derived from the + * given file. + */ + public List getEventIDsForFile(AbstractFile file, boolean includedDerivedArtifacts) { + return eventDB.getEventIDsForFile(file, includedDerivedArtifacts); + } + + /** + * Get a List of event IDs for the events that are derived from the given + * artifact. + * + * @param artifact The BlackboardArtifact to get derived event IDs for. + * + * @return A List of event IDs for the events that are derived from the + * given artifact. + */ + public List getEventIDsForArtifact(BlackboardArtifact artifact) { + return eventDB.getEventIDsForArtifact(artifact); + } + private void invalidateCaches() { minCache.invalidateAll(); maxCache.invalidateAll(); @@ -597,10 +629,10 @@ public class EventsRepository { timeMap.put(FileSystemTypes.FILE_MODIFIED, f.getMtime()); /* - * if there are no legitimate ( greater than zero ) time stamps ( eg, - * logical/local files) skip the rest of the event generation: this - * should result in droping logical files, since they do not have - * legitimate time stamps. + * if there are no legitimate ( greater than zero ) time stamps ( + * eg, logical/local files) skip the rest of the event generation: + * this should result in dropping logical files, since they do not + * have legitimate time stamps. */ if (Collections.max(timeMap.values()) > 0) { final String uniquePath = f.getUniquePath(); @@ -655,7 +687,7 @@ public class EventsRepository { private void populateEventType(final ArtifactEventType type, EventDB.EventTransaction trans) { try { //get all the blackboard artifacts corresponding to the given event sub_type - final ArrayList blackboardArtifacts = skCase.getBlackboardArtifacts(type.getArtifactType().getTypeID()); + final ArrayList blackboardArtifacts = skCase.getBlackboardArtifacts(type.getArtifactTypeID()); final int numArtifacts = blackboardArtifacts.size(); restartProgressHandle(Bundle.progressWindow_populatingXevents(type.getDisplayName()), "", 0D, numArtifacts, true); for (int i = 0; i < numArtifacts; i++) { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/events/ViewInTimelineRequestedEvent.java b/Core/src/org/sleuthkit/autopsy/timeline/events/ViewInTimelineRequestedEvent.java new file mode 100644 index 0000000000..4e170f8955 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/timeline/events/ViewInTimelineRequestedEvent.java @@ -0,0 +1,61 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2011-2016 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.timeline.events; + +import java.util.Set; +import org.joda.time.Interval; + +/** + * Encapsulates the result of the ShowInTimelineDialog: a Set of event IDs and + * an Interval. + */ +public final class ViewInTimelineRequestedEvent { + + private final Set eventIDs; + private final Interval range; + + /** + * Constructor + * + * @param eventIDs The event IDs to include. + * @param range The Interval to show. + */ + public ViewInTimelineRequestedEvent(Set eventIDs, Interval range) { + this.eventIDs = eventIDs; + this.range = range; + } + + /** + * Get the event IDs. + * + * @return The event IDs + */ + public Set getEventIDs() { + return eventIDs; + } + + /** + * Get the Interval. + * + * @return The Interval. + */ + public Interval getInterval() { + return range; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventNode.java b/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventNode.java index ee22cfd295..eb2abdde37 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventNode.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/explorernodes/EventNode.java @@ -19,6 +19,7 @@ package org.sleuthkit.autopsy.timeline.explorernodes; import java.lang.reflect.InvocationTargetException; +import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -33,16 +34,18 @@ 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.datamodel.DataModelActionsFactory; import org.sleuthkit.autopsy.datamodel.DisplayableItemNode; import org.sleuthkit.autopsy.datamodel.DisplayableItemNodeVisitor; import org.sleuthkit.autopsy.datamodel.NodeProperty; import org.sleuthkit.autopsy.timeline.TimeLineController; +import org.sleuthkit.autopsy.timeline.actions.ViewFileInTimelineAction; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; import org.sleuthkit.autopsy.timeline.datamodel.SingleEvent; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; -import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.BlackboardAttribute; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TskCoreException; @@ -107,15 +110,41 @@ public class EventNode extends DisplayableItemNode { } @Override + @NbBundle.Messages({ + "EventNode.getAction.errorTitle=Error getting actions", + "EventNode.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) { Action[] superActions = super.getActions(context); List actionsList = new ArrayList<>(); actionsList.addAll(Arrays.asList(superActions)); - final Content content = getLookup().lookup(Content.class); - final BlackboardArtifact artifact = getLookup().lookup(BlackboardArtifact.class); + final AbstractFile sourceFile = getLookup().lookup(AbstractFile.class); - final List factoryActions = DataModelActionsFactory.getActions(content, artifact != null); + /* + * if this event is derived from an artifact, add actions to view the + * source file and a "linked" file, if present. + */ + final BlackboardArtifact artifact = getLookup().lookup(BlackboardArtifact.class); + if (artifact != null) { + try { + AbstractFile linkedfile = findLinked(artifact); + if (linkedfile != null) { + actionsList.add(ViewFileInTimelineAction.createViewFileAction(linkedfile)); + } + } 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.EventNode_getAction_errorTitle(), Bundle.EventNode_getAction_linkedFileMessage()); + } + + //if this event has associated content, add the action to view the content in the timeline + if (null != sourceFile) { + actionsList.add(ViewFileInTimelineAction.createViewSourceFileAction(sourceFile)); + } + } + + //get default actions for the source file + final List factoryActions = DataModelActionsFactory.getActions(sourceFile, artifact != null); actionsList.addAll(factoryActions); return actionsList.toArray(new Action[actionsList.size()]); @@ -207,4 +236,30 @@ public class EventNode extends DisplayableItemNode { return new EventNode(eventById, file); } } + + /** + * this code started as a cut and past of + * DataResultFilterNode.GetPopupActionsDisplayableItemNodeVisitor.findLinked(BlackboardArtifactNode + * ba) + * + * It is now in DisplayableItemNode too, but is not accesible across + * packages + * + * @param artifact + * + * @return + */ + static AbstractFile findLinked(BlackboardArtifact artifact) throws TskCoreException { + + BlackboardAttribute pathIDAttribute = artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH_ID)); + + if (pathIDAttribute != null) { + long contentID = pathIDAttribute.getValueLong(); + if (contentID != -1) { + return artifact.getSleuthkitCase().getAbstractFileById(contentID); + } + } + + return null; + } } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractTimeLineView.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractTimeLineView.java index 7f6a2efe14..9cc3124b29 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractTimeLineView.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/AbstractTimeLineView.java @@ -227,6 +227,7 @@ public abstract class AbstractTimeLineView extends BorderPane { TimeLineController.getTimeZone().removeListener(updateListener); updateListener = null; filteredEvents.unRegisterForEvents(this); + controller.unRegisterForEvents(this); } /** diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/ViewFrame.fxml b/Core/src/org/sleuthkit/autopsy/timeline/ui/ViewFrame.fxml index 5de950392e..a1103e6717 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/ViewFrame.fxml +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/ViewFrame.fxml @@ -50,7 +50,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/ViewFrame.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/ViewFrame.java index 679f28e7b0..46df5150e9 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/ViewFrame.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/ViewFrame.java @@ -394,13 +394,8 @@ final public class ViewFrame extends BorderPane { zoomMenuButton.getItems().clear(); for (ZoomRanges zoomRange : ZoomRanges.values()) { zoomMenuButton.getItems().add(ActionUtils.createMenuItem( - new Action(zoomRange.getDisplayName(), event -> { - if (zoomRange != ZoomRanges.ALL) { - controller.pushPeriod(zoomRange.getPeriod()); - } else { - controller.showFullRange(); - } - }))); + new Action(zoomRange.getDisplayName(), event -> controller.pushPeriod(zoomRange.getPeriod())) + )); } zoomMenuButton.setText(Bundle.ViewFrame_zoomMenuButton_text()); ActionUtils.configureButton(new ZoomOut(controller), zoomOutButton); @@ -643,55 +638,54 @@ final public class ViewFrame extends BorderPane { private void syncViewMode() { ViewMode newViewMode = controller.getViewMode(); - Platform.runLater(() -> { - //clear out old view. - if (hostedView != null) { - hostedView.dispose(); - } + //clear out old view. + if (hostedView != null) { + hostedView.dispose(); + } - //Set a new AbstractTimeLineView as the one hosted by this ViewFrame. - switch (newViewMode) { - case LIST: - hostedView = new ListViewPane(controller); - //TODO: should remove listeners from events tree - break; - case COUNTS: - hostedView = new CountsViewPane(controller); - //TODO: should remove listeners from events tree - break; - case DETAIL: - DetailViewPane detailViewPane = new DetailViewPane(controller); - //link events tree to detailview instance. - detailViewPane.setHighLightedEvents(eventsTree.getSelectedEvents()); - eventsTree.setDetailViewPane(detailViewPane); - hostedView = detailViewPane; - break; - default: - throw new IllegalArgumentException("Unknown ViewMode: " + newViewMode.toString());//NON-NLS - } + //Set a new AbstractTimeLineView as the one hosted by this ViewFrame. + switch (newViewMode) { + case LIST: + hostedView = new ListViewPane(controller); + //TODO: should remove listeners from events tree + break; + case COUNTS: + hostedView = new CountsViewPane(controller); + //TODO: should remove listeners from events tree + break; + case DETAIL: + DetailViewPane detailViewPane = new DetailViewPane(controller); + //link events tree to detailview instance. + detailViewPane.setHighLightedEvents(eventsTree.getSelectedEvents()); + eventsTree.setDetailViewPane(detailViewPane); + hostedView = detailViewPane; + break; + default: + throw new IllegalArgumentException("Unknown ViewMode: " + newViewMode.toString());//NON-NLS + } + controller.registerForEvents(hostedView); - viewModeToggleGroup.setValue(newViewMode); //this selects the right toggle automatically + viewModeToggleGroup.setValue(newViewMode); //this selects the right toggle automatically - //configure settings and time navigation nodes - setViewSettingsControls(hostedView.getSettingsControls()); - setTimeNavigationControls(hostedView.hasCustomTimeNavigationControls() - ? hostedView.getTimeNavigationControls() - : defaultTimeNavigationNodes); + //configure settings and time navigation nodes + setViewSettingsControls(hostedView.getSettingsControls()); + setTimeNavigationControls(hostedView.hasCustomTimeNavigationControls() + ? hostedView.getTimeNavigationControls() + : defaultTimeNavigationNodes); - //do further setup of new view. - ActionUtils.configureButton(new Refresh(), refreshButton);//configure new refresh action for new view - hostedView.refresh(); - notificationPane.setContent(hostedView); - //listen to has events property and show "dialog" if it is false. - hostedView.hasVisibleEventsProperty().addListener(hasEvents -> { - notificationPane.setContent(hostedView.hasVisibleEvents() - ? hostedView - : new StackPane(hostedView, - NO_EVENTS_BACKGROUND, - new NoEventsDialog(() -> notificationPane.setContent(hostedView)) - ) - ); - }); + //do further setup of new view. + ActionUtils.configureButton(new Refresh(), refreshButton);//configure new refresh action for new view + hostedView.refresh(); + notificationPane.setContent(hostedView); + //listen to has events property and show "dialog" if it is false. + hostedView.hasVisibleEventsProperty().addListener(hasEvents -> { + notificationPane.setContent(hostedView.hasVisibleEvents() + ? hostedView + : new StackPane(hostedView, + NO_EVENTS_BACKGROUND, + new NoEventsDialog(() -> notificationPane.setContent(hostedView)) + ) + ); }); } diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/ZoomRanges.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/ZoomRanges.java index fe96e83a29..f71ba00983 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/ZoomRanges.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/ZoomRanges.java @@ -30,7 +30,7 @@ public enum ZoomRanges { THREE_YEARS(NbBundle.getMessage(ZoomRanges.class, "Timeline.ui.ZoomRanges.threeyears.text"), Years.THREE), FIVE_YEARS(NbBundle.getMessage(ZoomRanges.class, "Timeline.ui.ZoomRanges.fiveyears.text"), Years.years(5)), TEN_YEARS(NbBundle.getMessage(ZoomRanges.class, "Timeline.ui.ZoomRanges.tenyears.text"), Years.years(10)), - ALL(NbBundle.getMessage(ZoomRanges.class, "Timeline.ui.ZoomRanges.all.text"), Minutes.ONE); + ALL(NbBundle.getMessage(ZoomRanges.class, "Timeline.ui.ZoomRanges.all.text"), Years.years(1_000_000)); private ZoomRanges(String displayName, ReadablePeriod period) { this.displayName = displayName; diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java index d8b444ff29..22af382125 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/detailview/DetailViewPane.java @@ -367,7 +367,7 @@ public class DetailViewPane extends AbstractTimelineChart, ObservableValue> CELL_VALUE_FACTORY = param -> new SimpleObjectProperty<>(param.getValue()); + private static final List SCROLL_BY_UNITS = Arrays.asList( ChronoField.YEAR, ChronoField.MONTH_OF_YEAR, @@ -122,6 +121,8 @@ class ListTimeline extends BorderPane { ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE); + private static final int DEFAULT_ROW_HEIGHT = 24; + @FXML private HBox navControls; @@ -160,23 +161,36 @@ class ListTimeline extends BorderPane { private TableColumn hashHitColumn; /** - * Observable list used to track selected events. + * Since TableView does not expose what cells/items are visible, we track + * them in this set. It is sorted by index in the TableView's model. */ - private final ObservableList selectedEventIDs = FXCollections.observableArrayList(); - - private final ConcurrentSkipListSet visibleEvents; + private final SortedSet visibleEvents; private final TimeLineController controller; private final SleuthkitCase sleuthkitCase; private final TagsManager tagsManager; + /** + * Listener attached to the table's selection model that pushes that + * selection to the controller. Maps from Combined event in table to EventID + * in controller via CombinedEvent.getRepresentativeEventID. + */ + private final ListChangeListener selectedEventListener = new ListChangeListener() { + @Override + public void onChanged(ListChangeListener.Change c) { + controller.selectEventIDs(table.getSelectionModel().getSelectedItems().stream() + .filter(Objects::nonNull) + .map(CombinedEvent::getRepresentativeEventID) + .collect(Collectors.toSet())); + } + }; + /** * Constructor * * @param controller The controller for this timeline */ ListTimeline(TimeLineController controller) { - this.controller = controller; sleuthkitCase = controller.getAutopsyCase().getSleuthkitCase(); tagsManager = controller.getAutopsyCase().getServices().getTagsManager(); @@ -197,17 +211,17 @@ class ListTimeline extends BorderPane { assert typeColumn != null : "fx:id=\"typeColumn\" was not injected: check your FXML file 'ListViewPane.fxml'."; //NON-NLS assert knownColumn != null : "fx:id=\"knownColumn\" was not injected: check your FXML file 'ListViewPane.fxml'."; //NON-NLS + //configure scroll controls scrollInrementComboBox.setButtonCell(new ChronoFieldListCell()); scrollInrementComboBox.setCellFactory(comboBox -> new ChronoFieldListCell()); scrollInrementComboBox.getItems().setAll(SCROLL_BY_UNITS); scrollInrementComboBox.getSelectionModel().select(ChronoField.YEAR); - ActionUtils.configureButton(new ScrollToFirst(), firstButton); ActionUtils.configureButton(new ScrollToPrevious(), previousButton); ActionUtils.configureButton(new ScrollToNext(), nextButton); ActionUtils.configureButton(new ScrollToLast(), lastButton); - //override default row with one that provides context menus + //override default table row with one that provides context menus table.setRowFactory(tableView -> new EventRow()); //remove idColumn (can be restored for debugging). @@ -247,22 +261,10 @@ class ListTimeline extends BorderPane { } }); + // use listener to keep controller selection in sync with table selection. + table.getSelectionModel().getSelectedItems().addListener(selectedEventListener); table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); - table.getSelectionModel().getSelectedItems().addListener((Observable observable) -> { - //keep the selectedEventsIDs in sync with the table's selection model, via getRepresentitiveEventID(). - selectedEventIDs.setAll(table.getSelectionModel().getSelectedItems().stream() - .filter(Objects::nonNull) - .map(CombinedEvent::getRepresentativeEventID) - .collect(Collectors.toSet())); - }); - } - - /** - * Clear all the events out of the table. - */ - @ThreadConfined(type = ThreadConfined.ThreadType.JFX) - void clear() { - table.getItems().clear(); + selectEvents(controller.getSelectedEventIDs()); //grab initial selection } /** @@ -275,51 +277,83 @@ class ListTimeline extends BorderPane { table.getItems().setAll(events); } - /** - * Get an ObservableList of IDs of events that are selected in this table. - * - * @return An ObservableList of IDs of events that are selected in this - * table. - */ - ObservableList getSelectedEventIDs() { - return selectedEventIDs; - } - - /** - * Get an ObservableList of combined events that are selected in this table. - * - * @return An ObservableList of combined events that are selected in this - * table. - */ - ObservableList getSelectedEvents() { - return table.getSelectionModel().getSelectedItems(); - } - /** * Set the combined events that are selected in this view. * - * @param selectedEvents The events that should be selected. + * @param selectedEventIDs The events that should be selected. */ - void selectEvents(Collection selectedEvents) { - CombinedEvent firstSelected = selectedEvents.stream().min(Comparator.comparing(CombinedEvent::getStartMillis)).orElse(null); - table.getSelectionModel().clearSelection(); - table.scrollTo(firstSelected); - selectedEvents.forEach(table.getSelectionModel()::select); - table.requestFocus(); + void selectEvents(Collection selectedEventIDs) { + if (selectedEventIDs.isEmpty()) { + //this is the final selection, so we don't need to mess with the listener + table.getSelectionModel().clearSelection(); + } else { + /* + * Changes in the table selection are propogated to the controller + * by a listener. There is no API on TableView's selection model to + * clear the selection and select multiple rows as one "action". + * Therefore we clear the selection and then make the new selection, + * but we don't want this intermediate state of no selection to be + * pushed to the controller as it interferes with maintaining the + * right selection. To avoid notifying the controller, we remove the + * listener, clear the selection, then re-attach it. + */ + table.getSelectionModel().getSelectedItems().removeListener(selectedEventListener); + + table.getSelectionModel().clearSelection(); + + table.getSelectionModel().getSelectedItems().addListener(selectedEventListener); + + //find the indices of the CombinedEvents that will be selected + int[] selectedIndices = table.getItems().stream() + .filter(combinedEvent -> Collections.disjoint(combinedEvent.getEventIDs(), selectedEventIDs) == false) + .mapToInt(table.getItems()::indexOf) + .toArray(); + + //select indices and scroll to the first one + if (selectedIndices.length > 0) { + Integer firstSelectedIndex = selectedIndices[0]; + table.getSelectionModel().selectIndices(firstSelectedIndex, selectedIndices); + scrollTo(firstSelectedIndex); + table.requestFocus(); //grab focus so selection is clearer to user + } + } } - List getNavControls() { + /** + * Get the time navigation controls that this ListTimeline's parent + * ListViewPane will provide to its host ViewFrame. + * + * @return A List of time navigation controls in the from of JavaFX scene + * graph Nodes. + */ + List getTimeNavigationControls() { return Collections.singletonList(navControls); } + /** + * Scroll the table to the given index (if it is not already visible) and + * focus it. + * + * @param index The index of the item that should be scrolled in to view and + * focused. + */ private void scrollToAndFocus(Integer index) { table.requestFocus(); - if (visibleEvents.contains(table.getItems().get(index)) == false) { - table.scrollTo(index); - } + scrollTo(index); table.getFocusModel().focus(index); } + /** + * Scroll the table to the given index (if it is not already visible). + * + * @param index The index of the item that should be scrolled in to view. + */ + private void scrollTo(Integer index) { + if (visibleEvents.contains(table.getItems().get(index)) == false) { + table.scrollTo(DoubleMath.roundToInt(index - ((table.getHeight() / DEFAULT_ROW_HEIGHT)) / 2, RoundingMode.HALF_EVEN)); + } + } + /** * TableCell to show the (sub) type of an event. */ @@ -483,7 +517,7 @@ class ListTimeline extends BorderPane { setTooltip(null); } else { /* - * if the cell is not empty and the event's file is a hash hit, + * If the cell is not empty and the event's file is a hash hit, * show the hash hit icon, and show a list of hash set names in * the tooltip */ @@ -650,21 +684,6 @@ class ListTimeline extends BorderPane { } } - private class ChronoFieldListCell extends ListCell { - - @Override - protected void updateItem(ChronoField item, boolean empty) { - super.updateItem(item, empty); - - if (empty || item == null) { - setText(null); - } else { - String displayName = item.getDisplayName(Locale.getDefault()); - setText(String.join(" ", StringUtils.splitByCharacterTypeCamelCase(displayName))); - } - } - } - private class ScrollToFirst extends org.controlsfx.control.action.Action { ScrollToFirst() { diff --git a/Core/src/org/sleuthkit/autopsy/timeline/ui/listvew/ListViewPane.java b/Core/src/org/sleuthkit/autopsy/timeline/ui/listvew/ListViewPane.java index a213cdb3f8..ea0a76b936 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/ui/listvew/ListViewPane.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/ui/listvew/ListViewPane.java @@ -19,10 +19,11 @@ package org.sleuthkit.autopsy.timeline.ui.listvew; import com.google.common.collect.ImmutableList; -import java.util.HashSet; +import com.google.common.collect.ImmutableSet; +import com.google.common.eventbus.Subscribe; import java.util.List; +import java.util.Set; import javafx.application.Platform; -import javafx.beans.Observable; import javafx.concurrent.Task; import javafx.scene.Node; import org.joda.time.Interval; @@ -31,6 +32,7 @@ import org.sleuthkit.autopsy.timeline.TimeLineController; import org.sleuthkit.autopsy.timeline.ViewMode; import org.sleuthkit.autopsy.timeline.datamodel.CombinedEvent; import org.sleuthkit.autopsy.timeline.datamodel.FilteredEventsModel; +import org.sleuthkit.autopsy.timeline.events.ViewInTimelineRequestedEvent; import org.sleuthkit.autopsy.timeline.ui.AbstractTimeLineView; /** @@ -47,15 +49,12 @@ public class ListViewPane extends AbstractTimeLineView { */ public ListViewPane(TimeLineController controller) { super(controller); + listTimeline = new ListTimeline(controller); //initialize chart; setCenter(listTimeline); - //keep controller's list of selected event IDs in sync with this list's - listTimeline.getSelectedEventIDs().addListener((Observable selectedIDs) -> { - controller.selectEventIDs(listTimeline.getSelectedEventIDs()); - }); } @Override @@ -63,9 +62,14 @@ public class ListViewPane extends AbstractTimeLineView { return new ListUpdateTask(); } + /** + * This method is supposed to clear all the data from this View, but it + * might have been interfering with the "View in Timeline" action and was + * not strictly necessary so this implementation is a no-op. + */ @Override protected void clearData() { - listTimeline.clear(); + } @Override @@ -80,7 +84,7 @@ public class ListViewPane extends AbstractTimeLineView { @Override protected ImmutableList getTimeNavigationControls() { - return ImmutableList.copyOf(listTimeline.getNavControls()); + return ImmutableList.copyOf(listTimeline.getTimeNavigationControls()); } @Override @@ -88,10 +92,15 @@ public class ListViewPane extends AbstractTimeLineView { return true; } + @Subscribe + public void handleViewInTimelineRequested(ViewInTimelineRequestedEvent event) { + listTimeline.selectEvents(event.getEventIDs()); + } + private class ListUpdateTask extends ViewRefreshTask { @NbBundle.Messages({ - "ListViewPane.loggedTask.queryDb=Retreiving event data", + "ListViewPane.loggedTask.queryDb=Retrieving event data", "ListViewPane.loggedTask.name=Updating List View", "ListViewPane.loggedTask.updateUI=Populating view"}) ListUpdateTask() { @@ -107,8 +116,12 @@ public class ListViewPane extends AbstractTimeLineView { FilteredEventsModel eventsModel = getEventsModel(); + Set selectedEventIDs; + TimeLineController controller = getController(); //grab the currently selected event - HashSet selectedEvents = new HashSet<>(listTimeline.getSelectedEvents()); + synchronized (controller) { + selectedEventIDs = ImmutableSet.copyOf(controller.getSelectedEventIDs()); + } //clear the chart and set the time range. resetView(eventsModel.getTimeRange()); @@ -121,12 +134,11 @@ public class ListViewPane extends AbstractTimeLineView { Platform.runLater(() -> { //put the combined events into the table. listTimeline.setCombinedEvents(combinedEvents); - //restore the selected event - listTimeline.selectEvents(selectedEvents); + //restore the selected events + listTimeline.selectEvents(selectedEventIDs); }); return combinedEvents.isEmpty() == false; - } @Override diff --git a/Core/src/org/sleuthkit/autopsy/timeline/utils/IntervalUtils.java b/Core/src/org/sleuthkit/autopsy/timeline/utils/IntervalUtils.java index 7cc8205c65..d423445bd4 100644 --- a/Core/src/org/sleuthkit/autopsy/timeline/utils/IntervalUtils.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/utils/IntervalUtils.java @@ -18,6 +18,8 @@ */ package org.sleuthkit.autopsy.timeline.utils; +import java.time.Instant; +import java.time.temporal.TemporalAmount; import java.util.Collection; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; @@ -68,6 +70,13 @@ public class IntervalUtils { return newInterval; } + static public Interval getIntervalAround(Instant aroundInstant, TemporalAmount temporalAmount) { + long start = aroundInstant.minus(temporalAmount).toEpochMilli(); + long end = aroundInstant.plusMillis(1).plus(temporalAmount).toEpochMilli(); + final Interval newInterval = new Interval(start, Math.max(start + 1, end)); + return newInterval; + } + /** * Get an interval the length of the given period, centered around the * center of the given interval. diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalEditListPanel.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalEditListPanel.java index a381e98059..8190c5d83b 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalEditListPanel.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalEditListPanel.java @@ -454,7 +454,7 @@ class GlobalEditListPanel extends javax.swing.JPanel implements ListSelectionLis chRegex.setSelected(false); addWordField.setText(""); pcs.firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null); - + setFocusOnKeywordTextBox(); setButtonStates(); }//GEN-LAST:event_addWordButtonActionPerformed diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalListsManagementPanel.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalListsManagementPanel.java index 070fe4b99f..1f7baf6ece 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalListsManagementPanel.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/GlobalListsManagementPanel.java @@ -26,6 +26,7 @@ import java.util.ArrayList; import java.util.List; import javax.swing.JFileChooser; import javax.swing.JOptionPane; +import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.filechooser.FileNameExtensionFilter; import javax.swing.table.AbstractTableModel; @@ -65,6 +66,12 @@ class GlobalListsManagementPanel extends javax.swing.JPanel implements OptionsPa listsTable.setRowSelectionAllowed(true); tableModel.resync(); + listsTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() { + @Override + public void valueChanged(ListSelectionEvent e) { + globalListSettingsPanel.setFocusOnKeywordTextBox(); + } + }); /* * XmlKeywordSearchList.getCurrent().addPropertyChangeListener(new * PropertyChangeListener() { diff --git a/build-windows.xml b/build-windows.xml index d462070294..41a96016ab 100644 --- a/build-windows.xml +++ b/build-windows.xml @@ -53,6 +53,7 @@ + @@ -80,7 +81,8 @@ - + + diff --git a/build.xml b/build.xml index daae174c11..c898e32189 100755 --- a/build.xml +++ b/build.xml @@ -115,6 +115,7 @@ + diff --git a/docs/doxygen-user/images/activemq.PNG b/docs/doxygen-user/images/activemq.PNG old mode 100644 new mode 100755 index 709d998eab..9118780f7a Binary files a/docs/doxygen-user/images/activemq.PNG and b/docs/doxygen-user/images/activemq.PNG differ diff --git a/docs/doxygen-user/images/case-newcase.PNG b/docs/doxygen-user/images/case-newcase.PNG old mode 100755 new mode 100644 diff --git a/docs/doxygen-user/installActiveMQ.dox b/docs/doxygen-user/installActiveMQ.dox index d10b50d173..117a264dfd 100755 --- a/docs/doxygen-user/installActiveMQ.dox +++ b/docs/doxygen-user/installActiveMQ.dox @@ -5,7 +5,7 @@ To install ActiveMQ, perform the following steps: You will need: - 64-bit version of the Java Runtime Environment (JRE) from http://www.oracle.com/technetwork/java/javase/downloads/jre8-downloads-2133155.html. -- Download ActiveMQ-5.11.1 from: http://activemq.apache.org/activemq-5111-release.html +- Download ActiveMQ-5.13.3 from: http://activemq.apache.org/activemq-5133-release.html \section install_activemq_install Installation @@ -20,7 +20,7 @@ If you need the JRE, install it with the default settings. \subsection install_activemq_install_mq ActiveMQ Installation -1. Extract the contents of the ActiveMQ archive folder to a location of your choice, bearing in mind that the files should be in a location that the running process will have write permissions to the folder. A typical folder choice is C:\\Program Files\\apache-activemq-5.11.1. Typically, it will ask for administrator permission to move the folder. Allow it if required. +1. Extract the contents of the ActiveMQ archive folder to a location of your choice, bearing in mind that the files should be in a location that the running process will have write permissions to the folder. A typical folder choice is C:\\Program Files\\apache-activemq-5.13.3. Typically, it will ask for administrator permission to move the folder. Allow it if required. 2. Edit the conf\\activemq.xml in the extracted folder to add "&wireFormat.maxInactivityDuration=0" to the URI for the _transportConnector_ named _openwire_. Add the text highlighted in yellow below:

diff --git a/docs/doxygen-user/installPostgres.dox b/docs/doxygen-user/installPostgres.dox index 686df9c0e4..e013de31ad 100755 --- a/docs/doxygen-user/installPostgres.dox +++ b/docs/doxygen-user/installPostgres.dox @@ -1,9 +1,9 @@ /*! \page install_postgresql Install and Configure PostgreSQL To install PostgreSQL, perform the following steps: -1. Download a 64-bit PostgreSQL version 9.4.1 installer from http://www.enterprisedb.com/products-services-training/pgdownload#windows Choose the one that says _Win X86-64_. +1. Download a 64-bit PostgreSQL version 9.5.3 installer from http://www.enterprisedb.com/products-services-training/pgdownload#windows Choose the one that says _Win X86-64_. -2. Run _postgresql-9.4.4-1-windows-x64.exe_ +2. Run _postgresql-9.5.3-1-windows-x64.exe_ 3. You may accept defaults for all items except for the password as you work through the wizard. Do not lose the password you enter in. This is the PostgreSQL administrator login password. @@ -49,7 +49,7 @@ When you see the _CREATE ROLE_ output as shown in the screenshot below, the new \image html postgresqlinstall2.PNG
-6. Edit C:\\Program Files\\PostgreSQL\\9.4\\data\\pg_hba.conf to add an entry to allow external computers to connect via the network. +6. Edit C:\\Program Files\\PostgreSQL\\9.5\\data\\pg_hba.conf to add an entry to allow external computers to connect via the network.

First, find your machine's IPv4 address and Subnet Mask (Press _Start_, type _cmd_, type _ipconfig_ and parse the results. The IP address is shown in yellow below.
@@ -72,7 +72,7 @@ Add the line highlighted in yellow below, formatted with spaces between the entr If you intend to use PostgreSQL from machines on a different subnet, you need an entry in the _pg_hba.conf_ file for each subnet.

-7. Uncomment the following entires in the configuration file located at C:\\Program Files\\PostgreSQL\\9.4\\data\\postgresql.conf by removing the leading "#", and change their values "off" as shown below. +7. Uncomment the following entires in the configuration file located at C:\\Program Files\\PostgreSQL\\9.5\\data\\postgresql.conf by removing the leading "#", and change their values "off" as shown below.
> fsync = off
> synchronous_commit = off
@@ -89,12 +89,12 @@ To this: Note the removal of the leading number symbol-this uncomments that entry.

-8. Still in "C:\Program Files\PostgreSQL\9.4\data\postgresql.conf", find the entry named _max_connections_ and set it to the number of suggested connections for your configuration. A rule of thumb is add 100 connections for each Automated Ingest Node and 100 connections for each Reviewer node you plan to have in the network. More information is available at 5.1.1. See the screenshot below. +8. Still in "C:\Program Files\PostgreSQL\9.5\data\postgresql.conf", find the entry named _max_connections_ and set it to the number of suggested connections for your configuration. A rule of thumb is add 100 connections for each Automated Ingest Node and 100 connections for each Reviewer node you plan to have in the network. More information is available at 5.1.1. See the screenshot below.

\image html maxConnections.PNG

-9. Press _Start_, type _services.msc_, and press _Enter_. Select _postgresql-x64-9.4 PostgreSQL Server 9.4_ in the services list and click the link that says _Stop the service_ then click the link that says _Start the service_ as shown in the screenshot below. +9. Press _Start_, type _services.msc_, and press _Enter_. Select _postgresql-x64-9.5_ in the services list and click the link that says _Stop the service_ then click the link that says _Start the service_ as shown in the screenshot below.

\image html postgresqlinstall7.PNG

diff --git a/docs/doxygen-user/installSolr.dox b/docs/doxygen-user/installSolr.dox index 2b299586fc..9f02632a79 100755 --- a/docs/doxygen-user/installSolr.dox +++ b/docs/doxygen-user/installSolr.dox @@ -7,7 +7,7 @@ A central Solr server is needed to store keyword indexes. To install Solr, perfo You will need: - 64-bit version of the Java Runtime Environment (JRE) from http://www.oracle.com/technetwork/java/javase/downloads/jre8-downloads-2133155.html. -- Download the Apache Solr 4.10.3-0 installation package from https://bitnami.com/stack/solr/installer. +- Download the Apache Solr 4.10.3-0 installation package from https://sourceforge.net/projects/autopsy/files/CollaborativeServices/Solr or Direct Download Link - Access to an installed version of Autopsy so that you can copy files from it. - A network-accessible machine to install Solr upon. Note that the Solr process will need to write data out to the main shared storage drive, and needs adequate permissions to write to this location, which may be across a network.