Merge remote-tracking branch 'upstream/develop' into 5773-collect-attachments-for-ShareIt

This commit is contained in:
Mark McKinnon 2019-11-19 11:41:39 -05:00
commit 743b955a1d
56 changed files with 4494 additions and 353 deletions

View File

@ -2300,7 +2300,6 @@ public class Case {
} else { } else {
throw new CaseActionException(Bundle.Case_open_exception_multiUserCaseNotEnabled()); throw new CaseActionException(Bundle.Case_open_exception_multiUserCaseNotEnabled());
} }
caseDb.registerForEvents(sleuthkitEventListener);
} catch (TskUnsupportedSchemaVersionException ex) { } catch (TskUnsupportedSchemaVersionException ex) {
throw new CaseActionException(Bundle.Case_exceptionMessage_unsupportedSchemaVersionMessage(ex.getLocalizedMessage()), ex); throw new CaseActionException(Bundle.Case_exceptionMessage_unsupportedSchemaVersionMessage(ex.getLocalizedMessage()), ex);
} catch (UserPreferencesException ex) { } catch (UserPreferencesException ex) {
@ -2321,6 +2320,12 @@ public class Case {
private void openCaseLevelServices(ProgressIndicator progressIndicator) { private void openCaseLevelServices(ProgressIndicator progressIndicator) {
progressIndicator.progress(Bundle.Case_progressMessage_openingCaseLevelServices()); progressIndicator.progress(Bundle.Case_progressMessage_openingCaseLevelServices());
this.caseServices = new Services(caseDb); 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);
} }
/** /**

View File

@ -18,10 +18,13 @@
*/ */
package org.sleuthkit.autopsy.communications.relationships; package org.sleuthkit.autopsy.communications.relationships;
import com.google.gson.Gson;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.function.Consumer;
import java.util.logging.Level; import java.util.logging.Level;
import org.openide.nodes.Children; import org.openide.nodes.Children;
import org.openide.nodes.Node; import org.openide.nodes.Node;
@ -32,15 +35,18 @@ import org.sleuthkit.autopsy.datamodel.AbstractAbstractFileNode;
import org.sleuthkit.autopsy.datamodel.FileNode; import org.sleuthkit.autopsy.datamodel.FileNode;
import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardArtifact;
import org.sleuthkit.datamodel.BlackboardAttribute;
import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskCoreException;
import org.sleuthkit.datamodel.blackboardutils.FileAttachment;
import org.sleuthkit.datamodel.blackboardutils.MessageAttachments;
/** /**
* Factory for creating thumbnail children nodes. * Factory for creating thumbnail children nodes.
*/ */
final class AttachmentsChildren extends Children.Keys<AbstractFile> { final class AttachmentThumbnailsChildren extends Children.Keys<AbstractFile> {
private static final Logger logger = Logger.getLogger(AttachmentsChildren.class.getName()); private static final Logger LOGGER = Logger.getLogger(AttachmentThumbnailsChildren.class.getName());
private final Set<BlackboardArtifact> artifacts; private final Set<BlackboardArtifact> artifacts;
@ -51,17 +57,16 @@ final class AttachmentsChildren extends Children.Keys<AbstractFile> {
* The thumbnails will be initialls sorted by size, then name so that they * The thumbnails will be initialls sorted by size, then name so that they
* appear sorted by size by default. * appear sorted by size by default.
*/ */
AttachmentsChildren(Set<BlackboardArtifact> artifacts) { AttachmentThumbnailsChildren(Set<BlackboardArtifact> artifacts) {
super(false); super(false);
this.artifacts = artifacts; this.artifacts = artifacts;
} }
@Override @Override
protected Node[] createNodes(AbstractFile t) { protected Node[] createNodes(AbstractFile t) {
return new Node[]{new AttachementNode(t)}; return new Node[]{new AttachementThumbnailNode(t)};
} }
@Override @Override
@ -77,15 +82,36 @@ final class AttachmentsChildren extends Children.Keys<AbstractFile> {
return result; return result;
}); });
artifacts.forEach((bba) -> { artifacts.forEach(new Consumer<BlackboardArtifact>() {
try { @Override
for (Content childContent : bba.getChildren()) { public void accept(BlackboardArtifact bba) {
if (childContent instanceof AbstractFile) { try {
thumbnails.add((AbstractFile) childContent); // 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<FileAttachment> 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<AbstractFile> {
/** /**
* A node for representing a thumbnail. * 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); super(file, false);
} }

View File

@ -36,6 +36,7 @@ import static org.sleuthkit.datamodel.BlackboardAttribute.TSK_BLACKBOARD_ATTRIBU
import org.sleuthkit.datamodel.TimeUtilities; import org.sleuthkit.datamodel.TimeUtilities;
import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskCoreException;
import org.sleuthkit.autopsy.communications.Utils; import org.sleuthkit.autopsy.communications.Utils;
import org.sleuthkit.autopsy.coreutils.PhoneNumUtil;
import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.Content;
@ -70,7 +71,7 @@ final class ContactNode extends BlackboardArtifactNode {
@Override @Override
protected Sheet createSheet() { protected Sheet createSheet() {
Sheet sheet = new Sheet(); Sheet sheet = new Sheet();
final BlackboardArtifact artifact = getArtifact(); final BlackboardArtifact artifact = getArtifact();
BlackboardArtifact.ARTIFACT_TYPE fromID = BlackboardArtifact.ARTIFACT_TYPE.fromID(artifact.getArtifactTypeID()); BlackboardArtifact.ARTIFACT_TYPE fromID = BlackboardArtifact.ARTIFACT_TYPE.fromID(artifact.getArtifactTypeID());
if (fromID != TSK_CONTACT) { if (fromID != TSK_CONTACT) {
@ -103,26 +104,26 @@ final class ContactNode extends BlackboardArtifactNode {
otherList.add(bba); otherList.add(bba);
} }
} }
addPropertiesToSheet(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME.getLabel(), addPropertiesToSheet(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME.getLabel(),
sheetSet, nameList); sheetSet, nameList);
addPropertiesToSheet(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER.getLabel(), addPropertiesToSheet(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER.getLabel(),
sheetSet, phoneNumList); sheetSet, phoneNumList);
addPropertiesToSheet(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_EMAIL.getLabel(), addPropertiesToSheet(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_EMAIL.getLabel(),
sheetSet, emailList); sheetSet, emailList);
for (BlackboardAttribute bba : otherList) { for (BlackboardAttribute bba : otherList) {
sheetSet.put(new NodeProperty<>(bba.getAttributeType().getTypeName(), bba.getAttributeType().getDisplayName(), "", bba.getDisplayString())); sheetSet.put(new NodeProperty<>(bba.getAttributeType().getTypeName(), bba.getAttributeType().getDisplayName(), "", bba.getDisplayString()));
} }
List<Content> children = artifact.getChildren(); List<Content> children = artifact.getChildren();
if(children != null) { if (children != null) {
int count = 0; int count = 0;
String imageLabelPrefix = "Image"; String imageLabelPrefix = "Image";
for(Content child: children) { for (Content child : children) {
if(child instanceof AbstractFile) { if (child instanceof AbstractFile) {
String imageLabel = imageLabelPrefix; String imageLabel = imageLabelPrefix;
if(count > 0) { if (count > 0) {
imageLabel = imageLabelPrefix + "-" + count; imageLabel = imageLabelPrefix + "-" + count;
} }
sheetSet.put(new NodeProperty<>(imageLabel, imageLabel, imageLabel, child.getName())); sheetSet.put(new NodeProperty<>(imageLabel, imageLabel, imageLabel, child.getName()));
@ -136,14 +137,32 @@ final class ContactNode extends BlackboardArtifactNode {
return sheet; return sheet;
} }
private void addPropertiesToSheet(String propertyID, Sheet.Set sheetSet, List<BlackboardAttribute> attributeList) { private void addPropertiesToSheet(String propertyID, Sheet.Set sheetSet, List<BlackboardAttribute> attributeList) {
int count = 0; int count = 0;
for (BlackboardAttribute bba : attributeList) { for (BlackboardAttribute bba : attributeList) {
if (count++ > 0) { 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 { } 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() { public String getSourceName() {
return getDisplayName(); return getDisplayName();
} }
} }

View File

@ -129,7 +129,7 @@ final class MediaViewer extends JPanel implements RelationshipsViewer, ExplorerM
thumbnailViewer.resetComponent(); 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 @Override

View File

@ -18,6 +18,7 @@
*/ */
package org.sleuthkit.autopsy.communications.relationships; package org.sleuthkit.autopsy.communications.relationships;
import com.google.gson.Gson;
import java.util.logging.Level; import java.util.logging.Level;
import javax.swing.Action; import javax.swing.Action;
import org.apache.commons.lang3.StringUtils; 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 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_EMAIL_MSG;
import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_MESSAGE; 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 * 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(), "", sheetSet.put(new NodeProperty<>("Subject", Bundle.MessageNode_Node_Property_Subject(), "",
getAttributeDisplayString(artifact, TSK_SUBJECT))); //NON-NLS getAttributeDisplayString(artifact, TSK_SUBJECT))); //NON-NLS
try { 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) { } catch (TskCoreException ex) {
logger.log(Level.WARNING, "Error loading attachment count for " + artifact, ex); //NON-NLS logger.log(Level.WARNING, "Error loading attachment count for " + artifact, ex); //NON-NLS
} }
@ -144,4 +147,21 @@ class MessageNode extends BlackboardArtifactNode {
public Action getPreferredAction() { public Action getPreferredAction() {
return preferredAction; 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;
}
} }

View File

@ -18,15 +18,17 @@
*/ */
package org.sleuthkit.autopsy.contentviewers; package org.sleuthkit.autopsy.contentviewers;
import org.sleuthkit.autopsy.datamodel.AttachmentNode;
import com.google.gson.Gson;
import java.awt.Color; import java.awt.Color;
import java.awt.Component; import java.awt.Component;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Collectors;
import javax.swing.text.JTextComponent; import javax.swing.text.JTextComponent;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
@ -35,15 +37,12 @@ import org.openide.explorer.ExplorerManager;
import org.openide.nodes.AbstractNode; import org.openide.nodes.AbstractNode;
import org.openide.nodes.Children; import org.openide.nodes.Children;
import org.openide.nodes.Node; import org.openide.nodes.Node;
import org.openide.nodes.Sheet;
import org.openide.util.NbBundle; import org.openide.util.NbBundle;
import org.openide.util.lookup.ServiceProvider; import org.openide.util.lookup.ServiceProvider;
import org.sleuthkit.autopsy.corecomponentinterfaces.DataContentViewer; import org.sleuthkit.autopsy.corecomponentinterfaces.DataContentViewer;
import org.sleuthkit.autopsy.corecomponents.DataResultPanel; import org.sleuthkit.autopsy.corecomponents.DataResultPanel;
import org.sleuthkit.autopsy.corecomponents.TableFilterNode; import org.sleuthkit.autopsy.corecomponents.TableFilterNode;
import org.sleuthkit.autopsy.coreutils.Logger; 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.DataResultFilterNode;
import org.sleuthkit.autopsy.directorytree.NewWindowViewAction; import org.sleuthkit.autopsy.directorytree.NewWindowViewAction;
import org.sleuthkit.datamodel.AbstractFile; 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_PHONE_NUMBER_TO;
import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SUBJECT; import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SUBJECT;
import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_TEXT; import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_TEXT;
import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.TskCoreException; 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 * Shows SMS/MMS/EMail messages
@ -541,11 +545,34 @@ public class MessageContentViewer extends javax.swing.JPanel implements DataCont
} }
private void configureAttachments() throws TskCoreException { private void configureAttachments() throws TskCoreException {
//TODO: Replace this with code to get the actual attachements!
final Set<AbstractFile> attachments = artifact.getChildren().stream() final Set<Attachment> attachments;
.filter(AbstractFile.class::isInstance)
.map(AbstractFile.class::cast) // Attachments are specified in an attribute TSK_ATTACHMENTS as JSON attribute
.collect(Collectors.toSet()); 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<FileAttachment> fileAttachments = msgAttachments.getFileAttachments();
for (FileAttachment fileAttachment: fileAttachments) {
attachments.add(fileAttachment);
}
Collection<URLAttachment> 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(); final int numberOfAttachments = attachments.size();
msgbodyTabbedPane.setEnabledAt(ATTM_TAB_INDEX, numberOfAttachments > 0); msgbodyTabbedPane.setEnabledAt(ATTM_TAB_INDEX, numberOfAttachments > 0);
@ -633,16 +660,20 @@ public class MessageContentViewer extends javax.swing.JPanel implements DataCont
return doc.html(); return doc.html();
} }
private static class AttachmentsChildren extends Children.Keys<AbstractFile> {
/**
* Creates child nodes for message attachments.
*/
private static class AttachmentsChildren extends Children.Keys<Attachment> {
private final Set<AbstractFile> attachments; private final Set<Attachment> attachments;
AttachmentsChildren(Set<AbstractFile> attachments) { AttachmentsChildren(Set<Attachment> attachments) {
this.attachments = attachments; this.attachments = attachments;
} }
@Override @Override
protected Node[] createNodes(AbstractFile t) { protected Node[] createNodes(Attachment t) {
return new Node[]{new AttachmentNode(t)}; return new Node[]{new AttachmentNode(t)};
} }
@ -652,40 +683,4 @@ public class MessageContentViewer extends javax.swing.JPanel implements DataCont
setKeys(attachments); 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<String> 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;
}
}
} }

View File

@ -0,0 +1,207 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2017-2019 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.datamodel;
import java.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<Action> 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<AbstractFile> 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> T accept(DisplayableItemNodeVisitor<T> 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");
}
}
}

View File

@ -43,6 +43,11 @@ ArtifactStringContent.attrsTableHeader.type=Type
ArtifactStringContent.attrsTableHeader.value=Value ArtifactStringContent.attrsTableHeader.value=Value
ArtifactStringContent.failedToGetAttributes.message=Failed to get some or all attributes from case database 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 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} BaseChildFactory.NoSuchEventBusException.message=No event bus for node: {0}
BlackboardArtifactNode.createSheet.artifactDetails.displayName=Result Details BlackboardArtifactNode.createSheet.artifactDetails.displayName=Result Details
BlackboardArtifactNode.createSheet.artifactDetails.name=Result Details BlackboardArtifactNode.createSheet.artifactDetails.name=Result Details

View File

@ -186,6 +186,11 @@ public interface DisplayableItemNodeVisitor<T> {
T visit(InterestingHits.InterestingItemTypeNode aThis); T visit(InterestingHits.InterestingItemTypeNode aThis);
/*
* Attachments
*/
T visit(AttachmentNode node);
/** /**
* Visitor with an implementable default behavior for all types. Override * Visitor with an implementable default behavior for all types. Override
* specific visit types to not use the default behavior. * specific visit types to not use the default behavior.
@ -522,5 +527,11 @@ public interface DisplayableItemNodeVisitor<T> {
public T visit(Accounts.DefaultAccountTypeNode node) { public T visit(Accounts.DefaultAccountTypeNode node) {
return defaultVisit(node); return defaultVisit(node);
} }
@Override
public T visit(AttachmentNode node) {
return defaultVisit(node);
}
} }
} }

View File

@ -0,0 +1,164 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2019 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.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<BlackboardAttribute> 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<BlackboardAttribute> attributes, Content parent) throws TskCoreException;
}

View File

@ -0,0 +1,4 @@
XRYDataSourceProcessorConfigPanel.fileBrowserButton.text=Browse
XRYDataSourceProcessorConfigPanel.filePathTextField.text=
XRYDataSourceProcessorConfigPanel.xrySelectFolderLabel.text=Select an XRY Folder
XRYDataSourceProcessorConfigPanel.errorLabel.text=

View File

@ -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=

View File

@ -0,0 +1,175 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2019 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.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<String> XRY_KEYS = new HashSet<String>() {
{
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<String> XRY_NAMESPACES = new HashSet<String>() {
{
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<BlackboardAttribute> 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();
}
}

View File

@ -0,0 +1,74 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2019 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.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<String> XRY_KEYS = new HashSet<String>() {{
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<BlackboardAttribute> attributes, Content parent) throws TskCoreException {
BlackboardArtifact artifact = parent.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_CONTACT);
artifact.addAttributes(attributes);
}
}

View File

@ -0,0 +1,247 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2019 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.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<LocalFilesDataSource, Void> {
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<Path> nonXRYFiles = xryFolder.getNonXRYFiles();
List<String> 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()));
}
}
}
}

View File

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Form version="1.5" maxVersion="1.9" type="org.netbeans.modules.form.forminfo.JPanelFormInfo">
<AuxValues>
<AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="1"/>
<AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/>
<AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/>
<AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="true"/>
<AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="true"/>
<AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/>
<AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/>
<AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/>
<AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/>
</AuxValues>
<Layout>
<DimensionLayout dim="0">
<Group type="103" groupAlignment="0" attributes="0">
<Group type="102" alignment="0" attributes="0">
<EmptySpace max="-2" attributes="0"/>
<Group type="103" groupAlignment="0" attributes="0">
<Component id="errorLabel" max="32767" attributes="0"/>
<Component id="xrySelectFolderLabel" pref="380" max="32767" attributes="0"/>
<Group type="102" attributes="0">
<Component id="filePathTextField" max="32767" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Component id="fileBrowserButton" min="-2" max="-2" attributes="0"/>
</Group>
</Group>
<EmptySpace max="-2" attributes="0"/>
</Group>
</Group>
</DimensionLayout>
<DimensionLayout dim="1">
<Group type="103" groupAlignment="0" attributes="0">
<Group type="102" alignment="0" attributes="0">
<EmptySpace max="-2" attributes="0"/>
<Component id="xrySelectFolderLabel" min="-2" max="-2" attributes="0"/>
<EmptySpace max="-2" attributes="0"/>
<Group type="103" groupAlignment="3" attributes="0">
<Component id="filePathTextField" alignment="3" min="-2" max="-2" attributes="0"/>
<Component id="fileBrowserButton" alignment="3" min="-2" max="-2" attributes="0"/>
</Group>
<EmptySpace type="unrelated" max="-2" attributes="0"/>
<Component id="errorLabel" min="-2" max="-2" attributes="0"/>
<EmptySpace pref="235" max="32767" attributes="0"/>
</Group>
</Group>
</DimensionLayout>
</Layout>
<SubComponents>
<Component class="javax.swing.JTextField" name="filePathTextField">
<Properties>
<Property name="editable" type="boolean" value="false"/>
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/sleuthkit/autopsy/datasourceprocessors/xry/Bundle.properties" key="XRYDataSourceProcessorConfigPanel.filePathTextField.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</Property>
</Properties>
</Component>
<Component class="javax.swing.JButton" name="fileBrowserButton">
<Properties>
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/sleuthkit/autopsy/datasourceprocessors/xry/Bundle.properties" key="XRYDataSourceProcessorConfigPanel.fileBrowserButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</Property>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="fileBrowserButtonActionPerformed"/>
</Events>
</Component>
<Component class="javax.swing.JLabel" name="xrySelectFolderLabel">
<Properties>
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/sleuthkit/autopsy/datasourceprocessors/xry/Bundle.properties" key="XRYDataSourceProcessorConfigPanel.xrySelectFolderLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</Property>
</Properties>
</Component>
<Component class="javax.swing.JLabel" name="errorLabel">
<Properties>
<Property name="foreground" type="java.awt.Color" editor="org.netbeans.beaninfo.editors.ColorEditor">
<Color blue="0" green="0" red="ff" type="rgb"/>
</Property>
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/sleuthkit/autopsy/datasourceprocessors/xry/Bundle.properties" key="XRYDataSourceProcessorConfigPanel.errorLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</Property>
</Properties>
</Component>
</SubComponents>
</Form>

View File

@ -0,0 +1,178 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2019 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.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")
// <editor-fold defaultstate="collapsed" desc="Generated Code">//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))
);
}// </editor-fold>//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
}

View File

@ -0,0 +1,191 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2019 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.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<String, BlackboardAttribute.ATTRIBUTE_TYPE> KEY_TO_TYPE
= new HashMap<String, BlackboardAttribute.ATTRIBUTE_TYPE>() {
{
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<BlackboardAttribute> 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);
}
}

View File

@ -0,0 +1,46 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2019 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.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;
}

View File

@ -0,0 +1,80 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2019 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.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() {
}
}

View File

@ -45,36 +45,43 @@ import org.apache.commons.io.FilenameUtils;
* From * From
* Tel: 12345678 * Tel: 12345678
*/ */
public final class XRYFileReader implements AutoCloseable { final class XRYFileReader implements AutoCloseable {
private static final Logger logger = Logger.getLogger(XRYFileReader.class.getName()); private static final Logger logger = Logger.getLogger(XRYFileReader.class.getName());
//Assume UTF_16LE //Assume UTF_16LE
private static final Charset CHARSET = StandardCharsets.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. //Assume all XRY reports have the type on the 3rd line.
private static final int LINE_WITH_REPORT_TYPE = 3; private static final int LINE_WITH_REPORT_TYPE = 3;
//Assume all headers are 5 lines in length. //Assume all headers are 5 lines in length.
private static final int HEADER_LENGTH_IN_LINES = 5; 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. //Underlying reader for the xry file.
private final BufferedReader reader; 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 * Creates an XRYFileReader. As part of construction, the XRY file is opened
* and the reader is advanced past the header. This leaves the reader * and the reader is advanced past the header. This leaves the reader
* positioned at the start of the first XRY entity. * 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 * @param xryFile XRY file to read. It is assumed that the caller has read
* access to the path. * access to the path.
@ -82,6 +89,7 @@ public final class XRYFileReader implements AutoCloseable {
*/ */
public XRYFileReader(Path xryFile) throws IOException { public XRYFileReader(Path xryFile) throws IOException {
reader = Files.newBufferedReader(xryFile, CHARSET); reader = Files.newBufferedReader(xryFile, CHARSET);
xryFilePath = xryFile;
//Advance the reader to the start of the first XRY entity. //Advance the reader to the start of the first XRY entity.
for (int i = 0; i < HEADER_LENGTH_IN_LINES; i++) { for (int i = 0; i < HEADER_LENGTH_IN_LINES; i++) {
@ -91,6 +99,35 @@ public final class XRYFileReader implements AutoCloseable {
xryEntity = new StringBuilder(); 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<String> 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 * Advances the reader until a valid XRY entity is detected or EOF is
* reached. * reached.
@ -113,7 +150,7 @@ public final class XRYFileReader implements AutoCloseable {
return true; return true;
} }
} else { } 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. * 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. * @return A non-empty XRY entity.
* @throws IOException if an I/O error occurs. * @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. * Closes any file handles this reader may have open.
* *

View File

@ -24,18 +24,76 @@ import java.nio.file.Files;
import java.nio.file.LinkOption; import java.nio.file.LinkOption;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
import java.util.List;
import java.util.stream.Stream; import java.util.stream.Stream;
/** /**
* Extracts XRY files and (optionally) non-XRY files from a XRY (Report) folder. * 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 //Depth that will contain XRY files. All XRY files will be immediate
//children of their parent folder. //children of their parent folder.
private static final int XRY_FILES_DEPTH = 1; 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<Path> getNonXRYFiles() throws IOException {
try (Stream<Path> allFiles = Files.walk(xryFolderPath, XRY_FILES_DEPTH)) {
List<Path> otherFiles = new ArrayList<>();
Iterator<Path> 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<XRYFileReader> getXRYFileReaders() throws IOException {
try (Stream<Path> allFiles = Files.walk(xryFolderPath, XRY_FILES_DEPTH)) {
List<XRYFileReader> fileReaders = new ArrayList<>();
Iterator<Path> 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 * 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. * 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. * @return Indicates whether the Path is an XRY report.
* *
* @throws IOException Error occurred during File I/O. * @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. * the files.
*/ */
public static boolean isXRYFolder(Path folder) throws IOException { public static boolean isXRYFolder(Path folder) throws IOException {

View File

@ -0,0 +1,507 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2019 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.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<String> XRY_KEYS = new HashSet<String>() {
{
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<String> XRY_NAMESPACES = new HashSet<String>() {
{
add("to");
add("from");
add("participant");
}
};
//All known meta keys.
private static final Set<String> XRY_META_KEYS = new HashSet<String>() {
{
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<Integer> 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<BlackboardAttribute> 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();
}
}

View File

@ -0,0 +1,79 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2019 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.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<XRYFileReader> 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() {
}
}

View File

@ -0,0 +1,66 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2019 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.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<String, BlackboardAttribute.ATTRIBUTE_TYPE> KEY_TO_TYPE
= new HashMap<String, BlackboardAttribute.ATTRIBUTE_TYPE>() {
{
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<BlackboardAttribute> attributes, Content parent) throws TskCoreException {
BlackboardArtifact artifact = parent.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_BOOKMARK);
artifact.addAttributes(attributes);
}
}

View File

@ -7,3 +7,13 @@ RefreshPanel.closeButton.text=
MapPanel.cordLabel.text= MapPanel.cordLabel.text=
WaypointDetailPanel.closeButton.text= WaypointDetailPanel.closeButton.text=
WaypointDetailPanel.imageLabel.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

View File

@ -1,8 +1,17 @@
CTL_OpenGeolocation=Geolocation CTL_OpenGeolocation=Geolocation
CTL_GeolocationTopComponentAction=GeolocationTopComponent CTL_GeolocationTopComponentAction=GeolocationTopComponent
CTL_GeolocationTopComponent=Geolocation 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_initilzation_error=An error occurred during waypoint initilization. Geolocation data maybe incomplete.
GLTopComponent_name=Geolocation GLTopComponent_name=Geolocation
HidingPane_default_title=Filters
MayWaypoint_ExternalViewer_label=Open in ExternalViewer MayWaypoint_ExternalViewer_label=Open in ExternalViewer
OpenGeolocationAction_displayName=Geolocation OpenGeolocationAction_displayName=Geolocation
OpenGeolocationAction_name=Geolocation OpenGeolocationAction_name=Geolocation
@ -12,4 +21,14 @@ RefreshPanel.closeButton.text=
MapPanel.cordLabel.text= MapPanel.cordLabel.text=
WaypointDetailPanel.closeButton.text= WaypointDetailPanel.closeButton.text=
WaypointDetailPanel.imageLabel.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) WaypointExtractAction_label=Extract Files(s)

View File

@ -0,0 +1,109 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2019 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.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<T extends CheckBoxJList.CheckboxListItem> extends JList<T> {
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<CheckBoxJList.CheckboxListItem> {
private static final long serialVersionUID = 1L;
@Override
public Component getListCellRendererComponent(
JList<? extends CheckBoxJList.CheckboxListItem> list, CheckBoxJList.CheckboxListItem value, int index,
boolean isSelected, boolean cellHasFocus) {
setBackground(list.getBackground());
setSelected(value.isChecked());
setText(value.getDisplayName());
return this;
}
}
}

View File

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Form version="1.5" maxVersion="1.9" type="org.netbeans.modules.form.forminfo.JPanelFormInfo">
<AuxValues>
<AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="1"/>
<AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/>
<AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/>
<AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="true"/>
<AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="true"/>
<AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/>
<AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/>
<AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/>
<AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/>
<AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,1,44,0,0,1,-112"/>
</AuxValues>
<Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/>
<SubComponents>
<Component class="javax.swing.JLabel" name="titleLabel">
<Properties>
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/sleuthkit/autopsy/geolocation/Bundle.properties" key="CheckBoxListPanel.titleLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</Property>
</Properties>
<Constraints>
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
<GridBagConstraints gridX="0" gridY="0" gridWidth="3" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="18" weightX="0.0" weightY="0.0"/>
</Constraint>
</Constraints>
</Component>
<Component class="javax.swing.JButton" name="uncheckButton">
<Properties>
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/sleuthkit/autopsy/geolocation/Bundle.properties" key="CheckBoxListPanel.uncheckButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</Property>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="uncheckButtonActionPerformed"/>
</Events>
<AuxValues>
<AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/>
<AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/>
</AuxValues>
<Constraints>
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
<GridBagConstraints gridX="1" gridY="2" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="9" anchor="12" weightX="1.0" weightY="0.0"/>
</Constraint>
</Constraints>
</Component>
<Component class="javax.swing.JButton" name="checkButton">
<Properties>
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/sleuthkit/autopsy/geolocation/Bundle.properties" key="CheckBoxListPanel.checkButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</Property>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="checkButtonActionPerformed"/>
</Events>
<AuxValues>
<AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/>
<AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/>
</AuxValues>
<Constraints>
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
<GridBagConstraints gridX="2" gridY="2" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="12" weightX="0.0" weightY="0.0"/>
</Constraint>
</Constraints>
</Component>
<Container class="javax.swing.JScrollPane" name="scrollPane">
<Constraints>
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
<GridBagConstraints gridX="0" gridY="1" gridWidth="3" gridHeight="1" fill="1" ipadX="0" ipadY="0" insetsTop="5" insetsLeft="0" insetsBottom="9" insetsRight="0" anchor="10" weightX="1.0" weightY="1.0"/>
</Constraint>
</Constraints>
<Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/>
</Container>
</SubComponents>
</Form>

View File

@ -0,0 +1,239 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2019 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.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<T> extends javax.swing.JPanel {
private static final long serialVersionUID = 1L;
private final DefaultListModel<ObjectCheckBox<T>> model = new DefaultListModel<>();
private final CheckBoxJList<ObjectCheckBox<T>> 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<T> getSelectedElements() {
List<T> selectedElements = new ArrayList<>();
Enumeration<ObjectCheckBox<T>> elements = model.elements();
while (elements.hasMoreElements()) {
ObjectCheckBox<T> 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<ObjectCheckBox<T>> enumeration = model.elements();
while (enumeration.hasMoreElements()) {
ObjectCheckBox<T> 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")
// <editor-fold defaultstate="collapsed" desc="Generated Code">//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);
}// </editor-fold>//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 <T>
*/
final class ObjectCheckBox<T> 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;
}
}
}

View File

@ -0,0 +1,180 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Form version="1.4" maxVersion="1.9" type="org.netbeans.modules.form.forminfo.JPanelFormInfo">
<NonVisualComponents>
<Component class="javax.swing.ButtonGroup" name="buttonGroup">
<AuxValues>
<AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/>
<AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/>
</AuxValues>
</Component>
</NonVisualComponents>
<AuxValues>
<AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="1"/>
<AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/>
<AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/>
<AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="true"/>
<AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="true"/>
<AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/>
<AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/>
<AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/>
<AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/>
<AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,1,44,0,0,3,32"/>
</AuxValues>
<Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/>
<SubComponents>
<Container class="javax.swing.JPanel" name="waypointSettings">
<Properties>
<Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor">
<Border info="org.netbeans.modules.form.compat2.border.TitledBorderInfo">
<TitledBorder title="">
<ResourceString PropertyName="titleX" bundle="org/sleuthkit/autopsy/geolocation/Bundle.properties" key="GeoFilterPanel.waypointSettings.border.title" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</TitledBorder>
</Border>
</Property>
</Properties>
<AuxValues>
<AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/>
<AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/>
</AuxValues>
<Constraints>
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
<GridBagConstraints gridX="0" gridY="2" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="5" insetsLeft="15" insetsBottom="9" insetsRight="15" anchor="18" weightX="1.0" weightY="0.0"/>
</Constraint>
</Constraints>
<Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/>
<SubComponents>
<Component class="javax.swing.JRadioButton" name="allButton">
<Properties>
<Property name="buttonGroup" type="javax.swing.ButtonGroup" editor="org.netbeans.modules.form.RADComponent$ButtonGroupPropertyEditor">
<ComponentRef name="buttonGroup"/>
</Property>
<Property name="selected" type="boolean" value="true"/>
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/sleuthkit/autopsy/geolocation/Bundle.properties" key="GeoFilterPanel.allButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</Property>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="allButtonActionPerformed"/>
</Events>
<Constraints>
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
<GridBagConstraints gridX="0" gridY="0" gridWidth="4" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="18" weightX="1.0" weightY="0.0"/>
</Constraint>
</Constraints>
</Component>
<Component class="javax.swing.JRadioButton" name="mostRecentButton">
<Properties>
<Property name="buttonGroup" type="javax.swing.ButtonGroup" editor="org.netbeans.modules.form.RADComponent$ButtonGroupPropertyEditor">
<ComponentRef name="buttonGroup"/>
</Property>
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/sleuthkit/autopsy/geolocation/Bundle.properties" key="GeoFilterPanel.mostRecentButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</Property>
</Properties>
<Events>
<EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="mostRecentButtonActionPerformed"/>
</Events>
<Constraints>
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
<GridBagConstraints gridX="0" gridY="1" gridWidth="2" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="9" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="18" weightX="0.0" weightY="0.0"/>
</Constraint>
</Constraints>
</Component>
<Component class="javax.swing.JCheckBox" name="showWaypointsWOTSCheckBox">
<Properties>
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/sleuthkit/autopsy/geolocation/Bundle.properties" key="GeoFilterPanel.showWaypointsWOTSCheckBox.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</Property>
<Property name="enabled" type="boolean" value="false"/>
</Properties>
<Constraints>
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
<GridBagConstraints gridX="1" gridY="2" gridWidth="3" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="30" insetsBottom="0" insetsRight="0" anchor="18" weightX="0.0" weightY="0.0"/>
</Constraint>
</Constraints>
</Component>
<Component class="javax.swing.JSpinner" name="daysSpinner">
<Properties>
<Property name="enabled" type="boolean" value="false"/>
</Properties>
<AuxValues>
<AuxValue name="JavaCodeGenerator_CreateCodeCustom" type="java.lang.String" value="new javax.swing.JSpinner(numberModel)"/>
</AuxValues>
<Constraints>
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
<GridBagConstraints gridX="2" gridY="1" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="9" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="18" weightX="0.0" weightY="0.0"/>
</Constraint>
</Constraints>
</Component>
<Component class="javax.swing.JLabel" name="daysLabel">
<Properties>
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/sleuthkit/autopsy/geolocation/Bundle.properties" key="GeoFilterPanel.daysLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</Property>
</Properties>
<AuxValues>
<AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/>
<AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/>
</AuxValues>
<Constraints>
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
<GridBagConstraints gridX="3" gridY="1" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="9" insetsLeft="5" insetsBottom="0" insetsRight="0" anchor="17" weightX="1.0" weightY="0.0"/>
</Constraint>
</Constraints>
</Component>
</SubComponents>
</Container>
<Container class="javax.swing.JPanel" name="buttonPanel">
<AuxValues>
<AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/>
<AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/>
</AuxValues>
<Constraints>
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
<GridBagConstraints gridX="0" gridY="0" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="9" insetsLeft="15" insetsBottom="0" insetsRight="15" anchor="18" weightX="1.0" weightY="0.0"/>
</Constraint>
</Constraints>
<Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/>
<SubComponents>
<Component class="javax.swing.JButton" name="applyButton">
<Properties>
<Property name="icon" type="javax.swing.Icon" editor="org.netbeans.modules.form.editors2.IconEditor">
<Image iconType="3" name="/org/sleuthkit/autopsy/images/tick.png"/>
</Property>
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/sleuthkit/autopsy/geolocation/Bundle.properties" key="GeoFilterPanel.applyButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</Property>
</Properties>
<Constraints>
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
<GridBagConstraints gridX="-1" gridY="-1" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="12" weightX="1.0" weightY="0.0"/>
</Constraint>
</Constraints>
</Component>
</SubComponents>
</Container>
<Component class="javax.swing.JLabel" name="optionsLabel">
<Properties>
<Property name="icon" type="javax.swing.Icon" editor="org.netbeans.modules.form.editors2.IconEditor">
<Image iconType="3" name="/org/sleuthkit/autopsy/images/blueGeo16.png"/>
</Property>
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/sleuthkit/autopsy/geolocation/Bundle.properties" key="GeoFilterPanel.optionsLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
</Property>
</Properties>
<AuxValues>
<AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/>
<AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/>
</AuxValues>
<Constraints>
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
<GridBagConstraints gridX="0" gridY="1" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="15" insetsBottom="0" insetsRight="0" anchor="17" weightX="0.0" weightY="0.0"/>
</Constraint>
</Constraints>
</Component>
</SubComponents>
</Form>

View File

@ -0,0 +1,359 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2019 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.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<DataSource> 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<DataSource> 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")
// <editor-fold defaultstate="collapsed" desc="Generated Code">//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);
}// </editor-fold>//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<DataSource> 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<DataSource> 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<DataSource> getDataSources() {
return Collections.unmodifiableList(dataSources);
}
}
}

View File

@ -0,0 +1,46 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2019 Basis Technology Corp.
* contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.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);
}
}

View File

@ -11,6 +11,7 @@
<AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/>
<AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/>
<AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/>
<AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,1,44,0,0,1,-112"/>
</AuxValues> </AuxValues>
<Layout class="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout"/> <Layout class="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout"/>
@ -23,6 +24,17 @@
</Constraints> </Constraints>
<Layout class="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout"/> <Layout class="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout"/>
<SubComponents>
<Container class="org.sleuthkit.autopsy.geolocation.HidingPane" name="filterPane">
<Constraints>
<Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout" value="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout$BorderConstraintsDescription">
<BorderConstraints direction="Before"/>
</Constraint>
</Constraints>
<Layout class="org.netbeans.modules.form.compat2.layouts.support.JTabbedPaneSupportLayout"/>
</Container>
</SubComponents>
</Container> </Container>
</SubComponents> </SubComponents>
</Form> </Form>

View File

@ -25,9 +25,9 @@ import java.beans.PropertyChangeListener;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.logging.Level; 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.util.NbBundle.Messages;
import org.openide.windows.RetainLocation; import org.openide.windows.RetainLocation;
import org.openide.windows.TopComponent; import org.openide.windows.TopComponent;
@ -35,8 +35,12 @@ import org.openide.windows.WindowManager;
import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.Case;
import static org.sleuthkit.autopsy.casemodule.Case.Events.CURRENT_CASE; import static org.sleuthkit.autopsy.casemodule.Case.Events.CURRENT_CASE;
import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
import org.sleuthkit.autopsy.coreutils.ThreadConfined; 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 org.sleuthkit.autopsy.ingest.IngestManager;
import static org.sleuthkit.autopsy.ingest.IngestManager.IngestModuleEvent.DATA_ADDED; import static org.sleuthkit.autopsy.ingest.IngestManager.IngestModuleEvent.DATA_ADDED;
import org.sleuthkit.autopsy.ingest.ModuleDataEvent; import org.sleuthkit.autopsy.ingest.ModuleDataEvent;
@ -59,6 +63,7 @@ public final class GeolocationTopComponent extends TopComponent {
private static final Set<IngestManager.IngestModuleEvent> INGEST_MODULE_EVENTS_OF_INTEREST = EnumSet.of(DATA_ADDED); private static final Set<IngestManager.IngestModuleEvent> INGEST_MODULE_EVENTS_OF_INTEREST = EnumSet.of(DATA_ADDED);
private final PropertyChangeListener ingestListener; private final PropertyChangeListener ingestListener;
private final GeoFilterPanel geoFilterPanel;
final RefreshPanel refreshPanel = new RefreshPanel(); final RefreshPanel refreshPanel = new RefreshPanel();
@ -73,7 +78,7 @@ public final class GeolocationTopComponent extends TopComponent {
@ThreadConfined(type = ThreadConfined.ThreadType.AWT) @ThreadConfined(type = ThreadConfined.ThreadType.AWT)
public GeolocationTopComponent() { public GeolocationTopComponent() {
initComponents(); initComponents();
initWaypoints();
setName(Bundle.GLTopComponent_name()); setName(Bundle.GLTopComponent_name());
this.ingestListener = pce -> { this.ingestListener = pce -> {
@ -105,10 +110,19 @@ public final class GeolocationTopComponent extends TopComponent {
@Override @Override
public void actionPerformed(ActionEvent e) { public void actionPerformed(ActionEvent e) {
mapPanel.clearWaypoints(); mapPanel.clearWaypoints();
initWaypoints(); updateWaypoints();
showRefreshPanel(false); showRefreshPanel(false);
} }
}); });
geoFilterPanel = new GeoFilterPanel();
filterPane.setPanel(geoFilterPanel);
geoFilterPanel.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
updateWaypoints();
}
});
} }
@Override @Override
@ -118,7 +132,7 @@ public final class GeolocationTopComponent extends TopComponent {
Case.addEventTypeSubscriber(EnumSet.of(CURRENT_CASE), evt -> { Case.addEventTypeSubscriber(EnumSet.of(CURRENT_CASE), evt -> {
mapPanel.clearWaypoints(); mapPanel.clearWaypoints();
if (evt.getNewValue() != null) { if (evt.getNewValue() != null) {
initWaypoints(); updateWaypoints();
} }
}); });
} }
@ -134,6 +148,13 @@ public final class GeolocationTopComponent extends TopComponent {
super.componentOpened(); super.componentOpened();
WindowManager.getDefault().setTopComponentFloating(this, true); 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. * 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() { @Messages({
SwingWorker<List<MapWaypoint>, MapWaypoint> worker = new SwingWorker<List<MapWaypoint>, MapWaypoint>() { "GeoTopComponent_no_waypoints_returned_mgs=Applied filter failed to find waypoints that matched criteria.\nRevise filter options and try again.",
@Override "GeoTopComponent_no_waypoints_returned_Title=No Waypoints Found",
protected List<MapWaypoint> doInBackground() throws Exception { "GeoTopComponent_filter_exception_msg=Exception occured during waypoint filtering.",
Case currentCase = Case.getCurrentCaseThrows(); "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 SwingUtilities.invokeLater(new Runnable() {
protected void done() { public void run() {
if (isDone() && !isCancelled()) { Case currentCase = Case.getCurrentCase();
try { try {
List<MapWaypoint> waypoints = get(); WaypointBuilder.getAllWaypoints(currentCase.getSleuthkitCase(), filters.getDataSources(), filters.showAllWaypoints(), filters.getMostRecentNumDays(), filters.showWaypointsWithoutTimeStamp(), new WaypointFilterQueryCallBack() {
if (waypoints == null || waypoints.isEmpty()) { @Override
return; public void process(List<Waypoint> 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); });
} catch (GeoLocationDataException ex) {
// There might be a better way to decide how to center logger.log(Level.SEVERE, "Failed to filter waypoints.", ex);
// but for now just use the first way point. JOptionPane.showMessageDialog(GeolocationTopComponent.this,
mapPanel.setCenterLocation(waypoints.get(0)); Bundle.GeoTopComponent_filter_exception_Title(),
Bundle.GeoTopComponent_filter_exception_msg(),
} catch (ExecutionException ex) { JOptionPane.ERROR_MESSAGE);
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
}
} }
} }
}; });
worker.execute();
} }
/** /**
@ -199,13 +237,18 @@ public final class GeolocationTopComponent extends TopComponent {
private void initComponents() { private void initComponents() {
mapPanel = new org.sleuthkit.autopsy.geolocation.MapPanel(); mapPanel = new org.sleuthkit.autopsy.geolocation.MapPanel();
filterPane = new org.sleuthkit.autopsy.geolocation.HidingPane();
setLayout(new java.awt.BorderLayout()); setLayout(new java.awt.BorderLayout());
mapPanel.add(filterPane, java.awt.BorderLayout.LINE_START);
add(mapPanel, java.awt.BorderLayout.CENTER); add(mapPanel, java.awt.BorderLayout.CENTER);
}// </editor-fold>//GEN-END:initComponents }// </editor-fold>//GEN-END:initComponents
// Variables declaration - do not modify//GEN-BEGIN:variables // Variables declaration - do not modify//GEN-BEGIN:variables
private org.sleuthkit.autopsy.geolocation.HidingPane filterPane;
private org.sleuthkit.autopsy.geolocation.MapPanel mapPanel; private org.sleuthkit.autopsy.geolocation.MapPanel mapPanel;
// End of variables declaration//GEN-END:variables // End of variables declaration//GEN-END:variables
} }

View File

@ -0,0 +1,133 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2019 Basis Technology Corp.
* contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.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;
}
}
}

View File

@ -56,7 +56,7 @@ import org.sleuthkit.datamodel.TskCoreException;
/** /**
* The map panel. This panel contains the jxmapviewer MapViewer * 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()); 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 * Creates new form MapPanel
*/ */
MapPanel() { public MapPanel() {
initComponents(); initComponents();
initMap(); initMap();

View File

@ -49,6 +49,7 @@ import org.sleuthkit.autopsy.geolocation.datamodel.GeoLocationDataException;
import org.sleuthkit.autopsy.geolocation.datamodel.Route; import org.sleuthkit.autopsy.geolocation.datamodel.Route;
import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.autopsy.geolocation.datamodel.Waypoint; import org.sleuthkit.autopsy.geolocation.datamodel.Waypoint;
import org.sleuthkit.autopsy.geolocation.datamodel.WaypointBuilder;
import org.sleuthkit.autopsy.timeline.actions.ViewArtifactInTimelineAction; import org.sleuthkit.autopsy.timeline.actions.ViewArtifactInTimelineAction;
import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardArtifact;
@ -79,7 +80,7 @@ final class MapWaypoint extends KdTree.XYZPoint implements org.jxmapviewer.viewe
* @throws GeoLocationDataException * @throws GeoLocationDataException
*/ */
static List<MapWaypoint> getWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { static List<MapWaypoint> getWaypoints(SleuthkitCase skCase) throws GeoLocationDataException {
List<Waypoint> points = Waypoint.getAllWaypoints(skCase); List<Waypoint> points = WaypointBuilder.getAllWaypoints(skCase);
List<Route> routes = Route.getRoutes(skCase); List<Route> routes = Route.getRoutes(skCase);
for (Route route : routes) { for (Route route : routes) {
@ -94,6 +95,28 @@ final class MapWaypoint extends KdTree.XYZPoint implements org.jxmapviewer.viewe
return mapPoints; 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<MapWaypoint> getWaypoints(List<Waypoint> dmWaypoints) {
List<MapWaypoint> 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. * Returns a MapWaypoint without a reference to the datamodel waypoint.

View File

@ -42,7 +42,7 @@
<Component class="javax.swing.JButton" name="refreshButton"> <Component class="javax.swing.JButton" name="refreshButton">
<Properties> <Properties>
<Property name="icon" type="javax.swing.Icon" editor="org.netbeans.modules.form.editors2.IconEditor"> <Property name="icon" type="javax.swing.Icon" editor="org.netbeans.modules.form.editors2.IconEditor">
<Image iconType="3" name="/org/sleuthkit/autopsy/geolocation/images/arrow-circle-double-135.png"/> <Image iconType="3" name="/org/sleuthkit/autopsy/images/arrow-circle-double-135.png"/>
</Property> </Property>
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/sleuthkit/autopsy/geolocation/Bundle.properties" key="RefreshPanel.refreshButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/> <ResourceString bundle="org/sleuthkit/autopsy/geolocation/Bundle.properties" key="RefreshPanel.refreshButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
@ -63,7 +63,7 @@
<Color blue="0" green="0" red="0" type="rgb"/> <Color blue="0" green="0" red="0" type="rgb"/>
</Property> </Property>
<Property name="icon" type="javax.swing.Icon" editor="org.netbeans.modules.form.editors2.IconEditor"> <Property name="icon" type="javax.swing.Icon" editor="org.netbeans.modules.form.editors2.IconEditor">
<Image iconType="3" name="/org/sleuthkit/autopsy/geolocation/images/cross-script.png"/> <Image iconType="3" name="/org/sleuthkit/autopsy/images/close-icon.png"/>
</Property> </Property>
<Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
<ResourceString bundle="org/sleuthkit/autopsy/geolocation/Bundle.properties" key="RefreshPanel.closeButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/> <ResourceString bundle="org/sleuthkit/autopsy/geolocation/Bundle.properties" key="RefreshPanel.closeButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>

View File

@ -82,13 +82,13 @@ final class RefreshPanel extends JPanel {
gridBagConstraints.insets = new java.awt.Insets(15, 10, 15, 10); gridBagConstraints.insets = new java.awt.Insets(15, 10, 15, 10);
add(refreshLabel, gridBagConstraints); 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 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)); refreshButton.setMargin(new java.awt.Insets(2, 5, 2, 5));
add(refreshButton, new java.awt.GridBagConstraints()); add(refreshButton, new java.awt.GridBagConstraints());
closeButton.setBackground(new java.awt.Color(0, 0, 0)); 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 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.setMargin(new java.awt.Insets(0, 0, 0, 0));
closeButton.setOpaque(false); closeButton.setOpaque(false);

View File

@ -0,0 +1,124 @@
/*
*
* Autopsy Forensic Browser
*
* Copyright 2019 Basis Technology Corp.
* contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.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 &amp; 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 );
}
}

View File

@ -25,12 +25,9 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardArtifact;
import org.sleuthkit.datamodel.BlackboardAttribute; import org.sleuthkit.datamodel.BlackboardAttribute;
import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskCoreException;
/** /**
@ -68,8 +65,6 @@ public class Waypoint {
BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_END, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_END,
BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE_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. * Construct a waypoint with the given artifact.
* *
@ -262,186 +257,6 @@ public class Waypoint {
return attributeMap; 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<Waypoint> getAllWaypoints(SleuthkitCase skCase) throws GeoLocationDataException {
List<Waypoint> 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<Waypoint> getTrackpointWaypoints(SleuthkitCase skCase) throws GeoLocationDataException {
List<BlackboardArtifact> 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<Waypoint> 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<Waypoint> getEXIFWaypoints(SleuthkitCase skCase) throws GeoLocationDataException {
List<BlackboardArtifact> 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<Waypoint> 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<Waypoint> getSearchWaypoints(SleuthkitCase skCase) throws GeoLocationDataException {
List<BlackboardArtifact> 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<Waypoint> 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<Waypoint> getLastKnownWaypoints(SleuthkitCase skCase) throws GeoLocationDataException {
List<BlackboardArtifact> 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<Waypoint> 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<Waypoint> getBookmarkWaypoints(SleuthkitCase skCase) throws GeoLocationDataException {
List<BlackboardArtifact> 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<Waypoint> 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 * Get a list of Waypoint.Property objects for the given artifact. This list

View File

@ -0,0 +1,486 @@
/*
*
* Autopsy Forensic Browser
*
* Copyright 2019 Basis Technology Corp.
* contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.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<Waypoint> 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<Waypoint> getAllWaypoints(SleuthkitCase skCase) throws GeoLocationDataException {
List<Waypoint> 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<Waypoint> getTrackpointWaypoints(SleuthkitCase skCase) throws GeoLocationDataException {
List<BlackboardArtifact> 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<Waypoint> 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<Waypoint> getEXIFWaypoints(SleuthkitCase skCase) throws GeoLocationDataException {
List<BlackboardArtifact> 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<Waypoint> 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<Waypoint> getSearchWaypoints(SleuthkitCase skCase) throws GeoLocationDataException {
List<BlackboardArtifact> 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<Waypoint> 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<Waypoint> getLastKnownWaypoints(SleuthkitCase skCase) throws GeoLocationDataException {
List<BlackboardArtifact> 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<Waypoint> 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<Waypoint> getBookmarkWaypoints(SleuthkitCase skCase) throws GeoLocationDataException {
List<BlackboardArtifact> 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<Waypoint> 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<DataSource> 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<Waypoint> 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<DataSource> 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<DataSource> 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<DataSource> 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<Waypoint> getWaypointForArtifact(BlackboardArtifact artifact, BlackboardArtifact.ARTIFACT_TYPE type) throws GeoLocationDataException {
List<Waypoint> 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;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 623 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 741 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 753 B

View File

@ -47,6 +47,7 @@ import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
import org.sleuthkit.autopsy.geolocation.datamodel.GeoLocationDataException; import org.sleuthkit.autopsy.geolocation.datamodel.GeoLocationDataException;
import org.sleuthkit.autopsy.geolocation.datamodel.Waypoint; import org.sleuthkit.autopsy.geolocation.datamodel.Waypoint;
import org.sleuthkit.autopsy.geolocation.datamodel.Route; 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.ReportBranding;
import org.sleuthkit.autopsy.report.ReportProgressPanel; import org.sleuthkit.autopsy.report.ReportProgressPanel;
import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.AbstractFile;
@ -331,11 +332,11 @@ class KMLReport implements GeneralReportModule {
* @throws IOException * @throws IOException
*/ */
void addLocationsToReport(SleuthkitCase skCase, String baseReportDir) throws GeoLocationDataException, IOException { void addLocationsToReport(SleuthkitCase skCase, String baseReportDir) throws GeoLocationDataException, IOException {
addExifMetadataContent(Waypoint.getEXIFWaypoints(skCase), baseReportDir); addExifMetadataContent(WaypointBuilder.getEXIFWaypoints(skCase), baseReportDir);
addWaypoints(Waypoint.getBookmarkWaypoints(skCase), gpsBookmarksFolder, FeatureColor.BLUE, Bundle.Waypoint_Bookmark_Display_String()); addWaypoints(WaypointBuilder.getBookmarkWaypoints(skCase), gpsBookmarksFolder, FeatureColor.BLUE, Bundle.Waypoint_Bookmark_Display_String());
addWaypoints(Waypoint.getLastKnownWaypoints(skCase), gpsLastKnownLocationFolder, FeatureColor.PURPLE, Bundle.Waypoint_Last_Known_Display_String()); addWaypoints(WaypointBuilder.getLastKnownWaypoints(skCase), gpsLastKnownLocationFolder, FeatureColor.PURPLE, Bundle.Waypoint_Last_Known_Display_String());
addWaypoints(Waypoint.getSearchWaypoints(skCase), gpsSearchesFolder, FeatureColor.WHITE, Bundle.Waypoint_Search_Display_String()); addWaypoints(WaypointBuilder.getSearchWaypoints(skCase), gpsSearchesFolder, FeatureColor.WHITE, Bundle.Waypoint_Search_Display_String());
addWaypoints(Waypoint.getTrackpointWaypoints(skCase), gpsTrackpointsFolder, FeatureColor.WHITE, Bundle.Waypoint_Trackpoint_Display_String()); addWaypoints(WaypointBuilder.getTrackpointWaypoints(skCase), gpsTrackpointsFolder, FeatureColor.WHITE, Bundle.Waypoint_Trackpoint_Display_String());
} }
/** /**

View File

@ -25,6 +25,7 @@ import org.openide.util.NbBundle;
import org.openide.util.actions.SystemAction; import org.openide.util.actions.SystemAction;
import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
import org.sleuthkit.autopsy.coreutils.ModuleSettings;
import org.sleuthkit.autopsy.timeline.OpenTimelineAction; import org.sleuthkit.autopsy.timeline.OpenTimelineAction;
import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardArtifact;
import org.sleuthkit.datamodel.BlackboardAttribute; import org.sleuthkit.datamodel.BlackboardAttribute;
@ -43,6 +44,10 @@ public final class ViewArtifactInTimelineAction extends AbstractAction {
public ViewArtifactInTimelineAction(BlackboardArtifact artifact) { public ViewArtifactInTimelineAction(BlackboardArtifact artifact) {
super(Bundle.ViewArtifactInTimelineAction_displayName()); super(Bundle.ViewArtifactInTimelineAction_displayName());
this.artifact = artifact; this.artifact = artifact;
// If timeline functionality is not available this action is disabled.
if ("false".equals(ModuleSettings.getConfigSetting("timeline", "enable_timeline"))) {
setEnabled(false);
}
} }
@Override @Override

View File

@ -25,6 +25,7 @@ import org.openide.util.NbBundle;
import org.openide.util.actions.SystemAction; import org.openide.util.actions.SystemAction;
import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
import org.sleuthkit.autopsy.coreutils.ModuleSettings;
import org.sleuthkit.autopsy.timeline.OpenTimelineAction; import org.sleuthkit.autopsy.timeline.OpenTimelineAction;
import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskCoreException;
@ -54,6 +55,10 @@ public final class ViewFileInTimelineAction extends AbstractAction {
&& file.getAtime() <= 0)) { && file.getAtime() <= 0)) {
this.setEnabled(false); 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... "}) @NbBundle.Messages({"ViewFileInTimelineAction.viewFile.displayName=View File in Timeline... "})

View File

@ -49,7 +49,6 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Collectors;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.GuardedBy;
import javax.swing.SortOrder; import javax.swing.SortOrder;
@ -80,6 +79,7 @@ import org.sleuthkit.datamodel.TskData.DbType;
import org.sleuthkit.datamodel.TskDataException; import org.sleuthkit.datamodel.TskDataException;
import org.sleuthkit.datamodel.VersionNumber; import org.sleuthkit.datamodel.VersionNumber;
import org.sqlite.SQLiteJDBCLoader; import org.sqlite.SQLiteJDBCLoader;
import java.util.stream.Collectors;
/** /**
* Provides access to the drawables database and selected tables in the case * Provides access to the drawables database and selected tables in the case
@ -230,7 +230,7 @@ public final class DrawableDB {
dbWriteLock(); dbWriteLock();
try { try {
con = DriverManager.getConnection("jdbc:sqlite:" + dbPath.toString()); //NON-NLS con = DriverManager.getConnection("jdbc:sqlite:" + dbPath.toString()); //NON-NLS
if (!initializeDBSchema() || !upgradeDBSchema() || !prepareStatements() || !initializeStandardGroups() || !initializeImageList()) { if (!initializeDBSchema() || !upgradeDBSchema() || !prepareStatements() || !initializeStandardGroups() || !removeDeletedDataSources() || !initializeImageList()) {
close(); close();
throw new TskCoreException("Failed to initialize drawables database for Image Gallery use"); //NON-NLS 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<Long> 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<Long> 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 * * 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 * 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. * @param dataSourceID The object ID of the data source to delete.
* *
* @throws SQLException * @throws SQLException
* @throws TskCoreException * @throws TskCoreException
*/ */
public void deleteDataSource(long dataSourceID) throws SQLException, TskCoreException { public void deleteDataSource(long dataSourceID) throws SQLException, TskCoreException {

View File

@ -17,6 +17,11 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
""" """
import json
import traceback
import general
import ast
from java.io import File from java.io import File
from java.lang import Class from java.lang import Class
from java.lang import ClassNotFoundException 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.Blackboard import BlackboardException
from org.sleuthkit.datamodel import Account from org.sleuthkit.datamodel import Account
from org.sleuthkit.datamodel.blackboardutils import CommunicationArtifactsHelper 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 MessageReadStatus
from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import CommunicationDirection from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import CommunicationDirection
from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import CallMediaType from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import CallMediaType
import json
import traceback
import general
class FBMessengerAnalyzer(general.AndroidComponentAnalyzer): class FBMessengerAnalyzer(general.AndroidComponentAnalyzer):
@ -95,6 +99,7 @@ class FBMessengerAnalyzer(general.AndroidComponentAnalyzer):
* have no text, * have no text,
* admin_text_thread_rtc_event has the specific event * admin_text_thread_rtc_event has the specific event
"group-call-started", "group-call_ended" "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 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 thread_key column - identifies the message thread
--- A timestamp_ms column - date/time message was sent --- A timestamp_ms column - date/time message was sent
@ -210,6 +215,17 @@ class FBMessengerAnalyzer(general.AndroidComponentAnalyzer):
else: else:
direction = CommunicationDirection.INCOMING direction = CommunicationDirection.INCOMING
return direction 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 ## Analyzes messages
def analyzeMessages(self, threadsDb, threadsDBHelper): 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. ## The result set is processed to collect the multiple recipients for a given message.
sqlString = """ sqlString = """
SELECT msg_id, text, sender, timestamp_ms, msg_type, messages.thread_key as thread_key, 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 FROM messages
JOIN thread_participants ON messages.thread_key = thread_participants.thread_key JOIN thread_participants ON messages.thread_key = thread_participants.thread_key
JOIN thread_users ON thread_participants.user_key = thread_users.user_key JOIN thread_users ON thread_participants.user_key = thread_users.user_key
@ -241,6 +258,8 @@ class FBMessengerAnalyzer(general.AndroidComponentAnalyzer):
timeStamp = -1 timeStamp = -1
msgText = "" msgText = ""
threadId = "" threadId = ""
messageAttachments = None
currentCase = Case.getCurrentCaseThrows()
while messagesResultSet.next(): while messagesResultSet.next():
msgId = messagesResultSet.getString("msg_id") msgId = messagesResultSet.getString("msg_id")
@ -260,6 +279,10 @@ class FBMessengerAnalyzer(general.AndroidComponentAnalyzer):
msgText, msgText,
threadId) threadId)
if (messageAttachments is not None):
threadsDBHelper.addAttachments(messageArtifact, messageAttachments)
messageAttachments = None
oldMsgId = msgId oldMsgId = msgId
# New message - collect all attributes # New message - collect all attributes
@ -282,8 +305,42 @@ class FBMessengerAnalyzer(general.AndroidComponentAnalyzer):
if not msgText: if not msgText:
msgText = messagesResultSet.getString("snippet") 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") threadId = messagesResultSet.getString("thread_key")
else: # same msgId as last, just collect recipient from current row else: # same msgId as last, just collect recipient from current row

View File

@ -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 -- Linux: % sudo apt-get install testdisk
-- OS X: % brew install testdisk -- OS X: % brew install testdisk
- Install a Java 8 JRE and JavaFX 8 and set JAVA_HOME. - Install the BellSoft Java 8 JRE and JavaFX 8 distribution and set JAVA_HOME.
-- Linux: Any Java 8 version of OpenJDK/OpenJFX distribution should suffice. The following instructions use the Zulu Community distribution. * The BellSoft distribution bundles OpenJDK and OpenJFX. Other distributions we have tried either don't
1. Download a 64 bit Java 8 JRE for your specific platform from https://www.azul.com/downloads/zulu-community bundle OpenJFX (AdoptOpenJDK) or don't include all necessary binaries (Amazon Corretto).
2. Install the JRE. e.g. % sudo apt install ./zulu8.40.0.25-ca-jre8.0.222-linux_amd64.deb -- Linux:
3. Download a 64 bit Java 8 JavaFX for your specific platform from the same location. 1. Install BellSoft Java 8
- Note that you may need to select "Older Zulu versions" for FX to become available in the "Java Package" dropdown. % wget -q -O - https://download.bell-sw.com/pki/GPG-KEY-bellsoft | sudo apt-key add -
4. Extract the contents of the JavaFX archive into the folder where the JRE was installed. % echo "deb [arch=amd64] https://apt.bell-sw.com/ stable main" | sudo tee /etc/apt/sources.list.d/bellsoft.list
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 % 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 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. 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. -- OS X:
1. Install a 64 bit Java 8 JRE. 1. Install BellSoft Java 8.
% brew cask install adoptopenjdk8 % brew tap bell-sw/liberica
% brew cask install liberica-jdk8
2. Set JAVA_HOME environment variable to location of JRE installation. 2. Set JAVA_HOME environment variable to location of JRE installation.
e.g. add the following to ~/.bashrc e.g. add the following to ~/.bashrc
export JAVA_HOME=$(/usr/libexec/java_home -v 1.8) 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 * * 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. 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. - 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. - OS X: Install The Sleuth Kit from brew.
-- % brew install sleuthkit -- % brew install sleuthkit
@ -55,6 +63,24 @@ Autopsy depends on a specific version of The Sleuth Kit. You need the Java libr
- Run Autopsy - Run Autopsy
% ./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) * * Limitations (Updated May 2018) *
- Timeline does not work on OS X - Timeline does not work on OS X
- Video thumbnails are not generated (need to get a consistent version of OpenCV) - Video thumbnails are not generated (need to get a consistent version of OpenCV)