diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java index 99ec09d315..a66b6f5c71 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java @@ -2300,7 +2300,6 @@ public class Case { } else { throw new CaseActionException(Bundle.Case_open_exception_multiUserCaseNotEnabled()); } - caseDb.registerForEvents(sleuthkitEventListener); } catch (TskUnsupportedSchemaVersionException ex) { throw new CaseActionException(Bundle.Case_exceptionMessage_unsupportedSchemaVersionMessage(ex.getLocalizedMessage()), ex); } catch (UserPreferencesException ex) { @@ -2321,6 +2320,12 @@ public class Case { private void openCaseLevelServices(ProgressIndicator progressIndicator) { progressIndicator.progress(Bundle.Case_progressMessage_openingCaseLevelServices()); this.caseServices = new Services(caseDb); + /* + * RC Note: JM put this initialization here. I'm not sure why. However, + * my attempt to put it in the openCaseDatabase method seems to lead to + * intermittent unchecked exceptions concerning a missing subscriber. + */ + caseDb.registerForEvents(sleuthkitEventListener); } /** diff --git a/Core/src/org/sleuthkit/autopsy/communications/relationships/AttachmentsChildren.java b/Core/src/org/sleuthkit/autopsy/communications/relationships/AttachmentThumbnailsChildren.java similarity index 60% rename from Core/src/org/sleuthkit/autopsy/communications/relationships/AttachmentsChildren.java rename to Core/src/org/sleuthkit/autopsy/communications/relationships/AttachmentThumbnailsChildren.java index 7dbe2133e0..816eb05d75 100755 --- a/Core/src/org/sleuthkit/autopsy/communications/relationships/AttachmentsChildren.java +++ b/Core/src/org/sleuthkit/autopsy/communications/relationships/AttachmentThumbnailsChildren.java @@ -18,10 +18,13 @@ */ package org.sleuthkit.autopsy.communications.relationships; +import com.google.gson.Gson; import java.util.Arrays; +import java.util.Collection; import java.util.HashSet; import java.util.Set; import java.util.TreeSet; +import java.util.function.Consumer; import java.util.logging.Level; import org.openide.nodes.Children; import org.openide.nodes.Node; @@ -32,15 +35,18 @@ import org.sleuthkit.autopsy.datamodel.AbstractAbstractFileNode; import org.sleuthkit.autopsy.datamodel.FileNode; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.blackboardutils.FileAttachment; +import org.sleuthkit.datamodel.blackboardutils.MessageAttachments; /** * Factory for creating thumbnail children nodes. */ -final class AttachmentsChildren extends Children.Keys { +final class AttachmentThumbnailsChildren extends Children.Keys { - private static final Logger logger = Logger.getLogger(AttachmentsChildren.class.getName()); + private static final Logger LOGGER = Logger.getLogger(AttachmentThumbnailsChildren.class.getName()); private final Set artifacts; @@ -51,17 +57,16 @@ final class AttachmentsChildren extends Children.Keys { * The thumbnails will be initialls sorted by size, then name so that they * appear sorted by size by default. */ - AttachmentsChildren(Set artifacts) { + AttachmentThumbnailsChildren(Set artifacts) { super(false); this.artifacts = artifacts; - } @Override protected Node[] createNodes(AbstractFile t) { - return new Node[]{new AttachementNode(t)}; + return new Node[]{new AttachementThumbnailNode(t)}; } @Override @@ -77,15 +82,36 @@ final class AttachmentsChildren extends Children.Keys { return result; }); - artifacts.forEach((bba) -> { - try { - for (Content childContent : bba.getChildren()) { - if (childContent instanceof AbstractFile) { - thumbnails.add((AbstractFile) childContent); + artifacts.forEach(new Consumer() { + @Override + public void accept(BlackboardArtifact bba) { + try { + // Get the attachments from TSK_ATTACHMENTS attribute. + BlackboardAttribute attachmentsAttr = bba.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ATTACHMENTS)); + if (attachmentsAttr != null) { + + String jsonVal = attachmentsAttr.getValueString(); + MessageAttachments msgAttachments = new Gson().fromJson(jsonVal, MessageAttachments.class); + + Collection fileAttachments = msgAttachments.getFileAttachments(); + for (FileAttachment fileAttachment : fileAttachments) { + long attachedFileObjId = fileAttachment.getObjectId(); + if (attachedFileObjId >= 0) { + AbstractFile attachedFile = bba.getSleuthkitCase().getAbstractFileById(attachedFileObjId); + thumbnails.add(attachedFile); + } + } + } else { // backward compatibility - email message attachments are derived files, children of the message. + for (Content childContent : bba.getChildren()) { + if (childContent instanceof AbstractFile) { + thumbnails.add((AbstractFile) childContent); + } + } } + + } catch (TskCoreException ex) { + LOGGER.log(Level.WARNING, "Unable to get children from artifact.", ex); //NON-NLS } - } catch (TskCoreException ex) { - logger.log(Level.WARNING, "Unable to get children from artifact.", ex); //NON-NLS } }); @@ -95,9 +121,9 @@ final class AttachmentsChildren extends Children.Keys { /** * A node for representing a thumbnail. */ - static class AttachementNode extends FileNode { + static class AttachementThumbnailNode extends FileNode { - AttachementNode(AbstractFile file) { + AttachementThumbnailNode(AbstractFile file) { super(file, false); } diff --git a/Core/src/org/sleuthkit/autopsy/communications/relationships/ContactNode.java b/Core/src/org/sleuthkit/autopsy/communications/relationships/ContactNode.java index 8dbf58acef..769ba682a6 100755 --- a/Core/src/org/sleuthkit/autopsy/communications/relationships/ContactNode.java +++ b/Core/src/org/sleuthkit/autopsy/communications/relationships/ContactNode.java @@ -36,6 +36,7 @@ import static org.sleuthkit.datamodel.BlackboardAttribute.TSK_BLACKBOARD_ATTRIBU import org.sleuthkit.datamodel.TimeUtilities; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.autopsy.communications.Utils; +import org.sleuthkit.autopsy.coreutils.PhoneNumUtil; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.Content; @@ -70,7 +71,7 @@ final class ContactNode extends BlackboardArtifactNode { @Override protected Sheet createSheet() { Sheet sheet = new Sheet(); - + final BlackboardArtifact artifact = getArtifact(); BlackboardArtifact.ARTIFACT_TYPE fromID = BlackboardArtifact.ARTIFACT_TYPE.fromID(artifact.getArtifactTypeID()); if (fromID != TSK_CONTACT) { @@ -103,26 +104,26 @@ final class ContactNode extends BlackboardArtifactNode { otherList.add(bba); } } - - addPropertiesToSheet(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME.getLabel(), - sheetSet, nameList); - addPropertiesToSheet(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER.getLabel(), - sheetSet, phoneNumList); - addPropertiesToSheet(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_EMAIL.getLabel(), - sheetSet, emailList); + + addPropertiesToSheet(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME.getLabel(), + sheetSet, nameList); + addPropertiesToSheet(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER.getLabel(), + sheetSet, phoneNumList); + addPropertiesToSheet(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_EMAIL.getLabel(), + sheetSet, emailList); for (BlackboardAttribute bba : otherList) { sheetSet.put(new NodeProperty<>(bba.getAttributeType().getTypeName(), bba.getAttributeType().getDisplayName(), "", bba.getDisplayString())); } - + List children = artifact.getChildren(); - if(children != null) { + if (children != null) { int count = 0; String imageLabelPrefix = "Image"; - for(Content child: children) { - if(child instanceof AbstractFile) { + for (Content child : children) { + if (child instanceof AbstractFile) { String imageLabel = imageLabelPrefix; - if(count > 0) { + if (count > 0) { imageLabel = imageLabelPrefix + "-" + count; } sheetSet.put(new NodeProperty<>(imageLabel, imageLabel, imageLabel, child.getName())); @@ -136,14 +137,32 @@ final class ContactNode extends BlackboardArtifactNode { return sheet; } - + private void addPropertiesToSheet(String propertyID, Sheet.Set sheetSet, List attributeList) { int count = 0; for (BlackboardAttribute bba : attributeList) { if (count++ > 0) { - sheetSet.put(new NodeProperty<>(propertyID + "_" + count, bba.getAttributeType().getDisplayName(), "", bba.getDisplayString())); + if (bba.getAttributeType().getTypeName().startsWith("TSK_PHONE")) { + String phoneNumCountry = PhoneNumUtil.getCountryCode(bba.getValueString()); + if (phoneNumCountry.equals("")) { + sheetSet.put(new NodeProperty<>(propertyID + "_" + count, bba.getAttributeType().getDisplayName(), "", bba.getDisplayString())); + } else { + sheetSet.put(new NodeProperty<>(propertyID + "_" + count, bba.getAttributeType().getDisplayName(), "", bba.getDisplayString() + " [" + phoneNumCountry + "]")); + } + } else { + sheetSet.put(new NodeProperty<>(propertyID + "_" + count, bba.getAttributeType().getDisplayName(), "", bba.getDisplayString())); + } } else { - sheetSet.put(new NodeProperty<>(propertyID, bba.getAttributeType().getDisplayName(), "", bba.getDisplayString())); + if (bba.getAttributeType().getTypeName().startsWith("TSK_PHONE")) { + String phoneNumCountry = PhoneNumUtil.getCountryCode(bba.getValueString()); + if (phoneNumCountry.equals("")) { + sheetSet.put(new NodeProperty<>(propertyID, bba.getAttributeType().getDisplayName(), "", bba.getDisplayString())); + } else { + sheetSet.put(new NodeProperty<>(propertyID, bba.getAttributeType().getDisplayName(), "", bba.getDisplayString() + " [" + phoneNumCountry + "]")); + } + } else { + sheetSet.put(new NodeProperty<>(propertyID, bba.getAttributeType().getDisplayName(), "", bba.getDisplayString())); + } } } } @@ -175,4 +194,5 @@ final class ContactNode extends BlackboardArtifactNode { public String getSourceName() { return getDisplayName(); } + } diff --git a/Core/src/org/sleuthkit/autopsy/communications/relationships/MediaViewer.java b/Core/src/org/sleuthkit/autopsy/communications/relationships/MediaViewer.java index 07e6223a03..3bdb7a7a25 100755 --- a/Core/src/org/sleuthkit/autopsy/communications/relationships/MediaViewer.java +++ b/Core/src/org/sleuthkit/autopsy/communications/relationships/MediaViewer.java @@ -129,7 +129,7 @@ final class MediaViewer extends JPanel implements RelationshipsViewer, ExplorerM thumbnailViewer.resetComponent(); - thumbnailViewer.setNode(new TableFilterNode(new DataResultFilterNode(new AbstractNode(new AttachmentsChildren(artifactList)), tableEM), true, this.getClass().getName())); + thumbnailViewer.setNode(new TableFilterNode(new DataResultFilterNode(new AbstractNode(new AttachmentThumbnailsChildren(artifactList)), tableEM), true, this.getClass().getName())); } @Override diff --git a/Core/src/org/sleuthkit/autopsy/communications/relationships/MessageNode.java b/Core/src/org/sleuthkit/autopsy/communications/relationships/MessageNode.java index 1c18ba5952..aad258ed0f 100755 --- a/Core/src/org/sleuthkit/autopsy/communications/relationships/MessageNode.java +++ b/Core/src/org/sleuthkit/autopsy/communications/relationships/MessageNode.java @@ -18,6 +18,7 @@ */ package org.sleuthkit.autopsy.communications.relationships; +import com.google.gson.Gson; import java.util.logging.Level; import javax.swing.Action; import org.apache.commons.lang3.StringUtils; @@ -38,6 +39,8 @@ import static org.sleuthkit.autopsy.communications.relationships.RelationshipsNo import org.sleuthkit.autopsy.datamodel.BlackboardArtifactNode; import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_EMAIL_MSG; import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_MESSAGE; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.blackboardutils.MessageAttachments; /** * Wraps a BlackboardArtifact as an AbstractNode for use in an OutlookView @@ -97,7 +100,7 @@ class MessageNode extends BlackboardArtifactNode { sheetSet.put(new NodeProperty<>("Subject", Bundle.MessageNode_Node_Property_Subject(), "", getAttributeDisplayString(artifact, TSK_SUBJECT))); //NON-NLS try { - sheetSet.put(new NodeProperty<>("Attms", Bundle.MessageNode_Node_Property_Attms(), "", artifact.getChildrenCount())); //NON-NLS + sheetSet.put(new NodeProperty<>("Attms", Bundle.MessageNode_Node_Property_Attms(), "", getAttachmentsCount())); //NON-NLS } catch (TskCoreException ex) { logger.log(Level.WARNING, "Error loading attachment count for " + artifact, ex); //NON-NLS } @@ -144,4 +147,21 @@ class MessageNode extends BlackboardArtifactNode { public Action getPreferredAction() { return preferredAction; } + + private int getAttachmentsCount() throws TskCoreException { + final BlackboardArtifact artifact = getArtifact(); + int attachmentsCount; + + // Attachments are specified in an attribute TSK_ATTACHMENTS as JSON attribute + BlackboardAttribute attachmentsAttr = artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ATTACHMENTS)); + if (attachmentsAttr != null) { + String jsonVal = attachmentsAttr.getValueString(); + MessageAttachments msgAttachments = new Gson().fromJson(jsonVal, MessageAttachments.class); + attachmentsCount = msgAttachments.getAttachmentsCount(); + } else { // legacy attachments may be children of message artifact. + attachmentsCount = artifact.getChildrenCount(); + } + + return attachmentsCount; + } } diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/MessageContentViewer.java b/Core/src/org/sleuthkit/autopsy/contentviewers/MessageContentViewer.java index 0f36c84b9e..35f9efeddb 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/MessageContentViewer.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/MessageContentViewer.java @@ -18,15 +18,17 @@ */ package org.sleuthkit.autopsy.contentviewers; +import org.sleuthkit.autopsy.datamodel.AttachmentNode; +import com.google.gson.Gson; import java.awt.Color; import java.awt.Component; import java.util.Arrays; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.logging.Level; -import java.util.stream.Collectors; import javax.swing.text.JTextComponent; import org.apache.commons.lang3.StringUtils; import org.jsoup.Jsoup; @@ -35,15 +37,12 @@ import org.openide.explorer.ExplorerManager; import org.openide.nodes.AbstractNode; import org.openide.nodes.Children; import org.openide.nodes.Node; -import org.openide.nodes.Sheet; import org.openide.util.NbBundle; import org.openide.util.lookup.ServiceProvider; import org.sleuthkit.autopsy.corecomponentinterfaces.DataContentViewer; import org.sleuthkit.autopsy.corecomponents.DataResultPanel; import org.sleuthkit.autopsy.corecomponents.TableFilterNode; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.datamodel.AbstractAbstractFileNode; -import org.sleuthkit.autopsy.datamodel.FileNode; import org.sleuthkit.autopsy.directorytree.DataResultFilterNode; import org.sleuthkit.autopsy.directorytree.NewWindowViewAction; import org.sleuthkit.datamodel.AbstractFile; @@ -67,7 +66,12 @@ import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHO import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_TO; import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SUBJECT; import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_TEXT; +import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.blackboardutils.FileAttachment; +import org.sleuthkit.datamodel.blackboardutils.MessageAttachments; +import org.sleuthkit.datamodel.blackboardutils.Attachment; +import org.sleuthkit.datamodel.blackboardutils.URLAttachment; /** * Shows SMS/MMS/EMail messages @@ -541,11 +545,34 @@ public class MessageContentViewer extends javax.swing.JPanel implements DataCont } private void configureAttachments() throws TskCoreException { - //TODO: Replace this with code to get the actual attachements! - final Set attachments = artifact.getChildren().stream() - .filter(AbstractFile.class::isInstance) - .map(AbstractFile.class::cast) - .collect(Collectors.toSet()); + + final Set attachments; + + // Attachments are specified in an attribute TSK_ATTACHMENTS as JSON attribute + BlackboardAttribute attachmentsAttr = artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ATTACHMENTS)); + if(attachmentsAttr != null) { + + attachments = new HashSet<>(); + String jsonVal = attachmentsAttr.getValueString(); + MessageAttachments msgAttachments = new Gson().fromJson(jsonVal, MessageAttachments.class); + + Collection fileAttachments = msgAttachments.getFileAttachments(); + for (FileAttachment fileAttachment: fileAttachments) { + attachments.add(fileAttachment); + } + Collection urlAttachments = msgAttachments.getUrlAttachments(); + for (URLAttachment urlAttachment: urlAttachments) { + attachments.add(urlAttachment); + } + } else { // For backward compatibility - email attachements are derived files and children of the email message artifact + attachments = new HashSet<>(); + for (Content child: artifact.getChildren()) { + if (child instanceof AbstractFile) { + attachments.add(new FileAttachment((AbstractFile)child)); + } + } + } + final int numberOfAttachments = attachments.size(); msgbodyTabbedPane.setEnabledAt(ATTM_TAB_INDEX, numberOfAttachments > 0); @@ -633,16 +660,20 @@ public class MessageContentViewer extends javax.swing.JPanel implements DataCont return doc.html(); } - private static class AttachmentsChildren extends Children.Keys { + + /** + * Creates child nodes for message attachments. + */ + private static class AttachmentsChildren extends Children.Keys { - private final Set attachments; + private final Set attachments; - AttachmentsChildren(Set attachments) { + AttachmentsChildren(Set attachments) { this.attachments = attachments; } @Override - protected Node[] createNodes(AbstractFile t) { + protected Node[] createNodes(Attachment t) { return new Node[]{new AttachmentNode(t)}; } @@ -652,40 +683,4 @@ public class MessageContentViewer extends javax.swing.JPanel implements DataCont setKeys(attachments); } } - - /** - * Extension of FileNode customized for viewing attachments in the - * MessageContentViewer. It overrides createSheet() to customize what - * properties are shown in the table, and could also override getActions(), - * getPreferedAction(), etc. - */ - private static class AttachmentNode extends FileNode { - - AttachmentNode(AbstractFile file) { - super(file, false); - } - - @Override - protected Sheet createSheet() { - Sheet sheet = super.createSheet(); - Set keepProps = new HashSet<>(Arrays.asList( - NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.nameColLbl"), - NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.createSheet.score.name"), - NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.createSheet.comment.name"), - NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.createSheet.count.name"), - NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.sizeColLbl"), - NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.mimeType"), - NbBundle.getMessage(AbstractAbstractFileNode.class, "AbstractAbstractFileNode.knownColLbl"))); - - //Remove all other props except for the ones above - Sheet.Set sheetSet = sheet.get(Sheet.PROPERTIES); - for (Property p : sheetSet.getProperties()) { - if (!keepProps.contains(p.getName())) { - sheetSet.remove(p.getName()); - } - } - - return sheet; - } - } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/AttachmentNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/AttachmentNode.java new file mode 100644 index 0000000000..c095f48fee --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datamodel/AttachmentNode.java @@ -0,0 +1,207 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2017-2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datamodel; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +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; +import org.openide.util.NbBundle; +import org.openide.util.Utilities; +import org.openide.util.lookup.Lookups; +import org.sleuthkit.autopsy.actions.AddContentTagAction; +import org.sleuthkit.autopsy.actions.DeleteFileContentTagAction; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.contentviewers.MessageContentViewer; +import org.sleuthkit.autopsy.coreutils.ContextMenuExtensionPoint; +import org.sleuthkit.autopsy.coreutils.Logger; +import static org.sleuthkit.autopsy.datamodel.FileNode.getIconForFileType; +import org.sleuthkit.autopsy.directorytree.ExportCSVAction; +import org.sleuthkit.autopsy.directorytree.ExternalViewerAction; +import org.sleuthkit.autopsy.directorytree.ExternalViewerShortcutAction; +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.TskException; +import org.sleuthkit.datamodel.blackboardutils.Attachment; +import org.sleuthkit.datamodel.blackboardutils.FileAttachment; +import org.sleuthkit.datamodel.blackboardutils.URLAttachment; + +/** + * Node for a message attachment. + * + */ +public final class AttachmentNode extends DisplayableItemNode { + + private static final Logger LOGGER = Logger.getLogger(MessageContentViewer.class.getName()); + + private final Attachment attachment; + private final AbstractFile attachmentFile; + + public AttachmentNode(Attachment attachment) { + + super(Children.LEAF, createLookup(attachment)); + + super.setName(attachment.getLocation()); + super.setDisplayName(attachment.getLocation()); // SET NODE DISPLAY NAME, I.E., TEXT IN FIRST TABLE CELL + + this.attachment = attachment; + Long attachmentObjId = attachment.getObjId(); + AbstractFile attchmentAbstractFile = null; + + if (attachmentObjId != null && attachmentObjId > 0) { + try { + attchmentAbstractFile = Case.getCurrentCaseThrows().getSleuthkitCase().getAbstractFileById(attachmentObjId); + } catch (TskException | NoCurrentCaseException ex) { + LOGGER.log(Level.WARNING, "Error loading attachment file with object id " + attachmentObjId, ex); //NON-NLS + } + } + attachmentFile = attchmentAbstractFile; + + // set the icon for node + setIcon(); + } + + @Override + @NbBundle.Messages({ + "AttachmentNode.getActions.viewFileInDir.text=View File in Directory", + "AttachmentNode.getActions.viewInNewWin.text=View in New Window", + "AttachmentNode.getActions.openInExtViewer.text=Open in External Viewer Ctrl+E", + "AttachmentNode.getActions.searchFilesSameMD5.text=Search for files with the same MD5 hash"}) + public Action[] getActions(boolean context) { + + List actionsList = new ArrayList<>(); + actionsList.addAll(Arrays.asList(super.getActions(true))); + + // If there is an attachment file + if (this.attachmentFile != null) { + actionsList.add(new ViewContextAction(Bundle.AttachmentNode_getActions_viewFileInDir_text(), this.attachmentFile)); + actionsList.add(null); // Creates an item separator + + actionsList.add(new NewWindowViewAction(Bundle.AttachmentNode_getActions_viewInNewWin_text(), this)); + final Collection selectedFilesList + = new HashSet<>(Utilities.actionsGlobalContext().lookupAll(AbstractFile.class)); + if (selectedFilesList.size() == 1) { + actionsList.add(new ExternalViewerAction( + Bundle.AttachmentNode_getActions_openInExtViewer_text(), this)); + } else { + actionsList.add(ExternalViewerShortcutAction.getInstance()); + } + actionsList.add(ViewFileInTimelineAction.createViewFileAction(this.attachmentFile)); + actionsList.add(null); // Creates an item separator + + actionsList.add(ExtractAction.getInstance()); + actionsList.add(ExportCSVAction.getInstance()); + actionsList.add(null); // Creates an item separator + + actionsList.add(AddContentTagAction.getInstance()); + if (1 == selectedFilesList.size()) { + actionsList.add(DeleteFileContentTagAction.getInstance()); + } + actionsList.addAll(ContextMenuExtensionPoint.getActions()); + + } + return actionsList.toArray(new Action[0]); + } + + @Override + protected Sheet createSheet() { + + // Create a new property sheet. + Sheet sheet = new Sheet(); + Sheet.Set sheetSet = Sheet.createPropertiesSet(); + sheet.put(sheetSet); + + sheetSet.put(new NodeProperty<>("Location", "Location", "", this.attachment.getLocation())); + + if (attachmentFile != null) { + long size = attachmentFile.getSize(); + String mimeType = attachmentFile.getMIMEType(); + + // @TODO Vik-5762: get SCO Columns + + sheetSet.put(new NodeProperty<>("Size", "Size", "", size)); + if (StringUtils.isNotEmpty(mimeType)) { + sheetSet.put(new NodeProperty<>("Mime type", "Mime type", "", mimeType)); + } + sheetSet.put(new NodeProperty<>("Known", "Known", "", attachmentFile.getKnown().getName())); + } + + return sheet; + } + + @Override + public T accept(DisplayableItemNodeVisitor visitor) { + return visitor.visit(this); + } + + @Override + public boolean isLeafTypeNode() { + return true; + } + + @Override + public String getItemType() { + return getClass().getName(); + } + + private static Lookup createLookup(Attachment attachment) { + Long attachmentObjId = attachment.getObjId(); + if (attachmentObjId != null && attachmentObjId > 0) { + AbstractFile attachmentFile = null; + try { + attachmentFile = Case.getCurrentCaseThrows().getSleuthkitCase().getAbstractFileById(attachmentObjId); + if (attachmentFile != null) { + return Lookups.fixed(attachment, attachmentFile); + } else { + return Lookups.fixed(attachment); + } + } catch (TskException | NoCurrentCaseException ex) { + return Lookups.fixed(attachment); + } + } + return Lookups.fixed(attachment); + } + + /** + * Set the icon based on attachment type + */ + private void setIcon() { + if (attachmentFile != null) { + this.setIconBaseWithExtension(getIconForFileType(attachmentFile)); + } else if (attachment instanceof FileAttachment) { + this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/document-question-16.png"); + } else if (attachment instanceof URLAttachment) { + this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/url-16.png"); + } else { + this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/file-icon-deleted.png"); + } + + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties-MERGED index e41321f306..0e0df5a58c 100755 --- a/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties-MERGED @@ -43,6 +43,11 @@ ArtifactStringContent.attrsTableHeader.type=Type ArtifactStringContent.attrsTableHeader.value=Value ArtifactStringContent.failedToGetAttributes.message=Failed to get some or all attributes from case database ArtifactStringContent.failedToGetSourcePath.message=Failed to get source file path from case database +AttachmentNode.getActions.openInExtViewer.text=Open in External Viewer Ctrl+E +AttachmentNode.getActions.searchFilesSameMD5.text=Search for files with the same MD5 hash +AttachmentNode.getActions.viewFileInDir.text=View File in Directory +AttachmentNode.getActions.viewInNewWin.text=View in New Window +# {0} - node name BaseChildFactory.NoSuchEventBusException.message=No event bus for node: {0} BlackboardArtifactNode.createSheet.artifactDetails.displayName=Result Details BlackboardArtifactNode.createSheet.artifactDetails.name=Result Details diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNodeVisitor.java b/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNodeVisitor.java index bb324bc7bb..541cbc38f3 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNodeVisitor.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNodeVisitor.java @@ -186,6 +186,11 @@ public interface DisplayableItemNodeVisitor { T visit(InterestingHits.InterestingItemTypeNode aThis); + /* + * Attachments + */ + T visit(AttachmentNode node); + /** * Visitor with an implementable default behavior for all types. Override * specific visit types to not use the default behavior. @@ -522,5 +527,11 @@ public interface DisplayableItemNodeVisitor { public T visit(Accounts.DefaultAccountTypeNode node) { return defaultVisit(node); } + + @Override + public T visit(AttachmentNode node) { + return defaultVisit(node); + } + } } diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/AbstractSingleKeyValueParser.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/AbstractSingleKeyValueParser.java new file mode 100755 index 0000000000..08dad04991 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/AbstractSingleKeyValueParser.java @@ -0,0 +1,164 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourceprocessors.xry; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Template parse method for reports that make blackboard attributes from a + * single key value pair. + * + * This parse implementation will create 1 artifact per XRY entity. + */ +abstract class AbstractSingleKeyValueParser implements XRYFileParser { + + private static final Logger logger = Logger.getLogger(AbstractSingleKeyValueParser.class.getName()); + + private static final char KEY_VALUE_DELIMITER = ':'; + + protected static final String PARSER_NAME = "XRY DSP"; + + @Override + public void parse(XRYFileReader reader, Content parent) throws IOException, TskCoreException { + Path reportPath = reader.getReportPath(); + logger.log(Level.INFO, String.format("[XRY DSP] Processing report at [ %s ]", reportPath.toString())); + + while (reader.hasNextEntity()) { + String xryEntity = reader.nextEntity(); + String[] xryLines = xryEntity.split("\n"); + + List attributes = new ArrayList<>(); + + //First line of the entity is the title. + if (xryLines.length > 0) { + logger.log(Level.INFO, String.format("[XRY DSP] Processing [ %s ]", xryLines[0])); + } + + String namespace = ""; + //Process each line, searching for a key value pair or a namespace. + //If neither are found, an error message is logged. + for (int i = 1; i < xryLines.length; i++) { + String xryLine = xryLines[i]; + + String candidateNamespace = xryLine.trim(); + //Check if the line is a namespace, which gives context to the keys + //that follow. + if (isNamespace(candidateNamespace)) { + namespace = candidateNamespace; + continue; + } + + //Find the XRY key on this line. Assume key is the value between + //the start of the line and the first delimiter. + int keyDelimiter = xryLine.indexOf(KEY_VALUE_DELIMITER); + if (keyDelimiter == -1) { + logger.log(Level.SEVERE, String.format("[XRY DSP] Expected a key value " + + "pair on this line (in brackets) [ %s ], but one was not detected." + + " Here is the previous line [ %s ]. What does this mean?", xryLine, xryLines[i - 1])); + continue; + } + String key = xryLine.substring(0, keyDelimiter).trim(); + String value = xryLine.substring(keyDelimiter + 1).trim(); + + if (!isKey(key)) { + logger.log(Level.SEVERE, String.format("[XRY DSP] The following key, " + + "value pair (in brackets, respectively) [ %s ], [ %s ] was not recognized. Discarding..." + + " Here is the previous line [ %s ] for context. What does this key mean?", key, value, xryLines[i - 1])); + continue; + } + + if (value.isEmpty()) { + logger.log(Level.SEVERE, String.format("[XRY DSP] The following key " + + "(in brackets) [ %s ] was recognized, but the value was empty. Discarding..." + + " Here is the previous line for context [ %s ]. What does this mean?", key, xryLines[i - 1])); + continue; + } + + BlackboardAttribute attribute = makeAttribute(namespace, key, value); + //Temporarily allowing null to be valid return type until a decision + //is made about how to handle keys we are choosing to ignore. + if (attribute != null) { + attributes.add(makeAttribute(namespace, key, value)); + } + } + + //Only create artifacts with non-empty attributes. + if (!attributes.isEmpty()) { + makeArtifact(attributes, parent); + } + } + } + + /** + * Determines if the key candidate is a known key. A key candidate is a + * string literal that begins a line and is terminated by a semi-colon. + * + * Ex: + * + * Call Type : Missed + * + * "Call Type" would be the key candidate that was extracted. + * + * @param key Key to test. These keys are trimmed of whitespace only. + * @return Indication if this key can be processed. + */ + abstract boolean isKey(String key); + + /** + * Determines if the namespace candidate is a known namespace. A namespace + * candidate is a string literal that makes up an entire line. + * + * Ex: + * + * To + * Tel : +1245325 + * + * "To" would be the candidate namespace that was extracted. + * + * @param nameSpace Namespace to test. Namespaces are trimmed of whitespace + * only. + * @return Indication if this namespace can be processed. + */ + abstract boolean isNamespace(String nameSpace); + + /** + * Creates an attribute from the extracted key value pair. + * + * @param nameSpace The namespace of this key value pair. + * It will have been verified with isNamespace, otherwise it will be empty. + * @param key The key that was verified with isKey. + * @param value The value associated with that key. + * @return + */ + abstract BlackboardAttribute makeAttribute(String nameSpace, String key, String value); + + /** + * Makes an artifact from the parsed attributes. + */ + abstract void makeArtifact(List attributes, Content parent) throws TskCoreException; + +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/Bundle.properties b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/Bundle.properties new file mode 100755 index 0000000000..05c5199962 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/Bundle.properties @@ -0,0 +1,4 @@ +XRYDataSourceProcessorConfigPanel.fileBrowserButton.text=Browse +XRYDataSourceProcessorConfigPanel.filePathTextField.text= +XRYDataSourceProcessorConfigPanel.xrySelectFolderLabel.text=Select an XRY Folder +XRYDataSourceProcessorConfigPanel.errorLabel.text= diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/Bundle.properties-MERGED new file mode 100755 index 0000000000..f8d924a1a2 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/Bundle.properties-MERGED @@ -0,0 +1,13 @@ +XRYDataSourceProcessor.dataSourceType=XRY Logical Report +XRYDataSourceProcessor.fileAdded=Added %s to the case database +XRYDataSourceProcessor.ioError=I/O error occured trying to test the XRY report folder +XRYDataSourceProcessor.noPathSelected=Please select a XRY folder +XRYDataSourceProcessor.notReadable=Could not read from the selected folder +XRYDataSourceProcessor.notXRYFolder=Selected folder did not contain any XRY files +XRYDataSourceProcessor.preppingFiles=Preparing to add files to the case database +XRYDataSourceProcessor.processingFiles=Processing all XRY files... +XRYDataSourceProcessor.unexpectedError=Internal error occurred while processing XRY report +XRYDataSourceProcessorConfigPanel.fileBrowserButton.text=Browse +XRYDataSourceProcessorConfigPanel.filePathTextField.text= +XRYDataSourceProcessorConfigPanel.xrySelectFolderLabel.text=Select an XRY Folder +XRYDataSourceProcessorConfigPanel.errorLabel.text= diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYCallsFileParser.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYCallsFileParser.java new file mode 100755 index 0000000000..596f25e7b1 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYCallsFileParser.java @@ -0,0 +1,175 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourceprocessors.xry; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Set; +import java.util.HashSet; +import java.util.List; +import java.util.logging.Level; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Parses XRY Calls files and creates artifacts. + */ +final class XRYCallsFileParser extends AbstractSingleKeyValueParser { + + private static final Logger logger = Logger.getLogger(XRYCallsFileParser.class.getName()); + + private static final DateTimeFormatter DATE_TIME_PARSER + = DateTimeFormatter.ofPattern("M/d/y h:m:s [a][ z]"); + + private static final String INCOMING = "Incoming"; + + //All known XRY keys for call reports. + private static final Set XRY_KEYS = new HashSet() { + { + add("tel"); + add("number"); + add("call type"); + add("name (matched)"); + add("time"); + add("duration"); + add("storage"); + add("index"); + } + }; + + //All known XRY namespaces for call reports. + private static final Set XRY_NAMESPACES = new HashSet() { + { + add("to"); + add("from"); + } + }; + + @Override + boolean isKey(String key) { + String normalizedKey = key.toLowerCase(); + return XRY_KEYS.contains(normalizedKey); + } + + @Override + boolean isNamespace(String nameSpace) { + String normalizedNamespace = nameSpace.toLowerCase(); + return XRY_NAMESPACES.contains(normalizedNamespace); + } + + @Override + BlackboardAttribute makeAttribute(String nameSpace, String key, String value) { + String normalizedKey = key.toLowerCase(); + String normalizedNamespace = nameSpace.toLowerCase(); + + switch (normalizedKey) { + case "time": + //Tranform value to epoch ms + try { + String dateTime = removeDateTimeLocale(value); + String normalizedDateTime = dateTime.trim(); + long dateTimeInEpoch = calculateSecondsSinceEpoch(normalizedDateTime); + return new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_START, PARSER_NAME, dateTimeInEpoch); + } catch (DateTimeParseException ex) { + logger.log(Level.SEVERE, String.format("[XRY DSP] Assumption about the date time " + + "formatting of call logs is not right. Here is the value [ %s ]", value), ex); + return null; + } + case "duration": + //Ignore for now. + return null; + case "storage": + //Ignore for now. + return null; + case "index": + //Ignore for now. + return null; + case "tel": + //Apply the namespace + if(normalizedNamespace.equals("from")) { + return new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_FROM, PARSER_NAME, value); + } else { + return new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_TO, PARSER_NAME, value); + } + case "call type": + String normalizedValue = value.toLowerCase(); + switch (normalizedValue) { + case "missed": + case "received": + return new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DIRECTION, PARSER_NAME, INCOMING); + case "dialed": + //Ignore for now. + return null; + case "last dialed": + //Ignore for now. + return null; + default: + logger.log(Level.SEVERE, String.format("Call type (in brackets) [ %s ] not recognized.", value)); + return null; + } + case "number": + return new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER, PARSER_NAME, value); + case "name (matched)": + return new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME, PARSER_NAME, value); + default: + throw new IllegalArgumentException(String.format("key [ %s ] was not recognized.", key)); + } + } + + @Override + void makeArtifact(List attributes, Content parent) throws TskCoreException { + BlackboardArtifact artifact = parent.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_CALLLOG); + artifact.addAttributes(attributes); + } + + /** + * Removes the locale from the date time value. + * + * Locale in this case being (Device) or (Network). + * + * @param dateTime XRY datetime value to be sanitized. + * @return A purer date time value. + */ + private String removeDateTimeLocale(String dateTime) { + int index = dateTime.indexOf('('); + if (index == -1) { + return dateTime; + } + + return dateTime.substring(0, index); + } + + /** + * Parses the date time value and calculates ms since epoch. The time zone is + * assumed to be UTC. + * + * @param dateTime + * @return + */ + private long calculateSecondsSinceEpoch(String dateTime) { + LocalDateTime localDateTime = LocalDateTime.parse(dateTime, DATE_TIME_PARSER); + //Assume dates have no offset. + return localDateTime.toInstant(ZoneOffset.UTC).getEpochSecond(); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYContactsFileParser.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYContactsFileParser.java new file mode 100755 index 0000000000..ec8fd40187 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYContactsFileParser.java @@ -0,0 +1,74 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourceprocessors.xry; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Parses XRY Contacts-Contacts files and creates artifacts. + */ +final class XRYContactsFileParser extends AbstractSingleKeyValueParser { + + //All of the known XRY keys for contacts. + private static final Set XRY_KEYS = new HashSet() {{ + add("name"); + add("tel"); + add("storage"); + }}; + + @Override + boolean isKey(String key) { + String normalizedKey = key.toLowerCase(); + return XRY_KEYS.contains(normalizedKey); + } + + @Override + boolean isNamespace(String nameSpace) { + //No namespaces are currently known for this report type. + return false; + } + + @Override + BlackboardAttribute makeAttribute(String nameSpace, String key, String value) { + String normalizedKey = key.toLowerCase(); + switch(normalizedKey) { + case "name": + return new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME, PARSER_NAME, value); + case "tel": + return new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER, PARSER_NAME, value); + case "storage": + //Ignore for now. + return null; + default: + throw new IllegalArgumentException(String.format("Key [ %s ] was not recognized", key)); + } + } + + @Override + void makeArtifact(List attributes, Content parent) throws TskCoreException { + BlackboardArtifact artifact = parent.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_CONTACT); + artifact.addAttributes(attributes); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYDataSourceProcessor.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYDataSourceProcessor.java new file mode 100755 index 0000000000..082f098a61 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYDataSourceProcessor.java @@ -0,0 +1,247 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourceprocessors.xry; + +import com.google.common.collect.Lists; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import java.util.stream.Collectors; +import javax.swing.JPanel; +import javax.swing.SwingWorker; +import org.openide.util.NbBundle; +import org.openide.util.lookup.ServiceProvider; +import org.openide.util.lookup.ServiceProviders; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.casemodule.services.FileManager; +import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; +import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback; +import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgressMonitor; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.LocalFilesDataSource; +import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.TskDataException; + +/** + * An XRY Report data source processor. + */ +@ServiceProviders(value = { + @ServiceProvider(service = DataSourceProcessor.class)} +) +public class XRYDataSourceProcessor implements DataSourceProcessor { + + private final XRYDataSourceProcessorConfigPanel configPanel; + + //Background processor to relieve the EDT from adding files to the case + //database and parsing the report files. + private XRYReportProcessorSwingWorker swingWorker; + + private static final Logger logger = Logger.getLogger(XRYDataSourceProcessor.class.getName()); + + public XRYDataSourceProcessor() { + configPanel = XRYDataSourceProcessorConfigPanel.getInstance(); + } + + @Override + @NbBundle.Messages({ + "XRYDataSourceProcessor.dataSourceType=XRY Logical Report" + }) + public String getDataSourceType() { + return Bundle.XRYDataSourceProcessor_dataSourceType(); + } + + @Override + public JPanel getPanel() { + return configPanel; + } + + @Override + @NbBundle.Messages({ + "XRYDataSourceProcessor.noPathSelected=Please select a XRY folder", + "XRYDataSourceProcessor.notReadable=Could not read from the selected folder", + "XRYDataSourceProcessor.notXRYFolder=Selected folder did not contain any XRY files", + "XRYDataSourceProcessor.ioError=I/O error occured trying to test the XRY report folder" + }) + public boolean isPanelValid() { + configPanel.clearErrorText(); + String selectedFilePath = configPanel.getSelectedFilePath(); + if(selectedFilePath.isEmpty()) { + configPanel.setErrorText(Bundle.XRYDataSourceProcessor_noPathSelected()); + return false; + } + + File selectedFile = new File(selectedFilePath); + Path selectedPath = selectedFile.toPath(); + + //Test permissions + if (!Files.isReadable(selectedPath)) { + configPanel.setErrorText(Bundle.XRYDataSourceProcessor_notReadable()); + return false; + } + + try { + //Validate the folder. + if (!XRYFolder.isXRYFolder(selectedPath)) { + configPanel.setErrorText(Bundle.XRYDataSourceProcessor_notXRYFolder()); + return false; + } + } catch (IOException ex) { + configPanel.setErrorText(Bundle.XRYDataSourceProcessor_ioError()); + logger.log(Level.WARNING, "[XRY DSP] I/O exception encountered trying to test the XRY folder.", ex); + return false; + } + + return true; + } + + /** + * Processes the XRY folder that the examiner selected. The heavy lifting is + * done off of the EDT. + */ + @Override + public void run(DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) { + progressMonitor.setIndeterminate(true); + + String selectedFilePath = configPanel.getSelectedFilePath(); + File selectedFile = new File(selectedFilePath); + Path selectedPath = selectedFile.toPath(); + + try { + XRYFolder xryFolder = new XRYFolder(selectedPath); + FileManager fileManager = Case.getCurrentCaseThrows() + .getServices().getFileManager(); + + //Move heavy lifting to a backround task. + swingWorker = new XRYReportProcessorSwingWorker(xryFolder, progressMonitor, + callback, fileManager); + swingWorker.execute(); + } catch (NoCurrentCaseException ex) { + logger.log(Level.WARNING, "[XRY DSP] No case is currently open.", ex); + } + } + + @Override + public void cancel() { + if (swingWorker != null) { + swingWorker.cancel(true); + } + } + + @Override + public void reset() { + //Clear the current selected file path. + configPanel.clearSelectedFilePath(); + } + + /** + * Relieves the EDT from having to process the XRY report and write to the + * case database. + */ + private class XRYReportProcessorSwingWorker extends SwingWorker { + + private final DataSourceProcessorProgressMonitor progressMonitor; + private final DataSourceProcessorCallback callback; + private final FileManager fileManager; + private final XRYFolder xryFolder; + + public XRYReportProcessorSwingWorker(XRYFolder folder, DataSourceProcessorProgressMonitor progressMonitor, + DataSourceProcessorCallback callback, FileManager fileManager) { + this.xryFolder = folder; + this.progressMonitor = progressMonitor; + this.callback = callback; + this.fileManager = fileManager; + } + + @Override + @NbBundle.Messages({ + "XRYDataSourceProcessor.preppingFiles=Preparing to add files to the case database", + "XRYDataSourceProcessor.processingFiles=Processing all XRY files..." + }) + protected LocalFilesDataSource doInBackground() throws TskCoreException, + TskDataException, IOException { + progressMonitor.setProgressText(Bundle.XRYDataSourceProcessor_preppingFiles()); + + List nonXRYFiles = xryFolder.getNonXRYFiles(); + List filePaths = nonXRYFiles.stream() + //Map paths to string representations. + .map(Path::toString) + .collect(Collectors.toList()); + String uniqueUUID = UUID.randomUUID().toString(); + LocalFilesDataSource dataSource = fileManager.addLocalFilesDataSource( + uniqueUUID, + "XRY Report", //Name + "", //Timezone + filePaths, + new ProgressMonitorAdapter(progressMonitor)); + + //Process the report files. + progressMonitor.setProgressText(Bundle.XRYDataSourceProcessor_processingFiles()); + XRYReportProcessor.process(xryFolder, dataSource); + return dataSource; + } + + @Override + @NbBundle.Messages({ + "XRYDataSourceProcessor.unexpectedError=Internal error occurred while processing XRY report" + }) + public void done() { + try { + LocalFilesDataSource newDataSource = get(); + callback.done(DataSourceProcessorCallback.DataSourceProcessorResult.NO_ERRORS, + Lists.newArrayList(), Lists.newArrayList(newDataSource)); + } catch (InterruptedException ex) { + logger.log(Level.WARNING, "[XRY DSP] Thread was interrupted while processing the XRY report." + + " The case may or may not have the complete XRY report.", ex); + } catch (ExecutionException ex) { + logger.log(Level.SEVERE, "[XRY DSP] Unexpected internal error while processing XRY report.", ex); + callback.done(DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS, + Lists.newArrayList(Bundle.XRYDataSourceProcessor_unexpectedError(), + ex.toString()), Lists.newArrayList()); + } + } + + /** + * Makes the DSP progress monitor compatible with the File Manager + * progress updater. + */ + private class ProgressMonitorAdapter implements FileManager.FileAddProgressUpdater { + + private final DataSourceProcessorProgressMonitor progressMonitor; + + ProgressMonitorAdapter(DataSourceProcessorProgressMonitor progressMonitor) { + this.progressMonitor = progressMonitor; + } + + @Override + @NbBundle.Messages({ + "XRYDataSourceProcessor.fileAdded=Added %s to the case database" + }) + public void fileAdded(AbstractFile newFile) { + progressMonitor.setProgressText(String.format(Bundle.XRYDataSourceProcessor_fileAdded(), newFile.getName())); + } + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYDataSourceProcessorConfigPanel.form b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYDataSourceProcessorConfigPanel.form new file mode 100755 index 0000000000..2fd4848e91 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYDataSourceProcessorConfigPanel.form @@ -0,0 +1,88 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYDataSourceProcessorConfigPanel.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYDataSourceProcessorConfigPanel.java new file mode 100755 index 0000000000..c35666d9c1 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYDataSourceProcessorConfigPanel.java @@ -0,0 +1,178 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourceprocessors.xry; + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.io.File; +import javax.swing.JFileChooser; +import javax.swing.JPanel; +import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; + +/** + * Allows an examiner to configure the XRY Data source processor. + */ +@SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives +final class XRYDataSourceProcessorConfigPanel extends JPanel { + + private static final long serialVersionUID = 1L; + private static final XRYDataSourceProcessorConfigPanel INSTANCE = + new XRYDataSourceProcessorConfigPanel(); + + //Communicates + private final PropertyChangeSupport pcs; + + /** + * Creates new form XRYDataSourceConfigPanel. + * Prevent direct instantiation. + */ + private XRYDataSourceProcessorConfigPanel() { + initComponents(); + pcs = new PropertyChangeSupport(this); + } + + /** + * Gets the singleton XRYDataSourceProcessorConfigPanel. + */ + static XRYDataSourceProcessorConfigPanel getInstance() { + return INSTANCE; + } + + /** + * Clears the error label. + */ + void clearErrorText() { + errorLabel.setText(null); + } + + /** + * Sets the error label to show the supplied text. + */ + void setErrorText(String text) { + errorLabel.setText(text); + } + + /** + * Clears the selected file path. + */ + void clearSelectedFilePath() { + filePathTextField.setText(null); + } + + /** + * Gets the file path selected by the examiner. + */ + String getSelectedFilePath() { + return filePathTextField.getText(); + } + + /** + * Adds a property change listener to this config panel. + */ + @Override + public synchronized void addPropertyChangeListener(PropertyChangeListener pcl) { + super.addPropertyChangeListener(pcl); + pcs.addPropertyChangeListener(pcl); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + filePathTextField = new javax.swing.JTextField(); + fileBrowserButton = new javax.swing.JButton(); + xrySelectFolderLabel = new javax.swing.JLabel(); + errorLabel = new javax.swing.JLabel(); + + filePathTextField.setEditable(false); + filePathTextField.setText(org.openide.util.NbBundle.getMessage(XRYDataSourceProcessorConfigPanel.class, "XRYDataSourceProcessorConfigPanel.filePathTextField.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(fileBrowserButton, org.openide.util.NbBundle.getMessage(XRYDataSourceProcessorConfigPanel.class, "XRYDataSourceProcessorConfigPanel.fileBrowserButton.text")); // NOI18N + fileBrowserButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + fileBrowserButtonActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(xrySelectFolderLabel, org.openide.util.NbBundle.getMessage(XRYDataSourceProcessorConfigPanel.class, "XRYDataSourceProcessorConfigPanel.xrySelectFolderLabel.text")); // NOI18N + + errorLabel.setForeground(new java.awt.Color(255, 0, 0)); + org.openide.awt.Mnemonics.setLocalizedText(errorLabel, org.openide.util.NbBundle.getMessage(XRYDataSourceProcessorConfigPanel.class, "XRYDataSourceProcessorConfigPanel.errorLabel.text")); // NOI18N + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(errorLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(xrySelectFolderLabel, javax.swing.GroupLayout.DEFAULT_SIZE, 380, Short.MAX_VALUE) + .addGroup(layout.createSequentialGroup() + .addComponent(filePathTextField) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(fileBrowserButton))) + .addContainerGap()) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(xrySelectFolderLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(filePathTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(fileBrowserButton)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(errorLabel) + .addContainerGap(235, Short.MAX_VALUE)) + ); + }// //GEN-END:initComponents + + /** + * Opens a JFileChooser instance so that the examiner can select a XRY + * report folder. + */ + private void fileBrowserButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_fileBrowserButtonActionPerformed + JFileChooser fileChooser = new JFileChooser(); + fileChooser.setMultiSelectionEnabled(false); + fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + int returnVal = fileChooser.showOpenDialog(this); + if (returnVal == JFileChooser.APPROVE_OPTION) { + File selection = fileChooser.getSelectedFile(); + filePathTextField.setText(selection.getAbsolutePath()); + + //This will notify the wizard to revalidate the data source processor. + pcs.firePropertyChange(DataSourceProcessor.DSP_PANEL_EVENT.UPDATE_UI.toString(), false, true); + } + }//GEN-LAST:event_fileBrowserButtonActionPerformed + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JLabel errorLabel; + private javax.swing.JButton fileBrowserButton; + private javax.swing.JTextField filePathTextField; + private javax.swing.JLabel xrySelectFolderLabel; + // End of variables declaration//GEN-END:variables +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYDeviceGenInfoFileParser.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYDeviceGenInfoFileParser.java new file mode 100755 index 0000000000..d3bba45bfc --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYDeviceGenInfoFileParser.java @@ -0,0 +1,191 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourceprocessors.xry; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Parses XRY Device-General Information files and creates artifacts. + */ +final class XRYDeviceGenInfoFileParser implements XRYFileParser { + + private static final Logger logger = Logger.getLogger(XRYDeviceGenInfoFileParser.class.getName()); + + //Human readable name of this parser. + private static final String PARSER_NAME = "XRY DSP"; + private static final char KEY_VALUE_DELIMITER = ':'; + + //All known XRY keys for Device Gen Info reports. + private static final String ATTRIBUTE_KEY = "attribute"; + private static final String DATA_KEY = "data"; + + //All of the known XRY Attribute values for device gen info. The value of the + //attribute keys are actionable for this parser. See parse header for more + //details. + private static final Map KEY_TO_TYPE + = new HashMap() { + { + put("device name", BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DEVICE_NAME); + put("device family", BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DEVICE_MODEL); + put("device type", BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DEVICE_MAKE); + put("mobile id (imei)", BlackboardAttribute.ATTRIBUTE_TYPE.TSK_IMEI); + put("security code", BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PASSWORD); + } + }; + + /** + * Device-General Information reports have 2 key value pairs for every + * attribute. The two only known keys are "Data" and "Attribute", where data + * is some generic information that the Attribute key describes. + * + * Example: + * + * Data: Nokia XYZ + * Attribute: Device Name + * + * This parse implementation assumes that the data field does not span + * multiple lines. If the data does span multiple lines, it will log an + * error describing an expectation for an "Attribute" key that is not found. + * + * @param reader The XRYFileReader that reads XRY entities from the + * Device-General Information report. + * @param parent The parent Content to create artifacts from. + * @throws IOException If an I/O error is encountered during report reading + * @throws TskCoreException If an error during artifact creation is encountered. + */ + @Override + public void parse(XRYFileReader reader, Content parent) throws IOException, TskCoreException { + Path reportPath = reader.getReportPath(); + logger.log(Level.INFO, String.format("[XRY DSP] Processing report at [ %s ]", reportPath.toString())); + + while (reader.hasNextEntity()) { + String xryEntity = reader.nextEntity(); + String[] xryLines = xryEntity.split("\n"); + + List attributes = new ArrayList<>(); + + //First line of the entity is the title. + if (xryLines.length > 0) { + logger.log(Level.INFO, String.format("[XRY DSP] Processing [ %s ]", xryLines[0])); + } + + for (int i = 1; i < xryLines.length; i++) { + String xryLine = xryLines[i]; + + //Expecting to see a "Data" key. + if (!hasDataKey(xryLine)) { + logger.log(Level.SEVERE, String.format("[XRY DSP] Expected a 'Data' key " + + "on this line (in brackets) [ %s ], but none was found. " + + "Discarding... Here is the previous line for context [ %s ]. " + + "What does this mean?", xryLine, xryLines[i - 1])); + continue; + } + + if (i + 1 == xryLines.length) { + logger.log(Level.SEVERE, String.format("[XRY DSP] Found a 'Data' key " + + "but no corresponding 'Attribute' key. Discarding... Here " + + "is the 'Data' line (in brackets) [ %s ]. Here is the previous " + + "line for context [ %s ]. What does this mean?", xryLine, xryLines[i - 1])); + continue; + } + + int dataKeyIndex = xryLine.indexOf(KEY_VALUE_DELIMITER); + String dataValue = xryLine.substring(dataKeyIndex + 1).trim(); + + String nextXryLine = xryLines[++i]; + + //Expecting to see an "Attribute" key + if (!hasAttributeKey(nextXryLine)) { + logger.log(Level.SEVERE, String.format("[XRY DSP] Expected an 'Attribute' " + + "key on this line (in brackets) [ %s ], but none was found. " + + "Discarding... Here is the previous line for context [ %s ]. " + + "What does this mean?", nextXryLine, xryLine)); + continue; + } + + int attributeKeyIndex = nextXryLine.indexOf(KEY_VALUE_DELIMITER); + String attributeValue = nextXryLine.substring(attributeKeyIndex + 1).trim(); + String normalizedAttributeValue = attributeValue.toLowerCase(); + + //Check if the attribute value is recognized. + if (KEY_TO_TYPE.containsKey(normalizedAttributeValue)) { + //All of the attribute types in the map expect a string. + attributes.add(new BlackboardAttribute(KEY_TO_TYPE.get(normalizedAttributeValue), PARSER_NAME, dataValue)); + } else { + logger.log(Level.SEVERE, String.format("[XRY DSP] Attribute type (in brackets) " + + "[ %s ] was not recognized. Discarding... Here is the " + + "previous line for context [ %s ]. What does this mean?", nextXryLine, xryLine)); + } + } + + if(!attributes.isEmpty()) { + //Build the artifact. + BlackboardArtifact artifact = parent.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_DEVICE_INFO); + artifact.addAttributes(attributes); + } + } + } + + /** + * Determines if the XRY line has a data key on it. + * + * @param xryLine + * @return + */ + private boolean hasDataKey(String xryLine) { + int dataKeyIndex = xryLine.indexOf(KEY_VALUE_DELIMITER); + //No key structure found. + if (dataKeyIndex == -1) { + return false; + } + + String normalizedDataKey = xryLine.substring(0, + dataKeyIndex).trim().toLowerCase(); + return normalizedDataKey.equals(DATA_KEY); + } + + /** + * Determines if the XRY line has an attribute key on it. + * + * @param xryLine + * @return + */ + private boolean hasAttributeKey(String xryLine) { + int attributeKeyIndex = xryLine.indexOf(KEY_VALUE_DELIMITER); + //No key structure found. + if (attributeKeyIndex == -1) { + return false; + } + + String normalizedDataKey = xryLine.substring(0, + attributeKeyIndex).trim().toLowerCase(); + return normalizedDataKey.equals(ATTRIBUTE_KEY); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFileParser.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFileParser.java new file mode 100755 index 0000000000..1787641e78 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFileParser.java @@ -0,0 +1,46 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourceprocessors.xry; + +import java.io.IOException; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Interface for XRY file parsing. + */ +interface XRYFileParser { + + /** + * Parses XRY entities and creates artifacts from the interpreted content. + * + * See XRYFileReader for more information on XRY entities. It is expected + * that implementations will create artifacts on the supplied Content + * object. + * + * @param reader Produces XRY entities from a given XRY file. + * @param parent Content object that will act as the source of the + * artifacts. + * @throws IOException If an I/O error occurs during reading. + * @throws TskCoreException If an error occurs during artifact creation. + */ + void parse(XRYFileReader reader, Content parent) throws IOException, TskCoreException; + +} + diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFileParserFactory.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFileParserFactory.java new file mode 100755 index 0000000000..06492de07b --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFileParserFactory.java @@ -0,0 +1,80 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourceprocessors.xry; + +/** + * Instantiates XRYFileParsers by report type. + */ +final class XRYFileParserFactory { + + /** + * Creates the correct implementation of a XRYFileParser for the specified + * report type. + * + * It is assumed that the report type is supported, which means the client + * needs to have tested with supports beforehand. Otherwise, an + * IllegalArgumentException is thrown. + * + * @param reportType A supported XRY report type. + * @return A XRYFileParser with defined behavior for the report type. + * @throws IllegalArgumentException if the report type is not supported or + * is null. This is a misuse of the API. It is assumed that the report type + * has been tested with the supports method. + */ + static XRYFileParser get(String reportType) { + if (reportType == null) { + throw new IllegalArgumentException("Report type cannot be null"); + } + + switch (reportType.trim().toLowerCase()) { + case "calls": + return new XRYCallsFileParser(); + case "contacts/contacts": + return new XRYContactsFileParser(); + case "device/general information": + return new XRYDeviceGenInfoFileParser(); + case "messages/sms": + return new XRYMessagesFileParser(); + case "web/bookmarks": + return new XRYWebBookmarksFileParser(); + default: + throw new IllegalArgumentException(reportType + " not recognized."); + } + } + + /** + * Tests if a XRYFileParser implementation exists for the report type. + * + * @param reportType Report type to test. + * @return Indication if the report type can be parsed. + */ + static boolean supports(String reportType) { + try { + //Attempt a get. + get(reportType); + return true; + } catch (IllegalArgumentException ex) { + return false; + } + } + + //Prevent direct instantiation + private XRYFileParserFactory() { + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFileReader.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFileReader.java index ff854a16ea..bc3c04259b 100755 --- a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFileReader.java +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFileReader.java @@ -45,36 +45,43 @@ import org.apache.commons.io.FilenameUtils; * From * Tel: 12345678 */ -public final class XRYFileReader implements AutoCloseable { +final class XRYFileReader implements AutoCloseable { private static final Logger logger = Logger.getLogger(XRYFileReader.class.getName()); //Assume UTF_16LE private static final Charset CHARSET = StandardCharsets.UTF_16LE; - //Assume TXT extension - private static final String EXTENSION = "txt"; - - //Assume 0xFFFE is the BOM - private static final int[] BOM = {0xFF, 0xFE}; - //Assume all XRY reports have the type on the 3rd line. private static final int LINE_WITH_REPORT_TYPE = 3; //Assume all headers are 5 lines in length. private static final int HEADER_LENGTH_IN_LINES = 5; + //Assume TXT extension + private static final String EXTENSION = "txt"; + + //Assume 0xFFFE is the BOM + private static final int[] BOM = {0xFF, 0xFE}; + + //Entity to be consumed during file iteration. + private final StringBuilder xryEntity; + //Underlying reader for the xry file. private final BufferedReader reader; - private final StringBuilder xryEntity; + //Reference to the original xry file. + private final Path xryFilePath; /** * Creates an XRYFileReader. As part of construction, the XRY file is opened * and the reader is advanced past the header. This leaves the reader * positioned at the start of the first XRY entity. * - * The file is assumed to be encoded in UTF-16LE. + * The file is assumed to be encoded in UTF-16LE and is NOT verified to be + * an XRY file before reading. It is expected that the isXRYFile function + * has been called on the path beforehand. Otherwise, the behavior is + * undefined. * * @param xryFile XRY file to read. It is assumed that the caller has read * access to the path. @@ -82,6 +89,7 @@ public final class XRYFileReader implements AutoCloseable { */ public XRYFileReader(Path xryFile) throws IOException { reader = Files.newBufferedReader(xryFile, CHARSET); + xryFilePath = xryFile; //Advance the reader to the start of the first XRY entity. for (int i = 0; i < HEADER_LENGTH_IN_LINES; i++) { @@ -91,6 +99,35 @@ public final class XRYFileReader implements AutoCloseable { xryEntity = new StringBuilder(); } + /** + * Extracts the report type from the XRY file. + * + * @return The XRY report type + * @throws IOException if an I/O error occurs. + * @throws IllegalArgumentExcepton If the XRY file does not have a report + * type. This is a misuse of the API. The validity of the Path should have + * been checked with isXRYFile before creating an XRYFileReader. + */ + public String getReportType() throws IOException { + Optional reportType = getType(xryFilePath); + if (reportType.isPresent()) { + return reportType.get(); + } + + throw new IllegalArgumentException(xryFilePath.toString() + " does not " + + "have a report type."); + } + + /** + * Returns the raw path of the XRY report file. + * + * @return + * @throws IOException + */ + public Path getReportPath() throws IOException { + return xryFilePath; + } + /** * Advances the reader until a valid XRY entity is detected or EOF is * reached. @@ -113,7 +150,7 @@ public final class XRYFileReader implements AutoCloseable { return true; } } else { - xryEntity.append(line).append("\n"); + xryEntity.append(line).append('\n'); } } @@ -123,6 +160,7 @@ public final class XRYFileReader implements AutoCloseable { /** * Returns an XRY entity if there is one, otherwise an exception is thrown. + * Clients should test for another entity by calling hasNextEntity(). * * @return A non-empty XRY entity. * @throws IOException if an I/O error occurs. @@ -139,6 +177,23 @@ public final class XRYFileReader implements AutoCloseable { } } + /** + * Peek at the next XRY entity without consuming it. If there are not more + * XRY entities left, an exception is thrown. Clients should test for + * another entity by calling hasNextEntity(). + * + * @return A non-empty XRY entity. + * @throws IOException if an I/O error occurs. + * @throws NoSuchElementException if there are no more XRY entities to peek. + */ + public String peek() throws IOException { + if (hasNextEntity()) { + return xryEntity.toString(); + } else { + throw new NoSuchElementException(); + } + } + /** * Closes any file handles this reader may have open. * diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFolder.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFolder.java index b9b999f270..f78154d796 100755 --- a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFolder.java +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFolder.java @@ -24,18 +24,76 @@ import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; import java.util.Iterator; +import java.util.List; import java.util.stream.Stream; /** * Extracts XRY files and (optionally) non-XRY files from a XRY (Report) folder. */ -public final class XRYFolder { +final class XRYFolder { //Depth that will contain XRY files. All XRY files will be immediate //children of their parent folder. private static final int XRY_FILES_DEPTH = 1; + //Raw path to the XRY folder. + private final Path xryFolderPath; + + public XRYFolder(Path folder) { + xryFolderPath = folder; + } + + /** + * Finds all paths in the XRY report folder which are not XRY files. Only + * the first directory level is searched. As a result, some paths may point + * to directories. + * + * @return A non-null collection of paths + * @throws IOException If an I/O error occurs. + */ + public List getNonXRYFiles() throws IOException { + try (Stream allFiles = Files.walk(xryFolderPath, XRY_FILES_DEPTH)) { + List otherFiles = new ArrayList<>(); + Iterator allFilesIterator = allFiles.iterator(); + while (allFilesIterator.hasNext()) { + Path currentPath = allFilesIterator.next(); + if (!currentPath.equals(xryFolderPath) + && !XRYFileReader.isXRYFile(currentPath)) { + otherFiles.add(currentPath); + } + } + return otherFiles; + } catch (UncheckedIOException ex) { + throw ex.getCause(); + } + } + + /** + * Creates XRYFileReader instances for all XRY files found in the top level + * of the folder. + * + * @return A non-null collection of file readers. + * @throws IOException If an I/O error occurs. + */ + public List getXRYFileReaders() throws IOException { + try (Stream allFiles = Files.walk(xryFolderPath, XRY_FILES_DEPTH)) { + List fileReaders = new ArrayList<>(); + + Iterator allFilesIterator = allFiles.iterator(); + while (allFilesIterator.hasNext()) { + Path currentFile = allFilesIterator.next(); + if (XRYFileReader.isXRYFile(currentFile)) { + fileReaders.add(new XRYFileReader(currentFile)); + } + } + return fileReaders; + } catch (UncheckedIOException ex) { + throw ex.getCause(); + } + } + /** * Searches for XRY files at the top level of a given folder. If at least * one file matches, the entire directory is assumed to be an XRY report. @@ -48,7 +106,7 @@ public final class XRYFolder { * @return Indicates whether the Path is an XRY report. * * @throws IOException Error occurred during File I/O. - * @throws SecurityException If the security manager denies access any of + * @throws SecurityException If the security manager denies access to any of * the files. */ public static boolean isXRYFolder(Path folder) throws IOException { diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYMessagesFileParser.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYMessagesFileParser.java new file mode 100755 index 0000000000..820a6ebaad --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYMessagesFileParser.java @@ -0,0 +1,507 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourceprocessors.xry; + +import java.io.IOException; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Level; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Parses Messages-SMS files and creates artifacts. + */ +final class XRYMessagesFileParser implements XRYFileParser { + + private static final Logger logger = Logger.getLogger( + XRYMessagesFileParser.class.getName()); + + private static final String PARSER_NAME = "XRY DSP"; + private static final char KEY_VALUE_DELIMITER = ':'; + private static final DateTimeFormatter DATE_TIME_PARSER + = DateTimeFormatter.ofPattern("M/d/y h:m:s [a][ z]"); + + //Meta keys. These describe how the XRY message entites are split + //up in the report file. + private static final String SEGMENT_COUNT = "segments"; + private static final String SEGMENT_NUMBER = "segment number"; + private static final String REFERENCE_NUMBER = "reference number"; + + //A more readable version of these values. Referring to if the user + //has read the message. + private static final int READ = 1; + private static final int UNREAD = 0; + + private static final String TEXT_KEY = "text"; + + //All known XRY keys for message reports. + private static final Set XRY_KEYS = new HashSet() { + { + add(TEXT_KEY); + add("direction"); + add("time"); + add("status"); + add("tel"); + add("storage"); + add("index"); + add("folder"); + add("service center"); + add("type"); + add("name"); + } + }; + + //All known XRY namespaces for message reports. + private static final Set XRY_NAMESPACES = new HashSet() { + { + add("to"); + add("from"); + add("participant"); + } + }; + + //All known meta keys. + private static final Set XRY_META_KEYS = new HashSet() { + { + add(REFERENCE_NUMBER); + add(SEGMENT_NUMBER); + add(SEGMENT_COUNT); + } + }; + + /** + * Message-SMS report artifacts can span multiple XRY entities and their + * attributes can span multiple lines. The "Text" key is the only known key + * value pair that can span multiple lines. Messages can be segmented, + * meaning that their "Text" content can appear in multiple XRY entities. + * Our goal for a segmented message is to aggregate all of the text pieces and + * create 1 artifact. + * + * This parse implementation assumes that segments are contiguous and that + * they ascend incrementally. There are checks in place to verify this + * assumption is correct, otherwise an error will appear in the logs. + * + * @param reader The XRYFileReader that reads XRY entities from the + * Message-SMS report. + * @param parent The parent Content to create artifacts from. + * @throws IOException If an I/O error is encountered during report reading + * @throws TskCoreException If an error during artifact creation is + * encountered. + */ + @Override + public void parse(XRYFileReader reader, Content parent) throws IOException, TskCoreException { + Path reportPath = reader.getReportPath(); + logger.log(Level.INFO, String.format("[XRY DSP] Processing report at [ %s ]", reportPath.toString())); + + //Keep track of the reference numbers that have been parsed. + Set referenceNumbersSeen = new HashSet<>(); + + while (reader.hasNextEntity()) { + String xryEntity = reader.nextEntity(); + String[] xryLines = xryEntity.split("\n"); + + //First line of the entity is the title. + if (xryLines.length > 0) { + logger.log(Level.INFO, String.format("[XRY DSP] Processing [ %s ]", xryLines[0])); + } + + List attributes = new ArrayList<>(); + + String namespace = ""; + for (int i = 1; i < xryLines.length; i++) { + String xryLine = xryLines[i]; + String candidateNamespace = xryLine.trim().toLowerCase(); + + if (XRY_NAMESPACES.contains(candidateNamespace)) { + namespace = xryLine.trim(); + continue; + } + + //Find the XRY key on this line. + int keyDelimiter = xryLine.indexOf(KEY_VALUE_DELIMITER); + if (keyDelimiter == -1) { + logger.log(Level.SEVERE, String.format("[XRY DSP] Expected a key value " + + "pair on this line (in brackets) [ %s ], but one was not detected." + + " Is this the continuation of a previous line?" + + " Here is the previous line (in brackets) [ %s ]. " + + "What does this key mean?", xryLine, xryLines[i - 1])); + continue; + } + + //Extract the key value pair + String key = xryLine.substring(0, keyDelimiter).trim(); + String value = xryLine.substring(keyDelimiter + 1).trim(); + + String normalizedKey = key.toLowerCase(); + + if (XRY_META_KEYS.contains(normalizedKey)) { + //Skip meta keys, they are being dealt with seperately. + continue; + } + + if (!XRY_KEYS.contains(normalizedKey)) { + logger.log(Level.SEVERE, String.format("[XRY DSP] The following key, " + + "value pair (in brackets, respectively) [ %s ], [ %s ] " + + "was not recognized. Discarding... Here is the previous line " + + "[ %s ] for context. What does this key mean?", key, value, xryLines[i - 1])); + continue; + } + + if (value.isEmpty()) { + logger.log(Level.SEVERE, String.format("[XRY DSP] The following key " + + "(in brackets) [ %s ] was recognized, but the value " + + "was empty. Discarding... Here is the previous line " + + "for context [ %s ]. Is this a continuation of this line? " + + "What does an empty key mean?", key, xryLines[i - 1])); + continue; + } + + //Assume text is the only field that can span multiple lines. + if (normalizedKey.equals(TEXT_KEY)) { + //Build up multiple lines. + for (; (i + 1) < xryLines.length + && !hasKey(xryLines[i + 1]) + && !hasNamespace(xryLines[i + 1]); i++) { + String continuedValue = xryLines[i + 1].trim(); + //Assume multi lined values are split by word. + value = value + " " + continuedValue; + } + + int referenceNumber = getMetaInfo(xryLines, REFERENCE_NUMBER); + //Check if there is any segmented text. Min val is used to + //signify that no reference number was found. + if (referenceNumber != Integer.MIN_VALUE) { + logger.log(Level.INFO, String.format("[XRY DSP] Message entity " + + "appears to be segmented with reference number [ %d ]", referenceNumber)); + + if (referenceNumbersSeen.contains(referenceNumber)) { + logger.log(Level.SEVERE, String.format("[XRY DSP] This reference [ %d ] has already " + + "been seen. This means that the segments are not " + + "contiguous. Any segments contiguous with this " + + "one will be aggregated and another " + + "(otherwise duplicate) artifact will be created.", referenceNumber)); + } + + referenceNumbersSeen.add(referenceNumber); + + int segmentNumber = getMetaInfo(xryLines, SEGMENT_NUMBER); + + //Unify segmented text, if there is any. + String segmentedText = getSegmentedText(referenceNumber, + segmentNumber, reader); + //Assume it was segmented by word. + value = value + " " + segmentedText; + } + } + + BlackboardAttribute attribute = makeAttribute(namespace, key, value); + if (attribute != null) { + attributes.add(attribute); + } + } + + //Only create artifacts with non-empty attributes. + if(!attributes.isEmpty()) { + BlackboardArtifact artifact = parent.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_MESSAGE); + artifact.addAttributes(attributes); + } + } + } + + /** + * Builds up segmented message entities so that the text is unified in the + * artifact. + * + * @param referenceNumber Reference number that messages are group by + * @param segmentNumber Segment number of the starting segment. + * @param reader + * @return + * @throws IOException + */ + private String getSegmentedText(int referenceNumber, int segmentNumber, XRYFileReader reader) throws IOException { + StringBuilder segmentedText = new StringBuilder(); + + int currentSegmentNumber = segmentNumber; + while (reader.hasNextEntity()) { + //Peek at the next to see if it has the same reference number. + String nextEntity = reader.peek(); + String[] nextEntityLines = nextEntity.split("\n"); + int nextReferenceNumber = getMetaInfo(nextEntityLines, REFERENCE_NUMBER); + + if (nextReferenceNumber != referenceNumber) { + //Don't consume the next entity. It is not related + //to the current message thread. + break; + } + + //Consume the entity. + reader.nextEntity(); + + int nextSegmentNumber = getMetaInfo(nextEntityLines, SEGMENT_NUMBER); + + //Extract the text key from the entity, which is potentially + //multi-lined. + if (nextEntityLines.length > 0) { + logger.log(Level.INFO, String.format("[XRY DSP] Processing [ %s ] " + + "segment with reference number [ %d ]", nextEntityLines[0], referenceNumber)); + } + + if(nextSegmentNumber == Integer.MIN_VALUE) { + logger.log(Level.SEVERE, String.format("[XRY DSP] Segment with reference" + + " number [ %d ] did not have a segment number associated with it." + + " It cannot be determined if the reconstructed text will be in order.", referenceNumber)); + } else if (nextSegmentNumber != currentSegmentNumber + 1) { + logger.log(Level.SEVERE, String.format("[XRY DSP] Contiguous " + + "segments are not ascending incrementally. Encountered " + + "segment [ %d ] after segment [ %d ]. This means the reconstructed " + + "text will be out of order.", nextSegmentNumber, currentSegmentNumber)); + } + + for (int i = 1; i < nextEntityLines.length; i++) { + String xryLine = nextEntityLines[i]; + //Find the XRY key on this line. + int keyDelimiter = xryLine.indexOf(KEY_VALUE_DELIMITER); + if (keyDelimiter == -1) { + //Skip this line, we are searching only for a text key-value pair. + continue; + } + + String key = xryLine.substring(0, keyDelimiter); + String normalizedKey = key.trim().toLowerCase(); + if (normalizedKey.equals(TEXT_KEY)) { + String value = xryLine.substring(keyDelimiter + 1).trim(); + segmentedText.append(value).append(' '); + + //Build up multiple lines. + for (; (i + 1) < nextEntityLines.length + && !hasKey(nextEntityLines[i + 1]) + && !hasNamespace(nextEntityLines[i + 1]); i++) { + String continuedValue = nextEntityLines[i + 1].trim(); + segmentedText.append(continuedValue).append(' '); + } + } + } + + currentSegmentNumber = nextSegmentNumber; + } + + //Remove the trailing space. + if (segmentedText.length() > 0) { + segmentedText.setLength(segmentedText.length() - 1); + } + return segmentedText.toString(); + } + + /** + * Determines if the line has recognized key value on it. + * + * @param xryLine + * @return + */ + private boolean hasKey(String xryLine) { + int delimiter = xryLine.indexOf(':'); + if (delimiter != -1) { + String key = xryLine.substring(0, delimiter); + String normalizedKey = key.trim().toLowerCase(); + return XRY_KEYS.contains(normalizedKey); + } else { + return false; + } + } + + /** + * Determines if the line is a recognized namespace. + * + * @param xryLine + * @return + */ + private boolean hasNamespace(String xryLine) { + String normalizedLine = xryLine.trim().toLowerCase(); + return XRY_NAMESPACES.contains(normalizedLine); + } + + /** + * Extracts meta keys from the XRY entity. All of the known meta + * keys are integers and describe the message segments. + * + * @param xryLines Current XRY entity + * @param expectedKey The meta key to search for + * @return The interpreted integer value or Integer.MIN_VALUE if + * no meta key was found. + */ + private int getMetaInfo(String[] xryLines, String metaKey) { + for (int i = 0; i < xryLines.length; i++) { + String xryLine = xryLines[i]; + + String normalizedXryLine = xryLine.trim().toLowerCase(); + int firstDelimiter = normalizedXryLine.indexOf(KEY_VALUE_DELIMITER); + if (firstDelimiter != -1) { + String key = normalizedXryLine.substring(0, firstDelimiter); + if (key.equals(metaKey)) { + String value = normalizedXryLine.substring(firstDelimiter + 1).trim(); + try { + return Integer.parseInt(value); + } catch (NumberFormatException ex) { + logger.log(Level.SEVERE, String.format("[XRY DSP] Value [ %s ] for " + + "meta key [ %s ] was not an integer.", value, metaKey), ex); + } + } + } + } + + return Integer.MIN_VALUE; + } + + /** + * Creates an attribute from the extracted key value pair. + * + * @param nameSpace The namespace of this key value pair. + * It will have been verified beforehand, otherwise it will be empty. + * @param key The key that was verified beforehand + * @param value The value associated with that key. + * @return + */ + private BlackboardAttribute makeAttribute(String namespace, String key, String value) { + String normalizedKey = key.toLowerCase(); + String normalizedNamespace = namespace.toLowerCase(); + String normalizedValue = value.toLowerCase(); + + switch (normalizedKey) { + case "time": + //Tranform value to epoch ms + try { + String dateTime = removeDateTimeLocale(value); + String normalizedDateTime = dateTime.trim(); + long dateTimeInEpoch = calculateSecondsSinceEpoch(normalizedDateTime); + return new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME, PARSER_NAME, dateTimeInEpoch); + } catch (DateTimeParseException ex) { + logger.log(Level.SEVERE, String.format("[XRY DSP] Assumption " + + "about the date time formatting of messages is not " + + "right. Here is the value [ %s ].", value), ex); + return null; + } + case "direction": + return new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DIRECTION, PARSER_NAME, value); + case "text": + return new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_TEXT, PARSER_NAME, value); + case "status": + switch (normalizedValue) { + case "read": + return new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_READ_STATUS, PARSER_NAME, READ); + case "unread": + return new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_READ_STATUS, PARSER_NAME, UNREAD); + case "sending failed": + //Ignore for now. + return null; + case "deleted": + //Ignore for now. + return null; + case "unsent": + //Ignore for now. + return null; + default: + logger.log(Level.SEVERE, String.format("[XRY DSP] Unrecognized " + + "status value [ %s ].", value)); + return null; + } + case "type": + switch (normalizedValue) { + case "deliver": + //Ignore for now. + return null; + case "submit": + //Ignore for now. + return null; + case "status report": + //Ignore for now. + return null; + default: + logger.log(Level.SEVERE, String.format("[XRY DSP] Unrecognized " + + "type value [ %s ]", value)); + return null; + } + case "storage": + //Ignore for now. + return null; + case "index": + //Ignore for now. + return null; + case "folder": + //Ignore for now. + return null; + case "name": + //Ignore for now. + return null; + case "service center": + //Ignore for now. + return null; + case "tel": + //Apply the namespace + if (normalizedNamespace.equals("from")) { + return new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_FROM, PARSER_NAME, value); + } else { + //Assume to and participant are both equivalent to TSK_PHONE_NUMBER_TO + return new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_TO, PARSER_NAME, value); + } + default: + throw new IllegalArgumentException(String.format("key [ %s ] was not recognized.", key)); + } + } + + /** + * Removes the locale from the date time value. + * + * Locale in this case being (Device) or (Network). + * + * @param dateTime XRY datetime value to be sanitized. + * @return A purer date time value. + */ + private String removeDateTimeLocale(String dateTime) { + int index = dateTime.indexOf('('); + if (index == -1) { + return dateTime; + } + + return dateTime.substring(0, index); + } + + /** + * Parses the date time value and calculates ms since epoch. The time zone is + * assumed to be UTC. + * + * @param dateTime + * @return + */ + private long calculateSecondsSinceEpoch(String dateTime) { + LocalDateTime localDateTime = LocalDateTime.parse(dateTime, DATE_TIME_PARSER); + //Assume dates have no offset. + return localDateTime.toInstant(ZoneOffset.UTC).getEpochSecond(); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYReportProcessor.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYReportProcessor.java new file mode 100755 index 0000000000..98ee693101 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYReportProcessor.java @@ -0,0 +1,79 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourceprocessors.xry; + +import java.io.IOException; +import java.util.List; +import java.util.logging.Level; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Processes all XRY files in an XRY folder. + */ +final class XRYReportProcessor { + + private static final Logger logger = Logger.getLogger(XRYReportProcessor.class.getName()); + + /** + * Processes all XRY Files and creates artifacts on the given Content + * instance. + * + * All resources will be closed if an exception is encountered. + * + * @param folder XRY folder to process + * @param parent Content instance to hold newly created artifacts. + * @throws IOException If an I/O exception occurs. + * @throws TskCoreException If an error occurs adding artifacts. + */ + static void process(XRYFolder folder, Content parent) throws IOException, TskCoreException { + //Get all XRY file readers from this folder. + List xryFileReaders = folder.getXRYFileReaders(); + + try { + for (XRYFileReader xryFileReader : xryFileReaders) { + String reportType = xryFileReader.getReportType(); + if (XRYFileParserFactory.supports(reportType)) { + XRYFileParser parser = XRYFileParserFactory.get(reportType); + parser.parse(xryFileReader, parent); + } else { + logger.log(Level.SEVERE, String.format("[XRY DSP] XRY File (in brackets) " + + "[ %s ] was found, but no parser to support its report type exists. " + + "Report type is [ %s ]", xryFileReader.getReportPath().toString(), reportType)); + } + } + } finally { + try { + //Try to close all resources + for (XRYFileReader xryFileReader : xryFileReaders) { + xryFileReader.close(); + } + } catch (IOException ex) { + logger.log(Level.WARNING, "[XRY DSP] Encountered I/O exception trying " + + "to close all xry file readers.", ex); + } + } + } + + //Prevent direct instantiation. + private XRYReportProcessor() { + + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYWebBookmarksFileParser.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYWebBookmarksFileParser.java new file mode 100755 index 0000000000..a7443e7f48 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYWebBookmarksFileParser.java @@ -0,0 +1,66 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourceprocessors.xry; + +import java.util.Map; +import java.util.HashMap; +import java.util.List; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Parses XRY Web-Bookmark files and creates artifacts. + */ +final class XRYWebBookmarksFileParser extends AbstractSingleKeyValueParser { + + //All known XRY keys for web bookmarks. + private static final Map KEY_TO_TYPE + = new HashMap() { + { + put("web address", BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL); + put("domain", BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN); + } + }; + + @Override + boolean isKey(String key) { + String normalizedKey = key.toLowerCase(); + return KEY_TO_TYPE.containsKey(normalizedKey); + } + + @Override + boolean isNamespace(String nameSpace) { + //No known namespaces for web reports. + return false; + } + + @Override + BlackboardAttribute makeAttribute(String nameSpace, String key, String value) { + String normalizedKey = key.toLowerCase(); + return new BlackboardAttribute(KEY_TO_TYPE.get(normalizedKey), PARSER_NAME, value); + } + + @Override + void makeArtifact(List attributes, Content parent) throws TskCoreException { + BlackboardArtifact artifact = parent.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_BOOKMARK); + artifact.addAttributes(attributes); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/Bundle.properties b/Core/src/org/sleuthkit/autopsy/geolocation/Bundle.properties index 8fa233ea33..bec0dda190 100755 --- a/Core/src/org/sleuthkit/autopsy/geolocation/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/geolocation/Bundle.properties @@ -7,3 +7,13 @@ RefreshPanel.closeButton.text= MapPanel.cordLabel.text= WaypointDetailPanel.closeButton.text= WaypointDetailPanel.imageLabel.text= +GeoFilterPanel.waypointSettings.border.title= +GeoFilterPanel.allButton.text=Show All +GeoFilterPanel.mostRecentButton.text=Show only last +GeoFilterPanel.applyButton.text=Apply +GeoFilterPanel.showWaypointsWOTSCheckBox.text=Include waypoints with no time stamps +GeoFilterPanel.daysLabel.text=days of activity +CheckBoxListPanel.titleLabel.text=jLabel1 +CheckBoxListPanel.checkButton.text=Check All +CheckBoxListPanel.uncheckButton.text=Uncheck All +GeoFilterPanel.optionsLabel.text=Waypoints diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/geolocation/Bundle.properties-MERGED index 95dd0d23f0..6b90c60944 100755 --- a/Core/src/org/sleuthkit/autopsy/geolocation/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/geolocation/Bundle.properties-MERGED @@ -1,8 +1,17 @@ CTL_OpenGeolocation=Geolocation CTL_GeolocationTopComponentAction=GeolocationTopComponent CTL_GeolocationTopComponent=Geolocation +GeoFilterPanel_DataSource_List_Title=Data Sources +GeoFilterPanel_empty_dataSource=Data Source list is empty. +GeoTopComponent_filer_data_invalid_msg=Unable to run waypoint filter.\nPlease select one or more data sources. +GeoTopComponent_filer_data_invalid_Title=Filter Failure +GeoTopComponent_filter_exception_msg=Exception occured during waypoint filtering. +GeoTopComponent_filter_exception_Title=Filter Failure +GeoTopComponent_no_waypoints_returned_mgs=Applied filter failed to find waypoints that matched criteria.\nRevise filter options and try again. +GeoTopComponent_no_waypoints_returned_Title=No Waypoints Found GLTopComponent_initilzation_error=An error occurred during waypoint initilization. Geolocation data maybe incomplete. GLTopComponent_name=Geolocation +HidingPane_default_title=Filters MayWaypoint_ExternalViewer_label=Open in ExternalViewer OpenGeolocationAction_displayName=Geolocation OpenGeolocationAction_name=Geolocation @@ -12,4 +21,14 @@ RefreshPanel.closeButton.text= MapPanel.cordLabel.text= WaypointDetailPanel.closeButton.text= WaypointDetailPanel.imageLabel.text= +GeoFilterPanel.waypointSettings.border.title= +GeoFilterPanel.allButton.text=Show All +GeoFilterPanel.mostRecentButton.text=Show only last +GeoFilterPanel.applyButton.text=Apply +GeoFilterPanel.showWaypointsWOTSCheckBox.text=Include waypoints with no time stamps +GeoFilterPanel.daysLabel.text=days of activity +CheckBoxListPanel.titleLabel.text=jLabel1 +CheckBoxListPanel.checkButton.text=Check All +CheckBoxListPanel.uncheckButton.text=Uncheck All +GeoFilterPanel.optionsLabel.text=Waypoints WaypointExtractAction_label=Extract Files(s) diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/CheckBoxJList.java b/Core/src/org/sleuthkit/autopsy/geolocation/CheckBoxJList.java new file mode 100755 index 0000000000..52276b6046 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/CheckBoxJList.java @@ -0,0 +1,109 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.geolocation; + +import java.awt.Component; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import javax.swing.JCheckBox; +import javax.swing.JList; +import javax.swing.ListCellRenderer; +import javax.swing.ListSelectionModel; + +/** + * A JList that renders the list items as check boxes. + */ +final class CheckBoxJList extends JList { + + private static final long serialVersionUID = 1L; + + /** + * Simple interface that must be implement for an object to be displayed as + * a checkbox in CheckBoxJList. + * + */ + interface CheckboxListItem { + + /** + * Returns the checkbox state. + * + * @return True if the check box should be checked + */ + boolean isChecked(); + + /** + * Set the state of the check box. + * + * @param checked + */ + void setChecked(boolean checked); + + /** + * Returns String to display as the check box label + * + * @return + */ + String getDisplayName(); + } + + /** + * Construct a new JCheckBoxList. + */ + CheckBoxJList() { + initalize(); + } + + /** + * Do all of the UI initialization. + */ + private void initalize() { + setCellRenderer(new CellRenderer()); + addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + int index = locationToIndex(e.getPoint()); + if (index != -1) { + CheckBoxJList.CheckboxListItem element = getModel().getElementAt(index); + element.setChecked(!element.isChecked()); + repaint(); + } + } + }); + setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + } + + /** + * A ListCellRenderer that renders list elements as check boxes. + */ + class CellRenderer extends JCheckBox implements ListCellRenderer { + + private static final long serialVersionUID = 1L; + + @Override + public Component getListCellRendererComponent( + JList list, CheckBoxJList.CheckboxListItem value, int index, + boolean isSelected, boolean cellHasFocus) { + + setBackground(list.getBackground()); + setSelected(value.isChecked()); + setText(value.getDisplayName()); + return this; + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/CheckBoxListPanel.form b/Core/src/org/sleuthkit/autopsy/geolocation/CheckBoxListPanel.form new file mode 100755 index 0000000000..112b734f1e --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/CheckBoxListPanel.form @@ -0,0 +1,79 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/CheckBoxListPanel.java b/Core/src/org/sleuthkit/autopsy/geolocation/CheckBoxListPanel.java new file mode 100755 index 0000000000..37dea163f5 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/CheckBoxListPanel.java @@ -0,0 +1,239 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.geolocation; + +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import javax.swing.DefaultListModel; +import javax.swing.Icon; + +/** + * A panel for showing Content objects in a check box list. + */ +final class CheckBoxListPanel extends javax.swing.JPanel { + + private static final long serialVersionUID = 1L; + + private final DefaultListModel> model = new DefaultListModel<>(); + private final CheckBoxJList> checkboxList; + + /** + * Creates new CheckboxFilterPanel + */ + CheckBoxListPanel() { + initComponents(); + + checkboxList = new CheckBoxJList<>(); + checkboxList.setModel(model); + scrollPane.setViewportView(checkboxList); + } + + /** + * Add a new element to the check box list. + * + * @param displayName display name for the checkbox + * @param obj Object that the checkbox represents + */ + void addElement(String displayName, T obj) { + model.addElement(new ObjectCheckBox<>(displayName, true, obj)); + } + + /** + * Remove all objects from the checkbox list. + */ + void clearList() { + model.removeAllElements(); + } + + /** + * Returns a list of all of the selected elements. + * + * @return List of selected elements. + */ + List getSelectedElements() { + List selectedElements = new ArrayList<>(); + Enumeration> elements = model.elements(); + + while (elements.hasMoreElements()) { + ObjectCheckBox element = elements.nextElement(); + if (element.isChecked()) { + selectedElements.add(element.getObject()); + } + } + + return selectedElements; + } + + /** + * Sets the selection state of the all the check boxes in the list. + * + * @param selected True to check the boxes, false to unchecked + */ + void setSetAllSelected(boolean selected) { + Enumeration> enumeration = model.elements(); + while (enumeration.hasMoreElements()) { + ObjectCheckBox element = enumeration.nextElement(); + element.setChecked(selected); + checkboxList.repaint(); + checkboxList.revalidate(); + + } + } + + /** + * Sets the panel title. + * + * @param title Panel title or null for no title. + */ + void setPanelTitle(String title) { + titleLabel.setText(title); + } + + /** + * Sets the panel title icon. + * + * @param icon Icon to set or null for no icon + */ + void setPanelTitleIcon(Icon icon) { + titleLabel.setIcon(icon); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + titleLabel = new javax.swing.JLabel(); + javax.swing.JButton uncheckButton = new javax.swing.JButton(); + javax.swing.JButton checkButton = new javax.swing.JButton(); + scrollPane = new javax.swing.JScrollPane(); + + setLayout(new java.awt.GridBagLayout()); + + org.openide.awt.Mnemonics.setLocalizedText(titleLabel, org.openide.util.NbBundle.getMessage(CheckBoxListPanel.class, "CheckBoxListPanel.titleLabel.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.gridwidth = 3; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + add(titleLabel, gridBagConstraints); + + org.openide.awt.Mnemonics.setLocalizedText(uncheckButton, org.openide.util.NbBundle.getMessage(CheckBoxListPanel.class, "CheckBoxListPanel.uncheckButton.text")); // NOI18N + uncheckButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + uncheckButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHEAST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 9); + add(uncheckButton, gridBagConstraints); + + org.openide.awt.Mnemonics.setLocalizedText(checkButton, org.openide.util.NbBundle.getMessage(CheckBoxListPanel.class, "CheckBoxListPanel.checkButton.text")); // NOI18N + checkButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + checkButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHEAST; + add(checkButton, gridBagConstraints); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.gridwidth = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(5, 0, 9, 0); + add(scrollPane, gridBagConstraints); + }// //GEN-END:initComponents + + private void uncheckButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_uncheckButtonActionPerformed + setSetAllSelected(false); + }//GEN-LAST:event_uncheckButtonActionPerformed + + private void checkButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_checkButtonActionPerformed + setSetAllSelected(true); + }//GEN-LAST:event_checkButtonActionPerformed + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JScrollPane scrollPane; + private javax.swing.JLabel titleLabel; + // End of variables declaration//GEN-END:variables + + /** + * Wrapper around T that implements CheckboxListItem + * + * @param + */ + final class ObjectCheckBox implements CheckBoxJList.CheckboxListItem { + + private static final long serialVersionUID = 1L; + + private final T object; + private final String displayName; + private boolean checked; + + /** + * Constructs a new ObjectCheckBox + * + * @param displayName String to show as the check box label + * @param initialState Sets the initial state of the check box + * @param object Object that the check box represents. + */ + ObjectCheckBox(String displayName, boolean initialState, T object) { + this.displayName = displayName; + this.object = object; + this.checked = initialState; + } + + T getObject() { + return object; + } + + @Override + public boolean isChecked() { + return checked; + } + + @Override + public void setChecked(boolean checked) { + this.checked = checked; + } + + @Override + public String getDisplayName() { + return displayName; + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/GeoFilterPanel.form b/Core/src/org/sleuthkit/autopsy/geolocation/GeoFilterPanel.form new file mode 100755 index 0000000000..79a852b4d4 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/GeoFilterPanel.form @@ -0,0 +1,180 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/GeoFilterPanel.java b/Core/src/org/sleuthkit/autopsy/geolocation/GeoFilterPanel.java new file mode 100755 index 0000000000..9e009d1245 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/GeoFilterPanel.java @@ -0,0 +1,359 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.geolocation; + +import java.awt.GridBagConstraints; +import java.awt.event.ActionListener; +import java.util.Collections; +import java.util.List; +import java.util.logging.Level; +import javax.swing.ImageIcon; +import javax.swing.SpinnerNumberModel; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.DataSource; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * + * Panel to display the filter options for geolocation waypoints. + */ +class GeoFilterPanel extends javax.swing.JPanel { + + private static final long serialVersionUID = 1L; + private static final Logger logger = Logger.getLogger(GeoFilterPanel.class.getName()); + + private final SpinnerNumberModel numberModel; + private final CheckBoxListPanel checkboxPanel; + + /** + * Creates new GeoFilterPanel + */ + @Messages({ + "GeoFilterPanel_DataSource_List_Title=Data Sources" + }) + GeoFilterPanel() { + // numberModel is used in initComponents + numberModel = new SpinnerNumberModel(10, 1, Integer.MAX_VALUE, 1); + + initComponents(); + + // The gui builder cannot handle using CheckBoxListPanel due to its + // use of generics so we will initalize it here. + checkboxPanel = new CheckBoxListPanel<>(); + checkboxPanel.setPanelTitle(Bundle.GeoFilterPanel_DataSource_List_Title()); + checkboxPanel.setPanelTitleIcon(new ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/image.png"))); + checkboxPanel.setSetAllSelected(true); + + GridBagConstraints gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 15, 0, 15); + add(checkboxPanel, gridBagConstraints); + } + + void updateDataSourceList() { + try { + initCheckboxList(); + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "Failed to initialize the CheckboxListPane", ex); //NON-NLS + } + } + + /** + * Adds an actionListener to listen for the filter apply action + * + * @param listener + */ + void addActionListener(ActionListener listener) { + applyButton.addActionListener(listener); + } + + /** + * Returns the selected filter values. + * + * @return A GeoFilter object with the user selected filter values + * + * @throws GeoLocationUIException + */ + @Messages({ + "GeoFilterPanel_empty_dataSource=Data Source list is empty." + }) + GeoFilter getFilterState() throws GeoLocationUIException { + List dataSources = checkboxPanel.getSelectedElements(); + + if (dataSources.isEmpty()) { + throw new GeoLocationUIException(Bundle.GeoFilterPanel_empty_dataSource()); + } + return new GeoFilter(allButton.isSelected(), + showWaypointsWOTSCheckBox.isSelected(), + numberModel.getNumber().intValue(), + dataSources); + } + + /** + * Initialize the checkbox list panel + * + * @throws TskCoreException + */ + private void initCheckboxList() throws TskCoreException { + final SleuthkitCase sleuthkitCase = Case.getCurrentCase().getSleuthkitCase(); + + checkboxPanel.clearList(); + + for (DataSource dataSource : sleuthkitCase.getDataSources()) { + String dsName = sleuthkitCase.getContentById(dataSource.getId()).getName(); + checkboxPanel.addElement(dsName, dataSource); + } + } + + /** + * Based on the state of mostRecent radio button Change the state of the cnt + * spinner and the time stamp checkbox. + */ + private void updateWaypointOptions() { + boolean selected = mostRecentButton.isSelected(); + showWaypointsWOTSCheckBox.setEnabled(selected); + daysSpinner.setEnabled(selected); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + javax.swing.ButtonGroup buttonGroup = new javax.swing.ButtonGroup(); + javax.swing.JPanel waypointSettings = new javax.swing.JPanel(); + allButton = new javax.swing.JRadioButton(); + mostRecentButton = new javax.swing.JRadioButton(); + showWaypointsWOTSCheckBox = new javax.swing.JCheckBox(); + daysSpinner = new javax.swing.JSpinner(numberModel); + javax.swing.JLabel daysLabel = new javax.swing.JLabel(); + javax.swing.JPanel buttonPanel = new javax.swing.JPanel(); + applyButton = new javax.swing.JButton(); + javax.swing.JLabel optionsLabel = new javax.swing.JLabel(); + + setLayout(new java.awt.GridBagLayout()); + + waypointSettings.setBorder(javax.swing.BorderFactory.createTitledBorder(org.openide.util.NbBundle.getMessage(GeoFilterPanel.class, "GeoFilterPanel.waypointSettings.border.title"))); // NOI18N + waypointSettings.setLayout(new java.awt.GridBagLayout()); + + buttonGroup.add(allButton); + allButton.setSelected(true); + org.openide.awt.Mnemonics.setLocalizedText(allButton, org.openide.util.NbBundle.getMessage(GeoFilterPanel.class, "GeoFilterPanel.allButton.text")); // NOI18N + allButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + allButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.gridwidth = 4; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + waypointSettings.add(allButton, gridBagConstraints); + + buttonGroup.add(mostRecentButton); + org.openide.awt.Mnemonics.setLocalizedText(mostRecentButton, org.openide.util.NbBundle.getMessage(GeoFilterPanel.class, "GeoFilterPanel.mostRecentButton.text")); // NOI18N + mostRecentButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + mostRecentButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(9, 0, 0, 0); + waypointSettings.add(mostRecentButton, gridBagConstraints); + + org.openide.awt.Mnemonics.setLocalizedText(showWaypointsWOTSCheckBox, org.openide.util.NbBundle.getMessage(GeoFilterPanel.class, "GeoFilterPanel.showWaypointsWOTSCheckBox.text")); // NOI18N + showWaypointsWOTSCheckBox.setEnabled(false); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 2; + gridBagConstraints.gridwidth = 3; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(0, 30, 0, 0); + waypointSettings.add(showWaypointsWOTSCheckBox, gridBagConstraints); + + daysSpinner.setEnabled(false); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(9, 0, 0, 0); + waypointSettings.add(daysSpinner, gridBagConstraints); + + org.openide.awt.Mnemonics.setLocalizedText(daysLabel, org.openide.util.NbBundle.getMessage(GeoFilterPanel.class, "GeoFilterPanel.daysLabel.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 3; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(9, 5, 0, 0); + waypointSettings.add(daysLabel, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(5, 15, 9, 15); + add(waypointSettings, gridBagConstraints); + + buttonPanel.setLayout(new java.awt.GridBagLayout()); + + applyButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/tick.png"))); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(applyButton, org.openide.util.NbBundle.getMessage(GeoFilterPanel.class, "GeoFilterPanel.applyButton.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHEAST; + gridBagConstraints.weightx = 1.0; + buttonPanel.add(applyButton, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(9, 15, 0, 15); + add(buttonPanel, gridBagConstraints); + + optionsLabel.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/blueGeo16.png"))); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(optionsLabel, org.openide.util.NbBundle.getMessage(GeoFilterPanel.class, "GeoFilterPanel.optionsLabel.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 15, 0, 0); + add(optionsLabel, gridBagConstraints); + }// //GEN-END:initComponents + + private void allButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_allButtonActionPerformed + updateWaypointOptions(); + }//GEN-LAST:event_allButtonActionPerformed + + private void mostRecentButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_mostRecentButtonActionPerformed + updateWaypointOptions(); + }//GEN-LAST:event_mostRecentButtonActionPerformed + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JRadioButton allButton; + private javax.swing.JButton applyButton; + private javax.swing.JSpinner daysSpinner; + private javax.swing.JRadioButton mostRecentButton; + private javax.swing.JCheckBox showWaypointsWOTSCheckBox; + // End of variables declaration//GEN-END:variables + + /** + * Class to store the values of the Geolocation user set filter parameters + */ + final class GeoFilter { + + private final boolean showAll; + private final boolean showWithoutTimeStamp; + private final int mostRecentNumDays; + private final List dataSources; + + /** + * Construct a Geolocation filter. showAll and mostRecentNumDays are + * exclusive filters, ie they cannot be used together. + * + * withoutTimeStamp is only applicable if mostRecentNumDays is true. + * + * When using the filters "most recent days" means to include waypoints + * for the numbers of days after the most recent waypoint, not the + * current date. + * + * @param showAll True if all waypoints should be shown + * @param withoutTimeStamp True to show waypoints without timeStamps, + * this filter is only applicable if + * mostRecentNumDays is true + * @param mostRecentNumDays Show Waypoint for the most recent given + * number of days. This parameter is ignored if + * showAll is true. + * @param dataSources A list of dataSources to filter waypoint + * for. + */ + GeoFilter(boolean showAll, boolean withoutTimeStamp, int mostRecentNumDays, List dataSources) { + this.showAll = showAll; + this.showWithoutTimeStamp = withoutTimeStamp; + this.mostRecentNumDays = mostRecentNumDays; + this.dataSources = dataSources; + } + + /** + * Returns whether or not to show all waypoints. + * + * @return True if all waypoints should be shown. + */ + boolean showAllWaypoints() { + return showAll; + } + + /** + * Returns whether or not to include waypoints with time stamps. + * + * This filter is only applicable if "showAll" is true. + * + * @return True if waypoints with time stamps should be shown. + */ + boolean showWaypointsWithoutTimeStamp() { + return showWithoutTimeStamp; + } + + /** + * Returns the number of most recent days to show waypoints for. This + * value should be ignored if showAll is true. + * + * @return The number of most recent days to show waypoints for + */ + int getMostRecentNumDays() { + return mostRecentNumDays; + } + + /** + * Returns a list of data sources to filter the waypoints by, or null if + * all datasources should be include. + * + * @return A list of dataSources or null if all dataSources should be + * included. + */ + List getDataSources() { + return Collections.unmodifiableList(dataSources); + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/GeoLocationUIException.java b/Core/src/org/sleuthkit/autopsy/geolocation/GeoLocationUIException.java new file mode 100755 index 0000000000..7ce6837914 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/GeoLocationUIException.java @@ -0,0 +1,46 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.geolocation; + +/** + * + * An exception call for Exceptions that occure in the geolocation dialog. + */ +public class GeoLocationUIException extends Exception{ + private static final long serialVersionUID = 1L; + + /** + * Create exception containing the error message + * + * @param msg the message + */ + public GeoLocationUIException(String msg) { + super(msg); + } + + /** + * Create exception containing the error message and cause exception + * + * @param msg the message + * @param ex cause exception + */ + public GeoLocationUIException(String msg, Exception ex) { + super(msg, ex); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/GeolocationTopComponent.form b/Core/src/org/sleuthkit/autopsy/geolocation/GeolocationTopComponent.form index bddebcbc0c..16cd5368a6 100755 --- a/Core/src/org/sleuthkit/autopsy/geolocation/GeolocationTopComponent.form +++ b/Core/src/org/sleuthkit/autopsy/geolocation/GeolocationTopComponent.form @@ -11,6 +11,7 @@ + @@ -23,6 +24,17 @@ + + + + + + + + + + + - + \ No newline at end of file diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/GeolocationTopComponent.java b/Core/src/org/sleuthkit/autopsy/geolocation/GeolocationTopComponent.java index bf54ece196..e1556dc27a 100755 --- a/Core/src/org/sleuthkit/autopsy/geolocation/GeolocationTopComponent.java +++ b/Core/src/org/sleuthkit/autopsy/geolocation/GeolocationTopComponent.java @@ -25,9 +25,9 @@ import java.beans.PropertyChangeListener; import java.util.EnumSet; import java.util.List; import java.util.Set; -import java.util.concurrent.ExecutionException; import java.util.logging.Level; -import javax.swing.SwingWorker; +import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; import org.openide.util.NbBundle.Messages; import org.openide.windows.RetainLocation; import org.openide.windows.TopComponent; @@ -35,8 +35,12 @@ import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.casemodule.Case; import static org.sleuthkit.autopsy.casemodule.Case.Events.CURRENT_CASE; import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.coreutils.ThreadConfined; +import org.sleuthkit.autopsy.geolocation.GeoFilterPanel.GeoFilter; +import org.sleuthkit.autopsy.geolocation.datamodel.GeoLocationDataException; +import org.sleuthkit.autopsy.geolocation.datamodel.Waypoint; +import org.sleuthkit.autopsy.geolocation.datamodel.WaypointBuilder; +import org.sleuthkit.autopsy.geolocation.datamodel.WaypointBuilder.WaypointFilterQueryCallBack; import org.sleuthkit.autopsy.ingest.IngestManager; import static org.sleuthkit.autopsy.ingest.IngestManager.IngestModuleEvent.DATA_ADDED; import org.sleuthkit.autopsy.ingest.ModuleDataEvent; @@ -59,6 +63,7 @@ public final class GeolocationTopComponent extends TopComponent { private static final Set INGEST_MODULE_EVENTS_OF_INTEREST = EnumSet.of(DATA_ADDED); private final PropertyChangeListener ingestListener; + private final GeoFilterPanel geoFilterPanel; final RefreshPanel refreshPanel = new RefreshPanel(); @@ -73,7 +78,7 @@ public final class GeolocationTopComponent extends TopComponent { @ThreadConfined(type = ThreadConfined.ThreadType.AWT) public GeolocationTopComponent() { initComponents(); - initWaypoints(); + setName(Bundle.GLTopComponent_name()); this.ingestListener = pce -> { @@ -105,10 +110,19 @@ public final class GeolocationTopComponent extends TopComponent { @Override public void actionPerformed(ActionEvent e) { mapPanel.clearWaypoints(); - initWaypoints(); + updateWaypoints(); showRefreshPanel(false); } }); + + geoFilterPanel = new GeoFilterPanel(); + filterPane.setPanel(geoFilterPanel); + geoFilterPanel.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + updateWaypoints(); + } + }); } @Override @@ -118,7 +132,7 @@ public final class GeolocationTopComponent extends TopComponent { Case.addEventTypeSubscriber(EnumSet.of(CURRENT_CASE), evt -> { mapPanel.clearWaypoints(); if (evt.getNewValue() != null) { - initWaypoints(); + updateWaypoints(); } }); } @@ -134,6 +148,13 @@ public final class GeolocationTopComponent extends TopComponent { super.componentOpened(); WindowManager.getDefault().setTopComponentFloating(this, true); } + + @Override + public void open() { + super.open(); + geoFilterPanel.updateDataSourceList(); + updateWaypoints(); + } /** * Set the state of the refresh panel at the top of the mapPanel. @@ -150,43 +171,60 @@ public final class GeolocationTopComponent extends TopComponent { } /** - * Use a SwingWorker thread to get a list of waypoints. - * + * Filters the list of waypoints based on the user selections in the filter + * pane. */ - private void initWaypoints() { - SwingWorker, MapWaypoint> worker = new SwingWorker, MapWaypoint>() { - @Override - protected List doInBackground() throws Exception { - Case currentCase = Case.getCurrentCaseThrows(); + @Messages({ + "GeoTopComponent_no_waypoints_returned_mgs=Applied filter failed to find waypoints that matched criteria.\nRevise filter options and try again.", + "GeoTopComponent_no_waypoints_returned_Title=No Waypoints Found", + "GeoTopComponent_filter_exception_msg=Exception occured during waypoint filtering.", + "GeoTopComponent_filter_exception_Title=Filter Failure", + "GeoTopComponent_filer_data_invalid_msg=Unable to run waypoint filter.\nPlease select one or more data sources.", + "GeoTopComponent_filer_data_invalid_Title=Filter Failure" + }) + private void updateWaypoints() { + GeoFilter filters; - return MapWaypoint.getWaypoints(currentCase.getSleuthkitCase()); - } + // Show a warning message if the user has not selected a data source + try { + filters = geoFilterPanel.getFilterState(); + } catch (GeoLocationUIException ex) { + JOptionPane.showMessageDialog(this, + Bundle.GeoTopComponent_filer_data_invalid_msg(), + Bundle.GeoTopComponent_filer_data_invalid_Title(), + JOptionPane.INFORMATION_MESSAGE); + return; + } - @Override - protected void done() { - if (isDone() && !isCancelled()) { - try { - List waypoints = get(); - if (waypoints == null || waypoints.isEmpty()) { - return; + SwingUtilities.invokeLater(new Runnable() { + public void run() { + Case currentCase = Case.getCurrentCase(); + try { + WaypointBuilder.getAllWaypoints(currentCase.getSleuthkitCase(), filters.getDataSources(), filters.showAllWaypoints(), filters.getMostRecentNumDays(), filters.showWaypointsWithoutTimeStamp(), new WaypointFilterQueryCallBack() { + @Override + public void process(List waypoints) { + // If the list is empty, tell the user and do not change + // the visible waypoints. + if (waypoints == null || waypoints.isEmpty()) { + JOptionPane.showMessageDialog(GeolocationTopComponent.this, + Bundle.GeoTopComponent_no_waypoints_returned_Title(), + Bundle.GeoTopComponent_no_waypoints_returned_mgs(), + JOptionPane.INFORMATION_MESSAGE); + + return; + } + mapPanel.setWaypoints(MapWaypoint.getWaypoints(waypoints)); } - mapPanel.setWaypoints(waypoints); - - // There might be a better way to decide how to center - // but for now just use the first way point. - mapPanel.setCenterLocation(waypoints.get(0)); - - } catch (ExecutionException ex) { - logger.log(Level.WARNING, "An exception occured while initializing waypoints for geolocation window.", ex); //NON-NLS - MessageNotifyUtil.Message.error(Bundle.GLTopComponent_initilzation_error()); - } catch (InterruptedException ex) { - logger.log(Level.WARNING, "The initializing thread for geolocation window was interrupted.", ex); //NON-NLS - } + }); + } catch (GeoLocationDataException ex) { + logger.log(Level.SEVERE, "Failed to filter waypoints.", ex); + JOptionPane.showMessageDialog(GeolocationTopComponent.this, + Bundle.GeoTopComponent_filter_exception_Title(), + Bundle.GeoTopComponent_filter_exception_msg(), + JOptionPane.ERROR_MESSAGE); } } - }; - - worker.execute(); + }); } /** @@ -199,13 +237,18 @@ public final class GeolocationTopComponent extends TopComponent { private void initComponents() { mapPanel = new org.sleuthkit.autopsy.geolocation.MapPanel(); + filterPane = new org.sleuthkit.autopsy.geolocation.HidingPane(); setLayout(new java.awt.BorderLayout()); + + mapPanel.add(filterPane, java.awt.BorderLayout.LINE_START); + add(mapPanel, java.awt.BorderLayout.CENTER); }// //GEN-END:initComponents // Variables declaration - do not modify//GEN-BEGIN:variables + private org.sleuthkit.autopsy.geolocation.HidingPane filterPane; private org.sleuthkit.autopsy.geolocation.MapPanel mapPanel; // End of variables declaration//GEN-END:variables } diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/HidingPane.java b/Core/src/org/sleuthkit/autopsy/geolocation/HidingPane.java new file mode 100755 index 0000000000..e842e5df86 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/HidingPane.java @@ -0,0 +1,133 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.geolocation; + +import java.awt.BorderLayout; +import java.awt.Font; +import java.awt.Point; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import javax.swing.Icon; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTabbedPane; +import org.openide.util.NbBundle.Messages; + +/** + * + * A JTabbed pane with one tab that says "Filters". When the user clicks on that + * table the content of the tab will be hidden. + * + * The content pane provides support for scrolling. + */ +public final class HidingPane extends JTabbedPane { + + private static final long serialVersionUID = 1L; + + private final JScrollPane scrollPane; + private final JPanel panel; + private final JLabel tabLabel; + + private boolean panelVisible = true; + + /** + * Constructs a new HidingFilterPane + */ + @Messages({ + "HidingPane_default_title=Filters" + }) + public HidingPane() { + super(); + + scrollPane = new JScrollPane(); + panel = new JPanel(); + panel.setLayout(new BorderLayout()); + panel.add(scrollPane, BorderLayout.CENTER); + tabLabel = new JLabel(Bundle.HidingPane_default_title()); + tabLabel.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/funnel.png"))); + tabLabel.setUI(new VerticalLabelUI(true)); + tabLabel.setOpaque(false); + Font font = tabLabel.getFont().deriveFont(18).deriveFont(Font.BOLD); + tabLabel.setFont(font); + + addTab(null, panel); + setTabComponentAt(0, tabLabel); + + this.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent evt) { + handleMouseClick(evt.getPoint()); + } + }); + + this.setTabPlacement(JTabbedPane.RIGHT); + } + + /** + * Change the title of the tab. + * + * @param title + */ + void setTitle(String title) { + tabLabel.setText(title); + } + + /** + * Set the icon that appears on the tab. + * + * @param icon + */ + void setIcon(Icon icon) { + tabLabel.setIcon(icon); + } + + /** + * Set the content for this panel. + * + * @param panel A panel to display in the tabbed pane. + */ + void setPanel(JPanel panel) { + scrollPane.setViewportView(panel); + } + + /** + * Handle the mouse click. + * + * @param point + */ + private void handleMouseClick(Point point) { + int index = indexAtLocation(point.x, point.y); + + if(index == -1) { + return; + } + + if(panelVisible) { + panel.removeAll(); + panel.revalidate(); + panelVisible = false; + } else { + panel.add(scrollPane, BorderLayout.CENTER); + panel.revalidate(); + panelVisible = true; + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/MapPanel.java b/Core/src/org/sleuthkit/autopsy/geolocation/MapPanel.java index 326d33f23e..da4a704dcf 100755 --- a/Core/src/org/sleuthkit/autopsy/geolocation/MapPanel.java +++ b/Core/src/org/sleuthkit/autopsy/geolocation/MapPanel.java @@ -56,7 +56,7 @@ import org.sleuthkit.datamodel.TskCoreException; /** * The map panel. This panel contains the jxmapviewer MapViewer */ -final class MapPanel extends javax.swing.JPanel { +final public class MapPanel extends javax.swing.JPanel { private static final Logger logger = Logger.getLogger(MapPanel.class.getName()); @@ -76,7 +76,7 @@ final class MapPanel extends javax.swing.JPanel { /** * Creates new form MapPanel */ - MapPanel() { + public MapPanel() { initComponents(); initMap(); diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/MapWaypoint.java b/Core/src/org/sleuthkit/autopsy/geolocation/MapWaypoint.java index 7eb765151a..7a28f49737 100755 --- a/Core/src/org/sleuthkit/autopsy/geolocation/MapWaypoint.java +++ b/Core/src/org/sleuthkit/autopsy/geolocation/MapWaypoint.java @@ -49,6 +49,7 @@ import org.sleuthkit.autopsy.geolocation.datamodel.GeoLocationDataException; import org.sleuthkit.autopsy.geolocation.datamodel.Route; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.autopsy.geolocation.datamodel.Waypoint; +import org.sleuthkit.autopsy.geolocation.datamodel.WaypointBuilder; import org.sleuthkit.autopsy.timeline.actions.ViewArtifactInTimelineAction; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; @@ -79,7 +80,7 @@ final class MapWaypoint extends KdTree.XYZPoint implements org.jxmapviewer.viewe * @throws GeoLocationDataException */ static List getWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { - List points = Waypoint.getAllWaypoints(skCase); + List points = WaypointBuilder.getAllWaypoints(skCase); List routes = Route.getRoutes(skCase); for (Route route : routes) { @@ -94,6 +95,28 @@ final class MapWaypoint extends KdTree.XYZPoint implements org.jxmapviewer.viewe return mapPoints; } + + /** + * Returns a list of of MapWaypoint objects for the given list of + * datamodel.Waypoint objects. + * + * @param dmWaypoints + * + * @return List of MapWaypoint objects. List will be empty if dmWaypoints was + * empty or null. + */ + static List getWaypoints(List dmWaypoints) { + List mapPoints = new ArrayList<>(); + + if (dmWaypoints != null) { + + for (Waypoint point : dmWaypoints) { + mapPoints.add(new MapWaypoint(point)); + } + } + + return mapPoints; + } /** * Returns a MapWaypoint without a reference to the datamodel waypoint. diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/RefreshPanel.form b/Core/src/org/sleuthkit/autopsy/geolocation/RefreshPanel.form index 400c788d42..145c22444a 100755 --- a/Core/src/org/sleuthkit/autopsy/geolocation/RefreshPanel.form +++ b/Core/src/org/sleuthkit/autopsy/geolocation/RefreshPanel.form @@ -42,7 +42,7 @@ - + @@ -63,7 +63,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/RefreshPanel.java b/Core/src/org/sleuthkit/autopsy/geolocation/RefreshPanel.java index a4e30c9129..f1f93a620d 100755 --- a/Core/src/org/sleuthkit/autopsy/geolocation/RefreshPanel.java +++ b/Core/src/org/sleuthkit/autopsy/geolocation/RefreshPanel.java @@ -82,13 +82,13 @@ final class RefreshPanel extends JPanel { gridBagConstraints.insets = new java.awt.Insets(15, 10, 15, 10); add(refreshLabel, gridBagConstraints); - refreshButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/geolocation/images/arrow-circle-double-135.png"))); // NOI18N + refreshButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/arrow-circle-double-135.png"))); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(refreshButton, org.openide.util.NbBundle.getMessage(RefreshPanel.class, "RefreshPanel.refreshButton.text")); // NOI18N refreshButton.setMargin(new java.awt.Insets(2, 5, 2, 5)); add(refreshButton, new java.awt.GridBagConstraints()); closeButton.setBackground(new java.awt.Color(0, 0, 0)); - closeButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/geolocation/images/cross-script.png"))); // NOI18N + closeButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/close-icon.png"))); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(closeButton, org.openide.util.NbBundle.getMessage(RefreshPanel.class, "RefreshPanel.closeButton.text")); // NOI18N closeButton.setMargin(new java.awt.Insets(0, 0, 0, 0)); closeButton.setOpaque(false); diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/VerticalLabelUI.java b/Core/src/org/sleuthkit/autopsy/geolocation/VerticalLabelUI.java new file mode 100755 index 0000000000..1a76953267 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/VerticalLabelUI.java @@ -0,0 +1,124 @@ +/* + * + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.geolocation; + +import java.awt.Dimension; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Insets; +import java.awt.Rectangle; +import java.awt.geom.AffineTransform; + +import javax.swing.Icon; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.plaf.basic.BasicLabelUI; + + +/** + * This class is an overload of BasicLabelUI to draw labels vertically. + * + * This code was found at: + * https://tech.chitgoks.com/2009/11/13/rotate-jlabel-vertically/ + * + */ +final class VerticalLabelUI extends BasicLabelUI { + + private static final Rectangle paintIconR = new Rectangle(); + private static final Rectangle paintTextR = new Rectangle(); + private static final Rectangle paintViewR = new Rectangle(); + private static Insets paintViewInsets = new Insets(0, 0, 0, 0); + + static { + labelUI = new VerticalLabelUI(false); + } + + final boolean clockwise; + + /** + * Construct a new VerticalLabelUI + * @param clockwise + */ + VerticalLabelUI(boolean clockwise) { + super(); + this.clockwise = clockwise; + } + + @Override + public Dimension getPreferredSize(JComponent c) { + Dimension dim = super.getPreferredSize(c); + return new Dimension( dim.height, dim.width ); + } + + @Override + public void paint(Graphics g, JComponent c) { + JLabel label = (JLabel)c; + String text = label.getText(); + Icon icon = (label.isEnabled()) ? label.getIcon() : label.getDisabledIcon(); + + if ((icon == null) && (text == null)) { + return; + } + + FontMetrics fm = g.getFontMetrics(); + paintViewInsets = c.getInsets(paintViewInsets); + + paintViewR.x = paintViewInsets.left; + paintViewR.y = paintViewInsets.top; + + // Use inverted height & width + paintViewR.height = c.getWidth() - (paintViewInsets.left + paintViewInsets.right); + paintViewR.width = c.getHeight() - (paintViewInsets.top + paintViewInsets.bottom); + + paintIconR.x = paintIconR.y = paintIconR.width = paintIconR.height = 0; + paintTextR.x = paintTextR.y = paintTextR.width = paintTextR.height = 0; + + String clippedText = layoutCL(label, fm, text, icon, paintViewR, paintIconR, paintTextR); + + Graphics2D g2 = (Graphics2D) g; + AffineTransform tr = g2.getTransform(); + if (clockwise) { + g2.rotate( Math.PI / 2 ); + g2.translate( 0, - c.getWidth() ); + } else { + g2.rotate( - Math.PI / 2 ); + g2.translate( - c.getHeight(), 0 ); + } + + if (icon != null) { + icon.paintIcon(c, g, paintIconR.x, paintIconR.y); + } + + if (text != null) { + int textX = paintTextR.x; + int textY = paintTextR.y + fm.getAscent(); + + if (label.isEnabled()) { + paintEnabledText(label, g, clippedText, textX, textY); + } else { + paintDisabledText(label, g, clippedText, textX, textY); + } + } + g2.setTransform( tr ); + } + +} + diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Waypoint.java b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Waypoint.java index bd5b39b6b7..27e5da0dbd 100755 --- a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Waypoint.java +++ b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Waypoint.java @@ -25,12 +25,9 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.logging.Level; -import java.util.logging.Logger; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardAttribute; -import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TskCoreException; /** @@ -68,8 +65,6 @@ public class Waypoint { BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_END, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE_END,}; - private static final Logger logger = Logger.getLogger(Waypoint.class.getName()); - /** * Construct a waypoint with the given artifact. * @@ -262,186 +257,6 @@ public class Waypoint { return attributeMap; } - - /** - * Returns a list of Waypoints for the artifacts with geolocation - * information. - * - * List will include artifacts of type: TSK_GPS_TRACKPOINT TSK_GPS_SEARCH - * TSK_GPS_LAST_KNOWN_LOCATION TSK_GPS_BOOKMARK TSK_METADATA_EXIF - * - * @param skCase Currently open SleuthkitCase - * - * @return List of Waypoint - * - * @throws GeoLocationDataException - */ - public static List getAllWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { - List points = new ArrayList<>(); - - points.addAll(getTrackpointWaypoints(skCase)); - points.addAll(getEXIFWaypoints(skCase)); - points.addAll(getSearchWaypoints(skCase)); - points.addAll(getLastKnownWaypoints(skCase)); - points.addAll(getBookmarkWaypoints(skCase)); - - return points; - } - - /** - * Gets a list of Waypoints for TSK_GPS_TRACKPOINT artifacts. - * - * @param skCase Currently open SleuthkitCase - * - * @return List of Waypoint - * - * @throws GeoLocationDataException - */ - public static List getTrackpointWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { - List artifacts = null; - try { - artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_TRACKPOINT); - } catch (TskCoreException ex) { - throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_TRACKPOINT", ex); - } - - List points = new ArrayList<>(); - for (BlackboardArtifact artifact : artifacts) { - try { - Waypoint point = new TrackpointWaypoint(artifact); - points.add(point); - } catch (GeoLocationDataException ex) { - logger.log(Level.WARNING, String.format("No longitude or latitude available for TSK_GPS_TRACKPOINT artifactID: %d", artifact.getArtifactID())); - } - } - return points; - } - - /** - * Gets a list of Waypoints for TSK_METADATA_EXIF artifacts. - * - * @param skCase Currently open SleuthkitCase - * - * @return List of Waypoint - * - * @throws GeoLocationDataException - */ - static public List getEXIFWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { - List artifacts = null; - try { - artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_METADATA_EXIF); - } catch (TskCoreException ex) { - throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_LAST_KNOWN_LOCATION", ex); - } - - List points = new ArrayList<>(); - if (artifacts != null) { - for (BlackboardArtifact artifact : artifacts) { - try { - Waypoint point = new EXIFWaypoint(artifact); - points.add(point); - } catch (GeoLocationDataException ex) { - // I am a little relucant to log this error because I suspect - // this will happen more often than not. It is valid for - // METADAT_EXIF to not have longitude and latitude - } - } - } - return points; - } - - /** - * Gets a list of Waypoints for TSK_GPS_SEARCH artifacts. - * - * @param skCase Currently open SleuthkitCase - * - * @return List of Waypoint - * - * @throws GeoLocationDataException - */ - public static List getSearchWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { - List artifacts = null; - try { - artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_SEARCH); - } catch (TskCoreException ex) { - throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_SEARCH", ex); - } - - List points = new ArrayList<>(); - if (artifacts != null) { - for (BlackboardArtifact artifact : artifacts) { - try { - Waypoint point = new SearchWaypoint(artifact); - points.add(point); - } catch (GeoLocationDataException ex) { - logger.log(Level.WARNING, String.format("No longitude or latitude available for TSK_GPS_SEARCH artifactID: %d", artifact.getArtifactID())); - } - } - } - return points; - } - - /** - * Gets a list of Waypoints for TSK_GPS_LAST_KNOWN_LOCATION artifacts. - * - * @param skCase Currently open SleuthkitCase - * - * @return List of Waypoint - * - * @throws GeoLocationDataException - */ - public static List getLastKnownWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { - List artifacts = null; - try { - artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_LAST_KNOWN_LOCATION); - } catch (TskCoreException ex) { - throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_LAST_KNOWN_LOCATION", ex); - } - - List points = new ArrayList<>(); - if (artifacts != null) { - for (BlackboardArtifact artifact : artifacts) { - try { - Waypoint point = new LastKnownWaypoint(artifact); - points.add(point); - } catch (GeoLocationDataException ex) { - logger.log(Level.WARNING, String.format("No longitude or latitude available for TSK_GPS_LAST_KNOWN_LOCATION artifactID: %d", artifact.getArtifactID())); - } - } - } - return points; - } - - /** - * Gets a list of Waypoints for TSK_GPS_BOOKMARK artifacts. - * - * @param skCase Currently open SleuthkitCase - * - * @return List of Waypoint - * - * @throws GeoLocationDataException - */ - public static List getBookmarkWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { - List artifacts = null; - try { - artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_BOOKMARK); - } catch (TskCoreException ex) { - throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_BOOKMARK", ex); - } - - List points = new ArrayList<>(); - if (artifacts != null) { - for (BlackboardArtifact artifact : artifacts) { - try { - Waypoint point = new Waypoint(artifact); - points.add(point); - } catch (GeoLocationDataException ex) { - logger.log(Level.WARNING, String.format("No longitude or latitude available for TSK_GPS_BOOKMARK artifactID: %d", artifact.getArtifactID())); - } - } - } - return points; - } /** * Get a list of Waypoint.Property objects for the given artifact. This list diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/WaypointBuilder.java b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/WaypointBuilder.java new file mode 100755 index 0000000000..7189302809 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/WaypointBuilder.java @@ -0,0 +1,486 @@ +/* + * + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.geolocation.datamodel; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.CaseDbAccessManager; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.DataSource; + +/** + * Class for building lists of waypoints. + * + */ +public final class WaypointBuilder { + + private static final Logger logger = Logger.getLogger(WaypointBuilder.class.getName()); + + // SELECT statement for getting a list of waypoints. + final static String GEO_ARTIFACT_QUERY + = "SELECT artifact_id, artifact_type_id " + + "FROM blackboard_attributes " + + "WHERE attribute_type_id IN (%d, %d) "; //NON-NLS + + // SELECT statement to get only artifact_ids + final static String GEO_ARTIFACT_QUERY_ID_ONLY + = "SELECT artifact_id " + + "FROM blackboard_attributes " + + "WHERE attribute_type_id IN (%d, %d) "; //NON-NLS + + // This Query will return a list of waypoint artifacts + final static String GEO_ARTIFACT_WITH_DATA_SOURCES_QUERY + = "SELECT blackboard_attributes.artifact_id " + + "FROM blackboard_attributes, blackboard_artifacts " + + "WHERE blackboard_attributes.attribute_type_id IN(%d, %d) " + + "AND data_source_obj_id IN (%s)"; //NON-NLS + + // Select will return the "most recent" timestamp from all waypoings + final static String MOST_RECENT_TIME + = "SELECT MAX(value_int64) - (%d * 86400)" //86400 is the number of seconds in a day. + + "FROM blackboard_attributes " + + "WHERE attribute_type_id IN(%d, %d) " + + "AND artifact_id " + + "IN ( " + + "%s" //GEO_ARTIFACT with or without data source + + " )"; + + // Returns a list of artifacts with no time stamp + final static String SELECT_WO_TIMESTAMP = + "SELECT DISTINCT artifact_id, artifact_type_id " + + "FROM blackboard_attributes " + + "WHERE artifact_id NOT IN (%s) " + + "AND artifact_id IN (%s)"; //NON-NLS + + /** + * A callback interface to process the results of waypoint filtering. + */ + public interface WaypointFilterQueryCallBack { + + /** + * This function will be called after the waypoints have been filtered. + * + * @param wwaypoints This of waypoints. + */ + void process(List wwaypoints); + } + + /** + * private constructor + */ + private WaypointBuilder() { + + } + + /** + * Returns a list of Waypoints for the artifacts with geolocation + * information. + * + * List will include artifacts of type: TSK_GPS_TRACKPOINT TSK_GPS_SEARCH + * TSK_GPS_LAST_KNOWN_LOCATION TSK_GPS_BOOKMARK TSK_METADATA_EXIF + * + * @param skCase Currently open SleuthkitCase + * + * @return List of Waypoint + * + * @throws GeoLocationDataException + */ + public static List getAllWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { + List points = new ArrayList<>(); + + points.addAll(getTrackpointWaypoints(skCase)); + points.addAll(getEXIFWaypoints(skCase)); + points.addAll(getSearchWaypoints(skCase)); + points.addAll(getLastKnownWaypoints(skCase)); + points.addAll(getBookmarkWaypoints(skCase)); + + return points; + } + + /** + * Gets a list of Waypoints for TSK_GPS_TRACKPOINT artifacts. + * + * @param skCase Currently open SleuthkitCase + * + * @return List of Waypoint + * + * @throws GeoLocationDataException + */ + public static List getTrackpointWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { + List artifacts = null; + try { + artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_TRACKPOINT); + } catch (TskCoreException ex) { + throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_TRACKPOINT", ex);//NON-NLS + } + + List points = new ArrayList<>(); + for (BlackboardArtifact artifact : artifacts) { + try { + Waypoint point = new TrackpointWaypoint(artifact); + points.add(point); + } catch (GeoLocationDataException ex) { + logger.log(Level.WARNING, String.format("No longitude or latitude available for TSK_GPS_TRACKPOINT artifactID: %d", artifact.getArtifactID()));//NON-NLS + } + } + return points; + } + + /** + * Gets a list of Waypoints for TSK_METADATA_EXIF artifacts. + * + * @param skCase Currently open SleuthkitCase + * + * @return List of Waypoint + * + * @throws GeoLocationDataException + */ + static public List getEXIFWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { + List artifacts = null; + try { + artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_METADATA_EXIF); + } catch (TskCoreException ex) { + throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_LAST_KNOWN_LOCATION", ex);//NON-NLS + } + + List points = new ArrayList<>(); + if (artifacts != null) { + for (BlackboardArtifact artifact : artifacts) { + try { + Waypoint point = new EXIFWaypoint(artifact); + points.add(point); + } catch (GeoLocationDataException ex) { + // I am a little relucant to log this error because I suspect + // this will happen more often than not. It is valid for + // METADAT_EXIF to not have longitude and latitude + } + } + } + return points; + } + + /** + * Gets a list of Waypoints for TSK_GPS_SEARCH artifacts. + * + * @param skCase Currently open SleuthkitCase + * + * @return List of Waypoint + * + * @throws GeoLocationDataException + */ + public static List getSearchWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { + List artifacts = null; + try { + artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_SEARCH); + } catch (TskCoreException ex) { + throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_SEARCH", ex);//NON-NLS + } + + List points = new ArrayList<>(); + if (artifacts != null) { + for (BlackboardArtifact artifact : artifacts) { + try { + Waypoint point = new SearchWaypoint(artifact); + points.add(point); + } catch (GeoLocationDataException ex) { + logger.log(Level.WARNING, String.format("No longitude or latitude available for TSK_GPS_SEARCH artifactID: %d", artifact.getArtifactID()));//NON-NLS + } + } + } + return points; + } + + /** + * Gets a list of Waypoints for TSK_GPS_LAST_KNOWN_LOCATION artifacts. + * + * @param skCase Currently open SleuthkitCase + * + * @return List of Waypoint + * + * @throws GeoLocationDataException + */ + public static List getLastKnownWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { + List artifacts = null; + try { + artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_LAST_KNOWN_LOCATION); + } catch (TskCoreException ex) { + throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_LAST_KNOWN_LOCATION", ex);//NON-NLS + } + + List points = new ArrayList<>(); + if (artifacts != null) { + for (BlackboardArtifact artifact : artifacts) { + try { + Waypoint point = new LastKnownWaypoint(artifact); + points.add(point); + } catch (GeoLocationDataException ex) { + logger.log(Level.WARNING, String.format("No longitude or latitude available for TSK_GPS_LAST_KNOWN_LOCATION artifactID: %d", artifact.getArtifactID()));//NON-NLS + } + } + } + return points; + } + + /** + * Gets a list of Waypoints for TSK_GPS_BOOKMARK artifacts. + * + * @param skCase Currently open SleuthkitCase + * + * @return List of Waypoint + * + * @throws GeoLocationDataException + */ + public static List getBookmarkWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { + List artifacts = null; + try { + artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_BOOKMARK); + } catch (TskCoreException ex) { + throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_BOOKMARK", ex);//NON-NLS + } + + List points = new ArrayList<>(); + if (artifacts != null) { + for (BlackboardArtifact artifact : artifacts) { + try { + Waypoint point = new Waypoint(artifact); + points.add(point); + } catch (GeoLocationDataException ex) { + logger.log(Level.WARNING, String.format("No longitude or latitude available for TSK_GPS_BOOKMARK artifactID: %d", artifact.getArtifactID()), ex);//NON-NLS + } + } + } + return points; + } + + /** + * Get a filtered list of waypoints. + * + * If showAll is true, the values of cntDaysFromRecent and notTimeStamp will + * be ignored. + * + * To include data from all dataSources pass a null or empty dataSource + * list. + * + * + * @param skCase Currently open sleuthkit case. + * @param dataSources This of data sources to filter the waypoints by. + * Pass a null or empty list to show way points for + * all dataSources. + * + * @param showAll True to get all waypoints. + * + * @param cntDaysFromRecent Number of days from the most recent time stamp + * to get waypoints for. This parameter will be + * ignored if showAll is true; + * + * @param noTimeStamp True to include waypoints without timestamp. + * This parameter will be ignored if showAll is + * true. + * + * @param queryCallBack Function to call after the DB query has + * completed. + * + * @throws GeoLocationDataException + */ + static public void getAllWaypoints(SleuthkitCase skCase, List dataSources, boolean showAll, int cntDaysFromRecent, boolean noTimeStamp, WaypointFilterQueryCallBack queryCallBack) throws GeoLocationDataException { + String query = buildQuery(dataSources, showAll, cntDaysFromRecent, noTimeStamp); + + logger.log(Level.INFO, query); + + try { + // The CaseDBAccessManager.select function will add a SELECT + // to the beginning of the query + if (query.startsWith("SELECT")) { //NON-NLS + query = query.replaceFirst("SELECT", ""); //NON-NLS + } + + skCase.getCaseDbAccessManager().select(query, new CaseDbAccessManager.CaseDbAccessQueryCallback() { + @Override + public void process(ResultSet rs) { + List waypoints = new ArrayList<>(); + try { + while (rs.next()) { + int artifact_type_id = rs.getInt("artifact_type_id"); //NON-NLS + long artifact_id = rs.getLong("artifact_id"); //NON-NLS + + BlackboardArtifact.ARTIFACT_TYPE type = BlackboardArtifact.ARTIFACT_TYPE.fromID(artifact_type_id); + + waypoints.addAll(getWaypointForArtifact(skCase.getBlackboardArtifact(artifact_id), type)); + + } + queryCallBack.process(waypoints); + } catch (GeoLocationDataException | SQLException | TskCoreException ex) { + logger.log(Level.WARNING, "Failed to filter waypoint.", ex); //NON-NLS + } + + } + }); + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "Failed to filter waypoint.", ex); //NON-NLS + } + } + + /** + * Create the query for getting a list of waypoints that do not have time + * stamps. + * + * @param dataSources List of data Sources to filter by + * + * @return SQL SELECT statement + */ + static private String buildQueryForWaypointsWOTimeStamps(List dataSources) { + return String.format(SELECT_WO_TIMESTAMP, + String.format(GEO_ARTIFACT_QUERY_ID_ONLY, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME.getTypeID(), + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_CREATED.getTypeID()), + getWaypointListQuery(dataSources)); + } + + /** + * Build the query to filter the list of waypoints. + * + * If showAll is true, the values of cntDaysFromRecent and noTimeStamp are + * ignored. + * + * @param dataSources This of data sources to filter the waypoints by. + * Pass a null or empty list to show way points for + * all dataSources. + * + * @param showAll True to get all waypoints. + * + * @param cntDaysFromRecent Number of days from the most recent time stamp + * to get waypoints for. This parameter will be + * ignored if showAll is true; + * + * @param noTimeStamp True to include waypoints without timestamp. + * This parameter will be ignored if showAll is + * true. + * + * @return + */ + static private String buildQuery(List dataSources, boolean showAll, int cntDaysFromRecent, boolean noTimeStamp) { + String mostRecentQuery = ""; + + if (!showAll && cntDaysFromRecent > 0) { + mostRecentQuery = String.format("AND value_int64 > (%s)", //NON-NLS + String.format(MOST_RECENT_TIME, + cntDaysFromRecent, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME.getTypeID(), + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_CREATED.getTypeID(), + getWaypointListQuery(dataSources) + )); + } + + // This givens us all artifact_ID that have time stamp + String query = String.format(GEO_ARTIFACT_QUERY, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME.getTypeID(), + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_CREATED.getTypeID()); + + // That are in the list of artifacts for the given data Sources + query += String.format("AND artifact_id IN(%s)", getWaypointListQuery(dataSources)); //NON-NLS + query += mostRecentQuery; + + if (showAll || noTimeStamp) { + query = String.format("%s UNION %s", buildQueryForWaypointsWOTimeStamps(dataSources), query); //NON-NLS + } + + return query; + } + + /** + * Returns the query to get a list of waypoints filted by the given data + * sources. + * + * An artifact is assumed to be a "waypoint" if it has the attributes + * TSK_GEO_LATITUDE or TSK_GEO_LATITUDE_START + * + * @param dataSources A list of data sources to filter by. If the list is + * null or empty the data source list will be ignored. + * + * @return + */ + static private String getWaypointListQuery(List dataSources) { + + if (dataSources == null || dataSources.isEmpty()) { + return String.format(GEO_ARTIFACT_QUERY, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE.getTypeID(), + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_START.getTypeID()); + } + + String dataSourceList = ""; + for (DataSource source : dataSources) { + dataSourceList += Long.toString(source.getId()) + ","; + } + + if (!dataSourceList.isEmpty()) { + // Remove the last , + dataSourceList = dataSourceList.substring(0, dataSourceList.length() - 1); + } + + return String.format(GEO_ARTIFACT_WITH_DATA_SOURCES_QUERY, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE.getTypeID(), + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_START.getTypeID(), + dataSourceList); + } + + /** + * Create a Waypoint object for the given Blackboard artifact. + * + * @param artifact The artifact to create the waypoint from + * @param type The type of artifact + * + * @return A new waypoint object + * + * @throws GeoLocationDataException + */ + static private List getWaypointForArtifact(BlackboardArtifact artifact, BlackboardArtifact.ARTIFACT_TYPE type) throws GeoLocationDataException { + List waypoints = new ArrayList<>(); + switch (type) { + case TSK_METADATA_EXIF: + waypoints.add(new EXIFWaypoint(artifact)); + break; + case TSK_GPS_BOOKMARK: + waypoints.add(new Waypoint(artifact)); + break; + case TSK_GPS_TRACKPOINT: + waypoints.add(new TrackpointWaypoint(artifact)); + break; + case TSK_GPS_SEARCH: + waypoints.add(new SearchWaypoint(artifact)); + break; + case TSK_GPS_ROUTE: + Route route = new Route(artifact); + waypoints.addAll(route.getRoute()); + break; + default: + waypoints.add(new Waypoint(artifact)); + break; + } + + return waypoints; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/images/cross-script.png b/Core/src/org/sleuthkit/autopsy/geolocation/images/cross-script.png deleted file mode 100755 index f37cf41830..0000000000 Binary files a/Core/src/org/sleuthkit/autopsy/geolocation/images/cross-script.png and /dev/null differ diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/images/arrow-circle-double-135.png b/Core/src/org/sleuthkit/autopsy/images/arrow-circle-double-135.png similarity index 100% rename from Core/src/org/sleuthkit/autopsy/geolocation/images/arrow-circle-double-135.png rename to Core/src/org/sleuthkit/autopsy/images/arrow-circle-double-135.png diff --git a/Core/src/org/sleuthkit/autopsy/images/blueGeo16.png b/Core/src/org/sleuthkit/autopsy/images/blueGeo16.png new file mode 100755 index 0000000000..b89f013c41 Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/images/blueGeo16.png differ diff --git a/Core/src/org/sleuthkit/autopsy/images/blueGeo64.png b/Core/src/org/sleuthkit/autopsy/images/blueGeo64.png new file mode 100755 index 0000000000..cbbfbf3764 Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/images/blueGeo64.png differ diff --git a/Core/src/org/sleuthkit/autopsy/images/document-question-16.png b/Core/src/org/sleuthkit/autopsy/images/document-question-16.png new file mode 100644 index 0000000000..c084ead81a Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/images/document-question-16.png differ diff --git a/Core/src/org/sleuthkit/autopsy/images/funnel.png b/Core/src/org/sleuthkit/autopsy/images/funnel.png new file mode 100755 index 0000000000..1f69604528 Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/images/funnel.png differ diff --git a/Core/src/org/sleuthkit/autopsy/images/url-16.png b/Core/src/org/sleuthkit/autopsy/images/url-16.png new file mode 100644 index 0000000000..414cff505d Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/images/url-16.png differ diff --git a/Core/src/org/sleuthkit/autopsy/report/modules/kml/KMLReport.java b/Core/src/org/sleuthkit/autopsy/report/modules/kml/KMLReport.java index eabdeef98b..a140338524 100644 --- a/Core/src/org/sleuthkit/autopsy/report/modules/kml/KMLReport.java +++ b/Core/src/org/sleuthkit/autopsy/report/modules/kml/KMLReport.java @@ -47,6 +47,7 @@ import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.geolocation.datamodel.GeoLocationDataException; import org.sleuthkit.autopsy.geolocation.datamodel.Waypoint; import org.sleuthkit.autopsy.geolocation.datamodel.Route; +import org.sleuthkit.autopsy.geolocation.datamodel.WaypointBuilder; import org.sleuthkit.autopsy.report.ReportBranding; import org.sleuthkit.autopsy.report.ReportProgressPanel; import org.sleuthkit.datamodel.AbstractFile; @@ -331,11 +332,11 @@ class KMLReport implements GeneralReportModule { * @throws IOException */ void addLocationsToReport(SleuthkitCase skCase, String baseReportDir) throws GeoLocationDataException, IOException { - addExifMetadataContent(Waypoint.getEXIFWaypoints(skCase), baseReportDir); - addWaypoints(Waypoint.getBookmarkWaypoints(skCase), gpsBookmarksFolder, FeatureColor.BLUE, Bundle.Waypoint_Bookmark_Display_String()); - addWaypoints(Waypoint.getLastKnownWaypoints(skCase), gpsLastKnownLocationFolder, FeatureColor.PURPLE, Bundle.Waypoint_Last_Known_Display_String()); - addWaypoints(Waypoint.getSearchWaypoints(skCase), gpsSearchesFolder, FeatureColor.WHITE, Bundle.Waypoint_Search_Display_String()); - addWaypoints(Waypoint.getTrackpointWaypoints(skCase), gpsTrackpointsFolder, FeatureColor.WHITE, Bundle.Waypoint_Trackpoint_Display_String()); + addExifMetadataContent(WaypointBuilder.getEXIFWaypoints(skCase), baseReportDir); + addWaypoints(WaypointBuilder.getBookmarkWaypoints(skCase), gpsBookmarksFolder, FeatureColor.BLUE, Bundle.Waypoint_Bookmark_Display_String()); + addWaypoints(WaypointBuilder.getLastKnownWaypoints(skCase), gpsLastKnownLocationFolder, FeatureColor.PURPLE, Bundle.Waypoint_Last_Known_Display_String()); + addWaypoints(WaypointBuilder.getSearchWaypoints(skCase), gpsSearchesFolder, FeatureColor.WHITE, Bundle.Waypoint_Search_Display_String()); + addWaypoints(WaypointBuilder.getTrackpointWaypoints(skCase), gpsTrackpointsFolder, FeatureColor.WHITE, Bundle.Waypoint_Trackpoint_Display_String()); } /** diff --git a/Core/src/org/sleuthkit/autopsy/timeline/actions/ViewArtifactInTimelineAction.java b/Core/src/org/sleuthkit/autopsy/timeline/actions/ViewArtifactInTimelineAction.java index ed36bff868..69f998dc9b 100755 --- a/Core/src/org/sleuthkit/autopsy/timeline/actions/ViewArtifactInTimelineAction.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/actions/ViewArtifactInTimelineAction.java @@ -25,6 +25,7 @@ import org.openide.util.NbBundle; import org.openide.util.actions.SystemAction; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import org.sleuthkit.autopsy.coreutils.ModuleSettings; import org.sleuthkit.autopsy.timeline.OpenTimelineAction; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardAttribute; @@ -43,6 +44,10 @@ public final class ViewArtifactInTimelineAction extends AbstractAction { public ViewArtifactInTimelineAction(BlackboardArtifact artifact) { super(Bundle.ViewArtifactInTimelineAction_displayName()); this.artifact = artifact; + // If timeline functionality is not available this action is disabled. + if ("false".equals(ModuleSettings.getConfigSetting("timeline", "enable_timeline"))) { + setEnabled(false); + } } @Override diff --git a/Core/src/org/sleuthkit/autopsy/timeline/actions/ViewFileInTimelineAction.java b/Core/src/org/sleuthkit/autopsy/timeline/actions/ViewFileInTimelineAction.java index e763f4c428..8ef6bce42c 100755 --- a/Core/src/org/sleuthkit/autopsy/timeline/actions/ViewFileInTimelineAction.java +++ b/Core/src/org/sleuthkit/autopsy/timeline/actions/ViewFileInTimelineAction.java @@ -25,6 +25,7 @@ import org.openide.util.NbBundle; import org.openide.util.actions.SystemAction; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import org.sleuthkit.autopsy.coreutils.ModuleSettings; import org.sleuthkit.autopsy.timeline.OpenTimelineAction; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.TskCoreException; @@ -54,6 +55,10 @@ public final class ViewFileInTimelineAction extends AbstractAction { && file.getAtime() <= 0)) { this.setEnabled(false); } + // If timeline functionality is not available this action is disabled. + if ("false".equals(ModuleSettings.getConfigSetting("timeline", "enable_timeline"))) { + setEnabled(false); + } } @NbBundle.Messages({"ViewFileInTimelineAction.viewFile.displayName=View File in Timeline... "}) diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java index d875657cc1..7bc846fdc0 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java @@ -49,7 +49,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.logging.Level; -import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.annotation.concurrent.GuardedBy; import javax.swing.SortOrder; @@ -80,6 +79,7 @@ import org.sleuthkit.datamodel.TskData.DbType; import org.sleuthkit.datamodel.TskDataException; import org.sleuthkit.datamodel.VersionNumber; import org.sqlite.SQLiteJDBCLoader; +import java.util.stream.Collectors; /** * Provides access to the drawables database and selected tables in the case @@ -230,7 +230,7 @@ public final class DrawableDB { dbWriteLock(); try { con = DriverManager.getConnection("jdbc:sqlite:" + dbPath.toString()); //NON-NLS - if (!initializeDBSchema() || !upgradeDBSchema() || !prepareStatements() || !initializeStandardGroups() || !initializeImageList()) { + if (!initializeDBSchema() || !upgradeDBSchema() || !prepareStatements() || !initializeStandardGroups() || !removeDeletedDataSources() || !initializeImageList()) { close(); throw new TskCoreException("Failed to initialize drawables database for Image Gallery use"); //NON-NLS } @@ -374,6 +374,62 @@ public final class DrawableDB { } } + /** + * Removes any data sources from the local drawables database that have been + * deleted from the case database. This is necessary for multi-user cases + * where the case database is shared, but each user has his or her own local + * drawables database and may not have had the case open when a data source + * was deleted. + * + * @return True on success, false on failure. + */ + private boolean removeDeletedDataSources() { + dbWriteLock(); + try (SleuthkitCase.CaseDbQuery caseDbQuery = tskCase.executeQuery("SELECT obj_id FROM data_source_info"); //NON-NLS + Statement drawablesDbStmt = con.createStatement()) { + /* + * Get the data source object IDs from the case database. + */ + ResultSet caseDbResults = caseDbQuery.getResultSet(); + Set currentDataSourceObjIDs = new HashSet<>(); + while (caseDbResults.next()) { + currentDataSourceObjIDs.add(caseDbResults.getLong(1)); + } + + /* + * Get the data source object IDs from the drawables database and + * determine which ones, if any, have been deleted from the case + * database. + */ + List staleDataSourceObjIDs = new ArrayList<>(); + try (ResultSet drawablesDbResults = drawablesDbStmt.executeQuery("SELECT ds_obj_id FROM datasources")) { //NON-NLS + while (drawablesDbResults.next()) { + long dataSourceObjID = drawablesDbResults.getLong(1); + if (!currentDataSourceObjIDs.contains(dataSourceObjID)) { + staleDataSourceObjIDs.add(dataSourceObjID); + } + } + } + + /* + * Delete the surplus data sources from this local drawables + * database. The delete cascades. + */ + if (!staleDataSourceObjIDs.isEmpty()) { + String deleteCommand = "DELETE FROM datasources where ds_obj_id IN (" + StringUtils.join(staleDataSourceObjIDs, ',') + ")"; //NON-NLS + drawablesDbStmt.execute(deleteCommand); + } + return true; + + } catch (TskCoreException | SQLException ex) { + logger.log(Level.SEVERE, "Failed to remove deleted data sources from drawables database", ex); //NON-NLS + return false; + + } finally { + dbWriteUnlock(); + } + } + /** * Public factory method. Creates and opens a connection to a new database * * at the given path. If there is already a db at the path, it is checked @@ -2037,7 +2093,7 @@ public final class DrawableDB { * * @param dataSourceID The object ID of the data source to delete. * - * @throws SQLException + * @throws SQLException * @throws TskCoreException */ public void deleteDataSource(long dataSourceID) throws SQLException, TskCoreException { diff --git a/InternalPythonModules/android/fbmessenger.py b/InternalPythonModules/android/fbmessenger.py index 2347144f09..c19ae87796 100644 --- a/InternalPythonModules/android/fbmessenger.py +++ b/InternalPythonModules/android/fbmessenger.py @@ -17,6 +17,11 @@ See the License for the specific language governing permissions and limitations under the License. """ +import json +import traceback +import general +import ast + from java.io import File from java.lang import Class from java.lang import ClassNotFoundException @@ -43,14 +48,13 @@ from org.sleuthkit.datamodel import TskCoreException from org.sleuthkit.datamodel.Blackboard import BlackboardException from org.sleuthkit.datamodel import Account from org.sleuthkit.datamodel.blackboardutils import CommunicationArtifactsHelper +from org.sleuthkit.datamodel.blackboardutils import MessageAttachments +from org.sleuthkit.datamodel.blackboardutils import URLAttachment +from org.sleuthkit.datamodel.blackboardutils import FileAttachment from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import MessageReadStatus from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import CommunicationDirection from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import CallMediaType -import json -import traceback -import general - class FBMessengerAnalyzer(general.AndroidComponentAnalyzer): @@ -95,6 +99,7 @@ class FBMessengerAnalyzer(general.AndroidComponentAnalyzer): * have no text, * admin_text_thread_rtc_event has the specific event "group-call-started", "group-call_ended" + --- A pending_send_media_attachment - a JSON structure that has details of attachments that may or may not have been sent. --- A admin_text_thread_rtc_event column - has specific text events such as- "one-on-one-call-ended" --- A thread_key column - identifies the message thread --- A timestamp_ms column - date/time message was sent @@ -210,6 +215,17 @@ class FBMessengerAnalyzer(general.AndroidComponentAnalyzer): else: direction = CommunicationDirection.INCOMING return direction + + ## Get the arrayList from the json passed in + def getJPGListFromJson(self, jpgJson): + jpgArray = ArrayList() + # The urls attachment will come across as unicode unless we use ast.literal_eval to change it to a dictionary + jpgDict = ast.literal_eval(jpgJson) + for jpgPreview in jpgDict.iterkeys(): + # Need to use ast.literal_eval so that the string can be converted to a dictionary + jpgUrlDict = ast.literal_eval(jpgDict[jpgPreview]) + jpgArray.add(URLAttachment(jpgUrlDict["src"])) + return jpgArray ## Analyzes messages def analyzeMessages(self, threadsDb, threadsDBHelper): @@ -223,7 +239,8 @@ class FBMessengerAnalyzer(general.AndroidComponentAnalyzer): ## The result set is processed to collect the multiple recipients for a given message. sqlString = """ SELECT msg_id, text, sender, timestamp_ms, msg_type, messages.thread_key as thread_key, - snippet, thread_participants.user_key as user_key, thread_users.name as name + snippet, thread_participants.user_key as user_key, thread_users.name as name, + attachments, pending_send_media_attachment FROM messages JOIN thread_participants ON messages.thread_key = thread_participants.thread_key JOIN thread_users ON thread_participants.user_key = thread_users.user_key @@ -241,6 +258,8 @@ class FBMessengerAnalyzer(general.AndroidComponentAnalyzer): timeStamp = -1 msgText = "" threadId = "" + messageAttachments = None + currentCase = Case.getCurrentCaseThrows() while messagesResultSet.next(): msgId = messagesResultSet.getString("msg_id") @@ -260,6 +279,10 @@ class FBMessengerAnalyzer(general.AndroidComponentAnalyzer): msgText, threadId) + if (messageAttachments is not None): + threadsDBHelper.addAttachments(messageArtifact, messageAttachments) + messageAttachments = None + oldMsgId = msgId # New message - collect all attributes @@ -282,8 +305,42 @@ class FBMessengerAnalyzer(general.AndroidComponentAnalyzer): if not msgText: msgText = messagesResultSet.getString("snippet") - # TBD: get attachment + # Get attachments and pending attachments if they exist + attachment = messagesResultSet.getString("attachments") + pendingAttachment = messagesResultSet.getString("pending_send_media_attachment") + + urlAttachments = ArrayList() + fileAttachments = ArrayList() + + if ((attachment is not None) or (pendingAttachment is not None)): + if (attachment is not None): + attachmentDict = json.loads(attachment)[0] + if (attachmentDict["mime_type"] == "image/jpeg"): + urlAttachments = self.getJPGListFromJson(attachmentDict["urls"]) + elif (attachmentDict["mime_type"] == "video/mp4"): + # filename does not have an associated path with it so it will be ignored + urlAttachments = self.getJPGListFromJson(attachmentDict["urls"]) + urlAttachments.add(URLAttachment(attachmentDict["video_data_url"])) + urlAttachments.add(URLAttachment(attachmentDict["video_data_thumbnail_url"])) + + elif (attachmentDict["mime_type"] == "audio/mpeg"): + if (attachmentDict["audio_uri"] == ""): + continue + else: + audioUri = attachmentDict["audio_uri"] + fileAttachments.add(FileAttachment(currentCase.getSleuthkitCase(), threadsDb.getDBFile().getDataSource(), audioUri.replace("file://",""))) + + else: + self._logger.log(Level.INFO, "Attachment type not handled: " + attachmentDict["mime_type"]) + + if (pendingAttachment is not None): + pendingAttachmentDict = json.loads(pendingAttachment)[0] + pendingAttachmentUri = pendingAttachmentDict["uri"] + fileAttachments.add(FileAttachment(currentCase.getSleuthkitCase(), threadsDb.getDBFile().getDataSource(), pendingAttachmentUri.replace("file://",""))) + + messageAttachments = MessageAttachments(fileAttachments, urlAttachments) + threadId = messagesResultSet.getString("thread_key") else: # same msgId as last, just collect recipient from current row diff --git a/Running_Linux_OSX.txt b/Running_Linux_OSX.txt index bb60569a56..c47dc5b418 100644 --- a/Running_Linux_OSX.txt +++ b/Running_Linux_OSX.txt @@ -9,33 +9,41 @@ The following need to be done at least once. They do not need to be repeated for -- Linux: % sudo apt-get install testdisk -- OS X: % brew install testdisk -- Install a Java 8 JRE and JavaFX 8 and set JAVA_HOME. --- Linux: Any Java 8 version of OpenJDK/OpenJFX distribution should suffice. The following instructions use the Zulu Community distribution. - 1. Download a 64 bit Java 8 JRE for your specific platform from https://www.azul.com/downloads/zulu-community - 2. Install the JRE. e.g. % sudo apt install ./zulu8.40.0.25-ca-jre8.0.222-linux_amd64.deb - 3. Download a 64 bit Java 8 JavaFX for your specific platform from the same location. - - Note that you may need to select "Older Zulu versions" for FX to become available in the "Java Package" dropdown. - 4. Extract the contents of the JavaFX archive into the folder where the JRE was installed. - e.g. % sudo tar xzf ~/Downloads/zulu8.40.0.25-ca-fx-jre8.0.222-linux_x64.tar.gz -C /usr/lib/jvm/zre-8-amd64 --strip-components=1 - +- Install the BellSoft Java 8 JRE and JavaFX 8 distribution and set JAVA_HOME. + * The BellSoft distribution bundles OpenJDK and OpenJFX. Other distributions we have tried either don't + bundle OpenJFX (AdoptOpenJDK) or don't include all necessary binaries (Amazon Corretto). +-- Linux: + 1. Install BellSoft Java 8 + % wget -q -O - https://download.bell-sw.com/pki/GPG-KEY-bellsoft | sudo apt-key add - + % echo "deb [arch=amd64] https://apt.bell-sw.com/ stable main" | sudo tee /etc/apt/sources.list.d/bellsoft.list + % sudo apt-get update + % sudo apt-get install bellsoft-java8 + 2. Set JAVA_HOME + % export JAVA_HOME=/usr/lib/jvm/bellsoft-java8-amd64 + NOTE: You may need to log out and back in again after setting JAVA_HOME before the Autopsy unix_setup.sh script can see the value. --- OS X: Any Java 8 version of OpenJDK/OpenJFX distribution should suffice. The following instructions use the AdoptOpenJDK distribution. - 1. Install a 64 bit Java 8 JRE. - % brew cask install adoptopenjdk8 +-- OS X: + 1. Install BellSoft Java 8. + % brew tap bell-sw/liberica + % brew cask install liberica-jdk8 2. Set JAVA_HOME environment variable to location of JRE installation. e.g. add the following to ~/.bashrc export JAVA_HOME=$(/usr/libexec/java_home -v 1.8) - 3. Confirm your version of Java by running - % java -version +- Confirm your version of Java by running + % java -version + openjdk version "1.8.0.232" + OpenJDK Runtime Environment (build 1.8.0_232-BellSoft-b10) + OpenJDK 64-Bit Server VM (build 25.232-b10, mixed mode) + * Install The Sleuth Kit Java Bindings * Autopsy depends on a specific version of The Sleuth Kit. You need the Java libraries of The Sleuth Kit installed, which is not part of all packages. - Linux: Install the sleuthkit-java.deb file that you can download from github.com/sleuthkit/sleuthkit/releases. This will install libewf, etc. --- % sudo apt install ./sleuthkit-java_4.6.0-1_amd64.deb +-- % sudo apt install ./sleuthkit-java_4.7.0-1_amd64.deb - OS X: Install The Sleuth Kit from brew. -- % brew install sleuthkit @@ -55,6 +63,24 @@ Autopsy depends on a specific version of The Sleuth Kit. You need the Java libr - Run Autopsy % ./autopsy +* Troubleshooting * + +- If you see something like "Cannot create case: javafx/scene/paint/Color" it is an indication that Java FX + is not being found. + Confirm that the file $JAVA_HOME/jre/lib/ext/jfxrt.jar exists. If it does not exist, return to the Java + setup steps above. +- If you see something like "An illegal reflective access operation has occurred" it is an indication that + the wrong version of Java is being used to run Autopsy. + Check the version of Java reported in the ~/.autopsy/dev/var/log/messages.log file. It should contain lines that looks like: + Java; VM; Vendor = 1.8.0_232; OpenJDK 64-Bit Server V 25.232-b10; BellSoft + Runtime = OpenJDK Runtime Environment 1.8.0_232-BellSoft-b10 + Java Home = /usr/lib/jvm/bellsoft-java8-amd64/jre + + If your messages.log file indicates that Java 8 is not being used: + (a) confirm that you have a version of Java 8 installed and + (b) confirm that your JAVA_HOME environment variable is set correctly: + % echo $JAVA_HOME + * Limitations (Updated May 2018) * - Timeline does not work on OS X - Video thumbnails are not generated (need to get a consistent version of OpenCV)