diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java index 99ec09d315..a66b6f5c71 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java @@ -2300,7 +2300,6 @@ public class Case { } else { throw new CaseActionException(Bundle.Case_open_exception_multiUserCaseNotEnabled()); } - caseDb.registerForEvents(sleuthkitEventListener); } catch (TskUnsupportedSchemaVersionException ex) { throw new CaseActionException(Bundle.Case_exceptionMessage_unsupportedSchemaVersionMessage(ex.getLocalizedMessage()), ex); } catch (UserPreferencesException ex) { @@ -2321,6 +2320,12 @@ public class Case { private void openCaseLevelServices(ProgressIndicator progressIndicator) { progressIndicator.progress(Bundle.Case_progressMessage_openingCaseLevelServices()); this.caseServices = new Services(caseDb); + /* + * RC Note: JM put this initialization here. I'm not sure why. However, + * my attempt to put it in the openCaseDatabase method seems to lead to + * intermittent unchecked exceptions concerning a missing subscriber. + */ + caseDb.registerForEvents(sleuthkitEventListener); } /** diff --git a/Core/src/org/sleuthkit/autopsy/core/UserPreferences.java b/Core/src/org/sleuthkit/autopsy/core/UserPreferences.java index 0d2bfa90d3..b65d0873fe 100644 --- a/Core/src/org/sleuthkit/autopsy/core/UserPreferences.java +++ b/Core/src/org/sleuthkit/autopsy/core/UserPreferences.java @@ -78,6 +78,9 @@ public final class UserPreferences { public static final String EXTERNAL_HEX_EDITOR_PATH = "ExternalHexEditorPath"; public static final String SOLR_MAX_JVM_SIZE = "SolrMaxJVMSize"; public static final String RESULTS_TABLE_PAGE_SIZE = "ResultsTablePageSize"; + private static final String GEO_TILE_OPTION = "GeolocationTileOption"; + private static final String GEO_OSM_TILE_ZIP_PATH = "GeolocationOsmZipPath"; + private static final String GEO_OSM_SERVER_ADDRESS = "GeolocationOsmServerAddress"; // Prevent instantiation. private UserPreferences() { @@ -534,4 +537,59 @@ public final class UserPreferences { public static String getExternalHexEditorPath() { return preferences.get(EXTERNAL_HEX_EDITOR_PATH, Paths.get("C:", "Program Files", "HxD", "HxD.exe").toString()); } + + /** + * Set the geolocation tile server option. + * + * @param option + */ + public static void setGeolocationTileOption(int option) { + preferences.putInt(GEO_TILE_OPTION, option); + } + + /** + * Retrieves the Geolocation tile option. If not found, the value will + * default to 0. + * @return + */ + public static int getGeolocationtTileOption() { + return preferences.getInt(GEO_TILE_OPTION, 0); + } + + /** + * Sets the path to the OSM tile zip file. + * + * @param absolutePath + */ + public static void setGeolocationOsmZipPath(String absolutePath) { + preferences.put(GEO_OSM_TILE_ZIP_PATH, absolutePath); + } + + /** + * Retrieves the path for the OSM tile zip file or returns empty string if + * none was found. + * + * @return Path to zip file + */ + public static String getGeolocationOsmZipPath() { + return preferences.get(GEO_OSM_TILE_ZIP_PATH, ""); + } + + /** + * Sets the address of the OSM tile server. + * + * @param address + */ + public static void setGeolocationOsmServerAddress(String address) { + preferences.put(GEO_OSM_SERVER_ADDRESS, address); + } + + /** + * Retrieves the address to the OSM server or null if one was not found. + * + * @return Address of OSM server + */ + public static String getGeolocationOsmServerAddress() { + return preferences.get(GEO_OSM_SERVER_ADDRESS, ""); + } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/ExtractedContent.java b/Core/src/org/sleuthkit/autopsy/datamodel/ExtractedContent.java index cefd59a041..e52e50c31c 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/ExtractedContent.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/ExtractedContent.java @@ -42,6 +42,7 @@ import org.sleuthkit.autopsy.ingest.ModuleDataEvent; import org.sleuthkit.datamodel.Blackboard; import org.sleuthkit.datamodel.BlackboardArtifact; import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_ACCOUNT; +import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_ASSOCIATED_OBJECT; import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_DATA_SOURCE_USAGE; import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_EMAIL_MSG; import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_GEN_INFO; @@ -238,6 +239,7 @@ public class ExtractedContent implements AutopsyVisitableItem { doNotShow.add(new BlackboardArtifact.Type(TSK_ACCOUNT)); doNotShow.add(new BlackboardArtifact.Type(TSK_DATA_SOURCE_USAGE)); doNotShow.add(new BlackboardArtifact.Type(TSK_DOWNLOAD_SOURCE)); + doNotShow.add(new BlackboardArtifact.Type(TSK_ASSOCIATED_OBJECT)); } private final PropertyChangeListener pcl = (PropertyChangeEvent evt) -> { diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/AbstractSingleKeyValueParser.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/AbstractSingleKeyValueParser.java new file mode 100755 index 0000000000..08dad04991 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/AbstractSingleKeyValueParser.java @@ -0,0 +1,164 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourceprocessors.xry; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Template parse method for reports that make blackboard attributes from a + * single key value pair. + * + * This parse implementation will create 1 artifact per XRY entity. + */ +abstract class AbstractSingleKeyValueParser implements XRYFileParser { + + private static final Logger logger = Logger.getLogger(AbstractSingleKeyValueParser.class.getName()); + + private static final char KEY_VALUE_DELIMITER = ':'; + + protected static final String PARSER_NAME = "XRY DSP"; + + @Override + public void parse(XRYFileReader reader, Content parent) throws IOException, TskCoreException { + Path reportPath = reader.getReportPath(); + logger.log(Level.INFO, String.format("[XRY DSP] Processing report at [ %s ]", reportPath.toString())); + + while (reader.hasNextEntity()) { + String xryEntity = reader.nextEntity(); + String[] xryLines = xryEntity.split("\n"); + + List attributes = new ArrayList<>(); + + //First line of the entity is the title. + if (xryLines.length > 0) { + logger.log(Level.INFO, String.format("[XRY DSP] Processing [ %s ]", xryLines[0])); + } + + String namespace = ""; + //Process each line, searching for a key value pair or a namespace. + //If neither are found, an error message is logged. + for (int i = 1; i < xryLines.length; i++) { + String xryLine = xryLines[i]; + + String candidateNamespace = xryLine.trim(); + //Check if the line is a namespace, which gives context to the keys + //that follow. + if (isNamespace(candidateNamespace)) { + namespace = candidateNamespace; + continue; + } + + //Find the XRY key on this line. Assume key is the value between + //the start of the line and the first delimiter. + int keyDelimiter = xryLine.indexOf(KEY_VALUE_DELIMITER); + if (keyDelimiter == -1) { + logger.log(Level.SEVERE, String.format("[XRY DSP] Expected a key value " + + "pair on this line (in brackets) [ %s ], but one was not detected." + + " Here is the previous line [ %s ]. What does this mean?", xryLine, xryLines[i - 1])); + continue; + } + String key = xryLine.substring(0, keyDelimiter).trim(); + String value = xryLine.substring(keyDelimiter + 1).trim(); + + if (!isKey(key)) { + logger.log(Level.SEVERE, String.format("[XRY DSP] The following key, " + + "value pair (in brackets, respectively) [ %s ], [ %s ] was not recognized. Discarding..." + + " Here is the previous line [ %s ] for context. What does this key mean?", key, value, xryLines[i - 1])); + continue; + } + + if (value.isEmpty()) { + logger.log(Level.SEVERE, String.format("[XRY DSP] The following key " + + "(in brackets) [ %s ] was recognized, but the value was empty. Discarding..." + + " Here is the previous line for context [ %s ]. What does this mean?", key, xryLines[i - 1])); + continue; + } + + BlackboardAttribute attribute = makeAttribute(namespace, key, value); + //Temporarily allowing null to be valid return type until a decision + //is made about how to handle keys we are choosing to ignore. + if (attribute != null) { + attributes.add(makeAttribute(namespace, key, value)); + } + } + + //Only create artifacts with non-empty attributes. + if (!attributes.isEmpty()) { + makeArtifact(attributes, parent); + } + } + } + + /** + * Determines if the key candidate is a known key. A key candidate is a + * string literal that begins a line and is terminated by a semi-colon. + * + * Ex: + * + * Call Type : Missed + * + * "Call Type" would be the key candidate that was extracted. + * + * @param key Key to test. These keys are trimmed of whitespace only. + * @return Indication if this key can be processed. + */ + abstract boolean isKey(String key); + + /** + * Determines if the namespace candidate is a known namespace. A namespace + * candidate is a string literal that makes up an entire line. + * + * Ex: + * + * To + * Tel : +1245325 + * + * "To" would be the candidate namespace that was extracted. + * + * @param nameSpace Namespace to test. Namespaces are trimmed of whitespace + * only. + * @return Indication if this namespace can be processed. + */ + abstract boolean isNamespace(String nameSpace); + + /** + * Creates an attribute from the extracted key value pair. + * + * @param nameSpace The namespace of this key value pair. + * It will have been verified with isNamespace, otherwise it will be empty. + * @param key The key that was verified with isKey. + * @param value The value associated with that key. + * @return + */ + abstract BlackboardAttribute makeAttribute(String nameSpace, String key, String value); + + /** + * Makes an artifact from the parsed attributes. + */ + abstract void makeArtifact(List attributes, Content parent) throws TskCoreException; + +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/Bundle.properties b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/Bundle.properties new file mode 100755 index 0000000000..05c5199962 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/Bundle.properties @@ -0,0 +1,4 @@ +XRYDataSourceProcessorConfigPanel.fileBrowserButton.text=Browse +XRYDataSourceProcessorConfigPanel.filePathTextField.text= +XRYDataSourceProcessorConfigPanel.xrySelectFolderLabel.text=Select an XRY Folder +XRYDataSourceProcessorConfigPanel.errorLabel.text= diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/Bundle.properties-MERGED new file mode 100755 index 0000000000..2084998a0a --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/Bundle.properties-MERGED @@ -0,0 +1,16 @@ +XRYDataSourceProcessor.childNotReadable=Top level path [ %s ] is not readable +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.noCurrentCase=No case is open. +XRYDataSourceProcessor.noPathSelected=Please select a XRY folder +XRYDataSourceProcessor.notAFolder=The selected path is not a folder +XRYDataSourceProcessor.notReadable=Selected path is not readable +XRYDataSourceProcessor.notXRYFolder=Selected folder did not contain any XRY files +XRYDataSourceProcessor.preppingFiles=Preparing to add files to the case database +XRYDataSourceProcessor.processingFiles=Processing all XRY files... +XRYDataSourceProcessor.unexpectedError=Internal error occurred while processing XRY report +XRYDataSourceProcessorConfigPanel.fileBrowserButton.text=Browse +XRYDataSourceProcessorConfigPanel.filePathTextField.text= +XRYDataSourceProcessorConfigPanel.xrySelectFolderLabel.text=Select an XRY Folder +XRYDataSourceProcessorConfigPanel.errorLabel.text= diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYCallsFileParser.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYCallsFileParser.java new file mode 100755 index 0000000000..596f25e7b1 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYCallsFileParser.java @@ -0,0 +1,175 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourceprocessors.xry; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Set; +import java.util.HashSet; +import java.util.List; +import java.util.logging.Level; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Parses XRY Calls files and creates artifacts. + */ +final class XRYCallsFileParser extends AbstractSingleKeyValueParser { + + private static final Logger logger = Logger.getLogger(XRYCallsFileParser.class.getName()); + + private static final DateTimeFormatter DATE_TIME_PARSER + = DateTimeFormatter.ofPattern("M/d/y h:m:s [a][ z]"); + + private static final String INCOMING = "Incoming"; + + //All known XRY keys for call reports. + private static final Set XRY_KEYS = new HashSet() { + { + add("tel"); + add("number"); + add("call type"); + add("name (matched)"); + add("time"); + add("duration"); + add("storage"); + add("index"); + } + }; + + //All known XRY namespaces for call reports. + private static final Set XRY_NAMESPACES = new HashSet() { + { + add("to"); + add("from"); + } + }; + + @Override + boolean isKey(String key) { + String normalizedKey = key.toLowerCase(); + return XRY_KEYS.contains(normalizedKey); + } + + @Override + boolean isNamespace(String nameSpace) { + String normalizedNamespace = nameSpace.toLowerCase(); + return XRY_NAMESPACES.contains(normalizedNamespace); + } + + @Override + BlackboardAttribute makeAttribute(String nameSpace, String key, String value) { + String normalizedKey = key.toLowerCase(); + String normalizedNamespace = nameSpace.toLowerCase(); + + switch (normalizedKey) { + case "time": + //Tranform value to epoch ms + try { + String dateTime = removeDateTimeLocale(value); + String normalizedDateTime = dateTime.trim(); + long dateTimeInEpoch = calculateSecondsSinceEpoch(normalizedDateTime); + return new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_START, PARSER_NAME, dateTimeInEpoch); + } catch (DateTimeParseException ex) { + logger.log(Level.SEVERE, String.format("[XRY DSP] Assumption about the date time " + + "formatting of call logs is not right. Here is the value [ %s ]", value), ex); + return null; + } + case "duration": + //Ignore for now. + return null; + case "storage": + //Ignore for now. + return null; + case "index": + //Ignore for now. + return null; + case "tel": + //Apply the namespace + if(normalizedNamespace.equals("from")) { + return new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_FROM, PARSER_NAME, value); + } else { + return new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_TO, PARSER_NAME, value); + } + case "call type": + String normalizedValue = value.toLowerCase(); + switch (normalizedValue) { + case "missed": + case "received": + return new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DIRECTION, PARSER_NAME, INCOMING); + case "dialed": + //Ignore for now. + return null; + case "last dialed": + //Ignore for now. + return null; + default: + logger.log(Level.SEVERE, String.format("Call type (in brackets) [ %s ] not recognized.", value)); + return null; + } + case "number": + return new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER, PARSER_NAME, value); + case "name (matched)": + return new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME, PARSER_NAME, value); + default: + throw new IllegalArgumentException(String.format("key [ %s ] was not recognized.", key)); + } + } + + @Override + void makeArtifact(List attributes, Content parent) throws TskCoreException { + BlackboardArtifact artifact = parent.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_CALLLOG); + artifact.addAttributes(attributes); + } + + /** + * Removes the locale from the date time value. + * + * Locale in this case being (Device) or (Network). + * + * @param dateTime XRY datetime value to be sanitized. + * @return A purer date time value. + */ + private String removeDateTimeLocale(String dateTime) { + int index = dateTime.indexOf('('); + if (index == -1) { + return dateTime; + } + + return dateTime.substring(0, index); + } + + /** + * Parses the date time value and calculates ms since epoch. The time zone is + * assumed to be UTC. + * + * @param dateTime + * @return + */ + private long calculateSecondsSinceEpoch(String dateTime) { + LocalDateTime localDateTime = LocalDateTime.parse(dateTime, DATE_TIME_PARSER); + //Assume dates have no offset. + return localDateTime.toInstant(ZoneOffset.UTC).getEpochSecond(); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYContactsFileParser.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYContactsFileParser.java new file mode 100755 index 0000000000..ec8fd40187 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYContactsFileParser.java @@ -0,0 +1,74 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourceprocessors.xry; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Parses XRY Contacts-Contacts files and creates artifacts. + */ +final class XRYContactsFileParser extends AbstractSingleKeyValueParser { + + //All of the known XRY keys for contacts. + private static final Set XRY_KEYS = new HashSet() {{ + add("name"); + add("tel"); + add("storage"); + }}; + + @Override + boolean isKey(String key) { + String normalizedKey = key.toLowerCase(); + return XRY_KEYS.contains(normalizedKey); + } + + @Override + boolean isNamespace(String nameSpace) { + //No namespaces are currently known for this report type. + return false; + } + + @Override + BlackboardAttribute makeAttribute(String nameSpace, String key, String value) { + String normalizedKey = key.toLowerCase(); + switch(normalizedKey) { + case "name": + return new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME, PARSER_NAME, value); + case "tel": + return new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER, PARSER_NAME, value); + case "storage": + //Ignore for now. + return null; + default: + throw new IllegalArgumentException(String.format("Key [ %s ] was not recognized", key)); + } + } + + @Override + void makeArtifact(List attributes, Content parent) throws TskCoreException { + BlackboardArtifact artifact = parent.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_CONTACT); + artifact.addAttributes(attributes); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYDataSourceProcessor.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYDataSourceProcessor.java new file mode 100755 index 0000000000..0f890469bd --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYDataSourceProcessor.java @@ -0,0 +1,363 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourceprocessors.xry; + +import com.google.common.collect.Lists; +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Iterator; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import java.util.stream.Collectors; +import java.util.stream.Stream; +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.autopsy.datasourceprocessors.AutoIngestDataSourceProcessor; +import org.sleuthkit.autopsy.datasourceprocessors.AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException; +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), + @ServiceProvider(service = AutoIngestDataSourceProcessor.class) +}) +public class XRYDataSourceProcessor implements DataSourceProcessor, AutoIngestDataSourceProcessor { + + private final XRYDataSourceProcessorConfigPanel configPanel; + + private static final int XRY_FILES_DEPTH = 1; + + //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; + } + + /** + * Tests the selected path. + * + * This functions checks permissions to the path directly and then to each + * of its top most children, if it is a folder. + */ + @Override + @NbBundle.Messages({ + "XRYDataSourceProcessor.noPathSelected=Please select a XRY folder", + "XRYDataSourceProcessor.notReadable=Selected path is not readable", + "XRYDataSourceProcessor.notXRYFolder=Selected folder did not contain any XRY files", + "XRYDataSourceProcessor.ioError=I/O error occured trying to test the XRY report folder", + "XRYDataSourceProcessor.childNotReadable=Top level path [ %s ] is not readable", + "XRYDataSourceProcessor.notAFolder=The selected path is not a 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 { + BasicFileAttributes attr = Files.readAttributes(selectedPath, + BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + + if (!attr.isDirectory()) { + configPanel.setErrorText(Bundle.XRYDataSourceProcessor_notAFolder()); + return false; + } + + //Ensure all of the XRY_FILES_DEPTH paths are readable. + try (Stream allFiles = Files.walk(selectedPath, XRY_FILES_DEPTH)) { + Iterator allFilesIterator = allFiles.iterator(); + while (allFilesIterator.hasNext()) { + Path currentPath = allFilesIterator.next(); + if (!Files.isReadable(currentPath)) { + Path fileName = currentPath.subpath(currentPath.getNameCount() - 2, + currentPath.getNameCount()); + configPanel.setErrorText(String.format( + Bundle.XRYDataSourceProcessor_childNotReadable(), + fileName.toString())); + return false; + } + } + } + + //Validate the folder. + if (!XRYFolder.isXRYFolder(selectedPath)) { + configPanel.setErrorText(Bundle.XRYDataSourceProcessor_notXRYFolder()); + return false; + } + } catch (IOException | UncheckedIOException 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; + } + + /** + * Tests if the given path is an XRY Folder. + * + * This function assumes the calling thread has sufficient privileges to + * read the folder and its child content. + * + * @param dataSourcePath Path to test + * @return 100 if the folder passes the XRY Folder check, 0 otherwise. + * @throws AutoIngestDataSourceProcessorException if an I/O error occurs + * during disk reads. + */ + @Override + public int canProcess(Path dataSourcePath) throws AutoIngestDataSourceProcessorException { + try { + if (XRYFolder.isXRYFolder(dataSourcePath)) { + return 100; + } + } catch (IOException ex) { + throw new AutoIngestDataSourceProcessorException("[XRY DSP] encountered I/O error " + ex.getMessage(), ex); + } + return 0; + } + + /** + * Processes the XRY folder that the examiner selected. The heavy lifting is + * done off of the EDT, so this function will return while the + * path is still being processed. + * + * This function assumes the calling thread has sufficient privileges to + * read the folder and its child content, which should have been validated + * in isPanelValid(). + */ + @Override + @NbBundle.Messages({ + "XRYDataSourceProcessor.noCurrentCase=No case is open." + }) + 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(); + String uniqueUUID = UUID.randomUUID().toString(); + //Move heavy lifting to a background task. + swingWorker = new XRYReportProcessorSwingWorker(xryFolder, progressMonitor, + callback, fileManager, uniqueUUID); + swingWorker.execute(); + } catch (NoCurrentCaseException ex) { + logger.log(Level.WARNING, "[XRY DSP] No case is currently open.", ex); + callback.done(DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS, + Lists.newArrayList(Bundle.XRYDataSourceProcessor_noCurrentCase(), + ex.getMessage()), Lists.newArrayList()); + } + } + + /** + * Processes the XRY Folder encountered in an auto-ingest context. The heavy + * lifting is done off of the EDT, so this function will return while the + * path is still being processed. + * + * This function assumes the calling thread has sufficient privileges to + * read the folder and its child content. + * + * @param deviceId + * @param dataSourcePath + * @param progressMonitor + * @param callBack + */ + @Override + public void process(String deviceId, Path dataSourcePath, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callBack) { + progressMonitor.setIndeterminate(true); + + try { + XRYFolder xryFolder = new XRYFolder(dataSourcePath); + FileManager fileManager = Case.getCurrentCaseThrows() + .getServices().getFileManager(); + //Move heavy lifting to a background task. + swingWorker = new XRYReportProcessorSwingWorker(xryFolder, progressMonitor, + callBack, fileManager, deviceId); + swingWorker.execute(); + } catch (NoCurrentCaseException ex) { + logger.log(Level.WARNING, "[XRY DSP] No case is currently open.", ex); + callBack.done(DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS, + Lists.newArrayList(Bundle.XRYDataSourceProcessor_noCurrentCase(), + ex.getMessage()), Lists.newArrayList()); + } + } + + @Override + public void cancel() { + if (swingWorker != null) { + swingWorker.cancel(true); + } + } + + @Override + public void reset() { + //Clear the current selected file path. + configPanel.clearSelectedFilePath(); + } + + /** + * Relieves the EDT from having to process the XRY report and write to the + * case database. + */ + private class XRYReportProcessorSwingWorker extends SwingWorker { + + private final DataSourceProcessorProgressMonitor progressMonitor; + private final DataSourceProcessorCallback callback; + private final FileManager fileManager; + private final XRYFolder xryFolder; + private final String uniqueUUID; + + public XRYReportProcessorSwingWorker(XRYFolder folder, + DataSourceProcessorProgressMonitor progressMonitor, + DataSourceProcessorCallback callback, + FileManager fileManager, + String uniqueUUID) { + + this.xryFolder = folder; + this.progressMonitor = progressMonitor; + this.callback = callback; + this.fileManager = fileManager; + this.uniqueUUID = uniqueUUID; + } + + @Override + @NbBundle.Messages({ + "XRYDataSourceProcessor.preppingFiles=Preparing to add files to the case database", + "XRYDataSourceProcessor.processingFiles=Processing all XRY files..." + }) + protected LocalFilesDataSource doInBackground() throws TskCoreException, + TskDataException, IOException { + progressMonitor.setProgressText(Bundle.XRYDataSourceProcessor_preppingFiles()); + + List nonXRYFiles = xryFolder.getNonXRYFiles(); + List filePaths = nonXRYFiles.stream() + //Map paths to string representations. + .map(Path::toString) + .collect(Collectors.toList()); + 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); + callback.done(DataSourceProcessorCallback.DataSourceProcessorResult.NO_ERRORS, + Lists.newArrayList(), Lists.newArrayList()); + } catch (ExecutionException ex) { + logger.log(Level.SEVERE, "[XRY DSP] Unexpected internal error while processing XRY report.", ex); + callback.done(DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS, + Lists.newArrayList(Bundle.XRYDataSourceProcessor_unexpectedError(), + ex.toString()), Lists.newArrayList()); + } + } + + /** + * Makes the DSP progress monitor compatible with the File Manager + * progress updater. + */ + private class ProgressMonitorAdapter implements FileManager.FileAddProgressUpdater { + + private final DataSourceProcessorProgressMonitor progressMonitor; + + ProgressMonitorAdapter(DataSourceProcessorProgressMonitor progressMonitor) { + this.progressMonitor = progressMonitor; + } + + @Override + @NbBundle.Messages({ + "XRYDataSourceProcessor.fileAdded=Added %s to the case database" + }) + public void fileAdded(AbstractFile newFile) { + progressMonitor.setProgressText(String.format(Bundle.XRYDataSourceProcessor_fileAdded(), newFile.getName())); + } + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYDataSourceProcessorConfigPanel.form b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYDataSourceProcessorConfigPanel.form new file mode 100755 index 0000000000..2fd4848e91 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYDataSourceProcessorConfigPanel.form @@ -0,0 +1,88 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYDataSourceProcessorConfigPanel.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYDataSourceProcessorConfigPanel.java new file mode 100755 index 0000000000..c35666d9c1 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYDataSourceProcessorConfigPanel.java @@ -0,0 +1,178 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourceprocessors.xry; + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.io.File; +import javax.swing.JFileChooser; +import javax.swing.JPanel; +import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; + +/** + * Allows an examiner to configure the XRY Data source processor. + */ +@SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives +final class XRYDataSourceProcessorConfigPanel extends JPanel { + + private static final long serialVersionUID = 1L; + private static final XRYDataSourceProcessorConfigPanel INSTANCE = + new XRYDataSourceProcessorConfigPanel(); + + //Communicates + private final PropertyChangeSupport pcs; + + /** + * Creates new form XRYDataSourceConfigPanel. + * Prevent direct instantiation. + */ + private XRYDataSourceProcessorConfigPanel() { + initComponents(); + pcs = new PropertyChangeSupport(this); + } + + /** + * Gets the singleton XRYDataSourceProcessorConfigPanel. + */ + static XRYDataSourceProcessorConfigPanel getInstance() { + return INSTANCE; + } + + /** + * Clears the error label. + */ + void clearErrorText() { + errorLabel.setText(null); + } + + /** + * Sets the error label to show the supplied text. + */ + void setErrorText(String text) { + errorLabel.setText(text); + } + + /** + * Clears the selected file path. + */ + void clearSelectedFilePath() { + filePathTextField.setText(null); + } + + /** + * Gets the file path selected by the examiner. + */ + String getSelectedFilePath() { + return filePathTextField.getText(); + } + + /** + * Adds a property change listener to this config panel. + */ + @Override + public synchronized void addPropertyChangeListener(PropertyChangeListener pcl) { + super.addPropertyChangeListener(pcl); + pcs.addPropertyChangeListener(pcl); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + filePathTextField = new javax.swing.JTextField(); + fileBrowserButton = new javax.swing.JButton(); + xrySelectFolderLabel = new javax.swing.JLabel(); + errorLabel = new javax.swing.JLabel(); + + filePathTextField.setEditable(false); + filePathTextField.setText(org.openide.util.NbBundle.getMessage(XRYDataSourceProcessorConfigPanel.class, "XRYDataSourceProcessorConfigPanel.filePathTextField.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(fileBrowserButton, org.openide.util.NbBundle.getMessage(XRYDataSourceProcessorConfigPanel.class, "XRYDataSourceProcessorConfigPanel.fileBrowserButton.text")); // NOI18N + fileBrowserButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + fileBrowserButtonActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(xrySelectFolderLabel, org.openide.util.NbBundle.getMessage(XRYDataSourceProcessorConfigPanel.class, "XRYDataSourceProcessorConfigPanel.xrySelectFolderLabel.text")); // NOI18N + + errorLabel.setForeground(new java.awt.Color(255, 0, 0)); + org.openide.awt.Mnemonics.setLocalizedText(errorLabel, org.openide.util.NbBundle.getMessage(XRYDataSourceProcessorConfigPanel.class, "XRYDataSourceProcessorConfigPanel.errorLabel.text")); // NOI18N + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(errorLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(xrySelectFolderLabel, javax.swing.GroupLayout.DEFAULT_SIZE, 380, Short.MAX_VALUE) + .addGroup(layout.createSequentialGroup() + .addComponent(filePathTextField) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(fileBrowserButton))) + .addContainerGap()) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(xrySelectFolderLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(filePathTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(fileBrowserButton)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(errorLabel) + .addContainerGap(235, Short.MAX_VALUE)) + ); + }// //GEN-END:initComponents + + /** + * Opens a JFileChooser instance so that the examiner can select a XRY + * report folder. + */ + private void fileBrowserButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_fileBrowserButtonActionPerformed + JFileChooser fileChooser = new JFileChooser(); + fileChooser.setMultiSelectionEnabled(false); + fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + int returnVal = fileChooser.showOpenDialog(this); + if (returnVal == JFileChooser.APPROVE_OPTION) { + File selection = fileChooser.getSelectedFile(); + filePathTextField.setText(selection.getAbsolutePath()); + + //This will notify the wizard to revalidate the data source processor. + pcs.firePropertyChange(DataSourceProcessor.DSP_PANEL_EVENT.UPDATE_UI.toString(), false, true); + } + }//GEN-LAST:event_fileBrowserButtonActionPerformed + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JLabel errorLabel; + private javax.swing.JButton fileBrowserButton; + private javax.swing.JTextField filePathTextField; + private javax.swing.JLabel xrySelectFolderLabel; + // End of variables declaration//GEN-END:variables +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYDeviceGenInfoFileParser.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYDeviceGenInfoFileParser.java new file mode 100755 index 0000000000..d3bba45bfc --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYDeviceGenInfoFileParser.java @@ -0,0 +1,191 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourceprocessors.xry; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Parses XRY Device-General Information files and creates artifacts. + */ +final class XRYDeviceGenInfoFileParser implements XRYFileParser { + + private static final Logger logger = Logger.getLogger(XRYDeviceGenInfoFileParser.class.getName()); + + //Human readable name of this parser. + private static final String PARSER_NAME = "XRY DSP"; + private static final char KEY_VALUE_DELIMITER = ':'; + + //All known XRY keys for Device Gen Info reports. + private static final String ATTRIBUTE_KEY = "attribute"; + private static final String DATA_KEY = "data"; + + //All of the known XRY Attribute values for device gen info. The value of the + //attribute keys are actionable for this parser. See parse header for more + //details. + private static final Map KEY_TO_TYPE + = new HashMap() { + { + put("device name", BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DEVICE_NAME); + put("device family", BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DEVICE_MODEL); + put("device type", BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DEVICE_MAKE); + put("mobile id (imei)", BlackboardAttribute.ATTRIBUTE_TYPE.TSK_IMEI); + put("security code", BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PASSWORD); + } + }; + + /** + * Device-General Information reports have 2 key value pairs for every + * attribute. The two only known keys are "Data" and "Attribute", where data + * is some generic information that the Attribute key describes. + * + * Example: + * + * Data: Nokia XYZ + * Attribute: Device Name + * + * This parse implementation assumes that the data field does not span + * multiple lines. If the data does span multiple lines, it will log an + * error describing an expectation for an "Attribute" key that is not found. + * + * @param reader The XRYFileReader that reads XRY entities from the + * Device-General Information report. + * @param parent The parent Content to create artifacts from. + * @throws IOException If an I/O error is encountered during report reading + * @throws TskCoreException If an error during artifact creation is encountered. + */ + @Override + public void parse(XRYFileReader reader, Content parent) throws IOException, TskCoreException { + Path reportPath = reader.getReportPath(); + logger.log(Level.INFO, String.format("[XRY DSP] Processing report at [ %s ]", reportPath.toString())); + + while (reader.hasNextEntity()) { + String xryEntity = reader.nextEntity(); + String[] xryLines = xryEntity.split("\n"); + + List attributes = new ArrayList<>(); + + //First line of the entity is the title. + if (xryLines.length > 0) { + logger.log(Level.INFO, String.format("[XRY DSP] Processing [ %s ]", xryLines[0])); + } + + for (int i = 1; i < xryLines.length; i++) { + String xryLine = xryLines[i]; + + //Expecting to see a "Data" key. + if (!hasDataKey(xryLine)) { + logger.log(Level.SEVERE, String.format("[XRY DSP] Expected a 'Data' key " + + "on this line (in brackets) [ %s ], but none was found. " + + "Discarding... Here is the previous line for context [ %s ]. " + + "What does this mean?", xryLine, xryLines[i - 1])); + continue; + } + + if (i + 1 == xryLines.length) { + logger.log(Level.SEVERE, String.format("[XRY DSP] Found a 'Data' key " + + "but no corresponding 'Attribute' key. Discarding... Here " + + "is the 'Data' line (in brackets) [ %s ]. Here is the previous " + + "line for context [ %s ]. What does this mean?", xryLine, xryLines[i - 1])); + continue; + } + + int dataKeyIndex = xryLine.indexOf(KEY_VALUE_DELIMITER); + String dataValue = xryLine.substring(dataKeyIndex + 1).trim(); + + String nextXryLine = xryLines[++i]; + + //Expecting to see an "Attribute" key + if (!hasAttributeKey(nextXryLine)) { + logger.log(Level.SEVERE, String.format("[XRY DSP] Expected an 'Attribute' " + + "key on this line (in brackets) [ %s ], but none was found. " + + "Discarding... Here is the previous line for context [ %s ]. " + + "What does this mean?", nextXryLine, xryLine)); + continue; + } + + int attributeKeyIndex = nextXryLine.indexOf(KEY_VALUE_DELIMITER); + String attributeValue = nextXryLine.substring(attributeKeyIndex + 1).trim(); + String normalizedAttributeValue = attributeValue.toLowerCase(); + + //Check if the attribute value is recognized. + if (KEY_TO_TYPE.containsKey(normalizedAttributeValue)) { + //All of the attribute types in the map expect a string. + attributes.add(new BlackboardAttribute(KEY_TO_TYPE.get(normalizedAttributeValue), PARSER_NAME, dataValue)); + } else { + logger.log(Level.SEVERE, String.format("[XRY DSP] Attribute type (in brackets) " + + "[ %s ] was not recognized. Discarding... Here is the " + + "previous line for context [ %s ]. What does this mean?", nextXryLine, xryLine)); + } + } + + if(!attributes.isEmpty()) { + //Build the artifact. + BlackboardArtifact artifact = parent.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_DEVICE_INFO); + artifact.addAttributes(attributes); + } + } + } + + /** + * Determines if the XRY line has a data key on it. + * + * @param xryLine + * @return + */ + private boolean hasDataKey(String xryLine) { + int dataKeyIndex = xryLine.indexOf(KEY_VALUE_DELIMITER); + //No key structure found. + if (dataKeyIndex == -1) { + return false; + } + + String normalizedDataKey = xryLine.substring(0, + dataKeyIndex).trim().toLowerCase(); + return normalizedDataKey.equals(DATA_KEY); + } + + /** + * Determines if the XRY line has an attribute key on it. + * + * @param xryLine + * @return + */ + private boolean hasAttributeKey(String xryLine) { + int attributeKeyIndex = xryLine.indexOf(KEY_VALUE_DELIMITER); + //No key structure found. + if (attributeKeyIndex == -1) { + return false; + } + + String normalizedDataKey = xryLine.substring(0, + attributeKeyIndex).trim().toLowerCase(); + return normalizedDataKey.equals(ATTRIBUTE_KEY); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFileParser.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFileParser.java new file mode 100755 index 0000000000..1787641e78 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFileParser.java @@ -0,0 +1,46 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourceprocessors.xry; + +import java.io.IOException; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Interface for XRY file parsing. + */ +interface XRYFileParser { + + /** + * Parses XRY entities and creates artifacts from the interpreted content. + * + * See XRYFileReader for more information on XRY entities. It is expected + * that implementations will create artifacts on the supplied Content + * object. + * + * @param reader Produces XRY entities from a given XRY file. + * @param parent Content object that will act as the source of the + * artifacts. + * @throws IOException If an I/O error occurs during reading. + * @throws TskCoreException If an error occurs during artifact creation. + */ + void parse(XRYFileReader reader, Content parent) throws IOException, TskCoreException; + +} + diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFileParserFactory.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFileParserFactory.java new file mode 100755 index 0000000000..06492de07b --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFileParserFactory.java @@ -0,0 +1,80 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourceprocessors.xry; + +/** + * Instantiates XRYFileParsers by report type. + */ +final class XRYFileParserFactory { + + /** + * Creates the correct implementation of a XRYFileParser for the specified + * report type. + * + * It is assumed that the report type is supported, which means the client + * needs to have tested with supports beforehand. Otherwise, an + * IllegalArgumentException is thrown. + * + * @param reportType A supported XRY report type. + * @return A XRYFileParser with defined behavior for the report type. + * @throws IllegalArgumentException if the report type is not supported or + * is null. This is a misuse of the API. It is assumed that the report type + * has been tested with the supports method. + */ + static XRYFileParser get(String reportType) { + if (reportType == null) { + throw new IllegalArgumentException("Report type cannot be null"); + } + + switch (reportType.trim().toLowerCase()) { + case "calls": + return new XRYCallsFileParser(); + case "contacts/contacts": + return new XRYContactsFileParser(); + case "device/general information": + return new XRYDeviceGenInfoFileParser(); + case "messages/sms": + return new XRYMessagesFileParser(); + case "web/bookmarks": + return new XRYWebBookmarksFileParser(); + default: + throw new IllegalArgumentException(reportType + " not recognized."); + } + } + + /** + * Tests if a XRYFileParser implementation exists for the report type. + * + * @param reportType Report type to test. + * @return Indication if the report type can be parsed. + */ + static boolean supports(String reportType) { + try { + //Attempt a get. + get(reportType); + return true; + } catch (IllegalArgumentException ex) { + return false; + } + } + + //Prevent direct instantiation + private XRYFileParserFactory() { + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFileReader.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFileReader.java index ff854a16ea..bc3c04259b 100755 --- a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFileReader.java +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFileReader.java @@ -45,36 +45,43 @@ import org.apache.commons.io.FilenameUtils; * From * Tel: 12345678 */ -public final class XRYFileReader implements AutoCloseable { +final class XRYFileReader implements AutoCloseable { private static final Logger logger = Logger.getLogger(XRYFileReader.class.getName()); //Assume UTF_16LE private static final Charset CHARSET = StandardCharsets.UTF_16LE; - //Assume TXT extension - private static final String EXTENSION = "txt"; - - //Assume 0xFFFE is the BOM - private static final int[] BOM = {0xFF, 0xFE}; - //Assume all XRY reports have the type on the 3rd line. private static final int LINE_WITH_REPORT_TYPE = 3; //Assume all headers are 5 lines in length. private static final int HEADER_LENGTH_IN_LINES = 5; + //Assume TXT extension + private static final String EXTENSION = "txt"; + + //Assume 0xFFFE is the BOM + private static final int[] BOM = {0xFF, 0xFE}; + + //Entity to be consumed during file iteration. + private final StringBuilder xryEntity; + //Underlying reader for the xry file. private final BufferedReader reader; - private final StringBuilder xryEntity; + //Reference to the original xry file. + private final Path xryFilePath; /** * Creates an XRYFileReader. As part of construction, the XRY file is opened * and the reader is advanced past the header. This leaves the reader * positioned at the start of the first XRY entity. * - * The file is assumed to be encoded in UTF-16LE. + * The file is assumed to be encoded in UTF-16LE and is NOT verified to be + * an XRY file before reading. It is expected that the isXRYFile function + * has been called on the path beforehand. Otherwise, the behavior is + * undefined. * * @param xryFile XRY file to read. It is assumed that the caller has read * access to the path. @@ -82,6 +89,7 @@ public final class XRYFileReader implements AutoCloseable { */ public XRYFileReader(Path xryFile) throws IOException { reader = Files.newBufferedReader(xryFile, CHARSET); + xryFilePath = xryFile; //Advance the reader to the start of the first XRY entity. for (int i = 0; i < HEADER_LENGTH_IN_LINES; i++) { @@ -91,6 +99,35 @@ public final class XRYFileReader implements AutoCloseable { xryEntity = new StringBuilder(); } + /** + * Extracts the report type from the XRY file. + * + * @return The XRY report type + * @throws IOException if an I/O error occurs. + * @throws IllegalArgumentExcepton If the XRY file does not have a report + * type. This is a misuse of the API. The validity of the Path should have + * been checked with isXRYFile before creating an XRYFileReader. + */ + public String getReportType() throws IOException { + Optional reportType = getType(xryFilePath); + if (reportType.isPresent()) { + return reportType.get(); + } + + throw new IllegalArgumentException(xryFilePath.toString() + " does not " + + "have a report type."); + } + + /** + * Returns the raw path of the XRY report file. + * + * @return + * @throws IOException + */ + public Path getReportPath() throws IOException { + return xryFilePath; + } + /** * Advances the reader until a valid XRY entity is detected or EOF is * reached. @@ -113,7 +150,7 @@ public final class XRYFileReader implements AutoCloseable { return true; } } else { - xryEntity.append(line).append("\n"); + xryEntity.append(line).append('\n'); } } @@ -123,6 +160,7 @@ public final class XRYFileReader implements AutoCloseable { /** * Returns an XRY entity if there is one, otherwise an exception is thrown. + * Clients should test for another entity by calling hasNextEntity(). * * @return A non-empty XRY entity. * @throws IOException if an I/O error occurs. @@ -139,6 +177,23 @@ public final class XRYFileReader implements AutoCloseable { } } + /** + * Peek at the next XRY entity without consuming it. If there are not more + * XRY entities left, an exception is thrown. Clients should test for + * another entity by calling hasNextEntity(). + * + * @return A non-empty XRY entity. + * @throws IOException if an I/O error occurs. + * @throws NoSuchElementException if there are no more XRY entities to peek. + */ + public String peek() throws IOException { + if (hasNextEntity()) { + return xryEntity.toString(); + } else { + throw new NoSuchElementException(); + } + } + /** * Closes any file handles this reader may have open. * diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFolder.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFolder.java index b9b999f270..f78154d796 100755 --- a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFolder.java +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYFolder.java @@ -24,18 +24,76 @@ import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; import java.util.Iterator; +import java.util.List; import java.util.stream.Stream; /** * Extracts XRY files and (optionally) non-XRY files from a XRY (Report) folder. */ -public final class XRYFolder { +final class XRYFolder { //Depth that will contain XRY files. All XRY files will be immediate //children of their parent folder. private static final int XRY_FILES_DEPTH = 1; + //Raw path to the XRY folder. + private final Path xryFolderPath; + + public XRYFolder(Path folder) { + xryFolderPath = folder; + } + + /** + * Finds all paths in the XRY report folder which are not XRY files. Only + * the first directory level is searched. As a result, some paths may point + * to directories. + * + * @return A non-null collection of paths + * @throws IOException If an I/O error occurs. + */ + public List getNonXRYFiles() throws IOException { + try (Stream allFiles = Files.walk(xryFolderPath, XRY_FILES_DEPTH)) { + List otherFiles = new ArrayList<>(); + Iterator allFilesIterator = allFiles.iterator(); + while (allFilesIterator.hasNext()) { + Path currentPath = allFilesIterator.next(); + if (!currentPath.equals(xryFolderPath) + && !XRYFileReader.isXRYFile(currentPath)) { + otherFiles.add(currentPath); + } + } + return otherFiles; + } catch (UncheckedIOException ex) { + throw ex.getCause(); + } + } + + /** + * Creates XRYFileReader instances for all XRY files found in the top level + * of the folder. + * + * @return A non-null collection of file readers. + * @throws IOException If an I/O error occurs. + */ + public List getXRYFileReaders() throws IOException { + try (Stream allFiles = Files.walk(xryFolderPath, XRY_FILES_DEPTH)) { + List fileReaders = new ArrayList<>(); + + Iterator allFilesIterator = allFiles.iterator(); + while (allFilesIterator.hasNext()) { + Path currentFile = allFilesIterator.next(); + if (XRYFileReader.isXRYFile(currentFile)) { + fileReaders.add(new XRYFileReader(currentFile)); + } + } + return fileReaders; + } catch (UncheckedIOException ex) { + throw ex.getCause(); + } + } + /** * Searches for XRY files at the top level of a given folder. If at least * one file matches, the entire directory is assumed to be an XRY report. @@ -48,7 +106,7 @@ public final class XRYFolder { * @return Indicates whether the Path is an XRY report. * * @throws IOException Error occurred during File I/O. - * @throws SecurityException If the security manager denies access any of + * @throws SecurityException If the security manager denies access to any of * the files. */ public static boolean isXRYFolder(Path folder) throws IOException { diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYMessagesFileParser.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYMessagesFileParser.java new file mode 100755 index 0000000000..820a6ebaad --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYMessagesFileParser.java @@ -0,0 +1,507 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourceprocessors.xry; + +import java.io.IOException; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Level; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Parses Messages-SMS files and creates artifacts. + */ +final class XRYMessagesFileParser implements XRYFileParser { + + private static final Logger logger = Logger.getLogger( + XRYMessagesFileParser.class.getName()); + + private static final String PARSER_NAME = "XRY DSP"; + private static final char KEY_VALUE_DELIMITER = ':'; + private static final DateTimeFormatter DATE_TIME_PARSER + = DateTimeFormatter.ofPattern("M/d/y h:m:s [a][ z]"); + + //Meta keys. These describe how the XRY message entites are split + //up in the report file. + private static final String SEGMENT_COUNT = "segments"; + private static final String SEGMENT_NUMBER = "segment number"; + private static final String REFERENCE_NUMBER = "reference number"; + + //A more readable version of these values. Referring to if the user + //has read the message. + private static final int READ = 1; + private static final int UNREAD = 0; + + private static final String TEXT_KEY = "text"; + + //All known XRY keys for message reports. + private static final Set XRY_KEYS = new HashSet() { + { + add(TEXT_KEY); + add("direction"); + add("time"); + add("status"); + add("tel"); + add("storage"); + add("index"); + add("folder"); + add("service center"); + add("type"); + add("name"); + } + }; + + //All known XRY namespaces for message reports. + private static final Set XRY_NAMESPACES = new HashSet() { + { + add("to"); + add("from"); + add("participant"); + } + }; + + //All known meta keys. + private static final Set XRY_META_KEYS = new HashSet() { + { + add(REFERENCE_NUMBER); + add(SEGMENT_NUMBER); + add(SEGMENT_COUNT); + } + }; + + /** + * Message-SMS report artifacts can span multiple XRY entities and their + * attributes can span multiple lines. The "Text" key is the only known key + * value pair that can span multiple lines. Messages can be segmented, + * meaning that their "Text" content can appear in multiple XRY entities. + * Our goal for a segmented message is to aggregate all of the text pieces and + * create 1 artifact. + * + * This parse implementation assumes that segments are contiguous and that + * they ascend incrementally. There are checks in place to verify this + * assumption is correct, otherwise an error will appear in the logs. + * + * @param reader The XRYFileReader that reads XRY entities from the + * Message-SMS report. + * @param parent The parent Content to create artifacts from. + * @throws IOException If an I/O error is encountered during report reading + * @throws TskCoreException If an error during artifact creation is + * encountered. + */ + @Override + public void parse(XRYFileReader reader, Content parent) throws IOException, TskCoreException { + Path reportPath = reader.getReportPath(); + logger.log(Level.INFO, String.format("[XRY DSP] Processing report at [ %s ]", reportPath.toString())); + + //Keep track of the reference numbers that have been parsed. + Set referenceNumbersSeen = new HashSet<>(); + + while (reader.hasNextEntity()) { + String xryEntity = reader.nextEntity(); + String[] xryLines = xryEntity.split("\n"); + + //First line of the entity is the title. + if (xryLines.length > 0) { + logger.log(Level.INFO, String.format("[XRY DSP] Processing [ %s ]", xryLines[0])); + } + + List attributes = new ArrayList<>(); + + String namespace = ""; + for (int i = 1; i < xryLines.length; i++) { + String xryLine = xryLines[i]; + String candidateNamespace = xryLine.trim().toLowerCase(); + + if (XRY_NAMESPACES.contains(candidateNamespace)) { + namespace = xryLine.trim(); + continue; + } + + //Find the XRY key on this line. + int keyDelimiter = xryLine.indexOf(KEY_VALUE_DELIMITER); + if (keyDelimiter == -1) { + logger.log(Level.SEVERE, String.format("[XRY DSP] Expected a key value " + + "pair on this line (in brackets) [ %s ], but one was not detected." + + " Is this the continuation of a previous line?" + + " Here is the previous line (in brackets) [ %s ]. " + + "What does this key mean?", xryLine, xryLines[i - 1])); + continue; + } + + //Extract the key value pair + String key = xryLine.substring(0, keyDelimiter).trim(); + String value = xryLine.substring(keyDelimiter + 1).trim(); + + String normalizedKey = key.toLowerCase(); + + if (XRY_META_KEYS.contains(normalizedKey)) { + //Skip meta keys, they are being dealt with seperately. + continue; + } + + if (!XRY_KEYS.contains(normalizedKey)) { + logger.log(Level.SEVERE, String.format("[XRY DSP] The following key, " + + "value pair (in brackets, respectively) [ %s ], [ %s ] " + + "was not recognized. Discarding... Here is the previous line " + + "[ %s ] for context. What does this key mean?", key, value, xryLines[i - 1])); + continue; + } + + if (value.isEmpty()) { + logger.log(Level.SEVERE, String.format("[XRY DSP] The following key " + + "(in brackets) [ %s ] was recognized, but the value " + + "was empty. Discarding... Here is the previous line " + + "for context [ %s ]. Is this a continuation of this line? " + + "What does an empty key mean?", key, xryLines[i - 1])); + continue; + } + + //Assume text is the only field that can span multiple lines. + if (normalizedKey.equals(TEXT_KEY)) { + //Build up multiple lines. + for (; (i + 1) < xryLines.length + && !hasKey(xryLines[i + 1]) + && !hasNamespace(xryLines[i + 1]); i++) { + String continuedValue = xryLines[i + 1].trim(); + //Assume multi lined values are split by word. + value = value + " " + continuedValue; + } + + int referenceNumber = getMetaInfo(xryLines, REFERENCE_NUMBER); + //Check if there is any segmented text. Min val is used to + //signify that no reference number was found. + if (referenceNumber != Integer.MIN_VALUE) { + logger.log(Level.INFO, String.format("[XRY DSP] Message entity " + + "appears to be segmented with reference number [ %d ]", referenceNumber)); + + if (referenceNumbersSeen.contains(referenceNumber)) { + logger.log(Level.SEVERE, String.format("[XRY DSP] This reference [ %d ] has already " + + "been seen. This means that the segments are not " + + "contiguous. Any segments contiguous with this " + + "one will be aggregated and another " + + "(otherwise duplicate) artifact will be created.", referenceNumber)); + } + + referenceNumbersSeen.add(referenceNumber); + + int segmentNumber = getMetaInfo(xryLines, SEGMENT_NUMBER); + + //Unify segmented text, if there is any. + String segmentedText = getSegmentedText(referenceNumber, + segmentNumber, reader); + //Assume it was segmented by word. + value = value + " " + segmentedText; + } + } + + BlackboardAttribute attribute = makeAttribute(namespace, key, value); + if (attribute != null) { + attributes.add(attribute); + } + } + + //Only create artifacts with non-empty attributes. + if(!attributes.isEmpty()) { + BlackboardArtifact artifact = parent.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_MESSAGE); + artifact.addAttributes(attributes); + } + } + } + + /** + * Builds up segmented message entities so that the text is unified in the + * artifact. + * + * @param referenceNumber Reference number that messages are group by + * @param segmentNumber Segment number of the starting segment. + * @param reader + * @return + * @throws IOException + */ + private String getSegmentedText(int referenceNumber, int segmentNumber, XRYFileReader reader) throws IOException { + StringBuilder segmentedText = new StringBuilder(); + + int currentSegmentNumber = segmentNumber; + while (reader.hasNextEntity()) { + //Peek at the next to see if it has the same reference number. + String nextEntity = reader.peek(); + String[] nextEntityLines = nextEntity.split("\n"); + int nextReferenceNumber = getMetaInfo(nextEntityLines, REFERENCE_NUMBER); + + if (nextReferenceNumber != referenceNumber) { + //Don't consume the next entity. It is not related + //to the current message thread. + break; + } + + //Consume the entity. + reader.nextEntity(); + + int nextSegmentNumber = getMetaInfo(nextEntityLines, SEGMENT_NUMBER); + + //Extract the text key from the entity, which is potentially + //multi-lined. + if (nextEntityLines.length > 0) { + logger.log(Level.INFO, String.format("[XRY DSP] Processing [ %s ] " + + "segment with reference number [ %d ]", nextEntityLines[0], referenceNumber)); + } + + if(nextSegmentNumber == Integer.MIN_VALUE) { + logger.log(Level.SEVERE, String.format("[XRY DSP] Segment with reference" + + " number [ %d ] did not have a segment number associated with it." + + " It cannot be determined if the reconstructed text will be in order.", referenceNumber)); + } else if (nextSegmentNumber != currentSegmentNumber + 1) { + logger.log(Level.SEVERE, String.format("[XRY DSP] Contiguous " + + "segments are not ascending incrementally. Encountered " + + "segment [ %d ] after segment [ %d ]. This means the reconstructed " + + "text will be out of order.", nextSegmentNumber, currentSegmentNumber)); + } + + for (int i = 1; i < nextEntityLines.length; i++) { + String xryLine = nextEntityLines[i]; + //Find the XRY key on this line. + int keyDelimiter = xryLine.indexOf(KEY_VALUE_DELIMITER); + if (keyDelimiter == -1) { + //Skip this line, we are searching only for a text key-value pair. + continue; + } + + String key = xryLine.substring(0, keyDelimiter); + String normalizedKey = key.trim().toLowerCase(); + if (normalizedKey.equals(TEXT_KEY)) { + String value = xryLine.substring(keyDelimiter + 1).trim(); + segmentedText.append(value).append(' '); + + //Build up multiple lines. + for (; (i + 1) < nextEntityLines.length + && !hasKey(nextEntityLines[i + 1]) + && !hasNamespace(nextEntityLines[i + 1]); i++) { + String continuedValue = nextEntityLines[i + 1].trim(); + segmentedText.append(continuedValue).append(' '); + } + } + } + + currentSegmentNumber = nextSegmentNumber; + } + + //Remove the trailing space. + if (segmentedText.length() > 0) { + segmentedText.setLength(segmentedText.length() - 1); + } + return segmentedText.toString(); + } + + /** + * Determines if the line has recognized key value on it. + * + * @param xryLine + * @return + */ + private boolean hasKey(String xryLine) { + int delimiter = xryLine.indexOf(':'); + if (delimiter != -1) { + String key = xryLine.substring(0, delimiter); + String normalizedKey = key.trim().toLowerCase(); + return XRY_KEYS.contains(normalizedKey); + } else { + return false; + } + } + + /** + * Determines if the line is a recognized namespace. + * + * @param xryLine + * @return + */ + private boolean hasNamespace(String xryLine) { + String normalizedLine = xryLine.trim().toLowerCase(); + return XRY_NAMESPACES.contains(normalizedLine); + } + + /** + * Extracts meta keys from the XRY entity. All of the known meta + * keys are integers and describe the message segments. + * + * @param xryLines Current XRY entity + * @param expectedKey The meta key to search for + * @return The interpreted integer value or Integer.MIN_VALUE if + * no meta key was found. + */ + private int getMetaInfo(String[] xryLines, String metaKey) { + for (int i = 0; i < xryLines.length; i++) { + String xryLine = xryLines[i]; + + String normalizedXryLine = xryLine.trim().toLowerCase(); + int firstDelimiter = normalizedXryLine.indexOf(KEY_VALUE_DELIMITER); + if (firstDelimiter != -1) { + String key = normalizedXryLine.substring(0, firstDelimiter); + if (key.equals(metaKey)) { + String value = normalizedXryLine.substring(firstDelimiter + 1).trim(); + try { + return Integer.parseInt(value); + } catch (NumberFormatException ex) { + logger.log(Level.SEVERE, String.format("[XRY DSP] Value [ %s ] for " + + "meta key [ %s ] was not an integer.", value, metaKey), ex); + } + } + } + } + + return Integer.MIN_VALUE; + } + + /** + * Creates an attribute from the extracted key value pair. + * + * @param nameSpace The namespace of this key value pair. + * It will have been verified beforehand, otherwise it will be empty. + * @param key The key that was verified beforehand + * @param value The value associated with that key. + * @return + */ + private BlackboardAttribute makeAttribute(String namespace, String key, String value) { + String normalizedKey = key.toLowerCase(); + String normalizedNamespace = namespace.toLowerCase(); + String normalizedValue = value.toLowerCase(); + + switch (normalizedKey) { + case "time": + //Tranform value to epoch ms + try { + String dateTime = removeDateTimeLocale(value); + String normalizedDateTime = dateTime.trim(); + long dateTimeInEpoch = calculateSecondsSinceEpoch(normalizedDateTime); + return new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME, PARSER_NAME, dateTimeInEpoch); + } catch (DateTimeParseException ex) { + logger.log(Level.SEVERE, String.format("[XRY DSP] Assumption " + + "about the date time formatting of messages is not " + + "right. Here is the value [ %s ].", value), ex); + return null; + } + case "direction": + return new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DIRECTION, PARSER_NAME, value); + case "text": + return new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_TEXT, PARSER_NAME, value); + case "status": + switch (normalizedValue) { + case "read": + return new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_READ_STATUS, PARSER_NAME, READ); + case "unread": + return new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_READ_STATUS, PARSER_NAME, UNREAD); + case "sending failed": + //Ignore for now. + return null; + case "deleted": + //Ignore for now. + return null; + case "unsent": + //Ignore for now. + return null; + default: + logger.log(Level.SEVERE, String.format("[XRY DSP] Unrecognized " + + "status value [ %s ].", value)); + return null; + } + case "type": + switch (normalizedValue) { + case "deliver": + //Ignore for now. + return null; + case "submit": + //Ignore for now. + return null; + case "status report": + //Ignore for now. + return null; + default: + logger.log(Level.SEVERE, String.format("[XRY DSP] Unrecognized " + + "type value [ %s ]", value)); + return null; + } + case "storage": + //Ignore for now. + return null; + case "index": + //Ignore for now. + return null; + case "folder": + //Ignore for now. + return null; + case "name": + //Ignore for now. + return null; + case "service center": + //Ignore for now. + return null; + case "tel": + //Apply the namespace + if (normalizedNamespace.equals("from")) { + return new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_FROM, PARSER_NAME, value); + } else { + //Assume to and participant are both equivalent to TSK_PHONE_NUMBER_TO + return new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_TO, PARSER_NAME, value); + } + default: + throw new IllegalArgumentException(String.format("key [ %s ] was not recognized.", key)); + } + } + + /** + * Removes the locale from the date time value. + * + * Locale in this case being (Device) or (Network). + * + * @param dateTime XRY datetime value to be sanitized. + * @return A purer date time value. + */ + private String removeDateTimeLocale(String dateTime) { + int index = dateTime.indexOf('('); + if (index == -1) { + return dateTime; + } + + return dateTime.substring(0, index); + } + + /** + * Parses the date time value and calculates ms since epoch. The time zone is + * assumed to be UTC. + * + * @param dateTime + * @return + */ + private long calculateSecondsSinceEpoch(String dateTime) { + LocalDateTime localDateTime = LocalDateTime.parse(dateTime, DATE_TIME_PARSER); + //Assume dates have no offset. + return localDateTime.toInstant(ZoneOffset.UTC).getEpochSecond(); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYReportProcessor.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYReportProcessor.java new file mode 100755 index 0000000000..98ee693101 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYReportProcessor.java @@ -0,0 +1,79 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourceprocessors.xry; + +import java.io.IOException; +import java.util.List; +import java.util.logging.Level; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Processes all XRY files in an XRY folder. + */ +final class XRYReportProcessor { + + private static final Logger logger = Logger.getLogger(XRYReportProcessor.class.getName()); + + /** + * Processes all XRY Files and creates artifacts on the given Content + * instance. + * + * All resources will be closed if an exception is encountered. + * + * @param folder XRY folder to process + * @param parent Content instance to hold newly created artifacts. + * @throws IOException If an I/O exception occurs. + * @throws TskCoreException If an error occurs adding artifacts. + */ + static void process(XRYFolder folder, Content parent) throws IOException, TskCoreException { + //Get all XRY file readers from this folder. + List xryFileReaders = folder.getXRYFileReaders(); + + try { + for (XRYFileReader xryFileReader : xryFileReaders) { + String reportType = xryFileReader.getReportType(); + if (XRYFileParserFactory.supports(reportType)) { + XRYFileParser parser = XRYFileParserFactory.get(reportType); + parser.parse(xryFileReader, parent); + } else { + logger.log(Level.SEVERE, String.format("[XRY DSP] XRY File (in brackets) " + + "[ %s ] was found, but no parser to support its report type exists. " + + "Report type is [ %s ]", xryFileReader.getReportPath().toString(), reportType)); + } + } + } finally { + try { + //Try to close all resources + for (XRYFileReader xryFileReader : xryFileReaders) { + xryFileReader.close(); + } + } catch (IOException ex) { + logger.log(Level.WARNING, "[XRY DSP] Encountered I/O exception trying " + + "to close all xry file readers.", ex); + } + } + } + + //Prevent direct instantiation. + private XRYReportProcessor() { + + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYWebBookmarksFileParser.java b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYWebBookmarksFileParser.java new file mode 100755 index 0000000000..a7443e7f48 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourceprocessors/xry/XRYWebBookmarksFileParser.java @@ -0,0 +1,66 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.datasourceprocessors.xry; + +import java.util.Map; +import java.util.HashMap; +import java.util.List; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Parses XRY Web-Bookmark files and creates artifacts. + */ +final class XRYWebBookmarksFileParser extends AbstractSingleKeyValueParser { + + //All known XRY keys for web bookmarks. + private static final Map KEY_TO_TYPE + = new HashMap() { + { + put("web address", BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL); + put("domain", BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN); + } + }; + + @Override + boolean isKey(String key) { + String normalizedKey = key.toLowerCase(); + return KEY_TO_TYPE.containsKey(normalizedKey); + } + + @Override + boolean isNamespace(String nameSpace) { + //No known namespaces for web reports. + return false; + } + + @Override + BlackboardAttribute makeAttribute(String nameSpace, String key, String value) { + String normalizedKey = key.toLowerCase(); + return new BlackboardAttribute(KEY_TO_TYPE.get(normalizedKey), PARSER_NAME, value); + } + + @Override + void makeArtifact(List attributes, Content parent) throws TskCoreException { + BlackboardArtifact artifact = parent.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_BOOKMARK); + artifact.addAttributes(attributes); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/Bundle.properties b/Core/src/org/sleuthkit/autopsy/geolocation/Bundle.properties index 8fa233ea33..84d4ab700c 100755 --- a/Core/src/org/sleuthkit/autopsy/geolocation/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/geolocation/Bundle.properties @@ -7,3 +7,26 @@ RefreshPanel.closeButton.text= MapPanel.cordLabel.text= WaypointDetailPanel.closeButton.text= WaypointDetailPanel.imageLabel.text= +GeoFilterPanel.waypointSettings.border.title= +GeoFilterPanel.allButton.text=Show All +GeoFilterPanel.mostRecentButton.text=Show only last +GeoFilterPanel.applyButton.text=Apply +GeoFilterPanel.showWaypointsWOTSCheckBox.text=Include waypoints with no time stamps +GeoFilterPanel.daysLabel.text=days of activity +CheckBoxListPanel.titleLabel.text=jLabel1 +CheckBoxListPanel.checkButton.text=Check All +CheckBoxListPanel.uncheckButton.text=Uncheck All +GeoFilterPanel.optionsLabel.text=Waypoints +OptionsCategory_Name_Geolocation=Geolocation +OptionsCategory_Keywords_Geolocation=Geolocation Settings +GeolocationSettingsPanel.tilePane.border.title=Map Tile Source +OptionsCategory_Keywords_Geolocation=Geolocation +OptionsCategory_Name_Geolocation=Geolocation +GeolocationSettingsPanel.defaultButton.text=Default online tile server (bing.com/maps) +GeolocationSettingsPanel.tileServerButton.text=OpenStreetMap tile server +GeolocationSettingsPanel.tileServerFiled.text= +GeolocationSettingsPanel.osmZipButton.text=OpenStreeMap Tile Zip File +GeolocationSettingsPanel.osmZipFileField.text= +GeolocationSettingsPanel.osmZipFileBrowseButton.text=Browse +GeolocationSettingsPanel.serverTestButton.text=Test +GeolocationSettingsPanel.osmZipButton.actionCommand=OpenStreeMap tile ZIP file diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/geolocation/Bundle.properties-MERGED index 95dd0d23f0..c69264acae 100755 --- a/Core/src/org/sleuthkit/autopsy/geolocation/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/geolocation/Bundle.properties-MERGED @@ -1,8 +1,27 @@ CTL_OpenGeolocation=Geolocation CTL_GeolocationTopComponentAction=GeolocationTopComponent CTL_GeolocationTopComponent=Geolocation +GeoFilterPanel_DataSource_List_Title=Data Sources +GeoFilterPanel_empty_dataSource=Data Source list is empty. +GeolocationSettingsPanel_malformed_url_message=The supplies OSM tile server address is invalid.\nPlease supply a well formed url prefixed with http:// +GeolocationSettingsPanel_malformed_url_message_tile=Malformed URL +GeolocationSettingsPanel_osm_server_test_fail_message=OSM tile server test failed.\nUnable to connect to server. +GeolocationSettingsPanel_osm_server_test_fail_message_title=Error +GeolocationSettingsPanel_osm_server_test_success_message=The provide OSM tile server address is valid. +GeolocationSettingsPanel_osm_server_test_success_message_title=Success +GeolocationTC_connection_failure_message=Failed to connect to map title source.\nPlease review map source in Options dialog. +GeolocationTC_connection_failure_message_title=Connection 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 +GeoTopComponent_filter_exception_msg=Exception occured during waypoint filtering. +GeoTopComponent_filter_exception_Title=Filter Failure +GeoTopComponent_no_waypoints_returned_mgs=Applied filter failed to find waypoints that matched criteria.\nRevise filter options and try again. +GeoTopComponent_no_waypoints_returned_Title=No Waypoints Found GLTopComponent_initilzation_error=An error occurred during waypoint initilization. Geolocation data maybe incomplete. GLTopComponent_name=Geolocation +HidingPane_default_title=Filters +MapPanel_connection_failure_message=Failed to connect to new geolocation map tile source. +MapPanel_connection_failure_message_title=Connection Failure MayWaypoint_ExternalViewer_label=Open in ExternalViewer OpenGeolocationAction_displayName=Geolocation OpenGeolocationAction_name=Geolocation @@ -12,4 +31,27 @@ RefreshPanel.closeButton.text= MapPanel.cordLabel.text= WaypointDetailPanel.closeButton.text= WaypointDetailPanel.imageLabel.text= +GeoFilterPanel.waypointSettings.border.title= +GeoFilterPanel.allButton.text=Show All +GeoFilterPanel.mostRecentButton.text=Show only last +GeoFilterPanel.applyButton.text=Apply +GeoFilterPanel.showWaypointsWOTSCheckBox.text=Include waypoints with no time stamps +GeoFilterPanel.daysLabel.text=days of activity +CheckBoxListPanel.titleLabel.text=jLabel1 +CheckBoxListPanel.checkButton.text=Check All +CheckBoxListPanel.uncheckButton.text=Uncheck All +GeoFilterPanel.optionsLabel.text=Waypoints +OptionsCategory_Name_Geolocation=Geolocation +OptionsCategory_Keywords_Geolocation=Geolocation Settings +GeolocationSettingsPanel.tilePane.border.title=Map Tile Source +OptionsCategory_Keywords_Geolocation=Geolocation +OptionsCategory_Name_Geolocation=Geolocation +GeolocationSettingsPanel.defaultButton.text=Default online tile server (bing.com/maps) +GeolocationSettingsPanel.tileServerButton.text=OpenStreetMap tile server +GeolocationSettingsPanel.tileServerFiled.text= +GeolocationSettingsPanel.osmZipButton.text=OpenStreeMap Tile Zip File +GeolocationSettingsPanel.osmZipFileField.text= +GeolocationSettingsPanel.osmZipFileBrowseButton.text=Browse +GeolocationSettingsPanel.serverTestButton.text=Test +GeolocationSettingsPanel.osmZipButton.actionCommand=OpenStreeMap tile ZIP file WaypointExtractAction_label=Extract Files(s) diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/CheckBoxJList.java b/Core/src/org/sleuthkit/autopsy/geolocation/CheckBoxJList.java new file mode 100755 index 0000000000..52276b6046 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/CheckBoxJList.java @@ -0,0 +1,109 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.geolocation; + +import java.awt.Component; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import javax.swing.JCheckBox; +import javax.swing.JList; +import javax.swing.ListCellRenderer; +import javax.swing.ListSelectionModel; + +/** + * A JList that renders the list items as check boxes. + */ +final class CheckBoxJList extends JList { + + private static final long serialVersionUID = 1L; + + /** + * Simple interface that must be implement for an object to be displayed as + * a checkbox in CheckBoxJList. + * + */ + interface CheckboxListItem { + + /** + * Returns the checkbox state. + * + * @return True if the check box should be checked + */ + boolean isChecked(); + + /** + * Set the state of the check box. + * + * @param checked + */ + void setChecked(boolean checked); + + /** + * Returns String to display as the check box label + * + * @return + */ + String getDisplayName(); + } + + /** + * Construct a new JCheckBoxList. + */ + CheckBoxJList() { + initalize(); + } + + /** + * Do all of the UI initialization. + */ + private void initalize() { + setCellRenderer(new CellRenderer()); + addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + int index = locationToIndex(e.getPoint()); + if (index != -1) { + CheckBoxJList.CheckboxListItem element = getModel().getElementAt(index); + element.setChecked(!element.isChecked()); + repaint(); + } + } + }); + setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + } + + /** + * A ListCellRenderer that renders list elements as check boxes. + */ + class CellRenderer extends JCheckBox implements ListCellRenderer { + + private static final long serialVersionUID = 1L; + + @Override + public Component getListCellRendererComponent( + JList list, CheckBoxJList.CheckboxListItem value, int index, + boolean isSelected, boolean cellHasFocus) { + + setBackground(list.getBackground()); + setSelected(value.isChecked()); + setText(value.getDisplayName()); + return this; + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/CheckBoxListPanel.form b/Core/src/org/sleuthkit/autopsy/geolocation/CheckBoxListPanel.form new file mode 100755 index 0000000000..112b734f1e --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/CheckBoxListPanel.form @@ -0,0 +1,79 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/CheckBoxListPanel.java b/Core/src/org/sleuthkit/autopsy/geolocation/CheckBoxListPanel.java new file mode 100755 index 0000000000..37dea163f5 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/CheckBoxListPanel.java @@ -0,0 +1,239 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.geolocation; + +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import javax.swing.DefaultListModel; +import javax.swing.Icon; + +/** + * A panel for showing Content objects in a check box list. + */ +final class CheckBoxListPanel extends javax.swing.JPanel { + + private static final long serialVersionUID = 1L; + + private final DefaultListModel> model = new DefaultListModel<>(); + private final CheckBoxJList> checkboxList; + + /** + * Creates new CheckboxFilterPanel + */ + CheckBoxListPanel() { + initComponents(); + + checkboxList = new CheckBoxJList<>(); + checkboxList.setModel(model); + scrollPane.setViewportView(checkboxList); + } + + /** + * Add a new element to the check box list. + * + * @param displayName display name for the checkbox + * @param obj Object that the checkbox represents + */ + void addElement(String displayName, T obj) { + model.addElement(new ObjectCheckBox<>(displayName, true, obj)); + } + + /** + * Remove all objects from the checkbox list. + */ + void clearList() { + model.removeAllElements(); + } + + /** + * Returns a list of all of the selected elements. + * + * @return List of selected elements. + */ + List getSelectedElements() { + List selectedElements = new ArrayList<>(); + Enumeration> elements = model.elements(); + + while (elements.hasMoreElements()) { + ObjectCheckBox element = elements.nextElement(); + if (element.isChecked()) { + selectedElements.add(element.getObject()); + } + } + + return selectedElements; + } + + /** + * Sets the selection state of the all the check boxes in the list. + * + * @param selected True to check the boxes, false to unchecked + */ + void setSetAllSelected(boolean selected) { + Enumeration> enumeration = model.elements(); + while (enumeration.hasMoreElements()) { + ObjectCheckBox element = enumeration.nextElement(); + element.setChecked(selected); + checkboxList.repaint(); + checkboxList.revalidate(); + + } + } + + /** + * Sets the panel title. + * + * @param title Panel title or null for no title. + */ + void setPanelTitle(String title) { + titleLabel.setText(title); + } + + /** + * Sets the panel title icon. + * + * @param icon Icon to set or null for no icon + */ + void setPanelTitleIcon(Icon icon) { + titleLabel.setIcon(icon); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + titleLabel = new javax.swing.JLabel(); + javax.swing.JButton uncheckButton = new javax.swing.JButton(); + javax.swing.JButton checkButton = new javax.swing.JButton(); + scrollPane = new javax.swing.JScrollPane(); + + setLayout(new java.awt.GridBagLayout()); + + org.openide.awt.Mnemonics.setLocalizedText(titleLabel, org.openide.util.NbBundle.getMessage(CheckBoxListPanel.class, "CheckBoxListPanel.titleLabel.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.gridwidth = 3; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + add(titleLabel, gridBagConstraints); + + org.openide.awt.Mnemonics.setLocalizedText(uncheckButton, org.openide.util.NbBundle.getMessage(CheckBoxListPanel.class, "CheckBoxListPanel.uncheckButton.text")); // NOI18N + uncheckButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + uncheckButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHEAST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 0, 9); + add(uncheckButton, gridBagConstraints); + + org.openide.awt.Mnemonics.setLocalizedText(checkButton, org.openide.util.NbBundle.getMessage(CheckBoxListPanel.class, "CheckBoxListPanel.checkButton.text")); // NOI18N + checkButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + checkButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHEAST; + add(checkButton, gridBagConstraints); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.gridwidth = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(5, 0, 9, 0); + add(scrollPane, gridBagConstraints); + }// //GEN-END:initComponents + + private void uncheckButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_uncheckButtonActionPerformed + setSetAllSelected(false); + }//GEN-LAST:event_uncheckButtonActionPerformed + + private void checkButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_checkButtonActionPerformed + setSetAllSelected(true); + }//GEN-LAST:event_checkButtonActionPerformed + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JScrollPane scrollPane; + private javax.swing.JLabel titleLabel; + // End of variables declaration//GEN-END:variables + + /** + * Wrapper around T that implements CheckboxListItem + * + * @param + */ + final class ObjectCheckBox implements CheckBoxJList.CheckboxListItem { + + private static final long serialVersionUID = 1L; + + private final T object; + private final String displayName; + private boolean checked; + + /** + * Constructs a new ObjectCheckBox + * + * @param displayName String to show as the check box label + * @param initialState Sets the initial state of the check box + * @param object Object that the check box represents. + */ + ObjectCheckBox(String displayName, boolean initialState, T object) { + this.displayName = displayName; + this.object = object; + this.checked = initialState; + } + + T getObject() { + return object; + } + + @Override + public boolean isChecked() { + return checked; + } + + @Override + public void setChecked(boolean checked) { + this.checked = checked; + } + + @Override + public String getDisplayName() { + return displayName; + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/GeoFilterPanel.form b/Core/src/org/sleuthkit/autopsy/geolocation/GeoFilterPanel.form new file mode 100755 index 0000000000..79a852b4d4 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/GeoFilterPanel.form @@ -0,0 +1,180 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/GeoFilterPanel.java b/Core/src/org/sleuthkit/autopsy/geolocation/GeoFilterPanel.java new file mode 100755 index 0000000000..9e009d1245 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/GeoFilterPanel.java @@ -0,0 +1,359 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.geolocation; + +import java.awt.GridBagConstraints; +import java.awt.event.ActionListener; +import java.util.Collections; +import java.util.List; +import java.util.logging.Level; +import javax.swing.ImageIcon; +import javax.swing.SpinnerNumberModel; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.DataSource; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * + * Panel to display the filter options for geolocation waypoints. + */ +class GeoFilterPanel extends javax.swing.JPanel { + + private static final long serialVersionUID = 1L; + private static final Logger logger = Logger.getLogger(GeoFilterPanel.class.getName()); + + private final SpinnerNumberModel numberModel; + private final CheckBoxListPanel checkboxPanel; + + /** + * Creates new GeoFilterPanel + */ + @Messages({ + "GeoFilterPanel_DataSource_List_Title=Data Sources" + }) + GeoFilterPanel() { + // numberModel is used in initComponents + numberModel = new SpinnerNumberModel(10, 1, Integer.MAX_VALUE, 1); + + initComponents(); + + // The gui builder cannot handle using CheckBoxListPanel due to its + // use of generics so we will initalize it here. + checkboxPanel = new CheckBoxListPanel<>(); + checkboxPanel.setPanelTitle(Bundle.GeoFilterPanel_DataSource_List_Title()); + checkboxPanel.setPanelTitleIcon(new ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/image.png"))); + checkboxPanel.setSetAllSelected(true); + + GridBagConstraints gridBagConstraints = new GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 15, 0, 15); + add(checkboxPanel, gridBagConstraints); + } + + void updateDataSourceList() { + try { + initCheckboxList(); + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "Failed to initialize the CheckboxListPane", ex); //NON-NLS + } + } + + /** + * Adds an actionListener to listen for the filter apply action + * + * @param listener + */ + void addActionListener(ActionListener listener) { + applyButton.addActionListener(listener); + } + + /** + * Returns the selected filter values. + * + * @return A GeoFilter object with the user selected filter values + * + * @throws GeoLocationUIException + */ + @Messages({ + "GeoFilterPanel_empty_dataSource=Data Source list is empty." + }) + GeoFilter getFilterState() throws GeoLocationUIException { + List dataSources = checkboxPanel.getSelectedElements(); + + if (dataSources.isEmpty()) { + throw new GeoLocationUIException(Bundle.GeoFilterPanel_empty_dataSource()); + } + return new GeoFilter(allButton.isSelected(), + showWaypointsWOTSCheckBox.isSelected(), + numberModel.getNumber().intValue(), + dataSources); + } + + /** + * Initialize the checkbox list panel + * + * @throws TskCoreException + */ + private void initCheckboxList() throws TskCoreException { + final SleuthkitCase sleuthkitCase = Case.getCurrentCase().getSleuthkitCase(); + + checkboxPanel.clearList(); + + for (DataSource dataSource : sleuthkitCase.getDataSources()) { + String dsName = sleuthkitCase.getContentById(dataSource.getId()).getName(); + checkboxPanel.addElement(dsName, dataSource); + } + } + + /** + * Based on the state of mostRecent radio button Change the state of the cnt + * spinner and the time stamp checkbox. + */ + private void updateWaypointOptions() { + boolean selected = mostRecentButton.isSelected(); + showWaypointsWOTSCheckBox.setEnabled(selected); + daysSpinner.setEnabled(selected); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + javax.swing.ButtonGroup buttonGroup = new javax.swing.ButtonGroup(); + javax.swing.JPanel waypointSettings = new javax.swing.JPanel(); + allButton = new javax.swing.JRadioButton(); + mostRecentButton = new javax.swing.JRadioButton(); + showWaypointsWOTSCheckBox = new javax.swing.JCheckBox(); + daysSpinner = new javax.swing.JSpinner(numberModel); + javax.swing.JLabel daysLabel = new javax.swing.JLabel(); + javax.swing.JPanel buttonPanel = new javax.swing.JPanel(); + applyButton = new javax.swing.JButton(); + javax.swing.JLabel optionsLabel = new javax.swing.JLabel(); + + setLayout(new java.awt.GridBagLayout()); + + waypointSettings.setBorder(javax.swing.BorderFactory.createTitledBorder(org.openide.util.NbBundle.getMessage(GeoFilterPanel.class, "GeoFilterPanel.waypointSettings.border.title"))); // NOI18N + waypointSettings.setLayout(new java.awt.GridBagLayout()); + + buttonGroup.add(allButton); + allButton.setSelected(true); + org.openide.awt.Mnemonics.setLocalizedText(allButton, org.openide.util.NbBundle.getMessage(GeoFilterPanel.class, "GeoFilterPanel.allButton.text")); // NOI18N + allButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + allButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.gridwidth = 4; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + waypointSettings.add(allButton, gridBagConstraints); + + buttonGroup.add(mostRecentButton); + org.openide.awt.Mnemonics.setLocalizedText(mostRecentButton, org.openide.util.NbBundle.getMessage(GeoFilterPanel.class, "GeoFilterPanel.mostRecentButton.text")); // NOI18N + mostRecentButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + mostRecentButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(9, 0, 0, 0); + waypointSettings.add(mostRecentButton, gridBagConstraints); + + org.openide.awt.Mnemonics.setLocalizedText(showWaypointsWOTSCheckBox, org.openide.util.NbBundle.getMessage(GeoFilterPanel.class, "GeoFilterPanel.showWaypointsWOTSCheckBox.text")); // NOI18N + showWaypointsWOTSCheckBox.setEnabled(false); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 2; + gridBagConstraints.gridwidth = 3; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(0, 30, 0, 0); + waypointSettings.add(showWaypointsWOTSCheckBox, gridBagConstraints); + + daysSpinner.setEnabled(false); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(9, 0, 0, 0); + waypointSettings.add(daysSpinner, gridBagConstraints); + + org.openide.awt.Mnemonics.setLocalizedText(daysLabel, org.openide.util.NbBundle.getMessage(GeoFilterPanel.class, "GeoFilterPanel.daysLabel.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 3; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(9, 5, 0, 0); + waypointSettings.add(daysLabel, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(5, 15, 9, 15); + add(waypointSettings, gridBagConstraints); + + buttonPanel.setLayout(new java.awt.GridBagLayout()); + + applyButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/tick.png"))); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(applyButton, org.openide.util.NbBundle.getMessage(GeoFilterPanel.class, "GeoFilterPanel.applyButton.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHEAST; + gridBagConstraints.weightx = 1.0; + buttonPanel.add(applyButton, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(9, 15, 0, 15); + add(buttonPanel, gridBagConstraints); + + optionsLabel.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/blueGeo16.png"))); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(optionsLabel, org.openide.util.NbBundle.getMessage(GeoFilterPanel.class, "GeoFilterPanel.optionsLabel.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(0, 15, 0, 0); + add(optionsLabel, gridBagConstraints); + }// //GEN-END:initComponents + + private void allButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_allButtonActionPerformed + updateWaypointOptions(); + }//GEN-LAST:event_allButtonActionPerformed + + private void mostRecentButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_mostRecentButtonActionPerformed + updateWaypointOptions(); + }//GEN-LAST:event_mostRecentButtonActionPerformed + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JRadioButton allButton; + private javax.swing.JButton applyButton; + private javax.swing.JSpinner daysSpinner; + private javax.swing.JRadioButton mostRecentButton; + private javax.swing.JCheckBox showWaypointsWOTSCheckBox; + // End of variables declaration//GEN-END:variables + + /** + * Class to store the values of the Geolocation user set filter parameters + */ + final class GeoFilter { + + private final boolean showAll; + private final boolean showWithoutTimeStamp; + private final int mostRecentNumDays; + private final List dataSources; + + /** + * Construct a Geolocation filter. showAll and mostRecentNumDays are + * exclusive filters, ie they cannot be used together. + * + * withoutTimeStamp is only applicable if mostRecentNumDays is true. + * + * When using the filters "most recent days" means to include waypoints + * for the numbers of days after the most recent waypoint, not the + * current date. + * + * @param showAll True if all waypoints should be shown + * @param withoutTimeStamp True to show waypoints without timeStamps, + * this filter is only applicable if + * mostRecentNumDays is true + * @param mostRecentNumDays Show Waypoint for the most recent given + * number of days. This parameter is ignored if + * showAll is true. + * @param dataSources A list of dataSources to filter waypoint + * for. + */ + GeoFilter(boolean showAll, boolean withoutTimeStamp, int mostRecentNumDays, List dataSources) { + this.showAll = showAll; + this.showWithoutTimeStamp = withoutTimeStamp; + this.mostRecentNumDays = mostRecentNumDays; + this.dataSources = dataSources; + } + + /** + * Returns whether or not to show all waypoints. + * + * @return True if all waypoints should be shown. + */ + boolean showAllWaypoints() { + return showAll; + } + + /** + * Returns whether or not to include waypoints with time stamps. + * + * This filter is only applicable if "showAll" is true. + * + * @return True if waypoints with time stamps should be shown. + */ + boolean showWaypointsWithoutTimeStamp() { + return showWithoutTimeStamp; + } + + /** + * Returns the number of most recent days to show waypoints for. This + * value should be ignored if showAll is true. + * + * @return The number of most recent days to show waypoints for + */ + int getMostRecentNumDays() { + return mostRecentNumDays; + } + + /** + * Returns a list of data sources to filter the waypoints by, or null if + * all datasources should be include. + * + * @return A list of dataSources or null if all dataSources should be + * included. + */ + List getDataSources() { + return Collections.unmodifiableList(dataSources); + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/GeoLocationUIException.java b/Core/src/org/sleuthkit/autopsy/geolocation/GeoLocationUIException.java new file mode 100755 index 0000000000..7ce6837914 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/GeoLocationUIException.java @@ -0,0 +1,46 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.geolocation; + +/** + * + * An exception call for Exceptions that occure in the geolocation dialog. + */ +public class GeoLocationUIException extends Exception{ + private static final long serialVersionUID = 1L; + + /** + * Create exception containing the error message + * + * @param msg the message + */ + public GeoLocationUIException(String msg) { + super(msg); + } + + /** + * Create exception containing the error message and cause exception + * + * @param msg the message + * @param ex cause exception + */ + public GeoLocationUIException(String msg, Exception ex) { + super(msg, ex); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/GeolocationOptionPanelController.java b/Core/src/org/sleuthkit/autopsy/geolocation/GeolocationOptionPanelController.java new file mode 100755 index 0000000000..725baea022 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/GeolocationOptionPanelController.java @@ -0,0 +1,124 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.geolocation; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import javax.swing.JComponent; +import org.netbeans.spi.options.OptionsPanelController; +import org.openide.util.HelpCtx; +import org.openide.util.Lookup; + +@OptionsPanelController.TopLevelRegistration( + categoryName = "#OptionsCategory_Name_Geolocation", + iconBase = "org/sleuthkit/autopsy/images/blueGeo32.png", + keywords = "#OptionsCategory_Keywords_Geolocation", + keywordsCategory = "Geolcoation", + position = 18 +) +/** + * Controller for the Geolocation options pane in the Options dialog. + */ +public final class GeolocationOptionPanelController extends OptionsPanelController { + + private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); + private boolean changed; + private GeolocationSettingsPanel panel; + + /** + * Returns the GeolcoationSettingPanel. + * + * @return + */ + GeolocationSettingsPanel getPanel() { + if (panel == null) { + panel = new GeolocationSettingsPanel(); + panel.addPropertyChangeListener(new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (evt.getPropertyName().equals(OptionsPanelController.PROP_CHANGED)) { + updateChanged(); + } + } + + }); + } + return panel; + } + + @Override + public void update() { + getPanel().load(); + changed = false; + } + + @Override + public void applyChanges() { + getPanel().store(); + changed = false; + } + + @Override + public boolean isValid() { + return true; + } + + @Override + public boolean isChanged() { + return changed; + } + + @Override + public JComponent getComponent(Lookup masterLookup) { + return getPanel(); + } + + @Override + public HelpCtx getHelpCtx() { + return null; + } + + @Override + public void addPropertyChangeListener(PropertyChangeListener listener) { + pcs.addPropertyChangeListener(listener); + } + + @Override + public void removePropertyChangeListener(PropertyChangeListener listener) { + pcs.removePropertyChangeListener(listener); + } + + /** + * Helper function for updating the change state. + */ + void updateChanged() { + if (!changed) { + changed = true; + pcs.firePropertyChange(OptionsPanelController.PROP_CHANGED, false, true); + } + pcs.firePropertyChange(OptionsPanelController.PROP_VALID, null, null); + } + + @Override + public void cancel() { + getPanel().cancelChanges(); + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/GeolocationSettingsPanel.form b/Core/src/org/sleuthkit/autopsy/geolocation/GeolocationSettingsPanel.form new file mode 100755 index 0000000000..0fd5461656 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/GeolocationSettingsPanel.form @@ -0,0 +1,170 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/GeolocationSettingsPanel.java b/Core/src/org/sleuthkit/autopsy/geolocation/GeolocationSettingsPanel.java new file mode 100755 index 0000000000..9be1f08927 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/GeolocationSettingsPanel.java @@ -0,0 +1,343 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.geolocation; + +import java.awt.Color; +import java.io.File; +import java.util.Arrays; +import javax.swing.JFileChooser; +import javax.swing.JOptionPane; +import org.apache.commons.validator.routines.UrlValidator; +import org.jxmapviewer.OSMTileFactoryInfo; +import org.jxmapviewer.viewer.TileFactoryInfo; +import org.jxmapviewer.viewer.util.GeoUtil; +import org.netbeans.spi.options.OptionsPanelController; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.casemodule.GeneralFilter; +import org.sleuthkit.autopsy.core.UserPreferences; +import org.sleuthkit.autopsy.corecomponents.OptionsPanel; + +/** + * A panel to allow the user to set the custom properties of the geolocation + * window. + * + */ +final class GeolocationSettingsPanel extends javax.swing.JPanel implements OptionsPanel { + + private static final long serialVersionUID = 1L; + + /** + * Creates new GeolocationSettingsPanel + */ + GeolocationSettingsPanel() { + initComponents(); + updateControlState(); + } + + @Override + public void store() { + UserPreferences.setGeolocationTileOption(getServerOption().getValue()); + UserPreferences.setGeolocationOsmZipPath(osmZipFileField.getText()); + UserPreferences.setGeolocationOsmServerAddress(tileServerFiled.getText()); + } + + @Override + public void load() { + tileServerFiled.setText(UserPreferences.getGeolocationOsmServerAddress()); + osmZipFileField.setText(UserPreferences.getGeolocationOsmZipPath()); + switch (GeolocationTileOption.getOptionForValue(UserPreferences.getGeolocationtTileOption())) { + case ONLINE_USER_DEFINED_OSM_SERVER: + tileServerButton.setSelected(true); + break; + case OFFLINE_OSM_ZIP: + osmZipButton.setSelected(true); + break; + default: + defaultButton.setSelected(true); + break; + } + + updateControlState(); + } + + /** + * Update the state of the tile server options based on the radio button + * selection state. + */ + private void updateControlState() { + tileServerFiled.setEnabled(tileServerButton.isSelected()); + serverTestButton.setEnabled(tileServerButton.isSelected()); + osmZipFileField.setEnabled(osmZipButton.isSelected()); + osmZipFileBrowseButton.setEnabled(osmZipButton.isSelected()); + } + + /** + * Returns the GEOLOCATION_TILE_OPTION based on the selection state of the + * option radio buttons. + * + * @return Current GEOLOCATION_TILE_OPTION + */ + private GeolocationTileOption getServerOption() { + if (tileServerButton.isSelected()) { + return GeolocationTileOption.ONLINE_USER_DEFINED_OSM_SERVER; + } else if (osmZipButton.isSelected()) { + return GeolocationTileOption.OFFLINE_OSM_ZIP; + } + return GeolocationTileOption.ONLINE_DEFAULT_SERVER; + } + + /** + * Tests the validity of the given tile server address. + * + * This test assumes the server is able to serve up at least one tile, the + * tile at x=1, y=1 with a zoom level of 1. This tile should be the whole + * global. + * + * @param url String url to tile server. The string does not have to be + * prefixed with http:// + * + * @return True if the server was successfully contacted. + */ + private boolean testOSMServer(String url) { + TileFactoryInfo info = new OSMTileFactoryInfo("User Defined Server", url); //NON-NLS + return GeoUtil.isValidTile(1, 1, 1, info); + } + + void cancelChanges() { + load(); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + javax.swing.ButtonGroup buttonGroup = new javax.swing.ButtonGroup(); + javax.swing.JPanel tilePane = new javax.swing.JPanel(); + defaultButton = new javax.swing.JRadioButton(); + tileServerButton = new javax.swing.JRadioButton(); + tileServerFiled = new javax.swing.JTextField(); + osmZipButton = new javax.swing.JRadioButton(); + osmZipFileField = new javax.swing.JTextField(); + osmZipFileBrowseButton = new javax.swing.JButton(); + serverTestButton = new javax.swing.JButton(); + + setLayout(new java.awt.GridBagLayout()); + + tilePane.setBorder(javax.swing.BorderFactory.createTitledBorder(org.openide.util.NbBundle.getMessage(GeolocationSettingsPanel.class, "GeolocationSettingsPanel.tilePane.border.title"))); // NOI18N + tilePane.setLayout(new java.awt.GridBagLayout()); + + buttonGroup.add(defaultButton); + defaultButton.setSelected(true); + org.openide.awt.Mnemonics.setLocalizedText(defaultButton, org.openide.util.NbBundle.getMessage(GeolocationSettingsPanel.class, "GeolocationSettingsPanel.defaultButton.text")); // NOI18N + defaultButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + defaultButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.gridwidth = 3; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 5, 0); + tilePane.add(defaultButton, gridBagConstraints); + + buttonGroup.add(tileServerButton); + org.openide.awt.Mnemonics.setLocalizedText(tileServerButton, org.openide.util.NbBundle.getMessage(GeolocationSettingsPanel.class, "GeolocationSettingsPanel.tileServerButton.text")); // NOI18N + tileServerButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + tileServerButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 5, 0); + tilePane.add(tileServerButton, gridBagConstraints); + + tileServerFiled.setText(org.openide.util.NbBundle.getMessage(GeolocationSettingsPanel.class, "GeolocationSettingsPanel.tileServerFiled.text")); // NOI18N + tileServerFiled.setPreferredSize(new java.awt.Dimension(300, 26)); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 1; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 5, 0); + tilePane.add(tileServerFiled, gridBagConstraints); + + buttonGroup.add(osmZipButton); + org.openide.awt.Mnemonics.setLocalizedText(osmZipButton, org.openide.util.NbBundle.getMessage(GeolocationSettingsPanel.class, "GeolocationSettingsPanel.osmZipButton.text")); // NOI18N + osmZipButton.setActionCommand(org.openide.util.NbBundle.getMessage(GeolocationSettingsPanel.class, "GeolocationSettingsPanel.osmZipButton.actionCommand")); // NOI18N + osmZipButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + osmZipButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 9, 0); + tilePane.add(osmZipButton, gridBagConstraints); + + osmZipFileField.setText(org.openide.util.NbBundle.getMessage(GeolocationSettingsPanel.class, "GeolocationSettingsPanel.osmZipFileField.text")); // NOI18N + osmZipFileField.setPreferredSize(new java.awt.Dimension(300, 26)); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 2; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 9, 0); + tilePane.add(osmZipFileField, gridBagConstraints); + + org.openide.awt.Mnemonics.setLocalizedText(osmZipFileBrowseButton, org.openide.util.NbBundle.getMessage(GeolocationSettingsPanel.class, "GeolocationSettingsPanel.osmZipFileBrowseButton.text")); // NOI18N + osmZipFileBrowseButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + osmZipFileBrowseButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 2; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 9, 9, 9); + tilePane.add(osmZipFileBrowseButton, gridBagConstraints); + + org.openide.awt.Mnemonics.setLocalizedText(serverTestButton, org.openide.util.NbBundle.getMessage(GeolocationSettingsPanel.class, "GeolocationSettingsPanel.serverTestButton.text")); // NOI18N + serverTestButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + serverTestButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 1; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.insets = new java.awt.Insets(0, 9, 9, 9); + tilePane.add(serverTestButton, gridBagConstraints); + + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + add(tilePane, gridBagConstraints); + }// //GEN-END:initComponents + + private void osmZipFileBrowseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_osmZipFileBrowseButtonActionPerformed + JFileChooser fileWindow = new JFileChooser(); + fileWindow.setFileSelectionMode(JFileChooser.FILES_ONLY); + GeneralFilter fileFilter = new GeneralFilter(Arrays.asList(".zip"), "Zips (*.zip)"); //NON-NLS + fileWindow.setDragEnabled(false); + fileWindow.setFileFilter(fileFilter); + fileWindow.setMultiSelectionEnabled(false); + int returnVal = fileWindow.showSaveDialog(this); + if (returnVal == JFileChooser.APPROVE_OPTION) { + File zipFile = fileWindow.getSelectedFile(); + osmZipFileField.setForeground(Color.BLACK); + osmZipFileField.setText(zipFile.getAbsolutePath()); + firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null); + } + }//GEN-LAST:event_osmZipFileBrowseButtonActionPerformed + + private void defaultButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_defaultButtonActionPerformed + updateControlState(); + firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null); + }//GEN-LAST:event_defaultButtonActionPerformed + + private void tileServerButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_tileServerButtonActionPerformed + updateControlState(); + firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null); + }//GEN-LAST:event_tileServerButtonActionPerformed + + private void osmZipButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_osmZipButtonActionPerformed + updateControlState(); + firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null); + }//GEN-LAST:event_osmZipButtonActionPerformed + + @Messages({ + "GeolocationSettingsPanel_malformed_url_message=The supplies OSM tile server address is invalid.\nPlease supply a well formed url prefixed with http://", + "GeolocationSettingsPanel_malformed_url_message_tile=Malformed URL", + "GeolocationSettingsPanel_osm_server_test_fail_message=OSM tile server test failed.\nUnable to connect to server.", + "GeolocationSettingsPanel_osm_server_test_fail_message_title=Error", + "GeolocationSettingsPanel_osm_server_test_success_message=The provide OSM tile server address is valid.", + "GeolocationSettingsPanel_osm_server_test_success_message_title=Success",}) + private void serverTestButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_serverTestButtonActionPerformed + String address = tileServerFiled.getText(); + String message = Bundle.GeolocationSettingsPanel_osm_server_test_fail_message(); + String title = Bundle.GeolocationSettingsPanel_osm_server_test_fail_message_title(); + + String[] schemes = {"http", "https"}; //NON-NLS + UrlValidator urlValidator = new UrlValidator(schemes); + if (!urlValidator.isValid(address)) { + message = Bundle.GeolocationSettingsPanel_malformed_url_message(); + title = Bundle.GeolocationSettingsPanel_malformed_url_message_tile(); + } else if (testOSMServer(address)) { + message = Bundle.GeolocationSettingsPanel_osm_server_test_success_message(); + title = Bundle.GeolocationSettingsPanel_osm_server_test_success_message_title(); + } + + JOptionPane.showMessageDialog(this, message, title, JOptionPane.INFORMATION_MESSAGE); + }//GEN-LAST:event_serverTestButtonActionPerformed + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JRadioButton defaultButton; + private javax.swing.JRadioButton osmZipButton; + private javax.swing.JButton osmZipFileBrowseButton; + private javax.swing.JTextField osmZipFileField; + private javax.swing.JButton serverTestButton; + private javax.swing.JRadioButton tileServerButton; + private javax.swing.JTextField tileServerFiled; + // End of variables declaration//GEN-END:variables + + /** + * Tile server option enum. The enum was given values to simplify the + * storing of the user preference for a particular option. + */ + enum GeolocationTileOption{ + ONLINE_DEFAULT_SERVER(0), + ONLINE_USER_DEFINED_OSM_SERVER(1), + OFFLINE_OSM_ZIP(2); + + private final int value; + + GeolocationTileOption(int value) { + this.value = value; + } + + int getValue() { + return value; + } + + static GeolocationTileOption getOptionForValue(int value) { + for (GeolocationTileOption option : GeolocationTileOption.values()) { + if (option.getValue() == value) { + return option; + } + } + + return ONLINE_DEFAULT_SERVER; + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/GeolocationTopComponent.form b/Core/src/org/sleuthkit/autopsy/geolocation/GeolocationTopComponent.form index bddebcbc0c..16cd5368a6 100755 --- a/Core/src/org/sleuthkit/autopsy/geolocation/GeolocationTopComponent.form +++ b/Core/src/org/sleuthkit/autopsy/geolocation/GeolocationTopComponent.form @@ -11,6 +11,7 @@ + @@ -23,6 +24,17 @@ + + + + + + + + + + + - + \ No newline at end of file diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/GeolocationTopComponent.java b/Core/src/org/sleuthkit/autopsy/geolocation/GeolocationTopComponent.java index bf54ece196..a653979bb6 100755 --- a/Core/src/org/sleuthkit/autopsy/geolocation/GeolocationTopComponent.java +++ b/Core/src/org/sleuthkit/autopsy/geolocation/GeolocationTopComponent.java @@ -25,9 +25,9 @@ import java.beans.PropertyChangeListener; import java.util.EnumSet; import java.util.List; import java.util.Set; -import java.util.concurrent.ExecutionException; import java.util.logging.Level; -import javax.swing.SwingWorker; +import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; import org.openide.util.NbBundle.Messages; import org.openide.windows.RetainLocation; import org.openide.windows.TopComponent; @@ -37,6 +37,11 @@ import static org.sleuthkit.autopsy.casemodule.Case.Events.CURRENT_CASE; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; import org.sleuthkit.autopsy.coreutils.ThreadConfined; +import org.sleuthkit.autopsy.geolocation.GeoFilterPanel.GeoFilter; +import org.sleuthkit.autopsy.geolocation.datamodel.GeoLocationDataException; +import org.sleuthkit.autopsy.geolocation.datamodel.Waypoint; +import org.sleuthkit.autopsy.geolocation.datamodel.WaypointBuilder; +import org.sleuthkit.autopsy.geolocation.datamodel.WaypointBuilder.WaypointFilterQueryCallBack; import org.sleuthkit.autopsy.ingest.IngestManager; import static org.sleuthkit.autopsy.ingest.IngestManager.IngestModuleEvent.DATA_ADDED; import org.sleuthkit.autopsy.ingest.ModuleDataEvent; @@ -59,6 +64,7 @@ public final class GeolocationTopComponent extends TopComponent { private static final Set INGEST_MODULE_EVENTS_OF_INTEREST = EnumSet.of(DATA_ADDED); private final PropertyChangeListener ingestListener; + private final GeoFilterPanel geoFilterPanel; final RefreshPanel refreshPanel = new RefreshPanel(); @@ -73,7 +79,7 @@ public final class GeolocationTopComponent extends TopComponent { @ThreadConfined(type = ThreadConfined.ThreadType.AWT) public GeolocationTopComponent() { initComponents(); - initWaypoints(); + setName(Bundle.GLTopComponent_name()); this.ingestListener = pce -> { @@ -105,10 +111,19 @@ public final class GeolocationTopComponent extends TopComponent { @Override public void actionPerformed(ActionEvent e) { mapPanel.clearWaypoints(); - initWaypoints(); + updateWaypoints(); showRefreshPanel(false); } }); + + geoFilterPanel = new GeoFilterPanel(); + filterPane.setPanel(geoFilterPanel); + geoFilterPanel.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + updateWaypoints(); + } + }); } @Override @@ -118,7 +133,7 @@ public final class GeolocationTopComponent extends TopComponent { Case.addEventTypeSubscriber(EnumSet.of(CURRENT_CASE), evt -> { mapPanel.clearWaypoints(); if (evt.getNewValue() != null) { - initWaypoints(); + updateWaypoints(); } }); } @@ -134,6 +149,30 @@ public final class GeolocationTopComponent extends TopComponent { super.componentOpened(); WindowManager.getDefault().setTopComponentFloating(this, true); } + + @Messages({ + "GeolocationTC_connection_failure_message=Failed to connect to map title source.\nPlease review map source in Options dialog.", + "GeolocationTC_connection_failure_message_title=Connection Failure" + }) + @Override + public void open() { + super.open(); + geoFilterPanel.updateDataSourceList(); + try { + mapPanel.initMap(); + } catch (GeoLocationDataException ex) { + JOptionPane.showMessageDialog(this, + Bundle.GeolocationTC_connection_failure_message(), + Bundle.GeolocationTC_connection_failure_message_title(), + JOptionPane.ERROR_MESSAGE); + MessageNotifyUtil.Notify.error( + Bundle.GeolocationTC_connection_failure_message_title(), + Bundle.GeolocationTC_connection_failure_message()); + logger.log(Level.SEVERE, ex.getMessage(), ex); + return; // Doen't set the waypoints. + } + updateWaypoints(); + } /** * Set the state of the refresh panel at the top of the mapPanel. @@ -150,43 +189,60 @@ public final class GeolocationTopComponent extends TopComponent { } /** - * Use a SwingWorker thread to get a list of waypoints. - * + * Filters the list of waypoints based on the user selections in the filter + * pane. */ - private void initWaypoints() { - SwingWorker, MapWaypoint> worker = new SwingWorker, MapWaypoint>() { - @Override - protected List doInBackground() throws Exception { - Case currentCase = Case.getCurrentCaseThrows(); + @Messages({ + "GeoTopComponent_no_waypoints_returned_mgs=Applied filter failed to find waypoints that matched criteria.\nRevise filter options and try again.", + "GeoTopComponent_no_waypoints_returned_Title=No Waypoints Found", + "GeoTopComponent_filter_exception_msg=Exception occured during waypoint filtering.", + "GeoTopComponent_filter_exception_Title=Filter Failure", + "GeoTopComponent_filer_data_invalid_msg=Unable to run waypoint filter.\nPlease select one or more data sources.", + "GeoTopComponent_filer_data_invalid_Title=Filter Failure" + }) + private void updateWaypoints() { + GeoFilter filters; - return MapWaypoint.getWaypoints(currentCase.getSleuthkitCase()); - } + // Show a warning message if the user has not selected a data source + try { + filters = geoFilterPanel.getFilterState(); + } catch (GeoLocationUIException ex) { + JOptionPane.showMessageDialog(this, + Bundle.GeoTopComponent_filer_data_invalid_msg(), + Bundle.GeoTopComponent_filer_data_invalid_Title(), + JOptionPane.INFORMATION_MESSAGE); + return; + } - @Override - protected void done() { - if (isDone() && !isCancelled()) { - try { - List waypoints = get(); - if (waypoints == null || waypoints.isEmpty()) { - return; + SwingUtilities.invokeLater(new Runnable() { + public void run() { + Case currentCase = Case.getCurrentCase(); + try { + WaypointBuilder.getAllWaypoints(currentCase.getSleuthkitCase(), filters.getDataSources(), filters.showAllWaypoints(), filters.getMostRecentNumDays(), filters.showWaypointsWithoutTimeStamp(), new WaypointFilterQueryCallBack() { + @Override + public void process(List waypoints) { + // If the list is empty, tell the user and do not change + // the visible waypoints. + if (waypoints == null || waypoints.isEmpty()) { + JOptionPane.showMessageDialog(GeolocationTopComponent.this, + Bundle.GeoTopComponent_no_waypoints_returned_Title(), + Bundle.GeoTopComponent_no_waypoints_returned_mgs(), + JOptionPane.INFORMATION_MESSAGE); + + return; + } + mapPanel.setWaypoints(MapWaypoint.getWaypoints(waypoints)); } - mapPanel.setWaypoints(waypoints); - - // There might be a better way to decide how to center - // but for now just use the first way point. - mapPanel.setCenterLocation(waypoints.get(0)); - - } catch (ExecutionException ex) { - logger.log(Level.WARNING, "An exception occured while initializing waypoints for geolocation window.", ex); //NON-NLS - MessageNotifyUtil.Message.error(Bundle.GLTopComponent_initilzation_error()); - } catch (InterruptedException ex) { - logger.log(Level.WARNING, "The initializing thread for geolocation window was interrupted.", ex); //NON-NLS - } + }); + } catch (GeoLocationDataException ex) { + logger.log(Level.SEVERE, "Failed to filter waypoints.", ex); + JOptionPane.showMessageDialog(GeolocationTopComponent.this, + Bundle.GeoTopComponent_filter_exception_Title(), + Bundle.GeoTopComponent_filter_exception_msg(), + JOptionPane.ERROR_MESSAGE); } } - }; - - worker.execute(); + }); } /** @@ -199,13 +255,18 @@ public final class GeolocationTopComponent extends TopComponent { private void initComponents() { mapPanel = new org.sleuthkit.autopsy.geolocation.MapPanel(); + filterPane = new org.sleuthkit.autopsy.geolocation.HidingPane(); setLayout(new java.awt.BorderLayout()); + + mapPanel.add(filterPane, java.awt.BorderLayout.LINE_START); + add(mapPanel, java.awt.BorderLayout.CENTER); }// //GEN-END:initComponents // Variables declaration - do not modify//GEN-BEGIN:variables + private org.sleuthkit.autopsy.geolocation.HidingPane filterPane; private org.sleuthkit.autopsy.geolocation.MapPanel mapPanel; // End of variables declaration//GEN-END:variables } diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/HidingPane.java b/Core/src/org/sleuthkit/autopsy/geolocation/HidingPane.java new file mode 100755 index 0000000000..e842e5df86 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/HidingPane.java @@ -0,0 +1,133 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.geolocation; + +import java.awt.BorderLayout; +import java.awt.Font; +import java.awt.Point; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import javax.swing.Icon; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTabbedPane; +import org.openide.util.NbBundle.Messages; + +/** + * + * A JTabbed pane with one tab that says "Filters". When the user clicks on that + * table the content of the tab will be hidden. + * + * The content pane provides support for scrolling. + */ +public final class HidingPane extends JTabbedPane { + + private static final long serialVersionUID = 1L; + + private final JScrollPane scrollPane; + private final JPanel panel; + private final JLabel tabLabel; + + private boolean panelVisible = true; + + /** + * Constructs a new HidingFilterPane + */ + @Messages({ + "HidingPane_default_title=Filters" + }) + public HidingPane() { + super(); + + scrollPane = new JScrollPane(); + panel = new JPanel(); + panel.setLayout(new BorderLayout()); + panel.add(scrollPane, BorderLayout.CENTER); + tabLabel = new JLabel(Bundle.HidingPane_default_title()); + tabLabel.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/funnel.png"))); + tabLabel.setUI(new VerticalLabelUI(true)); + tabLabel.setOpaque(false); + Font font = tabLabel.getFont().deriveFont(18).deriveFont(Font.BOLD); + tabLabel.setFont(font); + + addTab(null, panel); + setTabComponentAt(0, tabLabel); + + this.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent evt) { + handleMouseClick(evt.getPoint()); + } + }); + + this.setTabPlacement(JTabbedPane.RIGHT); + } + + /** + * Change the title of the tab. + * + * @param title + */ + void setTitle(String title) { + tabLabel.setText(title); + } + + /** + * Set the icon that appears on the tab. + * + * @param icon + */ + void setIcon(Icon icon) { + tabLabel.setIcon(icon); + } + + /** + * Set the content for this panel. + * + * @param panel A panel to display in the tabbed pane. + */ + void setPanel(JPanel panel) { + scrollPane.setViewportView(panel); + } + + /** + * Handle the mouse click. + * + * @param point + */ + private void handleMouseClick(Point point) { + int index = indexAtLocation(point.x, point.y); + + if(index == -1) { + return; + } + + if(panelVisible) { + panel.removeAll(); + panel.revalidate(); + panelVisible = false; + } else { + panel.add(scrollPane, BorderLayout.CENTER); + panel.revalidate(); + panelVisible = true; + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/MapPanel.java b/Core/src/org/sleuthkit/autopsy/geolocation/MapPanel.java index 326d33f23e..fb5cdc23e5 100755 --- a/Core/src/org/sleuthkit/autopsy/geolocation/MapPanel.java +++ b/Core/src/org/sleuthkit/autopsy/geolocation/MapPanel.java @@ -28,13 +28,17 @@ import java.awt.event.ComponentEvent; import java.awt.geom.Point2D; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; +import java.io.File; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.logging.Level; +import java.util.prefs.PreferenceChangeEvent; +import java.util.prefs.PreferenceChangeListener; import javax.swing.JMenuItem; +import javax.swing.JOptionPane; import javax.swing.JPopupMenu; import javax.swing.JSeparator; import javax.swing.Popup; @@ -42,21 +46,28 @@ import javax.swing.PopupFactory; import javax.swing.Timer; import javax.swing.event.MouseInputListener; import org.jxmapviewer.OSMTileFactoryInfo; +import org.jxmapviewer.VirtualEarthTileFactoryInfo; import org.jxmapviewer.input.CenterMapListener; import org.jxmapviewer.input.PanMouseInputListener; import org.jxmapviewer.input.ZoomMouseWheelListenerCursor; import org.jxmapviewer.viewer.DefaultTileFactory; import org.jxmapviewer.viewer.GeoPosition; +import org.jxmapviewer.viewer.TileFactory; import org.jxmapviewer.viewer.TileFactoryInfo; import org.jxmapviewer.viewer.Waypoint; import org.jxmapviewer.viewer.WaypointPainter; +import org.jxmapviewer.viewer.util.GeoUtil; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.core.UserPreferences; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import org.sleuthkit.autopsy.geolocation.datamodel.GeoLocationDataException; import org.sleuthkit.datamodel.TskCoreException; /** * The map panel. This panel contains the jxmapviewer MapViewer */ -final class MapPanel extends javax.swing.JPanel { +final public class MapPanel extends javax.swing.JPanel { private static final Logger logger = Logger.getLogger(MapPanel.class.getName()); @@ -76,9 +87,12 @@ final class MapPanel extends javax.swing.JPanel { /** * Creates new form MapPanel */ - MapPanel() { + @Messages({ + "MapPanel_connection_failure_message=Failed to connect to new geolocation map tile source.", + "MapPanel_connection_failure_message_title=Connection Failure" + }) + public MapPanel() { initComponents(); - initMap(); zoomChanging = false; currentPopup = null; @@ -94,14 +108,33 @@ final class MapPanel extends javax.swing.JPanel { showDetailsPopup(); } }); + + UserPreferences.addChangeListener(new PreferenceChangeListener() { + @Override + public void preferenceChange(PreferenceChangeEvent evt) { + try { + mapViewer.setTileFactory(new DefaultTileFactory(getTileFactoryInfo())); + initializeZoomSlider(); + } catch (GeoLocationDataException ex) { + logger.log(Level.SEVERE, "Failed to connect to new geolocation tile server.", ex); //NON-NLS + JOptionPane.showMessageDialog(MapPanel.this, + Bundle.MapPanel_connection_failure_message(), + Bundle.MapPanel_connection_failure_message_title(), + JOptionPane.ERROR_MESSAGE); + MessageNotifyUtil.Notify.error( + Bundle.MapPanel_connection_failure_message_title(), + Bundle.MapPanel_connection_failure_message()); + } + } + }); } /** * Initialize the map. */ - private void initMap() { + void initMap() throws GeoLocationDataException { - TileFactoryInfo info = new OSMTileFactoryInfo(); + TileFactoryInfo info = getTileFactoryInfo(); DefaultTileFactory tileFactory = new DefaultTileFactory(info); mapViewer.setTileFactory(tileFactory); @@ -125,7 +158,8 @@ final class MapPanel extends javax.swing.JPanel { zoomSlider.setMaximum(tileFactory.getInfo().getMaximumZoomLevel()); setZoom(tileFactory.getInfo().getMaximumZoomLevel() - 1); - mapViewer.setAddressLocation(new GeoPosition(0, 0)); + + mapViewer.setCenterPosition(new GeoPosition(0,0)); // Basic painters for the way points. WaypointPainter waypointPainter = new WaypointPainter() { @@ -145,6 +179,79 @@ final class MapPanel extends javax.swing.JPanel { mapViewer.setOverlayPainter(waypointPainter); } + /** + * Setup the zoom slider based on the current tileFactory. + */ + void initializeZoomSlider() { + TileFactory tileFactory = mapViewer.getTileFactory(); + zoomSlider.setMinimum(tileFactory.getInfo().getMinimumZoomLevel()); + zoomSlider.setMaximum(tileFactory.getInfo().getMaximumZoomLevel()); + + zoomSlider.repaint(); + zoomSlider.revalidate(); + } + + /** + * Create the TileFactoryInfo object based on the user preference. + * + * @return + */ + TileFactoryInfo getTileFactoryInfo() throws GeoLocationDataException { + switch (GeolocationSettingsPanel.GeolocationTileOption.getOptionForValue(UserPreferences.getGeolocationtTileOption())) { + case ONLINE_USER_DEFINED_OSM_SERVER: + return createOnlineOSMFactory(UserPreferences.getGeolocationOsmServerAddress()); + case OFFLINE_OSM_ZIP: + return createOSMZipFactory(UserPreferences.getGeolocationOsmZipPath()); + default: + return new VirtualEarthTileFactoryInfo(VirtualEarthTileFactoryInfo.MAP); + } + } + + /** + * Create the TileFactoryInfo for an online OSM tile server. + * + * @param address Tile server address + * + * @return TileFactoryInfo object for server address. + * + * @throws GeoLocationDataException + */ + private TileFactoryInfo createOnlineOSMFactory(String address) throws GeoLocationDataException { + if (address.isEmpty()) { + throw new GeoLocationDataException("Invalid user preference for OSM user define tile server. Address is an empty string."); + } else { + TileFactoryInfo info = new OSMTileFactoryInfo("User Defined Server", address); + if (!GeoUtil.isValidTile(1, 1, 1, info)) { + throw new GeoLocationDataException(String.format("Invalid OSM user define tile server: %s", address)); + } + return info; + } + } + + /** + * Create the TileFactoryInfo for OSM zip File + * + * @param zipPath Path to zip file. + * + * @return TileFactoryInfo for zip file. + * + * @throws GeoLocationDataException + */ + private TileFactoryInfo createOSMZipFactory(String path) throws GeoLocationDataException { + if (path.isEmpty()) { + throw new GeoLocationDataException("Invalid OSM tile Zip file. User preference value is empty string."); + } else { + File file = new File(path); + if (!file.exists() || !file.canRead()) { + throw new GeoLocationDataException("Invalid OSM tile zip file. Unable to read file: " + path); + } + + String zipPath = path.replaceAll("\\\\", "/"); + + return new OSMTileFactoryInfo("ZIP archive", "jar:file:/" + zipPath + "!"); //NON-NLS + } + } + /** * Stores the given List of MapWaypoint in a KdTree object. * @@ -160,15 +267,6 @@ final class MapPanel extends javax.swing.JPanel { mapViewer.repaint(); } - /** - * Centers the view of the map on the given location. - * - * @param waypoint Location to center the map - */ - void setCenterLocation(Waypoint waypoint) { - mapViewer.setCenterPosition(waypoint.getPosition()); - } - /** * Set the current zoom level. * diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/MapWaypoint.java b/Core/src/org/sleuthkit/autopsy/geolocation/MapWaypoint.java index 7eb765151a..7a28f49737 100755 --- a/Core/src/org/sleuthkit/autopsy/geolocation/MapWaypoint.java +++ b/Core/src/org/sleuthkit/autopsy/geolocation/MapWaypoint.java @@ -49,6 +49,7 @@ import org.sleuthkit.autopsy.geolocation.datamodel.GeoLocationDataException; import org.sleuthkit.autopsy.geolocation.datamodel.Route; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.autopsy.geolocation.datamodel.Waypoint; +import org.sleuthkit.autopsy.geolocation.datamodel.WaypointBuilder; import org.sleuthkit.autopsy.timeline.actions.ViewArtifactInTimelineAction; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; @@ -79,7 +80,7 @@ final class MapWaypoint extends KdTree.XYZPoint implements org.jxmapviewer.viewe * @throws GeoLocationDataException */ static List getWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { - List points = Waypoint.getAllWaypoints(skCase); + List points = WaypointBuilder.getAllWaypoints(skCase); List routes = Route.getRoutes(skCase); for (Route route : routes) { @@ -94,6 +95,28 @@ final class MapWaypoint extends KdTree.XYZPoint implements org.jxmapviewer.viewe return mapPoints; } + + /** + * Returns a list of of MapWaypoint objects for the given list of + * datamodel.Waypoint objects. + * + * @param dmWaypoints + * + * @return List of MapWaypoint objects. List will be empty if dmWaypoints was + * empty or null. + */ + static List getWaypoints(List dmWaypoints) { + List mapPoints = new ArrayList<>(); + + if (dmWaypoints != null) { + + for (Waypoint point : dmWaypoints) { + mapPoints.add(new MapWaypoint(point)); + } + } + + return mapPoints; + } /** * Returns a MapWaypoint without a reference to the datamodel waypoint. diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/RefreshPanel.form b/Core/src/org/sleuthkit/autopsy/geolocation/RefreshPanel.form index 400c788d42..145c22444a 100755 --- a/Core/src/org/sleuthkit/autopsy/geolocation/RefreshPanel.form +++ b/Core/src/org/sleuthkit/autopsy/geolocation/RefreshPanel.form @@ -42,7 +42,7 @@ - + @@ -63,7 +63,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/RefreshPanel.java b/Core/src/org/sleuthkit/autopsy/geolocation/RefreshPanel.java index a4e30c9129..f1f93a620d 100755 --- a/Core/src/org/sleuthkit/autopsy/geolocation/RefreshPanel.java +++ b/Core/src/org/sleuthkit/autopsy/geolocation/RefreshPanel.java @@ -82,13 +82,13 @@ final class RefreshPanel extends JPanel { gridBagConstraints.insets = new java.awt.Insets(15, 10, 15, 10); add(refreshLabel, gridBagConstraints); - refreshButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/geolocation/images/arrow-circle-double-135.png"))); // NOI18N + refreshButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/arrow-circle-double-135.png"))); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(refreshButton, org.openide.util.NbBundle.getMessage(RefreshPanel.class, "RefreshPanel.refreshButton.text")); // NOI18N refreshButton.setMargin(new java.awt.Insets(2, 5, 2, 5)); add(refreshButton, new java.awt.GridBagConstraints()); closeButton.setBackground(new java.awt.Color(0, 0, 0)); - closeButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/geolocation/images/cross-script.png"))); // NOI18N + closeButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/close-icon.png"))); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(closeButton, org.openide.util.NbBundle.getMessage(RefreshPanel.class, "RefreshPanel.closeButton.text")); // NOI18N closeButton.setMargin(new java.awt.Insets(0, 0, 0, 0)); closeButton.setOpaque(false); diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/VerticalLabelUI.java b/Core/src/org/sleuthkit/autopsy/geolocation/VerticalLabelUI.java new file mode 100755 index 0000000000..1a76953267 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/VerticalLabelUI.java @@ -0,0 +1,124 @@ +/* + * + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.geolocation; + +import java.awt.Dimension; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Insets; +import java.awt.Rectangle; +import java.awt.geom.AffineTransform; + +import javax.swing.Icon; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.plaf.basic.BasicLabelUI; + + +/** + * This class is an overload of BasicLabelUI to draw labels vertically. + * + * This code was found at: + * https://tech.chitgoks.com/2009/11/13/rotate-jlabel-vertically/ + * + */ +final class VerticalLabelUI extends BasicLabelUI { + + private static final Rectangle paintIconR = new Rectangle(); + private static final Rectangle paintTextR = new Rectangle(); + private static final Rectangle paintViewR = new Rectangle(); + private static Insets paintViewInsets = new Insets(0, 0, 0, 0); + + static { + labelUI = new VerticalLabelUI(false); + } + + final boolean clockwise; + + /** + * Construct a new VerticalLabelUI + * @param clockwise + */ + VerticalLabelUI(boolean clockwise) { + super(); + this.clockwise = clockwise; + } + + @Override + public Dimension getPreferredSize(JComponent c) { + Dimension dim = super.getPreferredSize(c); + return new Dimension( dim.height, dim.width ); + } + + @Override + public void paint(Graphics g, JComponent c) { + JLabel label = (JLabel)c; + String text = label.getText(); + Icon icon = (label.isEnabled()) ? label.getIcon() : label.getDisabledIcon(); + + if ((icon == null) && (text == null)) { + return; + } + + FontMetrics fm = g.getFontMetrics(); + paintViewInsets = c.getInsets(paintViewInsets); + + paintViewR.x = paintViewInsets.left; + paintViewR.y = paintViewInsets.top; + + // Use inverted height & width + paintViewR.height = c.getWidth() - (paintViewInsets.left + paintViewInsets.right); + paintViewR.width = c.getHeight() - (paintViewInsets.top + paintViewInsets.bottom); + + paintIconR.x = paintIconR.y = paintIconR.width = paintIconR.height = 0; + paintTextR.x = paintTextR.y = paintTextR.width = paintTextR.height = 0; + + String clippedText = layoutCL(label, fm, text, icon, paintViewR, paintIconR, paintTextR); + + Graphics2D g2 = (Graphics2D) g; + AffineTransform tr = g2.getTransform(); + if (clockwise) { + g2.rotate( Math.PI / 2 ); + g2.translate( 0, - c.getWidth() ); + } else { + g2.rotate( - Math.PI / 2 ); + g2.translate( - c.getHeight(), 0 ); + } + + if (icon != null) { + icon.paintIcon(c, g, paintIconR.x, paintIconR.y); + } + + if (text != null) { + int textX = paintTextR.x; + int textY = paintTextR.y + fm.getAscent(); + + if (label.isEnabled()) { + paintEnabledText(label, g, clippedText, textX, textY); + } else { + paintDisabledText(label, g, clippedText, textX, textY); + } + } + g2.setTransform( tr ); + } + +} + diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Waypoint.java b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Waypoint.java index bd5b39b6b7..27e5da0dbd 100755 --- a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Waypoint.java +++ b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Waypoint.java @@ -25,12 +25,9 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.logging.Level; -import java.util.logging.Logger; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardAttribute; -import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TskCoreException; /** @@ -68,8 +65,6 @@ public class Waypoint { BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_END, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE_END,}; - private static final Logger logger = Logger.getLogger(Waypoint.class.getName()); - /** * Construct a waypoint with the given artifact. * @@ -262,186 +257,6 @@ public class Waypoint { return attributeMap; } - - /** - * Returns a list of Waypoints for the artifacts with geolocation - * information. - * - * List will include artifacts of type: TSK_GPS_TRACKPOINT TSK_GPS_SEARCH - * TSK_GPS_LAST_KNOWN_LOCATION TSK_GPS_BOOKMARK TSK_METADATA_EXIF - * - * @param skCase Currently open SleuthkitCase - * - * @return List of Waypoint - * - * @throws GeoLocationDataException - */ - public static List getAllWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { - List points = new ArrayList<>(); - - points.addAll(getTrackpointWaypoints(skCase)); - points.addAll(getEXIFWaypoints(skCase)); - points.addAll(getSearchWaypoints(skCase)); - points.addAll(getLastKnownWaypoints(skCase)); - points.addAll(getBookmarkWaypoints(skCase)); - - return points; - } - - /** - * Gets a list of Waypoints for TSK_GPS_TRACKPOINT artifacts. - * - * @param skCase Currently open SleuthkitCase - * - * @return List of Waypoint - * - * @throws GeoLocationDataException - */ - public static List getTrackpointWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { - List artifacts = null; - try { - artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_TRACKPOINT); - } catch (TskCoreException ex) { - throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_TRACKPOINT", ex); - } - - List points = new ArrayList<>(); - for (BlackboardArtifact artifact : artifacts) { - try { - Waypoint point = new TrackpointWaypoint(artifact); - points.add(point); - } catch (GeoLocationDataException ex) { - logger.log(Level.WARNING, String.format("No longitude or latitude available for TSK_GPS_TRACKPOINT artifactID: %d", artifact.getArtifactID())); - } - } - return points; - } - - /** - * Gets a list of Waypoints for TSK_METADATA_EXIF artifacts. - * - * @param skCase Currently open SleuthkitCase - * - * @return List of Waypoint - * - * @throws GeoLocationDataException - */ - static public List getEXIFWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { - List artifacts = null; - try { - artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_METADATA_EXIF); - } catch (TskCoreException ex) { - throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_LAST_KNOWN_LOCATION", ex); - } - - List points = new ArrayList<>(); - if (artifacts != null) { - for (BlackboardArtifact artifact : artifacts) { - try { - Waypoint point = new EXIFWaypoint(artifact); - points.add(point); - } catch (GeoLocationDataException ex) { - // I am a little relucant to log this error because I suspect - // this will happen more often than not. It is valid for - // METADAT_EXIF to not have longitude and latitude - } - } - } - return points; - } - - /** - * Gets a list of Waypoints for TSK_GPS_SEARCH artifacts. - * - * @param skCase Currently open SleuthkitCase - * - * @return List of Waypoint - * - * @throws GeoLocationDataException - */ - public static List getSearchWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { - List artifacts = null; - try { - artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_SEARCH); - } catch (TskCoreException ex) { - throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_SEARCH", ex); - } - - List points = new ArrayList<>(); - if (artifacts != null) { - for (BlackboardArtifact artifact : artifacts) { - try { - Waypoint point = new SearchWaypoint(artifact); - points.add(point); - } catch (GeoLocationDataException ex) { - logger.log(Level.WARNING, String.format("No longitude or latitude available for TSK_GPS_SEARCH artifactID: %d", artifact.getArtifactID())); - } - } - } - return points; - } - - /** - * Gets a list of Waypoints for TSK_GPS_LAST_KNOWN_LOCATION artifacts. - * - * @param skCase Currently open SleuthkitCase - * - * @return List of Waypoint - * - * @throws GeoLocationDataException - */ - public static List getLastKnownWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { - List artifacts = null; - try { - artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_LAST_KNOWN_LOCATION); - } catch (TskCoreException ex) { - throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_LAST_KNOWN_LOCATION", ex); - } - - List points = new ArrayList<>(); - if (artifacts != null) { - for (BlackboardArtifact artifact : artifacts) { - try { - Waypoint point = new LastKnownWaypoint(artifact); - points.add(point); - } catch (GeoLocationDataException ex) { - logger.log(Level.WARNING, String.format("No longitude or latitude available for TSK_GPS_LAST_KNOWN_LOCATION artifactID: %d", artifact.getArtifactID())); - } - } - } - return points; - } - - /** - * Gets a list of Waypoints for TSK_GPS_BOOKMARK artifacts. - * - * @param skCase Currently open SleuthkitCase - * - * @return List of Waypoint - * - * @throws GeoLocationDataException - */ - public static List getBookmarkWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { - List artifacts = null; - try { - artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_BOOKMARK); - } catch (TskCoreException ex) { - throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_BOOKMARK", ex); - } - - List points = new ArrayList<>(); - if (artifacts != null) { - for (BlackboardArtifact artifact : artifacts) { - try { - Waypoint point = new Waypoint(artifact); - points.add(point); - } catch (GeoLocationDataException ex) { - logger.log(Level.WARNING, String.format("No longitude or latitude available for TSK_GPS_BOOKMARK artifactID: %d", artifact.getArtifactID())); - } - } - } - return points; - } /** * Get a list of Waypoint.Property objects for the given artifact. This list diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/WaypointBuilder.java b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/WaypointBuilder.java new file mode 100755 index 0000000000..7189302809 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/WaypointBuilder.java @@ -0,0 +1,486 @@ +/* + * + * Autopsy Forensic Browser + * + * Copyright 2019 Basis Technology Corp. + * contact: carrier sleuthkit org + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.sleuthkit.autopsy.geolocation.datamodel; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.CaseDbAccessManager; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.DataSource; + +/** + * Class for building lists of waypoints. + * + */ +public final class WaypointBuilder { + + private static final Logger logger = Logger.getLogger(WaypointBuilder.class.getName()); + + // SELECT statement for getting a list of waypoints. + final static String GEO_ARTIFACT_QUERY + = "SELECT artifact_id, artifact_type_id " + + "FROM blackboard_attributes " + + "WHERE attribute_type_id IN (%d, %d) "; //NON-NLS + + // SELECT statement to get only artifact_ids + final static String GEO_ARTIFACT_QUERY_ID_ONLY + = "SELECT artifact_id " + + "FROM blackboard_attributes " + + "WHERE attribute_type_id IN (%d, %d) "; //NON-NLS + + // This Query will return a list of waypoint artifacts + final static String GEO_ARTIFACT_WITH_DATA_SOURCES_QUERY + = "SELECT blackboard_attributes.artifact_id " + + "FROM blackboard_attributes, blackboard_artifacts " + + "WHERE blackboard_attributes.attribute_type_id IN(%d, %d) " + + "AND data_source_obj_id IN (%s)"; //NON-NLS + + // Select will return the "most recent" timestamp from all waypoings + final static String MOST_RECENT_TIME + = "SELECT MAX(value_int64) - (%d * 86400)" //86400 is the number of seconds in a day. + + "FROM blackboard_attributes " + + "WHERE attribute_type_id IN(%d, %d) " + + "AND artifact_id " + + "IN ( " + + "%s" //GEO_ARTIFACT with or without data source + + " )"; + + // Returns a list of artifacts with no time stamp + final static String SELECT_WO_TIMESTAMP = + "SELECT DISTINCT artifact_id, artifact_type_id " + + "FROM blackboard_attributes " + + "WHERE artifact_id NOT IN (%s) " + + "AND artifact_id IN (%s)"; //NON-NLS + + /** + * A callback interface to process the results of waypoint filtering. + */ + public interface WaypointFilterQueryCallBack { + + /** + * This function will be called after the waypoints have been filtered. + * + * @param wwaypoints This of waypoints. + */ + void process(List wwaypoints); + } + + /** + * private constructor + */ + private WaypointBuilder() { + + } + + /** + * Returns a list of Waypoints for the artifacts with geolocation + * information. + * + * List will include artifacts of type: TSK_GPS_TRACKPOINT TSK_GPS_SEARCH + * TSK_GPS_LAST_KNOWN_LOCATION TSK_GPS_BOOKMARK TSK_METADATA_EXIF + * + * @param skCase Currently open SleuthkitCase + * + * @return List of Waypoint + * + * @throws GeoLocationDataException + */ + public static List getAllWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { + List points = new ArrayList<>(); + + points.addAll(getTrackpointWaypoints(skCase)); + points.addAll(getEXIFWaypoints(skCase)); + points.addAll(getSearchWaypoints(skCase)); + points.addAll(getLastKnownWaypoints(skCase)); + points.addAll(getBookmarkWaypoints(skCase)); + + return points; + } + + /** + * Gets a list of Waypoints for TSK_GPS_TRACKPOINT artifacts. + * + * @param skCase Currently open SleuthkitCase + * + * @return List of Waypoint + * + * @throws GeoLocationDataException + */ + public static List getTrackpointWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { + List artifacts = null; + try { + artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_TRACKPOINT); + } catch (TskCoreException ex) { + throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_TRACKPOINT", ex);//NON-NLS + } + + List points = new ArrayList<>(); + for (BlackboardArtifact artifact : artifacts) { + try { + Waypoint point = new TrackpointWaypoint(artifact); + points.add(point); + } catch (GeoLocationDataException ex) { + logger.log(Level.WARNING, String.format("No longitude or latitude available for TSK_GPS_TRACKPOINT artifactID: %d", artifact.getArtifactID()));//NON-NLS + } + } + return points; + } + + /** + * Gets a list of Waypoints for TSK_METADATA_EXIF artifacts. + * + * @param skCase Currently open SleuthkitCase + * + * @return List of Waypoint + * + * @throws GeoLocationDataException + */ + static public List getEXIFWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { + List artifacts = null; + try { + artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_METADATA_EXIF); + } catch (TskCoreException ex) { + throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_LAST_KNOWN_LOCATION", ex);//NON-NLS + } + + List points = new ArrayList<>(); + if (artifacts != null) { + for (BlackboardArtifact artifact : artifacts) { + try { + Waypoint point = new EXIFWaypoint(artifact); + points.add(point); + } catch (GeoLocationDataException ex) { + // I am a little relucant to log this error because I suspect + // this will happen more often than not. It is valid for + // METADAT_EXIF to not have longitude and latitude + } + } + } + return points; + } + + /** + * Gets a list of Waypoints for TSK_GPS_SEARCH artifacts. + * + * @param skCase Currently open SleuthkitCase + * + * @return List of Waypoint + * + * @throws GeoLocationDataException + */ + public static List getSearchWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { + List artifacts = null; + try { + artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_SEARCH); + } catch (TskCoreException ex) { + throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_SEARCH", ex);//NON-NLS + } + + List points = new ArrayList<>(); + if (artifacts != null) { + for (BlackboardArtifact artifact : artifacts) { + try { + Waypoint point = new SearchWaypoint(artifact); + points.add(point); + } catch (GeoLocationDataException ex) { + logger.log(Level.WARNING, String.format("No longitude or latitude available for TSK_GPS_SEARCH artifactID: %d", artifact.getArtifactID()));//NON-NLS + } + } + } + return points; + } + + /** + * Gets a list of Waypoints for TSK_GPS_LAST_KNOWN_LOCATION artifacts. + * + * @param skCase Currently open SleuthkitCase + * + * @return List of Waypoint + * + * @throws GeoLocationDataException + */ + public static List getLastKnownWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { + List artifacts = null; + try { + artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_LAST_KNOWN_LOCATION); + } catch (TskCoreException ex) { + throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_LAST_KNOWN_LOCATION", ex);//NON-NLS + } + + List points = new ArrayList<>(); + if (artifacts != null) { + for (BlackboardArtifact artifact : artifacts) { + try { + Waypoint point = new LastKnownWaypoint(artifact); + points.add(point); + } catch (GeoLocationDataException ex) { + logger.log(Level.WARNING, String.format("No longitude or latitude available for TSK_GPS_LAST_KNOWN_LOCATION artifactID: %d", artifact.getArtifactID()));//NON-NLS + } + } + } + return points; + } + + /** + * Gets a list of Waypoints for TSK_GPS_BOOKMARK artifacts. + * + * @param skCase Currently open SleuthkitCase + * + * @return List of Waypoint + * + * @throws GeoLocationDataException + */ + public static List getBookmarkWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { + List artifacts = null; + try { + artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_BOOKMARK); + } catch (TskCoreException ex) { + throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_BOOKMARK", ex);//NON-NLS + } + + List points = new ArrayList<>(); + if (artifacts != null) { + for (BlackboardArtifact artifact : artifacts) { + try { + Waypoint point = new Waypoint(artifact); + points.add(point); + } catch (GeoLocationDataException ex) { + logger.log(Level.WARNING, String.format("No longitude or latitude available for TSK_GPS_BOOKMARK artifactID: %d", artifact.getArtifactID()), ex);//NON-NLS + } + } + } + return points; + } + + /** + * Get a filtered list of waypoints. + * + * If showAll is true, the values of cntDaysFromRecent and notTimeStamp will + * be ignored. + * + * To include data from all dataSources pass a null or empty dataSource + * list. + * + * + * @param skCase Currently open sleuthkit case. + * @param dataSources This of data sources to filter the waypoints by. + * Pass a null or empty list to show way points for + * all dataSources. + * + * @param showAll True to get all waypoints. + * + * @param cntDaysFromRecent Number of days from the most recent time stamp + * to get waypoints for. This parameter will be + * ignored if showAll is true; + * + * @param noTimeStamp True to include waypoints without timestamp. + * This parameter will be ignored if showAll is + * true. + * + * @param queryCallBack Function to call after the DB query has + * completed. + * + * @throws GeoLocationDataException + */ + static public void getAllWaypoints(SleuthkitCase skCase, List dataSources, boolean showAll, int cntDaysFromRecent, boolean noTimeStamp, WaypointFilterQueryCallBack queryCallBack) throws GeoLocationDataException { + String query = buildQuery(dataSources, showAll, cntDaysFromRecent, noTimeStamp); + + logger.log(Level.INFO, query); + + try { + // The CaseDBAccessManager.select function will add a SELECT + // to the beginning of the query + if (query.startsWith("SELECT")) { //NON-NLS + query = query.replaceFirst("SELECT", ""); //NON-NLS + } + + skCase.getCaseDbAccessManager().select(query, new CaseDbAccessManager.CaseDbAccessQueryCallback() { + @Override + public void process(ResultSet rs) { + List waypoints = new ArrayList<>(); + try { + while (rs.next()) { + int artifact_type_id = rs.getInt("artifact_type_id"); //NON-NLS + long artifact_id = rs.getLong("artifact_id"); //NON-NLS + + BlackboardArtifact.ARTIFACT_TYPE type = BlackboardArtifact.ARTIFACT_TYPE.fromID(artifact_type_id); + + waypoints.addAll(getWaypointForArtifact(skCase.getBlackboardArtifact(artifact_id), type)); + + } + queryCallBack.process(waypoints); + } catch (GeoLocationDataException | SQLException | TskCoreException ex) { + logger.log(Level.WARNING, "Failed to filter waypoint.", ex); //NON-NLS + } + + } + }); + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "Failed to filter waypoint.", ex); //NON-NLS + } + } + + /** + * Create the query for getting a list of waypoints that do not have time + * stamps. + * + * @param dataSources List of data Sources to filter by + * + * @return SQL SELECT statement + */ + static private String buildQueryForWaypointsWOTimeStamps(List dataSources) { + return String.format(SELECT_WO_TIMESTAMP, + String.format(GEO_ARTIFACT_QUERY_ID_ONLY, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME.getTypeID(), + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_CREATED.getTypeID()), + getWaypointListQuery(dataSources)); + } + + /** + * Build the query to filter the list of waypoints. + * + * If showAll is true, the values of cntDaysFromRecent and noTimeStamp are + * ignored. + * + * @param dataSources This of data sources to filter the waypoints by. + * Pass a null or empty list to show way points for + * all dataSources. + * + * @param showAll True to get all waypoints. + * + * @param cntDaysFromRecent Number of days from the most recent time stamp + * to get waypoints for. This parameter will be + * ignored if showAll is true; + * + * @param noTimeStamp True to include waypoints without timestamp. + * This parameter will be ignored if showAll is + * true. + * + * @return + */ + static private String buildQuery(List dataSources, boolean showAll, int cntDaysFromRecent, boolean noTimeStamp) { + String mostRecentQuery = ""; + + if (!showAll && cntDaysFromRecent > 0) { + mostRecentQuery = String.format("AND value_int64 > (%s)", //NON-NLS + String.format(MOST_RECENT_TIME, + cntDaysFromRecent, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME.getTypeID(), + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_CREATED.getTypeID(), + getWaypointListQuery(dataSources) + )); + } + + // This givens us all artifact_ID that have time stamp + String query = String.format(GEO_ARTIFACT_QUERY, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME.getTypeID(), + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_CREATED.getTypeID()); + + // That are in the list of artifacts for the given data Sources + query += String.format("AND artifact_id IN(%s)", getWaypointListQuery(dataSources)); //NON-NLS + query += mostRecentQuery; + + if (showAll || noTimeStamp) { + query = String.format("%s UNION %s", buildQueryForWaypointsWOTimeStamps(dataSources), query); //NON-NLS + } + + return query; + } + + /** + * Returns the query to get a list of waypoints filted by the given data + * sources. + * + * An artifact is assumed to be a "waypoint" if it has the attributes + * TSK_GEO_LATITUDE or TSK_GEO_LATITUDE_START + * + * @param dataSources A list of data sources to filter by. If the list is + * null or empty the data source list will be ignored. + * + * @return + */ + static private String getWaypointListQuery(List dataSources) { + + if (dataSources == null || dataSources.isEmpty()) { + return String.format(GEO_ARTIFACT_QUERY, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE.getTypeID(), + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_START.getTypeID()); + } + + String dataSourceList = ""; + for (DataSource source : dataSources) { + dataSourceList += Long.toString(source.getId()) + ","; + } + + if (!dataSourceList.isEmpty()) { + // Remove the last , + dataSourceList = dataSourceList.substring(0, dataSourceList.length() - 1); + } + + return String.format(GEO_ARTIFACT_WITH_DATA_SOURCES_QUERY, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE.getTypeID(), + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_START.getTypeID(), + dataSourceList); + } + + /** + * Create a Waypoint object for the given Blackboard artifact. + * + * @param artifact The artifact to create the waypoint from + * @param type The type of artifact + * + * @return A new waypoint object + * + * @throws GeoLocationDataException + */ + static private List getWaypointForArtifact(BlackboardArtifact artifact, BlackboardArtifact.ARTIFACT_TYPE type) throws GeoLocationDataException { + List waypoints = new ArrayList<>(); + switch (type) { + case TSK_METADATA_EXIF: + waypoints.add(new EXIFWaypoint(artifact)); + break; + case TSK_GPS_BOOKMARK: + waypoints.add(new Waypoint(artifact)); + break; + case TSK_GPS_TRACKPOINT: + waypoints.add(new TrackpointWaypoint(artifact)); + break; + case TSK_GPS_SEARCH: + waypoints.add(new SearchWaypoint(artifact)); + break; + case TSK_GPS_ROUTE: + Route route = new Route(artifact); + waypoints.addAll(route.getRoute()); + break; + default: + waypoints.add(new Waypoint(artifact)); + break; + } + + return waypoints; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/images/cross-script.png b/Core/src/org/sleuthkit/autopsy/geolocation/images/cross-script.png deleted file mode 100755 index f37cf41830..0000000000 Binary files a/Core/src/org/sleuthkit/autopsy/geolocation/images/cross-script.png and /dev/null differ diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/images/arrow-circle-double-135.png b/Core/src/org/sleuthkit/autopsy/images/arrow-circle-double-135.png similarity index 100% rename from Core/src/org/sleuthkit/autopsy/geolocation/images/arrow-circle-double-135.png rename to Core/src/org/sleuthkit/autopsy/images/arrow-circle-double-135.png diff --git a/Core/src/org/sleuthkit/autopsy/images/blueGeo16.png b/Core/src/org/sleuthkit/autopsy/images/blueGeo16.png new file mode 100755 index 0000000000..b89f013c41 Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/images/blueGeo16.png differ diff --git a/Core/src/org/sleuthkit/autopsy/images/blueGeo32.png b/Core/src/org/sleuthkit/autopsy/images/blueGeo32.png new file mode 100755 index 0000000000..b7cf458039 Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/images/blueGeo32.png differ diff --git a/Core/src/org/sleuthkit/autopsy/images/blueGeo64.png b/Core/src/org/sleuthkit/autopsy/images/blueGeo64.png new file mode 100755 index 0000000000..cbbfbf3764 Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/images/blueGeo64.png differ diff --git a/Core/src/org/sleuthkit/autopsy/images/funnel.png b/Core/src/org/sleuthkit/autopsy/images/funnel.png new file mode 100755 index 0000000000..1f69604528 Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/images/funnel.png differ diff --git a/Core/src/org/sleuthkit/autopsy/report/modules/kml/KMLReport.java b/Core/src/org/sleuthkit/autopsy/report/modules/kml/KMLReport.java index eabdeef98b..a140338524 100644 --- a/Core/src/org/sleuthkit/autopsy/report/modules/kml/KMLReport.java +++ b/Core/src/org/sleuthkit/autopsy/report/modules/kml/KMLReport.java @@ -47,6 +47,7 @@ import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.geolocation.datamodel.GeoLocationDataException; import org.sleuthkit.autopsy.geolocation.datamodel.Waypoint; import org.sleuthkit.autopsy.geolocation.datamodel.Route; +import org.sleuthkit.autopsy.geolocation.datamodel.WaypointBuilder; import org.sleuthkit.autopsy.report.ReportBranding; import org.sleuthkit.autopsy.report.ReportProgressPanel; import org.sleuthkit.datamodel.AbstractFile; @@ -331,11 +332,11 @@ class KMLReport implements GeneralReportModule { * @throws IOException */ void addLocationsToReport(SleuthkitCase skCase, String baseReportDir) throws GeoLocationDataException, IOException { - addExifMetadataContent(Waypoint.getEXIFWaypoints(skCase), baseReportDir); - addWaypoints(Waypoint.getBookmarkWaypoints(skCase), gpsBookmarksFolder, FeatureColor.BLUE, Bundle.Waypoint_Bookmark_Display_String()); - addWaypoints(Waypoint.getLastKnownWaypoints(skCase), gpsLastKnownLocationFolder, FeatureColor.PURPLE, Bundle.Waypoint_Last_Known_Display_String()); - addWaypoints(Waypoint.getSearchWaypoints(skCase), gpsSearchesFolder, FeatureColor.WHITE, Bundle.Waypoint_Search_Display_String()); - addWaypoints(Waypoint.getTrackpointWaypoints(skCase), gpsTrackpointsFolder, FeatureColor.WHITE, Bundle.Waypoint_Trackpoint_Display_String()); + addExifMetadataContent(WaypointBuilder.getEXIFWaypoints(skCase), baseReportDir); + addWaypoints(WaypointBuilder.getBookmarkWaypoints(skCase), gpsBookmarksFolder, FeatureColor.BLUE, Bundle.Waypoint_Bookmark_Display_String()); + addWaypoints(WaypointBuilder.getLastKnownWaypoints(skCase), gpsLastKnownLocationFolder, FeatureColor.PURPLE, Bundle.Waypoint_Last_Known_Display_String()); + addWaypoints(WaypointBuilder.getSearchWaypoints(skCase), gpsSearchesFolder, FeatureColor.WHITE, Bundle.Waypoint_Search_Display_String()); + addWaypoints(WaypointBuilder.getTrackpointWaypoints(skCase), gpsTrackpointsFolder, FeatureColor.WHITE, Bundle.Waypoint_Trackpoint_Display_String()); } /** diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/BulkDrawableFilesTask.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/BulkDrawableFilesTask.java index f1d081689a..95ee3c9852 100755 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/BulkDrawableFilesTask.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/BulkDrawableFilesTask.java @@ -36,8 +36,8 @@ import org.sleuthkit.datamodel.TskData; * records for multiple drawable files. */ @NbBundle.Messages({ - "BulkDrawableFilesTask.committingDb.status=committing image/video database", - "BulkDrawableFilesTask.stopCopy.status=Stopping copy to drawable db task.", + "BulkDrawableFilesTask.committingDb.status=committing image/video database", + "BulkDrawableFilesTask.stopCopy.status=Stopping copy to drawable db task.", "BulkDrawableFilesTask.errPopulating.errMsg=There was an error populating Image Gallery database." }) abstract class BulkDrawableFilesTask extends DrawableDbTask { @@ -58,7 +58,7 @@ abstract class BulkDrawableFilesTask extends DrawableDbTask { this.taskDB = controller.getDrawablesDatabase(); this.tskCase = controller.getCaseDatabase(); this.dataSourceObjId = dataSourceObjId; - drawableQuery = " (data_source_obj_id = " + dataSourceObjId + ") " + drawableQuery = " (data_source_obj_id = " + dataSourceObjId + ") " + " AND ( meta_type = " + TskData.TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_REG.getValue() + ")" + " AND ( " + MIMETYPE_CLAUSE //NON-NLS + " OR mime_type LIKE 'video/%' OR mime_type LIKE 'image/%' )" //NON-NLS + " ORDER BY parent_path "; @@ -171,7 +171,11 @@ abstract class BulkDrawableFilesTask extends DrawableDbTask { // Mark to REBUILT_STALE if some files didnt' have MIME (ingest was still ongoing) or // if there was cancellation or errors DrawableDB.DrawableDbBuildStatusEnum datasourceDrawableDBStatus = ((hasFilesWithNoMime == true) || (endedEarly == true)) ? DrawableDB.DrawableDbBuildStatusEnum.REBUILT_STALE : DrawableDB.DrawableDbBuildStatusEnum.COMPLETE; - taskDB.insertOrUpdateDataSource(dataSourceObjId, datasourceDrawableDBStatus); + try { + taskDB.insertOrUpdateDataSource(dataSourceObjId, datasourceDrawableDBStatus); + } catch (SQLException ex) { + logger.log(Level.SEVERE, String.format("Error updating datasources table (data source object ID = %d, status = %s)", dataSourceObjId, datasourceDrawableDBStatus.toString(), ex)); //NON-NLS + } updateMessage(""); updateProgress(-1.0); } diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java index 088e0b8140..9d8c8b1269 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/ImageGalleryController.java @@ -801,7 +801,11 @@ public final class ImageGalleryController { if (((AutopsyEvent) event).getSourceType() == AutopsyEvent.SourceType.LOCAL) { Content newDataSource = (Content) event.getNewValue(); if (isListeningEnabled()) { - drawableDB.insertOrUpdateDataSource(newDataSource.getId(), DrawableDB.DrawableDbBuildStatusEnum.UNKNOWN); + try { + drawableDB.insertOrUpdateDataSource(newDataSource.getId(), DrawableDB.DrawableDbBuildStatusEnum.UNKNOWN); + } catch (SQLException ex) { + logger.log(Level.SEVERE, String.format("Error updating datasources table (data source object ID = %d, status = %s)", newDataSource.getId(), DrawableDB.DrawableDbBuildStatusEnum.UNKNOWN.toString()), ex); //NON-NLS + } } } break; @@ -883,7 +887,7 @@ public final class ImageGalleryController { default: break; } - } catch (TskCoreException ex) { + } catch (TskCoreException | SQLException ex) { logger.log(Level.SEVERE, String.format("Failed to handle %s event for %s (objId=%d)", dataSourceEvent.getPropertyName(), dataSource.getName(), dataSourceObjId), ex); } } @@ -898,7 +902,7 @@ public final class ImageGalleryController { * @throws TskCoreException If there is an error adding the data source to * the database. */ - private void handleDataSourceAnalysisStarted(DataSourceAnalysisEvent event) throws TskCoreException { + private void handleDataSourceAnalysisStarted(DataSourceAnalysisEvent event) throws TskCoreException, SQLException { if (event.getSourceType() == AutopsyEvent.SourceType.LOCAL && isListeningEnabled()) { Content dataSource = event.getDataSource(); long dataSourceObjId = dataSource.getId(); @@ -919,7 +923,7 @@ public final class ImageGalleryController { * @throws TskCoreException If there is an error updating the state ot the * data source in the database. */ - private void handleDataSourceAnalysisCompleted(DataSourceAnalysisEvent event) throws TskCoreException { + private void handleDataSourceAnalysisCompleted(DataSourceAnalysisEvent event) throws TskCoreException, SQLException { if (event.getSourceType() == AutopsyEvent.SourceType.LOCAL) { Content dataSource = event.getDataSource(); long dataSourceObjId = dataSource.getId(); diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java index d875657cc1..3e409f97fc 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/datamodel/DrawableDB.java @@ -49,7 +49,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.logging.Level; -import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.annotation.concurrent.GuardedBy; import javax.swing.SortOrder; @@ -80,130 +79,160 @@ import org.sleuthkit.datamodel.TskData.DbType; import org.sleuthkit.datamodel.TskDataException; import org.sleuthkit.datamodel.VersionNumber; import org.sqlite.SQLiteJDBCLoader; +import java.util.stream.Collectors; /** - * Provides access to the drawables database and selected tables in the case + * Provides access to the image gallery database and selected tables in the case * database. */ public final class DrawableDB { private static final Logger logger = Logger.getLogger(DrawableDB.class.getName()); - //column name constants////////////////////// - private static final String ANALYZED = "analyzed"; //NON-NLS - - private static final String OBJ_ID = "obj_id"; //NON-NLS - - private static final String HASH_SET_NAME = "hash_set_name"; //NON-NLS - - private static final String GROUPS_TABLENAME = "image_gallery_groups"; //NON-NLS - private static final String GROUPS_SEEN_TABLENAME = "image_gallery_groups_seen"; //NON-NLS - - private static final String IG_DB_INFO_TABLE = "image_gallery_db_info"; - + /* + * Schema version management constants. + */ + private static final VersionNumber IG_STARTING_SCHEMA_VERSION = new VersionNumber(1, 0, 0); // Historical - DO NOT CHANGE + private static final VersionNumber IG_SCHEMA_VERSION = new VersionNumber(1, 2, 0); // Current schema version private static final String IG_SCHEMA_MAJOR_VERSION_KEY = "IG_SCHEMA_MAJOR_VERSION"; private static final String IG_SCHEMA_MINOR_VERSION_KEY = "IG_SCHEMA_MINOR_VERSION"; private static final String IG_CREATION_SCHEMA_MAJOR_VERSION_KEY = "IG_CREATION_SCHEMA_MAJOR_VERSION"; private static final String IG_CREATION_SCHEMA_MINOR_VERSION_KEY = "IG_CREATION_SCHEMA_MINOR_VERSION"; + private static final String DB_INFO_TABLE_NAME = "image_gallery_db_info"; - private static final VersionNumber IG_STARTING_SCHEMA_VERSION = new VersionNumber(1, 0, 0); // IG Schema Starting version - DO NOT CHANGE - private static final VersionNumber IG_SCHEMA_VERSION = new VersionNumber(1, 2, 0); // IG Schema Current version - - private PreparedStatement insertHashSetStmt; - - private List preparedStatements = new ArrayList<>(); - - private PreparedStatement removeFileStmt; - - private PreparedStatement selectHashSetStmt; - - private PreparedStatement selectHashSetNamesStmt; - - private PreparedStatement insertHashHitStmt; - - private PreparedStatement removeHashHitStmt; - - private PreparedStatement updateDataSourceStmt; - - private PreparedStatement updateFileStmt; - private PreparedStatement insertFileStmt; - - private PreparedStatement pathGroupStmt; - - private PreparedStatement nameGroupStmt; - - private PreparedStatement created_timeGroupStmt; - - private PreparedStatement modified_timeGroupStmt; - - private PreparedStatement makeGroupStmt; - - private PreparedStatement modelGroupStmt; - - private PreparedStatement analyzedGroupStmt; - - private PreparedStatement hashSetGroupStmt; - - private PreparedStatement pathGroupFilterByDataSrcStmt; - - private PreparedStatement deleteDataSourceStmt; - - /** - * map from {@link DrawableAttribute} to the {@link PreparedStatement} that - * is used to select groups for that attribute + /* + * The image gallery stores data in both the case database and the image + * gallery database. The use of image gallery tables in the case database + * enables sharing of selected data between users of multi-user cases. This + * is necessary because the image gallery database is otherwise private to + * one node/machine. + * + * TODO: Consider refactoring to separate the image gallery database code + * from the case database code. */ + private static final String CASE_DB_GROUPS_TABLENAME = "image_gallery_groups"; //NON-NLS + private static final String CASE_DB_GROUPS_SEEN_TABLENAME = "image_gallery_groups_seen"; //NON-NLS + private final SleuthkitCase caseDb; + + /* + * The image gallery database is an SQLite database, so it has a local file + * path. For multi-user cases, there is a private image gallery database for + * each node/machine. + */ + private final Path dbPath; + + /* + * The write lock of a reentrant read-write lock is used to serialize access + * to the image gallery database. Empirically, this provides better + * performance than relying on internal SQLite locking. + */ + private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(true); //use fairness policy + private final Lock dbLock = rwLock.writeLock(); + @GuardedBy("dbLock") + private Connection con; + + /* + * Prepared statements. + */ + private List preparedStatements = new ArrayList<>(); + private PreparedStatement selectCountDataSourceIDs; + private PreparedStatement insertDataSourceStmt; + private PreparedStatement updateDataSourceStmt; + private PreparedStatement deleteDataSourceStmt; + private PreparedStatement insertFileStmt; + private PreparedStatement updateFileStmt; + private PreparedStatement deleteFileStmt; + private PreparedStatement insertHashSetStmt; + private PreparedStatement selectHashSetStmt; + private PreparedStatement selectHashSetNamesStmt; + private PreparedStatement insertHashHitStmt; + private PreparedStatement deleteHashHitStmt; + private PreparedStatement pathGroupStmt; // Not unused, used via collections below + private PreparedStatement nameGroupStmt; // Not unused, used via collections below + private PreparedStatement createdTimeGroupStmt; // Not unused, used via collections below + private PreparedStatement modifiedTimeGroupStmt; // Not unused, used via collections below + private PreparedStatement makeGroupStmt; // Not unused, used via collections below + private PreparedStatement modelGroupStmt; // Not unused, used via collections below + private PreparedStatement analyzedGroupStmt; // Not unused, used via collections below + private PreparedStatement hashSetGroupStmt; // Not unused, used via collections below + private PreparedStatement pathGroupFilterByDataSrcStmt; // Not unused, used via collections below private final Map, PreparedStatement> groupStatementMap = new HashMap<>(); private final Map, PreparedStatement> groupStatementFilterByDataSrcMap = new HashMap<>(); + /* + * Various caches are used to reduce the need for database queries. + */ + private final Cache groupCache = CacheBuilder.newBuilder().expireAfterWrite(5, TimeUnit.MINUTES).build(); + private final Cache, Boolean> groupSeenCache = CacheBuilder.newBuilder().expireAfterWrite(30, TimeUnit.SECONDS).build(); + private final Set hasTagsCache = new HashSet<>(); // Object IDs of files with tags + private final Set hasHashHitsCache = new HashSet<>(); // Object IDs of files with hash set hits + private final Set hasExifDataCache = new HashSet<>(); // Object IDs of files with EXIF data (make/model) + private final Object cacheLock = new Object(); + private boolean areCachesLoaded = false; + private int cacheBuildCount = 0; // Number of tasks that requested the caches be built + + /* + * This class is coupled to the image gallery controller and group manager. + * + * TODO: It would be better to reduce the coupling so that the controller + * and group manager call this class, but this class does not call them. + */ + private final ImageGalleryController controller; private final GroupManager groupManager; - private final Path dbPath; - - @GuardedBy("DBLock") - private Connection con; - - private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(true); //use fairness policy - - private final Lock DBLock = rwLock.writeLock(); // Currently serializing everything with one database connection - - // caches to make inserts / updates faster - private Cache groupCache = CacheBuilder.newBuilder().expireAfterWrite(5, TimeUnit.MINUTES).build(); - private final Cache, Boolean> groupSeenCache = CacheBuilder.newBuilder().expireAfterWrite(30, TimeUnit.SECONDS).build(); - private final Object cacheLock = new Object(); // protects access to the below cache-related objects - private boolean areCachesLoaded = false; // if true, the below caches contain valid data - private Set hasTagCache = new HashSet<>(); // contains obj id of files with tags - private Set hasHashCache = new HashSet<>(); // obj id of files with hash set hits - private Set hasExifCache = new HashSet<>(); // obj id of files with EXIF (make/model) - private int cacheBuildCount = 0; // number of tasks taht requested the caches be built - - static {//make sure sqlite driver is loaded // possibly redundant + /* + * Make sure the SQLite JDBC driver is loaded. + */ + static { try { Class.forName("org.sqlite.JDBC"); } catch (ClassNotFoundException ex) { logger.log(Level.SEVERE, "Failed to load sqlite JDBC driver", ex); //NON-NLS } } - private final SleuthkitCase tskCase; - private final ImageGalleryController controller; /** - * Enum to track Image gallery db rebuild status for a data source + * Enum for tracking the status of the image gallery database with respect + * to the data sources in the case. * - * DO NOT add in the middle. + * IMPORTANT: ADD NEW STATUSES TO THE END OF THE LIST + * + * TODO: I'm (RC) not sure why this is required, it looks like the enum + * element names are stored in the image gallery database. Are the raw + * cardinal values used somewhere? */ public enum DrawableDbBuildStatusEnum { - UNKNOWN, /// no known status - not yet analyzed - IN_PROGRESS, /// ingest or db rebuild is in progress - COMPLETE, /// At least one file in the data source had a MIME type. Ingest filters may have been applied. - REBUILT_STALE; /// data source was rebuilt, but MIME types were missing during rebuild + /** + * The data source has been added to the database, but no other data + * pertaining to it has been added. + */ + UNKNOWN, + /** + * Analyis (an ingest job or image gallery database rebuild) for the + * data source is in progress. + */ + IN_PROGRESS, + /** + * Analyis (an ingest job or image gallery database rebuild) for the + * data source has been completed and at least one file in the data + * source has a MIME type (ingest filters may have been applied, so some + * files may not have been typed). + */ + COMPLETE, + /** + * Analyis (an ingest job or image gallery database rebuild) for the + * data source has been completed, but the files for the data source + * were not assigned a MIME type (file typing was not enabled). + */ + REBUILT_STALE; } private void dbWriteLock() { - DBLock.lock(); + dbLock.lock(); } private void dbWriteUnlock() { - DBLock.unlock(); + dbLock.unlock(); } /** @@ -224,13 +253,13 @@ public final class DrawableDB { private DrawableDB(Path dbPath, ImageGalleryController controller) throws IOException, SQLException, TskCoreException { this.dbPath = dbPath; this.controller = controller; - tskCase = this.controller.getCaseDatabase(); + caseDb = this.controller.getCaseDatabase(); groupManager = this.controller.getGroupManager(); Files.createDirectories(this.dbPath.getParent()); dbWriteLock(); try { con = DriverManager.getConnection("jdbc:sqlite:" + dbPath.toString()); //NON-NLS - if (!initializeDBSchema() || !upgradeDBSchema() || !prepareStatements() || !initializeStandardGroups() || !initializeImageList()) { + if (!initializeDBSchema() || !upgradeDBSchema() || !prepareStatements() || !initializeStandardGroups() || !removeDeletedDataSources() || !initializeImageList()) { close(); throw new TskCoreException("Failed to initialize drawables database for Image Gallery use"); //NON-NLS } @@ -241,32 +270,29 @@ public final class DrawableDB { private boolean prepareStatements() { try { - updateFileStmt = prepareStatement( - "INSERT OR REPLACE INTO drawable_files (obj_id, data_source_obj_id, path, name, created_time, modified_time, make, model, analyzed) " //NON-NLS - + "VALUES (?,?,?,?,?,?,?,?,?)"); //NON-NLS - insertFileStmt = prepareStatement( - "INSERT OR IGNORE INTO drawable_files (obj_id, data_source_obj_id, path, name, created_time, modified_time, make, model, analyzed) " //NON-NLS - + "VALUES (?,?,?,?,?,?,?,?,?)"); //NON-NLS - updateDataSourceStmt = prepareStatement( - "INSERT OR REPLACE INTO datasources (ds_obj_id, drawable_db_build_status) " //NON-NLS - + " VALUES (?,?)"); //NON-NLS - removeFileStmt = prepareStatement("DELETE FROM drawable_files WHERE obj_id = ?"); //NON-NLS + selectCountDataSourceIDs = prepareStatement("SELECT COUNT(*) FROM datasources WHERE ds_obj_id = ?"); //NON-NLS + insertDataSourceStmt = prepareStatement("INSERT INTO datasources (ds_obj_id, drawable_db_build_status) VALUES (?,?)"); //NON-NLS + updateDataSourceStmt = prepareStatement("UPDATE datasources SET drawable_db_build_status = ? WHERE ds_obj_id = ?"); //NON-NLS + deleteDataSourceStmt = prepareStatement("DELETE FROM datasources where ds_obj_id = ?"); //NON-NLS + insertFileStmt = prepareStatement("INSERT OR IGNORE INTO drawable_files (obj_id, data_source_obj_id, path, name, created_time, modified_time, make, model, analyzed) VALUES (?,?,?,?,?,?,?,?,?)"); //NON-NLS + updateFileStmt = prepareStatement("INSERT OR REPLACE INTO drawable_files (obj_id, data_source_obj_id, path, name, created_time, modified_time, make, model, analyzed) VALUES (?,?,?,?,?,?,?,?,?)"); //NON-NLS + deleteFileStmt = prepareStatement("DELETE FROM drawable_files WHERE obj_id = ?"); //NON-NLS + insertHashSetStmt = prepareStatement("INSERT OR IGNORE INTO hash_sets (hash_set_name) VALUES (?)"); //NON-NLS + selectHashSetStmt = prepareStatement("SELECT hash_set_id FROM hash_sets WHERE hash_set_name = ?"); //NON-NLS + selectHashSetNamesStmt = prepareStatement("SELECT DISTINCT hash_set_name FROM hash_sets"); //NON-NLS + insertHashHitStmt = prepareStatement("INSERT OR IGNORE INTO hash_set_hits (hash_set_id, obj_id) VALUES (?,?)"); //NON-NLS + deleteHashHitStmt = prepareStatement("DELETE FROM hash_set_hits WHERE obj_id = ?"); //NON-NLS pathGroupStmt = prepareStatement("SELECT obj_id , analyzed FROM drawable_files WHERE path = ? ", DrawableAttribute.PATH); //NON-NLS nameGroupStmt = prepareStatement("SELECT obj_id , analyzed FROM drawable_files WHERE name = ? ", DrawableAttribute.NAME); //NON-NLS - created_timeGroupStmt = prepareStatement("SELECT obj_id , analyzed FROM drawable_files WHERE created_time = ? ", DrawableAttribute.CREATED_TIME); //NON-NLS - modified_timeGroupStmt = prepareStatement("SELECT obj_id , analyzed FROM drawable_files WHERE modified_time = ? ", DrawableAttribute.MODIFIED_TIME); //NON-NLS + createdTimeGroupStmt = prepareStatement("SELECT obj_id , analyzed FROM drawable_files WHERE created_time = ? ", DrawableAttribute.CREATED_TIME); //NON-NLS + modifiedTimeGroupStmt = prepareStatement("SELECT obj_id , analyzed FROM drawable_files WHERE modified_time = ? ", DrawableAttribute.MODIFIED_TIME); //NON-NLS makeGroupStmt = prepareStatement("SELECT obj_id , analyzed FROM drawable_files WHERE make = ? ", DrawableAttribute.MAKE); //NON-NLS modelGroupStmt = prepareStatement("SELECT obj_id , analyzed FROM drawable_files WHERE model = ? ", DrawableAttribute.MODEL); //NON-NLS analyzedGroupStmt = prepareStatement("SELECT obj_id , analyzed FROM drawable_files WHERE analyzed = ?", DrawableAttribute.ANALYZED); //NON-NLS hashSetGroupStmt = prepareStatement("SELECT drawable_files.obj_id AS obj_id, analyzed FROM drawable_files , hash_sets , hash_set_hits WHERE drawable_files.obj_id = hash_set_hits.obj_id AND hash_sets.hash_set_id = hash_set_hits.hash_set_id AND hash_sets.hash_set_name = ?", DrawableAttribute.HASHSET); //NON-NLS pathGroupFilterByDataSrcStmt = prepareFilterByDataSrcStatement("SELECT obj_id , analyzed FROM drawable_files WHERE path = ? AND data_source_obj_id = ?", DrawableAttribute.PATH); - selectHashSetNamesStmt = prepareStatement("SELECT DISTINCT hash_set_name FROM hash_sets"); //NON-NLS - insertHashSetStmt = prepareStatement("INSERT OR IGNORE INTO hash_sets (hash_set_name) VALUES (?)"); //NON-NLS - selectHashSetStmt = prepareStatement("SELECT hash_set_id FROM hash_sets WHERE hash_set_name = ?"); //NON-NLS - insertHashHitStmt = prepareStatement("INSERT OR IGNORE INTO hash_set_hits (hash_set_id, obj_id) VALUES (?,?)"); //NON-NLS - removeHashHitStmt = prepareStatement("DELETE FROM hash_set_hits WHERE obj_id = ?"); //NON-NLS - deleteDataSourceStmt = prepareStatement("DELETE FROM datasources where ds_obj_id = ?"); //NON-NLS return true; + } catch (TskCoreException | SQLException ex) { logger.log(Level.SEVERE, "Failed to prepare all statements", ex); //NON-NLS return false; @@ -276,7 +302,7 @@ public final class DrawableDB { private boolean initializeStandardGroups() { CaseDbTransaction caseDbTransaction = null; try { - caseDbTransaction = tskCase.beginTransaction(); + caseDbTransaction = caseDb.beginTransaction(); for (DhsImageCategory cat : DhsImageCategory.values()) { insertGroup(cat.getDisplayName(), DrawableAttribute.CATEGORY, caseDbTransaction); } @@ -374,6 +400,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 = caseDb.executeQuery("SELECT obj_id FROM data_source_info"); //NON-NLS + Statement drawablesDbStmt = con.createStatement()) { + /* + * Get the data source object IDs from the case database. + */ + ResultSet caseDbResults = caseDbQuery.getResultSet(); + Set currentDataSourceObjIDs = new HashSet<>(); + while (caseDbResults.next()) { + currentDataSourceObjIDs.add(caseDbResults.getLong(1)); + } + + /* + * Get the data source object IDs from the drawables database and + * determine which ones, if any, have been deleted from the case + * database. + */ + List staleDataSourceObjIDs = new ArrayList<>(); + try (ResultSet drawablesDbResults = drawablesDbStmt.executeQuery("SELECT ds_obj_id FROM datasources")) { //NON-NLS + while (drawablesDbResults.next()) { + long dataSourceObjID = drawablesDbResults.getLong(1); + if (!currentDataSourceObjIDs.contains(dataSourceObjID)) { + staleDataSourceObjIDs.add(dataSourceObjID); + } + } + } + + /* + * Delete the surplus data sources from this local drawables + * database. The delete cascades. + */ + if (!staleDataSourceObjIDs.isEmpty()) { + String deleteCommand = "DELETE FROM datasources where ds_obj_id IN (" + StringUtils.join(staleDataSourceObjIDs, ',') + ")"; //NON-NLS + drawablesDbStmt.execute(deleteCommand); + } + return true; + + } catch (TskCoreException | SQLException ex) { + logger.log(Level.SEVERE, "Failed to remove deleted data sources from drawables database", ex); //NON-NLS + return false; + + } finally { + dbWriteUnlock(); + } + } + /** * Public factory method. Creates and opens a connection to a new database * * at the given path. If there is already a db at the path, it is checked @@ -538,21 +620,21 @@ public final class DrawableDB { // Check if the database is new or an existing database drawableDbTablesExist = doesTableExist("drawable_files"); - if (false == doesTableExist(IG_DB_INFO_TABLE)) { + if (false == doesTableExist(DB_INFO_TABLE_NAME)) { try { VersionNumber ig_creation_schema_version = drawableDbTablesExist ? IG_STARTING_SCHEMA_VERSION : IG_SCHEMA_VERSION; - stmt.execute("CREATE TABLE IF NOT EXISTS " + IG_DB_INFO_TABLE + " (name TEXT PRIMARY KEY, value TEXT NOT NULL)"); + stmt.execute("CREATE TABLE IF NOT EXISTS " + DB_INFO_TABLE_NAME + " (name TEXT PRIMARY KEY, value TEXT NOT NULL)"); // backfill creation schema ver - stmt.execute(String.format("INSERT INTO %s (name, value) VALUES ('%s', '%s')", IG_DB_INFO_TABLE, IG_CREATION_SCHEMA_MAJOR_VERSION_KEY, ig_creation_schema_version.getMajor())); - stmt.execute(String.format("INSERT INTO %s (name, value) VALUES ('%s', '%s')", IG_DB_INFO_TABLE, IG_CREATION_SCHEMA_MINOR_VERSION_KEY, ig_creation_schema_version.getMinor())); + stmt.execute(String.format("INSERT INTO %s (name, value) VALUES ('%s', '%s')", DB_INFO_TABLE_NAME, IG_CREATION_SCHEMA_MAJOR_VERSION_KEY, ig_creation_schema_version.getMajor())); + stmt.execute(String.format("INSERT INTO %s (name, value) VALUES ('%s', '%s')", DB_INFO_TABLE_NAME, IG_CREATION_SCHEMA_MINOR_VERSION_KEY, ig_creation_schema_version.getMinor())); // set current schema ver: at DB initialization - current version is same as starting version - stmt.execute(String.format("INSERT INTO %s (name, value) VALUES ('%s', '%s')", IG_DB_INFO_TABLE, IG_SCHEMA_MAJOR_VERSION_KEY, ig_creation_schema_version.getMajor())); - stmt.execute(String.format("INSERT INTO %s (name, value) VALUES ('%s', '%s')", IG_DB_INFO_TABLE, IG_SCHEMA_MINOR_VERSION_KEY, ig_creation_schema_version.getMinor())); + stmt.execute(String.format("INSERT INTO %s (name, value) VALUES ('%s', '%s')", DB_INFO_TABLE_NAME, IG_SCHEMA_MAJOR_VERSION_KEY, ig_creation_schema_version.getMajor())); + stmt.execute(String.format("INSERT INTO %s (name, value) VALUES ('%s', '%s')", DB_INFO_TABLE_NAME, IG_SCHEMA_MINOR_VERSION_KEY, ig_creation_schema_version.getMinor())); } catch (SQLException ex) { logger.log(Level.SEVERE, "Failed to create ig_db_info table", ex); //NON-NLS @@ -653,10 +735,10 @@ public final class DrawableDB { /* * Create tables in the case database. */ - String autogenKeyType = (DbType.POSTGRESQL == tskCase.getDatabaseType()) ? "BIGSERIAL" : "INTEGER"; + String autogenKeyType = (DbType.POSTGRESQL == caseDb.getDatabaseType()) ? "BIGSERIAL" : "INTEGER"; try { - boolean caseDbTablesExist = tskCase.getCaseDbAccessManager().tableExists(GROUPS_TABLENAME); + boolean caseDbTablesExist = caseDb.getCaseDbAccessManager().tableExists(CASE_DB_GROUPS_TABLENAME); VersionNumber ig_creation_schema_version = caseDbTablesExist ? IG_STARTING_SCHEMA_VERSION : IG_SCHEMA_VERSION; @@ -664,7 +746,7 @@ public final class DrawableDB { String tableSchema = "( id " + autogenKeyType + " PRIMARY KEY, " + " name TEXT UNIQUE NOT NULL," + " value TEXT NOT NULL )"; - tskCase.getCaseDbAccessManager().createTable(IG_DB_INFO_TABLE, tableSchema); + caseDb.getCaseDbAccessManager().createTable(DB_INFO_TABLE_NAME, tableSchema); // backfill creation version String creationMajorVerSQL = String.format(" (name, value) VALUES ('%s', '%s')", IG_CREATION_SCHEMA_MAJOR_VERSION_KEY, ig_creation_schema_version.getMajor()); @@ -674,7 +756,7 @@ public final class DrawableDB { String currentMajorVerSQL = String.format(" (name, value) VALUES ('%s', '%s')", IG_SCHEMA_MAJOR_VERSION_KEY, ig_creation_schema_version.getMajor()); String currentMinorVerSQL = String.format(" (name, value) VALUES ('%s', '%s')", IG_SCHEMA_MINOR_VERSION_KEY, ig_creation_schema_version.getMinor()); - if (DbType.POSTGRESQL == tskCase.getDatabaseType()) { + if (DbType.POSTGRESQL == caseDb.getDatabaseType()) { creationMajorVerSQL += " ON CONFLICT DO NOTHING "; creationMinorVerSQL += " ON CONFLICT DO NOTHING "; @@ -682,11 +764,11 @@ public final class DrawableDB { currentMinorVerSQL += " ON CONFLICT DO NOTHING "; } - tskCase.getCaseDbAccessManager().insert(IG_DB_INFO_TABLE, creationMajorVerSQL); - tskCase.getCaseDbAccessManager().insert(IG_DB_INFO_TABLE, creationMinorVerSQL); + caseDb.getCaseDbAccessManager().insert(DB_INFO_TABLE_NAME, creationMajorVerSQL); + caseDb.getCaseDbAccessManager().insert(DB_INFO_TABLE_NAME, creationMinorVerSQL); - tskCase.getCaseDbAccessManager().insert(IG_DB_INFO_TABLE, currentMajorVerSQL); - tskCase.getCaseDbAccessManager().insert(IG_DB_INFO_TABLE, currentMinorVerSQL); + caseDb.getCaseDbAccessManager().insert(DB_INFO_TABLE_NAME, currentMajorVerSQL); + caseDb.getCaseDbAccessManager().insert(DB_INFO_TABLE_NAME, currentMinorVerSQL); } catch (TskCoreException ex) { logger.log(Level.SEVERE, "Failed to create ig_db_info table in Case database", ex); //NON-NLS @@ -702,9 +784,9 @@ public final class DrawableDB { + " is_analyzed integer DEFAULT 0, " + " UNIQUE(data_source_obj_id, value, attribute) )"; //NON-NLS - tskCase.getCaseDbAccessManager().createTable(GROUPS_TABLENAME, tableSchema); + caseDb.getCaseDbAccessManager().createTable(CASE_DB_GROUPS_TABLENAME, tableSchema); } catch (TskCoreException ex) { - logger.log(Level.SEVERE, String.format("Failed to create %s table in case database", GROUPS_TABLENAME), ex); //NON-NLS + logger.log(Level.SEVERE, String.format("Failed to create %s table in case database", CASE_DB_GROUPS_TABLENAME), ex); //NON-NLS return false; } try { @@ -715,13 +797,13 @@ public final class DrawableDB { + " examiner_id integer not null, " //NON-NLS + " seen integer DEFAULT 0, " //NON-NLS + " UNIQUE(group_id, examiner_id)," - + " FOREIGN KEY(group_id) REFERENCES " + GROUPS_TABLENAME + "(group_id) ON DELETE CASCADE," + + " FOREIGN KEY(group_id) REFERENCES " + CASE_DB_GROUPS_TABLENAME + "(group_id) ON DELETE CASCADE," + " FOREIGN KEY(examiner_id) REFERENCES tsk_examiners(examiner_id)" + " )"; //NON-NLS - tskCase.getCaseDbAccessManager().createTable(GROUPS_SEEN_TABLENAME, tableSchema); + caseDb.getCaseDbAccessManager().createTable(CASE_DB_GROUPS_SEEN_TABLENAME, tableSchema); } catch (TskCoreException ex) { - logger.log(Level.SEVERE, String.format("Failed to create %s table in case database", GROUPS_SEEN_TABLENAME), ex); //NON-NLS + logger.log(Level.SEVERE, String.format("Failed to create %s table in case database", CASE_DB_GROUPS_SEEN_TABLENAME), ex); //NON-NLS return false; } @@ -748,7 +830,7 @@ public final class DrawableDB { try { int majorVersion = -1; String majorVersionStr = null; - resultSet = statement.executeQuery(String.format("SELECT value FROM %s WHERE name='%s'", IG_DB_INFO_TABLE, IG_SCHEMA_MAJOR_VERSION_KEY)); + resultSet = statement.executeQuery(String.format("SELECT value FROM %s WHERE name='%s'", DB_INFO_TABLE_NAME, IG_SCHEMA_MAJOR_VERSION_KEY)); if (resultSet.next()) { majorVersionStr = resultSet.getString("value"); try { @@ -762,7 +844,7 @@ public final class DrawableDB { int minorVersion = -1; String minorVersionStr = null; - resultSet = statement.executeQuery(String.format("SELECT value FROM %s WHERE name='%s'", IG_DB_INFO_TABLE, IG_SCHEMA_MINOR_VERSION_KEY)); + resultSet = statement.executeQuery(String.format("SELECT value FROM %s WHERE name='%s'", DB_INFO_TABLE_NAME, IG_SCHEMA_MINOR_VERSION_KEY)); if (resultSet.next()) { minorVersionStr = resultSet.getString("value"); try { @@ -827,8 +909,8 @@ public final class DrawableDB { GetSchemaVersionQueryResultProcessor minorVersionResultProcessor = new GetSchemaVersionQueryResultProcessor(); String versionQueryTemplate = "value FROM %s WHERE name = \'%s\' "; - tskCase.getCaseDbAccessManager().select(String.format(versionQueryTemplate, IG_DB_INFO_TABLE, IG_SCHEMA_MAJOR_VERSION_KEY), majorVersionResultProcessor); - tskCase.getCaseDbAccessManager().select(String.format(versionQueryTemplate, IG_DB_INFO_TABLE, IG_SCHEMA_MINOR_VERSION_KEY), minorVersionResultProcessor); + caseDb.getCaseDbAccessManager().select(String.format(versionQueryTemplate, DB_INFO_TABLE_NAME, IG_SCHEMA_MAJOR_VERSION_KEY), majorVersionResultProcessor); + caseDb.getCaseDbAccessManager().select(String.format(versionQueryTemplate, DB_INFO_TABLE_NAME, IG_SCHEMA_MINOR_VERSION_KEY), minorVersionResultProcessor); return new VersionNumber(majorVersionResultProcessor.getVersion(), minorVersionResultProcessor.getVersion(), 0); } @@ -852,8 +934,8 @@ public final class DrawableDB { Statement statement = con.createStatement(); // update schema version - statement.execute(String.format("UPDATE %s SET value = '%s' WHERE name = '%s'", IG_DB_INFO_TABLE, version.getMajor(), IG_SCHEMA_MAJOR_VERSION_KEY)); - statement.execute(String.format("UPDATE %s SET value = '%s' WHERE name = '%s'", IG_DB_INFO_TABLE, version.getMinor(), IG_SCHEMA_MINOR_VERSION_KEY)); + statement.execute(String.format("UPDATE %s SET value = '%s' WHERE name = '%s'", DB_INFO_TABLE_NAME, version.getMajor(), IG_SCHEMA_MAJOR_VERSION_KEY)); + statement.execute(String.format("UPDATE %s SET value = '%s' WHERE name = '%s'", DB_INFO_TABLE_NAME, version.getMinor(), IG_SCHEMA_MINOR_VERSION_KEY)); statement.close(); } finally { @@ -872,8 +954,8 @@ public final class DrawableDB { private void updateCaseDbIgSchemaVersion(VersionNumber version, CaseDbTransaction caseDbTransaction) throws TskCoreException { String updateSQLTemplate = " SET value = %s WHERE name = '%s' "; - tskCase.getCaseDbAccessManager().update(IG_DB_INFO_TABLE, String.format(updateSQLTemplate, version.getMajor(), IG_SCHEMA_MAJOR_VERSION_KEY), caseDbTransaction); - tskCase.getCaseDbAccessManager().update(IG_DB_INFO_TABLE, String.format(updateSQLTemplate, version.getMinor(), IG_SCHEMA_MINOR_VERSION_KEY), caseDbTransaction); + caseDb.getCaseDbAccessManager().update(DB_INFO_TABLE_NAME, String.format(updateSQLTemplate, version.getMajor(), IG_SCHEMA_MAJOR_VERSION_KEY), caseDbTransaction); + caseDb.getCaseDbAccessManager().update(DB_INFO_TABLE_NAME, String.format(updateSQLTemplate, version.getMinor(), IG_SCHEMA_MINOR_VERSION_KEY), caseDbTransaction); } /** @@ -891,7 +973,7 @@ public final class DrawableDB { VersionNumber caseDbIgSchemaVersion = getCaseDbIgSchemaVersion(); // Upgrade Schema in both DrawableDB and CaseDB - CaseDbTransaction caseDbTransaction = tskCase.beginTransaction(); + CaseDbTransaction caseDbTransaction = caseDb.beginTransaction(); DrawableTransaction transaction = beginTransaction(); try { @@ -948,8 +1030,8 @@ public final class DrawableDB { // Add a 'is_analyzed' column to groups table in CaseDB String alterSQL = " ADD COLUMN is_analyzed integer DEFAULT 1 "; //NON-NLS - if (false == tskCase.getCaseDbAccessManager().columnExists(GROUPS_TABLENAME, "is_analyzed", caseDbTransaction)) { - tskCase.getCaseDbAccessManager().alterTable(GROUPS_TABLENAME, alterSQL, caseDbTransaction); + if (false == caseDb.getCaseDbAccessManager().columnExists(CASE_DB_GROUPS_TABLENAME, "is_analyzed", caseDbTransaction)) { + caseDb.getCaseDbAccessManager().alterTable(CASE_DB_GROUPS_TABLENAME, alterSQL, caseDbTransaction); } return new VersionNumber(1, 1, 0); } @@ -1047,7 +1129,7 @@ public final class DrawableDB { */ Set getHashSetsForFile(long fileID) throws TskCoreException { Set hashNames = new HashSet<>(); - ArrayList artifacts = tskCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT, fileID); + ArrayList artifacts = caseDb.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT, fileID); for (BlackboardArtifact a : artifacts) { BlackboardAttribute attribute = a.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME)); @@ -1069,7 +1151,7 @@ public final class DrawableDB { dbWriteLock(); try (ResultSet rs = selectHashSetNamesStmt.executeQuery();) { while (rs.next()) { - names.add(rs.getString(HASH_SET_NAME)); + names.add(rs.getString("hash_set_name")); } } catch (SQLException sQLException) { logger.log(Level.WARNING, "failed to get hash set names", sQLException); //NON-NLS @@ -1081,7 +1163,7 @@ public final class DrawableDB { static private String getGroupIdQuery(GroupKey groupKey) { // query to find the group id from attribute/value - return String.format(" SELECT group_id FROM " + GROUPS_TABLENAME + return String.format(" SELECT group_id FROM " + CASE_DB_GROUPS_TABLENAME + " WHERE attribute = \'%s\' AND value = \'%s\' AND data_source_obj_id = %d", SleuthkitCase.escapeSingleQuotes(groupKey.getAttribute().attrName.toString()), SleuthkitCase.escapeSingleQuotes(groupKey.getValueDisplayName()), @@ -1131,12 +1213,12 @@ public final class DrawableDB { GroupSeenQueryResultProcessor queryResultProcessor = new GroupSeenQueryResultProcessor(); try { - String groupSeenQueryStmt = "COUNT(*) as count FROM " + GROUPS_SEEN_TABLENAME + String groupSeenQueryStmt = "COUNT(*) as count FROM " + CASE_DB_GROUPS_SEEN_TABLENAME + " WHERE seen = 1 " + " AND group_id in ( " + getGroupIdQuery(groupKey) + ")" + (examinerId > 0 ? " AND examiner_id = " + examinerId : "");// query to find the group id from attribute/value - tskCase.getCaseDbAccessManager().select(groupSeenQueryStmt, queryResultProcessor); + caseDb.getCaseDbAccessManager().select(groupSeenQueryStmt, queryResultProcessor); return queryResultProcessor.get(); } catch (ExecutionException | InterruptedException | TskCoreException ex) { String msg = String.format("Failed to get is group seen for group key %s", groupKey.getValueDisplayName()); //NON-NLS @@ -1167,18 +1249,18 @@ public final class DrawableDB { } // query to find the group id from attribute/value - String innerQuery = String.format("( SELECT group_id FROM " + GROUPS_TABLENAME //NON-NLS + String innerQuery = String.format("( SELECT group_id FROM " + CASE_DB_GROUPS_TABLENAME//NON-NLS + " WHERE attribute = \'%s\' AND value = \'%s\' and data_source_obj_id = %d )", //NON-NLS SleuthkitCase.escapeSingleQuotes(groupKey.getAttribute().attrName.toString()), SleuthkitCase.escapeSingleQuotes(groupKey.getValueDisplayName()), groupKey.getAttribute() == DrawableAttribute.PATH ? groupKey.getDataSourceObjId() : 0); String insertSQL = String.format(" (group_id, examiner_id, seen) VALUES (%s, %d, %d)", innerQuery, examinerID, 1); //NON-NLS - if (DbType.POSTGRESQL == tskCase.getDatabaseType()) { + if (DbType.POSTGRESQL == caseDb.getDatabaseType()) { insertSQL += String.format(" ON CONFLICT (group_id, examiner_id) DO UPDATE SET seen = %d", 1); //NON-NLS } - tskCase.getCaseDbAccessManager().insertOrUpdate(GROUPS_SEEN_TABLENAME, insertSQL); + caseDb.getCaseDbAccessManager().insertOrUpdate(CASE_DB_GROUPS_SEEN_TABLENAME, insertSQL); groupSeenCache.put(groupKey, true); } @@ -1203,7 +1285,7 @@ public final class DrawableDB { } String updateSQL = String.format(" SET seen = 0 WHERE group_id in ( " + getGroupIdQuery(groupKey) + ")"); //NON-NLS - tskCase.getCaseDbAccessManager().update(GROUPS_SEEN_TABLENAME, updateSQL); + caseDb.getCaseDbAccessManager().update(CASE_DB_GROUPS_SEEN_TABLENAME, updateSQL); groupSeenCache.put(groupKey, false); } @@ -1224,7 +1306,7 @@ public final class DrawableDB { SleuthkitCase.escapeSingleQuotes(groupKey.getValueDisplayName()), groupKey.getAttribute() == DrawableAttribute.PATH ? groupKey.getDataSourceObjId() : 0); - tskCase.getCaseDbAccessManager().update(GROUPS_TABLENAME, updateSQL); + caseDb.getCaseDbAccessManager().update(CASE_DB_GROUPS_TABLENAME, updateSQL); } /** @@ -1268,7 +1350,7 @@ public final class DrawableDB { CaseDbTransaction caseDbTransaction = null; try { trans = beginTransaction(); - caseDbTransaction = tskCase.beginTransaction(); + caseDbTransaction = caseDb.beginTransaction(); updateFile(f, trans, caseDbTransaction); caseDbTransaction.commit(); commitTransaction(trans, true); @@ -1317,11 +1399,11 @@ public final class DrawableDB { try { // get tags - try (SleuthkitCase.CaseDbQuery dbQuery = tskCase.executeQuery("SELECT obj_id FROM content_tags")) { + try (SleuthkitCase.CaseDbQuery dbQuery = caseDb.executeQuery("SELECT obj_id FROM content_tags")) { ResultSet rs = dbQuery.getResultSet(); while (rs.next()) { long id = rs.getLong("obj_id"); - hasTagCache.add(id); + hasTagsCache.add(id); } } catch (SQLException ex) { logger.log(Level.SEVERE, "Error getting tags from DB", ex); //NON-NLS @@ -1332,11 +1414,11 @@ public final class DrawableDB { try { // hash sets - try (SleuthkitCase.CaseDbQuery dbQuery = tskCase.executeQuery("SELECT obj_id FROM blackboard_artifacts WHERE artifact_type_id = " + BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT.getTypeID())) { + try (SleuthkitCase.CaseDbQuery dbQuery = caseDb.executeQuery("SELECT obj_id FROM blackboard_artifacts WHERE artifact_type_id = " + BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT.getTypeID())) { ResultSet rs = dbQuery.getResultSet(); while (rs.next()) { long id = rs.getLong("obj_id"); - hasHashCache.add(id); + hasHashHitsCache.add(id); } } catch (SQLException ex) { @@ -1348,11 +1430,11 @@ public final class DrawableDB { try { // EXIF - try (SleuthkitCase.CaseDbQuery dbQuery = tskCase.executeQuery("SELECT obj_id FROM blackboard_artifacts WHERE artifact_type_id = " + BlackboardArtifact.ARTIFACT_TYPE.TSK_METADATA_EXIF.getTypeID())) { + try (SleuthkitCase.CaseDbQuery dbQuery = caseDb.executeQuery("SELECT obj_id FROM blackboard_artifacts WHERE artifact_type_id = " + BlackboardArtifact.ARTIFACT_TYPE.TSK_METADATA_EXIF.getTypeID())) { ResultSet rs = dbQuery.getResultSet(); while (rs.next()) { long id = rs.getLong("obj_id"); - hasExifCache.add(id); + hasExifDataCache.add(id); } } catch (SQLException ex) { @@ -1377,7 +1459,7 @@ public final class DrawableDB { if (cacheBuildCount == 0) { return; } - hasExifCache.add(objectID); + hasExifDataCache.add(objectID); } } @@ -1392,7 +1474,7 @@ public final class DrawableDB { if (cacheBuildCount == 0) { return; } - hasHashCache.add(objectID); + hasHashHitsCache.add(objectID); } } @@ -1407,7 +1489,7 @@ public final class DrawableDB { if (cacheBuildCount == 0) { return; } - hasTagCache.add(objectID); + hasTagsCache.add(objectID); } } @@ -1422,9 +1504,9 @@ public final class DrawableDB { } areCachesLoaded = false; - hasTagCache.clear(); - hasHashCache.clear(); - hasExifCache.clear(); + hasTagsCache.clear(); + hasHashHitsCache.clear(); + hasExifDataCache.clear(); } } @@ -1464,9 +1546,9 @@ public final class DrawableDB { boolean hasTag = true; synchronized (cacheLock) { if (areCachesLoaded) { - hasExif = hasExifCache.contains(f.getId()); - hasHashSet = hasHashCache.contains(f.getId()); - hasTag = hasTagCache.contains(f.getId()); + hasExif = hasExifDataCache.contains(f.getId()); + hasHashSet = hasHashHitsCache.contains(f.getId()); + hasTag = hasTagsCache.contains(f.getId()); } } @@ -1623,24 +1705,33 @@ public final class DrawableDB { } /** - * Insert/update given data source object id and it's DB rebuild status in - * the datasources table. + * Inserts the given data source object ID and its status into the + * datasources table. If a record for the data source already exists, an + * update of the status is done instead. * - * If the object id exists in the table already, it updates the status - * - * @param dsObjectId data source object id to insert - * @param status The db build statsus for datasource. + * @param dataSourceObjectID A data source object ID from the case database. + * @param status The status of the data source with respect to + * populating the image gallery database. */ - public void insertOrUpdateDataSource(long dsObjectId, DrawableDbBuildStatusEnum status) { + public void insertOrUpdateDataSource(long dataSourceObjectID, DrawableDbBuildStatusEnum status) throws SQLException { dbWriteLock(); try { - // "INSERT OR REPLACE INTO datasources (ds_obj_id, drawable_db_build_status) " //NON-NLS - updateDataSourceStmt.setLong(1, dsObjectId); - updateDataSourceStmt.setString(2, status.name()); - - updateDataSourceStmt.executeUpdate(); - } catch (SQLException | NullPointerException ex) { - logger.log(Level.SEVERE, "failed to insert/update datasources table", ex); //NON-NLS + // SELECT COUNT(*) FROM datasources WHERE ds_obj_id = ? + selectCountDataSourceIDs.setLong(1, dataSourceObjectID); + try (ResultSet resultSet = selectCountDataSourceIDs.executeQuery()) { + resultSet.next(); + if (resultSet.getInt(1) == 0) { + // INSERT INTO datasources (ds_obj_id, drawable_db_build_status) VALUES (?,?) + insertDataSourceStmt.setLong(1, dataSourceObjectID); + insertDataSourceStmt.setString(2, status.name()); + insertDataSourceStmt.execute(); + } else { + // UPDATE datasources SET drawable_db_build_status = ? WHERE ds_obj_id = ? + updateDataSourceStmt.setString(1, status.name()); + updateDataSourceStmt.setLong(2, dataSourceObjectID); + updateDataSourceStmt.executeUpdate(); + } + } } finally { dbWriteUnlock(); } @@ -1679,7 +1770,7 @@ public final class DrawableDB { //Can't make this a preprared statement because of the IN ( ... ) ResultSet analyzedQuery = stmt.executeQuery("SELECT COUNT(analyzed) AS analyzed FROM drawable_files WHERE analyzed = 1 AND obj_id IN (" + StringUtils.join(fileIds, ", ") + ")"); //NON-NLS while (analyzedQuery.next()) { - return analyzedQuery.getInt(ANALYZED) == fileIds.size(); + return analyzedQuery.getInt("analyzed") == fileIds.size(); } return false; } @@ -1724,13 +1815,13 @@ public final class DrawableDB { IsGroupAnalyzedQueryResultProcessor queryResultProcessor = new IsGroupAnalyzedQueryResultProcessor(); try { - String groupAnalyzedQueryStmt = String.format("is_analyzed FROM " + GROUPS_TABLENAME + String groupAnalyzedQueryStmt = String.format("is_analyzed FROM " + CASE_DB_GROUPS_TABLENAME + " WHERE attribute = \'%s\' AND value = \'%s\' and data_source_obj_id = %d ", SleuthkitCase.escapeSingleQuotes(groupKey.getAttribute().attrName.toString()), SleuthkitCase.escapeSingleQuotes(groupKey.getValueDisplayName()), groupKey.getAttribute() == DrawableAttribute.PATH ? groupKey.getDataSourceObjId() : 0); - tskCase.getCaseDbAccessManager().select(groupAnalyzedQueryStmt, queryResultProcessor); + caseDb.getCaseDbAccessManager().select(groupAnalyzedQueryStmt, queryResultProcessor); return queryResultProcessor.getIsAnalyzed(); } catch (TskCoreException ex) { String msg = String.format("Failed to get group is_analyzed for group key %s", groupKey.getValueDisplayName()); //NON-NLS @@ -1882,7 +1973,7 @@ public final class DrawableDB { * wrong, we know this should be of type A even if * JAVA doesn't */ - values.put(tskCase.getDataSource(results.getLong("data_source_obj_id")), + values.put(caseDb.getDataSource(results.getLong("data_source_obj_id")), (A) results.getObject(groupBy.attrName.toString())); } return values; @@ -1930,10 +2021,10 @@ public final class DrawableDB { int isAnalyzed = (groupBy == DrawableAttribute.PATH) ? 0 : 1; String insertSQL = String.format(" (data_source_obj_id, value, attribute, is_analyzed) VALUES (%d, \'%s\', \'%s\', %d)", ds_obj_id, SleuthkitCase.escapeSingleQuotes(value), SleuthkitCase.escapeSingleQuotes(groupBy.attrName.toString()), isAnalyzed); - if (DbType.POSTGRESQL == tskCase.getDatabaseType()) { + if (DbType.POSTGRESQL == caseDb.getDatabaseType()) { insertSQL += " ON CONFLICT DO NOTHING"; } - tskCase.getCaseDbAccessManager().insert(GROUPS_TABLENAME, insertSQL, caseDbTransaction); + caseDb.getCaseDbAccessManager().insert(CASE_DB_GROUPS_TABLENAME, insertSQL, caseDbTransaction); groupCache.put(cacheKey, Boolean.TRUE); } @@ -1946,7 +2037,7 @@ public final class DrawableDB { * {@link SleuthkitCase} */ public DrawableFile getFileFromID(Long id) throws TskCoreException { - AbstractFile f = tskCase.getAbstractFileById(id); + AbstractFile f = caseDb.getAbstractFileById(id); try { return DrawableFile.create(f, areFilesAnalyzed(Collections.singleton(id)), isVideoFile(f)); } catch (SQLException ex) { @@ -1974,7 +2065,7 @@ public final class DrawableDB { try (ResultSet valsResults = statement.executeQuery()) { while (valsResults.next()) { - files.add(valsResults.getLong(OBJ_ID)); + files.add(valsResults.getLong("obj_id")); } } } catch (SQLException ex) { @@ -2015,12 +2106,12 @@ public final class DrawableDB { removeImageFileFromList(id); //"delete from hash_set_hits where (obj_id = " + id + ")" - removeHashHitStmt.setLong(1, id); - removeHashHitStmt.executeUpdate(); + deleteHashHitStmt.setLong(1, id); + deleteHashHitStmt.executeUpdate(); //"delete from drawable_files where (obj_id = " + id + ")" - removeFileStmt.setLong(1, id); - removeFileStmt.executeUpdate(); + deleteFileStmt.setLong(1, id); + deleteFileStmt.executeUpdate(); tr.addRemovedFile(id); } catch (SQLException ex) { @@ -2037,7 +2128,7 @@ public final class DrawableDB { * * @param dataSourceID The object ID of the data source to delete. * - * @throws SQLException + * @throws SQLException * @throws TskCoreException */ public void deleteDataSource(long dataSourceID) throws SQLException, TskCoreException { @@ -2111,7 +2202,7 @@ public final class DrawableDB { try (Statement stmt = con.createStatement()) { ResultSet analyzedQuery = stmt.executeQuery("select obj_id from drawable_files"); while (analyzedQuery.next()) { - addImageFileToList(analyzedQuery.getLong(OBJ_ID)); + addImageFileToList(analyzedQuery.getLong("obj_id")); } return true; } catch (SQLException ex) { @@ -2160,7 +2251,7 @@ public final class DrawableDB { try { TagName tagName = controller.getTagsManager().getTagName(cat); if (nonNull(tagName)) { - return tskCase.getContentTagsByTagName(tagName).stream() + return caseDb.getContentTagsByTagName(tagName).stream() .map(ContentTag::getContent) .map(Content::getId) .filter(this::isInDB) @@ -2212,7 +2303,7 @@ public final class DrawableDB { String name = "SELECT COUNT(obj_id) as obj_count FROM tsk_files where obj_id IN " + fileIdsList //NON-NLS + " AND obj_id NOT IN (SELECT obj_id FROM content_tags WHERE content_tags.tag_name_id IN " + catTagNameIDs + ")"; //NON-NLS - try (SleuthkitCase.CaseDbQuery executeQuery = tskCase.executeQuery(name); + try (SleuthkitCase.CaseDbQuery executeQuery = caseDb.executeQuery(name); ResultSet resultSet = executeQuery.getResultSet();) { while (resultSet.next()) { return resultSet.getLong("obj_count"); //NON-NLS diff --git a/InternalPythonModules/android/fbmessenger.py b/InternalPythonModules/android/fbmessenger.py index 2347144f09..c19ae87796 100644 --- a/InternalPythonModules/android/fbmessenger.py +++ b/InternalPythonModules/android/fbmessenger.py @@ -17,6 +17,11 @@ See the License for the specific language governing permissions and limitations under the License. """ +import json +import traceback +import general +import ast + from java.io import File from java.lang import Class from java.lang import ClassNotFoundException @@ -43,14 +48,13 @@ from org.sleuthkit.datamodel import TskCoreException from org.sleuthkit.datamodel.Blackboard import BlackboardException from org.sleuthkit.datamodel import Account from org.sleuthkit.datamodel.blackboardutils import CommunicationArtifactsHelper +from org.sleuthkit.datamodel.blackboardutils import MessageAttachments +from org.sleuthkit.datamodel.blackboardutils import URLAttachment +from org.sleuthkit.datamodel.blackboardutils import FileAttachment from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import MessageReadStatus from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import CommunicationDirection from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import CallMediaType -import json -import traceback -import general - class FBMessengerAnalyzer(general.AndroidComponentAnalyzer): @@ -95,6 +99,7 @@ class FBMessengerAnalyzer(general.AndroidComponentAnalyzer): * have no text, * admin_text_thread_rtc_event has the specific event "group-call-started", "group-call_ended" + --- A pending_send_media_attachment - a JSON structure that has details of attachments that may or may not have been sent. --- A admin_text_thread_rtc_event column - has specific text events such as- "one-on-one-call-ended" --- A thread_key column - identifies the message thread --- A timestamp_ms column - date/time message was sent @@ -210,6 +215,17 @@ class FBMessengerAnalyzer(general.AndroidComponentAnalyzer): else: direction = CommunicationDirection.INCOMING return direction + + ## Get the arrayList from the json passed in + def getJPGListFromJson(self, jpgJson): + jpgArray = ArrayList() + # The urls attachment will come across as unicode unless we use ast.literal_eval to change it to a dictionary + jpgDict = ast.literal_eval(jpgJson) + for jpgPreview in jpgDict.iterkeys(): + # Need to use ast.literal_eval so that the string can be converted to a dictionary + jpgUrlDict = ast.literal_eval(jpgDict[jpgPreview]) + jpgArray.add(URLAttachment(jpgUrlDict["src"])) + return jpgArray ## Analyzes messages def analyzeMessages(self, threadsDb, threadsDBHelper): @@ -223,7 +239,8 @@ class FBMessengerAnalyzer(general.AndroidComponentAnalyzer): ## The result set is processed to collect the multiple recipients for a given message. sqlString = """ SELECT msg_id, text, sender, timestamp_ms, msg_type, messages.thread_key as thread_key, - snippet, thread_participants.user_key as user_key, thread_users.name as name + snippet, thread_participants.user_key as user_key, thread_users.name as name, + attachments, pending_send_media_attachment FROM messages JOIN thread_participants ON messages.thread_key = thread_participants.thread_key JOIN thread_users ON thread_participants.user_key = thread_users.user_key @@ -241,6 +258,8 @@ class FBMessengerAnalyzer(general.AndroidComponentAnalyzer): timeStamp = -1 msgText = "" threadId = "" + messageAttachments = None + currentCase = Case.getCurrentCaseThrows() while messagesResultSet.next(): msgId = messagesResultSet.getString("msg_id") @@ -260,6 +279,10 @@ class FBMessengerAnalyzer(general.AndroidComponentAnalyzer): msgText, threadId) + if (messageAttachments is not None): + threadsDBHelper.addAttachments(messageArtifact, messageAttachments) + messageAttachments = None + oldMsgId = msgId # New message - collect all attributes @@ -282,8 +305,42 @@ class FBMessengerAnalyzer(general.AndroidComponentAnalyzer): if not msgText: msgText = messagesResultSet.getString("snippet") - # TBD: get attachment + # Get attachments and pending attachments if they exist + attachment = messagesResultSet.getString("attachments") + pendingAttachment = messagesResultSet.getString("pending_send_media_attachment") + + urlAttachments = ArrayList() + fileAttachments = ArrayList() + + if ((attachment is not None) or (pendingAttachment is not None)): + if (attachment is not None): + attachmentDict = json.loads(attachment)[0] + if (attachmentDict["mime_type"] == "image/jpeg"): + urlAttachments = self.getJPGListFromJson(attachmentDict["urls"]) + elif (attachmentDict["mime_type"] == "video/mp4"): + # filename does not have an associated path with it so it will be ignored + urlAttachments = self.getJPGListFromJson(attachmentDict["urls"]) + urlAttachments.add(URLAttachment(attachmentDict["video_data_url"])) + urlAttachments.add(URLAttachment(attachmentDict["video_data_thumbnail_url"])) + + elif (attachmentDict["mime_type"] == "audio/mpeg"): + if (attachmentDict["audio_uri"] == ""): + continue + else: + audioUri = attachmentDict["audio_uri"] + fileAttachments.add(FileAttachment(currentCase.getSleuthkitCase(), threadsDb.getDBFile().getDataSource(), audioUri.replace("file://",""))) + + else: + self._logger.log(Level.INFO, "Attachment type not handled: " + attachmentDict["mime_type"]) + + if (pendingAttachment is not None): + pendingAttachmentDict = json.loads(pendingAttachment)[0] + pendingAttachmentUri = pendingAttachmentDict["uri"] + fileAttachments.add(FileAttachment(currentCase.getSleuthkitCase(), threadsDb.getDBFile().getDataSource(), pendingAttachmentUri.replace("file://",""))) + + messageAttachments = MessageAttachments(fileAttachments, urlAttachments) + threadId = messagesResultSet.getString("thread_key") else: # same msgId as last, just collect recipient from current row diff --git a/InternalPythonModules/android/shareit.py b/InternalPythonModules/android/shareit.py index 523155a78d..c80dbaec10 100644 --- a/InternalPythonModules/android/shareit.py +++ b/InternalPythonModules/android/shareit.py @@ -43,6 +43,8 @@ from org.sleuthkit.datamodel import TskCoreException from org.sleuthkit.datamodel.Blackboard import BlackboardException from org.sleuthkit.datamodel import Account from org.sleuthkit.datamodel.blackboardutils import CommunicationArtifactsHelper +from org.sleuthkit.datamodel.blackboardutils import FileAttachment +from org.sleuthkit.datamodel.blackboardutils import MessageAttachments from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import MessageReadStatus from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import CommunicationDirection @@ -96,6 +98,7 @@ class ShareItAnalyzer(general.AndroidComponentAnalyzer): direction = "" fromId = None toId = None + fileAttachments = ArrayList() if (historyResultSet.getInt("history_type") == 1): direction = CommunicationDirection.INCOMING @@ -104,10 +107,6 @@ class ShareItAnalyzer(general.AndroidComponentAnalyzer): direction = CommunicationDirection.OUTGOING toId = historyResultSet.getString("device_id") - msgBody = "" # there is no body. - attachments = [historyResultSet.getString("file_path")] - msgBody = general.appendAttachmentList(msgBody, attachments) - timeStamp = historyResultSet.getLong("timestamp") / 1000 messageArtifact = historyDbHelper.addMessage( self._MESSAGE_TYPE, @@ -117,10 +116,14 @@ class ShareItAnalyzer(general.AndroidComponentAnalyzer): timeStamp, MessageReadStatus.UNKNOWN, None, # subject - msgBody, + None, # message text None ) # thread id - # TBD: add the file as attachment ?? + # add the file as attachment + fileAttachments.add(FileAttachment(current_case.getSleuthkitCase(), historyDb.getDBFile().getDataSource(), historyResultSet.getString("file_path"))) + messageAttachments = MessageAttachments(fileAttachments, []) + historyDbHelper.addAttachments(messageArtifact, messageAttachments) + except SQLException as ex: self._logger.log(Level.WARNING, "Error processing query result for ShareIt history.", ex) diff --git a/InternalPythonModules/android/textnow.py b/InternalPythonModules/android/textnow.py index 1890c7ae42..071cdcb137 100644 --- a/InternalPythonModules/android/textnow.py +++ b/InternalPythonModules/android/textnow.py @@ -44,6 +44,8 @@ from org.sleuthkit.datamodel.Blackboard import BlackboardException from org.sleuthkit.autopsy.casemodule import NoCurrentCaseException from org.sleuthkit.datamodel import Account from org.sleuthkit.datamodel.blackboardutils import CommunicationArtifactsHelper +from org.sleuthkit.datamodel.blackboardutils import FileAttachment +from org.sleuthkit.datamodel.blackboardutils import MessageAttachments from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import MessageReadStatus from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import CommunicationDirection @@ -93,7 +95,7 @@ class TextNowAnalyzer(general.AndroidComponentAnalyzer): ) self.parse_contacts(textnow_db, helper) self.parse_calllogs(textnow_db, helper) - self.parse_messages(textnow_db, helper) + self.parse_messages(textnow_db, helper, current_case) except NoCurrentCaseException as ex: self._logger.log(Level.WARNING, "No case currently open.", ex) self._logger.log(Level.WARNING, traceback.format_exc()) @@ -159,23 +161,30 @@ class TextNowAnalyzer(general.AndroidComponentAnalyzer): "Error posting TextNow call log artifact to the blackboard", ex) self._logger.log(Level.WARNING, traceback.format_exc()) - def parse_messages(self, textnow_db, helper): + def parse_messages(self, textnow_db, helper, current_case): #Query for messages and iterate row by row adding #each message artifact try: messages_parser = TextNowMessagesParser(textnow_db) while messages_parser.next(): - helper.addMessage( - messages_parser.get_message_type(), - messages_parser.get_message_direction(), - messages_parser.get_phone_number_from(), - messages_parser.get_phone_number_to(), - messages_parser.get_message_date_time(), - messages_parser.get_message_read_status(), - messages_parser.get_message_subject(), - messages_parser.get_message_text(), - messages_parser.get_thread_id() - ) + message_artifact = helper.addMessage( + messages_parser.get_message_type(), + messages_parser.get_message_direction(), + messages_parser.get_phone_number_from(), + messages_parser.get_phone_number_to(), + messages_parser.get_message_date_time(), + messages_parser.get_message_read_status(), + messages_parser.get_message_subject(), + messages_parser.get_message_text(), + messages_parser.get_thread_id() + ) + if (len(messages_parser.get_file_attachment()) > 0): + file_attachments = ArrayList() + self._logger.log(Level.INFO, "SHow Attachment ==> " + str(len(messages_parser.get_file_attachment())) + " <> " + str(messages_parser.get_file_attachment())) + file_attachments.add(FileAttachment(current_case.getSleuthkitCase(), textnow_db.getDBFile().getDataSource(), messages_parser.get_file_attachment())) + message_attachments = MessageAttachments(file_attachments, []) + helper.addAttachments(message_artifact, message_attachments) + messages_parser.close() except SQLException as ex: #Error parsing TextNow db @@ -364,9 +373,6 @@ class TextNowMessagesParser(TskMessagesParser): def get_message_text(self): text = self.result_set.getString("message_text") - attachment = self.result_set.getString("attach") - if attachment != "": - text = general.appendAttachmentList(text, [attachment]) return text def get_thread_id(self): @@ -374,3 +380,9 @@ class TextNowMessagesParser(TskMessagesParser): if thread_id is None: return super(TextNowMessagesParser, self).get_thread_id() return thread_id + + def get_file_attachment(self): + attachment = self.result_set.getString("attach") + if attachment is None: + return None + return self.result_set.getString("attach") diff --git a/InternalPythonModules/android/xender.py b/InternalPythonModules/android/xender.py index 9600788f26..aeddcaf4e1 100644 --- a/InternalPythonModules/android/xender.py +++ b/InternalPythonModules/android/xender.py @@ -43,6 +43,8 @@ from org.sleuthkit.datamodel import TskCoreException from org.sleuthkit.datamodel.Blackboard import BlackboardException from org.sleuthkit.datamodel import Account from org.sleuthkit.datamodel.blackboardutils import CommunicationArtifactsHelper +from org.sleuthkit.datamodel.blackboardutils import FileAttachment +from org.sleuthkit.datamodel.blackboardutils import MessageAttachments from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import MessageReadStatus from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import CommunicationDirection import traceback @@ -107,6 +109,8 @@ class XenderAnalyzer(general.AndroidComponentAnalyzer): fromId = None toId = None + fileAttachments = ArrayList() + if (messagesResultSet.getInt("c_direction") == 1): direction = CommunicationDirection.OUTGOING toId = messagesResultSet.getString("r_device_id") @@ -114,10 +118,6 @@ class XenderAnalyzer(general.AndroidComponentAnalyzer): direction = CommunicationDirection.INCOMING fromId = messagesResultSet.getString("s_device_id") - msgBody = "" # there is no body. - attachments = [messagesResultSet.getString("f_path")] - msgBody = general.appendAttachmentList(msgBody, attachments) - timeStamp = messagesResultSet.getLong("f_create_time") / 1000 messageArtifact = transactionDbHelper.addMessage( self._MESSAGE_TYPE, @@ -127,10 +127,13 @@ class XenderAnalyzer(general.AndroidComponentAnalyzer): timeStamp, MessageReadStatus.UNKNOWN, None, # subject - msgBody, + None, # message text messagesResultSet.getString("c_session_id") ) - # TBD: add the file as attachment ?? + # add the file as attachment + fileAttachments.add(FileAttachment(current_case.getSleuthkitCase(), transactionDb.getDBFile().getDataSource(), messagesResultSet.getString("f_path"))) + messageAttachments = MessageAttachments(fileAttachments, []) + transactionDbHelper.addAttachments(messageArtifact, messageAttachments) except SQLException as ex: self._logger.log(Level.WARNING, "Error processing query result for profiles.", ex) diff --git a/InternalPythonModules/android/zapya.py b/InternalPythonModules/android/zapya.py index 6accd02eea..8843e772c7 100644 --- a/InternalPythonModules/android/zapya.py +++ b/InternalPythonModules/android/zapya.py @@ -43,6 +43,8 @@ from org.sleuthkit.datamodel import TskCoreException from org.sleuthkit.datamodel.Blackboard import BlackboardException from org.sleuthkit.datamodel import Account from org.sleuthkit.datamodel.blackboardutils import CommunicationArtifactsHelper +from org.sleuthkit.datamodel.blackboardutils import FileAttachment +from org.sleuthkit.datamodel.blackboardutils import MessageAttachments from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import MessageReadStatus from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import CommunicationDirection @@ -88,6 +90,7 @@ class ZapyaAnalyzer(general.AndroidComponentAnalyzer): direction = CommunicationDirection.UNKNOWN fromId = None toId = None + fileAttachments = ArrayList() if (transfersResultSet.getInt("direction") == 1): direction = CommunicationDirection.OUTGOING @@ -95,10 +98,6 @@ class ZapyaAnalyzer(general.AndroidComponentAnalyzer): else: direction = CommunicationDirection.INCOMING fromId = transfersResultSet.getString("device") - - msgBody = "" # there is no body. - attachments = [transfersResultSet.getString("path")] - msgBody = general.appendAttachmentList(msgBody, attachments) timeStamp = transfersResultSet.getLong("createtime") / 1000 messageArtifact = transferDbHelper.addMessage( @@ -109,10 +108,13 @@ class ZapyaAnalyzer(general.AndroidComponentAnalyzer): timeStamp, MessageReadStatus.UNKNOWN, None, # subject - msgBody, + None, # message Text None ) # thread id - # TBD: add the file as attachment ?? + # add the file as attachment + fileAttachments.add(FileAttachment(current_case.getSleuthkitCase(), transferDb.getDBFile().getDataSource(), transfersResultSet.getString("path"))) + messageAttachments = MessageAttachments(fileAttachments, []) + transferDbHelper.addAttachments(messageArtifact, messageAttachments) except SQLException as ex: self._logger.log(Level.WARNING, "Error processing query result for transfer.", ex) diff --git a/Running_Linux_OSX.txt b/Running_Linux_OSX.txt index bb60569a56..c47dc5b418 100644 --- a/Running_Linux_OSX.txt +++ b/Running_Linux_OSX.txt @@ -9,33 +9,41 @@ The following need to be done at least once. They do not need to be repeated for -- Linux: % sudo apt-get install testdisk -- OS X: % brew install testdisk -- Install a Java 8 JRE and JavaFX 8 and set JAVA_HOME. --- Linux: Any Java 8 version of OpenJDK/OpenJFX distribution should suffice. The following instructions use the Zulu Community distribution. - 1. Download a 64 bit Java 8 JRE for your specific platform from https://www.azul.com/downloads/zulu-community - 2. Install the JRE. e.g. % sudo apt install ./zulu8.40.0.25-ca-jre8.0.222-linux_amd64.deb - 3. Download a 64 bit Java 8 JavaFX for your specific platform from the same location. - - Note that you may need to select "Older Zulu versions" for FX to become available in the "Java Package" dropdown. - 4. Extract the contents of the JavaFX archive into the folder where the JRE was installed. - e.g. % sudo tar xzf ~/Downloads/zulu8.40.0.25-ca-fx-jre8.0.222-linux_x64.tar.gz -C /usr/lib/jvm/zre-8-amd64 --strip-components=1 - +- Install the BellSoft Java 8 JRE and JavaFX 8 distribution and set JAVA_HOME. + * The BellSoft distribution bundles OpenJDK and OpenJFX. Other distributions we have tried either don't + bundle OpenJFX (AdoptOpenJDK) or don't include all necessary binaries (Amazon Corretto). +-- Linux: + 1. Install BellSoft Java 8 + % wget -q -O - https://download.bell-sw.com/pki/GPG-KEY-bellsoft | sudo apt-key add - + % echo "deb [arch=amd64] https://apt.bell-sw.com/ stable main" | sudo tee /etc/apt/sources.list.d/bellsoft.list + % sudo apt-get update + % sudo apt-get install bellsoft-java8 + 2. Set JAVA_HOME + % export JAVA_HOME=/usr/lib/jvm/bellsoft-java8-amd64 + NOTE: You may need to log out and back in again after setting JAVA_HOME before the Autopsy unix_setup.sh script can see the value. --- OS X: Any Java 8 version of OpenJDK/OpenJFX distribution should suffice. The following instructions use the AdoptOpenJDK distribution. - 1. Install a 64 bit Java 8 JRE. - % brew cask install adoptopenjdk8 +-- OS X: + 1. Install BellSoft Java 8. + % brew tap bell-sw/liberica + % brew cask install liberica-jdk8 2. Set JAVA_HOME environment variable to location of JRE installation. e.g. add the following to ~/.bashrc export JAVA_HOME=$(/usr/libexec/java_home -v 1.8) - 3. Confirm your version of Java by running - % java -version +- Confirm your version of Java by running + % java -version + openjdk version "1.8.0.232" + OpenJDK Runtime Environment (build 1.8.0_232-BellSoft-b10) + OpenJDK 64-Bit Server VM (build 25.232-b10, mixed mode) + * Install The Sleuth Kit Java Bindings * Autopsy depends on a specific version of The Sleuth Kit. You need the Java libraries of The Sleuth Kit installed, which is not part of all packages. - Linux: Install the sleuthkit-java.deb file that you can download from github.com/sleuthkit/sleuthkit/releases. This will install libewf, etc. --- % sudo apt install ./sleuthkit-java_4.6.0-1_amd64.deb +-- % sudo apt install ./sleuthkit-java_4.7.0-1_amd64.deb - OS X: Install The Sleuth Kit from brew. -- % brew install sleuthkit @@ -55,6 +63,24 @@ Autopsy depends on a specific version of The Sleuth Kit. You need the Java libr - Run Autopsy % ./autopsy +* Troubleshooting * + +- If you see something like "Cannot create case: javafx/scene/paint/Color" it is an indication that Java FX + is not being found. + Confirm that the file $JAVA_HOME/jre/lib/ext/jfxrt.jar exists. If it does not exist, return to the Java + setup steps above. +- If you see something like "An illegal reflective access operation has occurred" it is an indication that + the wrong version of Java is being used to run Autopsy. + Check the version of Java reported in the ~/.autopsy/dev/var/log/messages.log file. It should contain lines that looks like: + Java; VM; Vendor = 1.8.0_232; OpenJDK 64-Bit Server V 25.232-b10; BellSoft + Runtime = OpenJDK Runtime Environment 1.8.0_232-BellSoft-b10 + Java Home = /usr/lib/jvm/bellsoft-java8-amd64/jre + + If your messages.log file indicates that Java 8 is not being used: + (a) confirm that you have a version of Java 8 installed and + (b) confirm that your JAVA_HOME environment variable is set correctly: + % echo $JAVA_HOME + * Limitations (Updated May 2018) * - Timeline does not work on OS X - Video thumbnails are not generated (need to get a consistent version of OpenCV) diff --git a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java index 5c42269a8a..f1260f269f 100644 --- a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java +++ b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java @@ -503,6 +503,9 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { DerivedFile df = fileManager.addDerivedFile(filename, relPath, size, cTime, crTime, aTime, mTime, true, messageArtifact, "", EmailParserModuleFactory.getModuleName(), EmailParserModuleFactory.getModuleVersion(), "", encodingType); + + associateAttachmentWithMesssge(messageArtifact, df); + files.add(df); } catch (TskCoreException ex) { postErrorMessage( @@ -516,6 +519,19 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule { return files; } + /** + * Creates a TSK_ASSOCIATED_OBJECT artifact between the attachment file and + * the message artifact. + */ + private BlackboardArtifact associateAttachmentWithMesssge(BlackboardArtifact message, AbstractFile attachedFile) throws TskCoreException { + Collection attributes = new ArrayList<>(); + attributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT, EmailParserModuleFactory.getModuleName(), message.getArtifactID())); + + BlackboardArtifact bba = attachedFile.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_ASSOCIATED_OBJECT); + bba.addAttributes(attributes); //write out to bb + return bba; + } + /** * Finds and returns a set of unique email addresses found in the input string *