diff --git a/BootstrapIvy.xml b/BootstrapIvy.xml index 031c1f2cf3..c668d480ba 100644 --- a/BootstrapIvy.xml +++ b/BootstrapIvy.xml @@ -15,7 +15,7 @@ - diff --git a/Core/build.xml b/Core/build.xml index 0e5c90ef04..5d91ade7e1 100644 --- a/Core/build.xml +++ b/Core/build.xml @@ -74,6 +74,7 @@ + diff --git a/Core/ivysettings.xml b/Core/ivysettings.xml index 7a4d38c65e..c27e095ddb 100644 --- a/Core/ivysettings.xml +++ b/Core/ivysettings.xml @@ -2,7 +2,7 @@ - + diff --git a/Core/nbproject/project.properties b/Core/nbproject/project.properties index c01abc4855..94dffdeb9b 100644 --- a/Core/nbproject/project.properties +++ b/Core/nbproject/project.properties @@ -120,6 +120,7 @@ file.reference.okhttp-2.7.5-javadoc.jar=release/modules/ext/okhttp-2.7.5-javadoc file.reference.okhttp-2.7.5-sources.jar=release/modules/ext/okhttp-2.7.5-sources.jar file.reference.okhttp-2.7.5.jar=release/modules/ext/okhttp-2.7.5.jar file.reference.okio-1.6.0.jar=release/modules/ext/okio-1.6.0.jar +file.reference.datcon.jar=release/modules/ext/DatCon.jar javac.source=1.8 javac.compilerargs=-Xlint -Xlint:-serial license.file=../LICENSE-2.0.txt diff --git a/Core/nbproject/project.xml b/Core/nbproject/project.xml index 743b58e965..c3f58c1ec6 100644 --- a/Core/nbproject/project.xml +++ b/Core/nbproject/project.xml @@ -803,6 +803,10 @@ ext/jutf7-1.0.0.jar release/modules/ext/jutf7-1.0.0.jar + + + ext/DatCon.jar + release/modules/ext/DatCon.jar diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/ModuleSettings.java b/Core/src/org/sleuthkit/autopsy/coreutils/ModuleSettings.java index 7f874395e3..39fec3d161 100644 --- a/Core/src/org/sleuthkit/autopsy/coreutils/ModuleSettings.java +++ b/Core/src/org/sleuthkit/autopsy/coreutils/ModuleSettings.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011 Basis Technology Corp. + * Copyright 2012-2020 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,6 +23,7 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; import java.util.Properties; @@ -30,47 +31,57 @@ import java.util.Set; import java.util.logging.Level; /** - * This class contains the framework to read, add, update, and remove from the - * property files located at %USERDIR%/Config/x.properties + * Provides utility methods for creating, updating, and deleting Java properties + * files with paths such as %USERDIR%/Config/[module name].properties, where + * "module name" is intended to be a module name and the properties file is + * intended to be a settings file for the module. + * + * Very coarse-grained thread safety is provided by these utilities if all + * modules confine themselves to their use when manipulating their settings + * files, with the consequence of serializing all such operations across the + * entire application. + * + * TODO (JIRA-5964): The error handling in this class is not consistent with + * Autopsy error handling policy. */ public class ModuleSettings { - // The directory where the properties file is located - private final static String moduleDirPath = PlatformUtil.getUserConfigDirectory(); + private final static Logger logger = Logger.getLogger(ModuleSettings.class.getName()); + private final static String MODULE_DIR_PATH = PlatformUtil.getUserConfigDirectory(); + private final static String SETTINGS_FILE_EXT = ".properties"; + + /* + * These SHOULD NOT be public and DO NOT belong in this file. They are being + * retained only for the sake of backwards compatibility. + */ public static final String DEFAULT_CONTEXT = "GeneralContext"; //NON-NLS public static final String MAIN_SETTINGS = "Case"; //NON-NLS public static final String CURRENT_CASE_TYPE = "Current_Case_Type"; //NON-NLS /** - * the constructor - */ - private ModuleSettings() { - } - - /** - * Makes a new config file of the specified name. Do not include the - * extension. + * Makes a new settings file for a module. * - * @param moduleName - The name of the config file to make + * @param moduleName The module name. * - * @return True if successfully created, false if already exists or an error - * is thrown. + * @return True if the settings file was created, false if the file already + * existed or could not be created. */ - public static boolean makeConfigFile(String moduleName) { + public static synchronized boolean makeConfigFile(String moduleName) { if (!configExists(moduleName)) { - File propPath = new File(moduleDirPath + File.separator + moduleName + ".properties"); + File propPath = new File(getSettingsFilePath(moduleName)); File parent = new File(propPath.getParent()); if (!parent.exists()) { parent.mkdirs(); } + Properties props = new Properties(); try { propPath.createNewFile(); - FileOutputStream fos = new FileOutputStream(propPath); - props.store(fos, ""); - fos.close(); - } catch (IOException e) { - Logger.getLogger(ModuleSettings.class.getName()).log(Level.WARNING, "Was not able to create a new properties file.", e); //NON-NLS + try (FileOutputStream fos = new FileOutputStream(propPath)) { + props.store(fos, "Created module settings file"); + } + } catch (IOException ex) { + logger.log(Level.SEVERE, String.format("Failed to create module settings file at %s)", propPath), ex); //NON-NLS return false; } return true; @@ -79,213 +90,217 @@ public class ModuleSettings { } /** - * Determines if a given properties file exists or not. + * Indicates whether or not a settings file exists for a given module. * - * @param moduleName - The name of the config file to evaluate + * @param moduleName The module name. * - * @return true if the config exists, false otherwise. + * @return True or false. */ - public static boolean configExists(String moduleName) { - File f = new File(moduleDirPath + File.separator + moduleName + ".properties"); - return f.exists(); + public static synchronized boolean configExists(String moduleName) { + return new File(getSettingsFilePath(moduleName)).exists(); } - public static boolean settingExists(String moduleName, String settingName) { + /** + * Determines whether or not a given setting exists in the settings file for + * a module. + * + * @param moduleName The module name. + * @param settingName The name of the setting (property). + * + * @return True if the setting file exists, can be read, and contains the + * specified setting (property), false otherwise. + */ + public static synchronized boolean settingExists(String moduleName, String settingName) { if (!configExists(moduleName)) { return false; } + try { Properties props = fetchProperties(moduleName); return (props.getProperty(settingName) != null); - } catch (IOException e) { + } catch (IOException ex) { + logger.log(Level.SEVERE, String.format("Failed to get %s setting from module settings file at %s)", settingName, getSettingsFilePath(moduleName)), ex); //NON-NLS return false; } - } /** - * Returns the path of the given properties file. + * Constructs a settings file path for a given module. * - * @param moduleName - The name of the config file to evaluate + * @param moduleName The module name. * - * @return The path of the given config file. Returns null if the config - * file doesn't exist. + * @return The settings file path as a string. */ - private static String getPropertyPath(String moduleName) { - if (configExists(moduleName)) { - return moduleDirPath + File.separator + moduleName + ".properties"; //NON-NLS - } - - return null; + private static String getSettingsFilePath(String moduleName) { + return Paths.get(MODULE_DIR_PATH, moduleName + SETTINGS_FILE_EXT).toString(); } /** - * Returns the given properties file's setting as specific by settingName. + * Gets the value of a setting (property) from a module settings file. * - * @param moduleName - The name of the config file to read from. - * @param settingName - The setting name to retrieve. + * NOTE: If the settings file does not already exist, it is created. * - * @return - the value associated with the setting. + * @param moduleName The module name. + * @param settingName The setting name. * - * @throws IOException + * @return The value of the setting or null if the file cannot be read or + * the setting is not found. */ - public static String getConfigSetting(String moduleName, String settingName) { + public static synchronized String getConfigSetting(String moduleName, String settingName) { if (!configExists(moduleName)) { makeConfigFile(moduleName); - Logger.getLogger(ModuleSettings.class.getName()).log(Level.INFO, "File did not exist. Created file [" + moduleName + ".properties]"); //NON-NLS NON-NLS } - + try { Properties props = fetchProperties(moduleName); - return props.getProperty(settingName); - } catch (IOException e) { - Logger.getLogger(ModuleSettings.class.getName()).log(Level.WARNING, "Could not read config file [" + moduleName + "]", e); //NON-NLS + } catch (IOException ex) { + logger.log(Level.SEVERE, String.format("Failed to get %s setting from module settings file at %s)", settingName, getSettingsFilePath(moduleName)), ex); //NON-NLS return null; } - } /** - * Returns the given properties file's map of settings. + * Gets the settings (properties) from a module settings file. * - * @param moduleName - the name of the config file to read from. + * NOTE: If the settings file does not already exist, it is created. * - * @return - the map of all key:value pairs representing the settings of the - * config. + * @param moduleName The module name. * - * @throws IOException + * @return A mapping of setting names to setting values from the settings + * file, may be empty. */ - public static Map< String, String> getConfigSettings(String moduleName) { - + public static synchronized Map getConfigSettings(String moduleName) { if (!configExists(moduleName)) { makeConfigFile(moduleName); - Logger.getLogger(ModuleSettings.class.getName()).log(Level.INFO, "File did not exist. Created file [" + moduleName + ".properties]"); //NON-NLS NON-NLS } + try { Properties props = fetchProperties(moduleName); - Set keys = props.stringPropertyNames(); - Map map = new HashMap(); - + Map map = new HashMap<>(); for (String s : keys) { map.put(s, props.getProperty(s)); } - return map; - } catch (IOException e) { - Logger.getLogger(ModuleSettings.class.getName()).log(Level.WARNING, "Could not read config file [" + moduleName + "]", e); //NON-NLS + } catch (IOException ex) { + logger.log(Level.SEVERE, String.format("Failed to get settings from module settings file at %s)", getSettingsFilePath(moduleName)), ex); //NON-NLS return null; } } /** - * Sets the given properties file to the given setting map. + * Adds a mapping of setting name to setting values to a module settings + * file. * - * @param moduleName - The name of the module to be written to. - * @param settings - The mapping of all key:value pairs of settings to add - * to the config. + * NOTE: If the settings file does not already exist, it is created. + * + * @param moduleName The module name. + * @param settings The module settings. */ public static synchronized void setConfigSettings(String moduleName, Map settings) { if (!configExists(moduleName)) { makeConfigFile(moduleName); - Logger.getLogger(ModuleSettings.class.getName()).log(Level.INFO, "File did not exist. Created file [" + moduleName + ".properties]"); //NON-NLS NON-NLS } + try { Properties props = fetchProperties(moduleName); - for (Map.Entry kvp : settings.entrySet()) { props.setProperty(kvp.getKey(), kvp.getValue()); } - - File path = new File(getPropertyPath(moduleName)); - FileOutputStream fos = new FileOutputStream(path); - props.store(fos, "Changed config settings(batch)"); //NON-NLS - fos.close(); - } catch (IOException e) { - Logger.getLogger(ModuleSettings.class.getName()).log(Level.WARNING, "Property file exists for [" + moduleName + "] at [" + getPropertyPath(moduleName) + "] but could not be loaded.", e); //NON-NLS NON-NLS NON-NLS + + File path = new File(getSettingsFilePath(moduleName)); + try (FileOutputStream fos = new FileOutputStream(path)) { + props.store(fos, "Set settings (batch)"); //NON-NLS + } + } catch (IOException ex) { + logger.log(Level.SEVERE, String.format("Error writing to module settings file at %s)", getSettingsFilePath(moduleName)), ex); //NON-NLS } } /** - * Sets the given properties file to the given settings. + * Sets the value of a setting (property) in a module settings file. * - * @param moduleName - The name of the module to be written to. - * @param settingName - The name of the setting to be modified. - * @param settingVal - the value to set the setting to. + * NOTE: If the settings file does not already exist, it is created. + * + * @param moduleName The module name. + * @param settingName The setting name. + * @param settingVal The setting value. */ public static synchronized void setConfigSetting(String moduleName, String settingName, String settingVal) { if (!configExists(moduleName)) { makeConfigFile(moduleName); - Logger.getLogger(ModuleSettings.class.getName()).log(Level.INFO, "File did not exist. Created file [" + moduleName + ".properties]"); //NON-NLS NON-NLS } - try { Properties props = fetchProperties(moduleName); - props.setProperty(settingName, settingVal); - - File path = new File(getPropertyPath(moduleName)); - FileOutputStream fos = new FileOutputStream(path); - props.store(fos, "Changed config settings(single)"); //NON-NLS - fos.close(); - } catch (IOException e) { - Logger.getLogger(ModuleSettings.class.getName()).log(Level.WARNING, "Property file exists for [" + moduleName + "] at [" + getPropertyPath(moduleName) + "] but could not be loaded.", e); //NON-NLS NON-NLS NON-NLS - } - } - - /** - * Removes the given key from the given properties file. - * - * @param moduleName - The name of the properties file to be modified. - * @param key - the name of the key to remove. - */ - public static synchronized void removeProperty(String moduleName, String key) { - try { - if (getConfigSetting(moduleName, key) != null) { - Properties props = fetchProperties(moduleName); - - props.remove(key); - File path = new File(getPropertyPath(moduleName)); - FileOutputStream fos = new FileOutputStream(path); - props.store(fos, "Removed " + key); //NON-NLS - fos.close(); + File path = new File(getSettingsFilePath(moduleName)); + try (FileOutputStream fos = new FileOutputStream(path)) { + props.store(fos, "Set " + settingName); //NON-NLS } - } catch (IOException e) { - Logger.getLogger(ModuleSettings.class.getName()).log(Level.WARNING, "Could not remove property from file, file not found", e); //NON-NLS + } catch (IOException ex) { + logger.log(Level.SEVERE, String.format("Error writing %s setting to module settings file at %s)", settingName, getSettingsFilePath(moduleName)), ex); //NON-NLS } } /** - * Returns the properties file as specified by moduleName. + * Removes a setting (property) in a module settings file. * - * @param moduleName - * - * @return Properties file as specified by moduleName. - * - * @throws IOException + * @param moduleName The module name. + * @param settingName The setting name. */ - private static Properties fetchProperties(String moduleName) throws IOException { - InputStream inputStream = new FileInputStream(getPropertyPath(moduleName)); - Properties props = new Properties(); - props.load(inputStream); - inputStream.close(); + public static synchronized void removeProperty(String moduleName, String settingName) { + try { + if (getConfigSetting(moduleName, settingName) != null) { + Properties props = fetchProperties(moduleName); + props.remove(settingName); + File path = new File(getSettingsFilePath(moduleName)); + try (FileOutputStream fos = new FileOutputStream(path)) { + props.store(fos, "Removed " + settingName); //NON-NLS + } + } + } catch (IOException ex) { + logger.log(Level.SEVERE, String.format("Error removing %s setting from module settings file at %s)", settingName, getSettingsFilePath(moduleName)), ex); //NON-NLS + } + } + + /** + * Gets the contents of a module settings file as a Properties object. + * + * @param moduleName The module name. + * + * @return The Properties object. + * + * @throws IOException If there is a problem reading the settings file. + */ + private static synchronized Properties fetchProperties(String moduleName) throws IOException { + Properties props; + try (InputStream inputStream = new FileInputStream(getSettingsFilePath(moduleName))) { + props = new Properties(); + props.load(inputStream); + } return props; } /** - * Gets the property file as specified. + * Gets a File object for a module settings (properties) file. * - * @param moduleName + * @param moduleName The module name. * - * @return A new file handle, returns null if the file does not exist. + * @return The File object or null if the file does not exist. */ - public static File getPropertyFile(String moduleName) { - String path = getPropertyPath(moduleName); - if (path == null) { - return null; - } else { - return new File(getPropertyPath(moduleName)); + public static synchronized File getPropertyFile(String moduleName) { + File configFile = null; + if (configExists(moduleName)) { + configFile = new File(getSettingsFilePath(moduleName)); } + return configFile; } + + /** + * Private constructor to prevent instantiation of this utility class. + */ + private ModuleSettings() { + } + } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/BaseChildFactory.java b/Core/src/org/sleuthkit/autopsy/datamodel/BaseChildFactory.java index d3a27a1eb3..123ec7d7c3 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/BaseChildFactory.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/BaseChildFactory.java @@ -286,8 +286,14 @@ public abstract class BaseChildFactory extends ChildFactory.D * If pageSize is set split keys into pages, otherwise create a * single page containing all keys. */ - if (keys.isEmpty()) { - pages.clear(); + if (keys.isEmpty() && !pages.isEmpty()) { + /** + * If we previously had keys (i.e. pages is not empty) and now + * we don't have keys, reset pages to an empty list. + * Cannot use a call to List.clear() here because the call to + * Lists.partition() below returns an unmodifiable list. + */ + pages = new ArrayList<>(); } else { pages = Lists.partition(keys, pageSize > 0 ? pageSize : keys.size()); } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java index b063a0f006..5a02ba9a55 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java @@ -312,7 +312,11 @@ public class BlackboardArtifactNode extends AbstractContentNode sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -19,6 +19,7 @@ package org.sleuthkit.autopsy.geolocation; import java.awt.BorderLayout; +import java.awt.Component; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.beans.PropertyChangeEvent; @@ -30,6 +31,7 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.EnumSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Set; @@ -49,6 +51,7 @@ 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.Track; import org.sleuthkit.autopsy.geolocation.datamodel.Waypoint; import org.sleuthkit.autopsy.geolocation.datamodel.WaypointBuilder; import org.sleuthkit.autopsy.geolocation.datamodel.WaypointBuilder.WaypointFilterQueryCallBack; @@ -85,12 +88,12 @@ public final class GeolocationTopComponent extends TopComponent { // This is the hardcoded report name from KMLReport.java private static final String REPORT_KML = "ReportKML.kml"; - + private boolean mapInitalized = false; @Messages({ "GLTopComponent_name=Geolocation", - "GLTopComponent_initilzation_error=An error occurred during waypoint initilization. Geolocation data maybe incomplete." + "GLTopComponent_initilzation_error=An error occurred during waypoint initilization. Geolocation data maybe incomplete." }) /** @@ -113,13 +116,14 @@ public final class GeolocationTopComponent extends TopComponent { || eventData.getBlackboardArtifactType().getTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_LAST_KNOWN_LOCATION.getTypeID() || eventData.getBlackboardArtifactType().getTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_ROUTE.getTypeID() || eventData.getBlackboardArtifactType().getTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_METADATA_EXIF.getTypeID() - || eventData.getBlackboardArtifactType().getTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_BOOKMARK.getTypeID())) { + || eventData.getBlackboardArtifactType().getTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_BOOKMARK.getTypeID() + || eventData.getBlackboardArtifactType().getTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_TRACK.getTypeID())) { showRefreshPanel(true); } } }; - + this.caseEventListener = pce -> { mapPanel.clearWaypoints(); if (pce.getNewValue() != null) { @@ -148,7 +152,7 @@ public final class GeolocationTopComponent extends TopComponent { filterPane.setPanel(geoFilterPanel); geoFilterPanel.addActionListener(new ActionListener() { @Override - public void actionPerformed(ActionEvent e) { + public void actionPerformed(ActionEvent e) { updateWaypoints(); } }); @@ -186,7 +190,7 @@ public final class GeolocationTopComponent extends TopComponent { public void componentOpened() { super.componentOpened(); WindowManager.getDefault().setTopComponentFloating(this, true); - + } @Messages({ @@ -199,7 +203,7 @@ public final class GeolocationTopComponent extends TopComponent { mapPanel.clearWaypoints(); geoFilterPanel.clearDataSourceList(); geoFilterPanel.updateDataSourceList(); - + // Let's make sure we only do this on the first open if (!mapInitalized) { try { @@ -217,7 +221,7 @@ public final class GeolocationTopComponent extends TopComponent { return; // Doen't set the waypoints. } } - mapPanel.setWaypoints(new ArrayList<>()); + mapPanel.setWaypoints(new LinkedHashSet<>()); updateWaypoints(); } @@ -227,12 +231,27 @@ public final class GeolocationTopComponent extends TopComponent { * @param show Whether to show or hide the panel. */ private void showRefreshPanel(boolean show) { - if (show) { - mapPanel.add(refreshPanel, BorderLayout.NORTH); - } else { - mapPanel.remove(refreshPanel); - } - mapPanel.revalidate(); + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + boolean isShowing = false; + Component[] comps = mapPanel.getComponents(); + for(Component comp: comps) { + if(comp.equals(refreshPanel)) { + isShowing = true; + break; + } + } + if (show && !isShowing) { + mapPanel.add(refreshPanel, BorderLayout.NORTH); + mapPanel.revalidate(); + } else if(!show && isShowing){ + mapPanel.remove(refreshPanel); + mapPanel.revalidate(); + } + } + }); + } /** @@ -242,7 +261,7 @@ public final class GeolocationTopComponent extends TopComponent { @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_msg=Exception occurred 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" @@ -378,7 +397,7 @@ public final class GeolocationTopComponent extends TopComponent { String reportBaseDir = createReportDirectory(); progressPanel.setLabels(REPORT_KML, reportBaseDir); - + SwingWorker worker = new SwingWorker() { @Override protected Void doInBackground() throws Exception { @@ -406,7 +425,7 @@ public final class GeolocationTopComponent extends TopComponent { /** * A runnable class for getting waypoints based on the current filters. */ - private class WaypointRunner implements Runnable { + private class WaypointRunner implements Runnable, WaypointFilterQueryCallBack { private final GeoFilter filters; @@ -428,7 +447,7 @@ public final class GeolocationTopComponent extends TopComponent { filters.showAllWaypoints(), filters.getMostRecentNumDays(), filters.showWaypointsWithoutTimeStamp(), - new WaypointCallBack()); + this); } catch (GeoLocationDataException ex) { logger.log(Level.SEVERE, "Failed to filter waypoints.", ex); @@ -439,30 +458,32 @@ public final class GeolocationTopComponent extends TopComponent { Bundle.GeoTopComponent_filter_exception_Title(), Bundle.GeoTopComponent_filter_exception_msg(), JOptionPane.ERROR_MESSAGE); - + setWaypointLoading(false); } }); } } - } - - /** - * Callback for getting waypoints. - */ - private class WaypointCallBack implements WaypointFilterQueryCallBack { - @Override - public void process(final List waypoints) { - // Make sure that the waypoints are added to the map panel in - // the correct thread. + public void process(List waypoints) { + + List tracks = null; + try { + tracks = Track.getTracks(Case.getCurrentCase().getSleuthkitCase(), filters.getDataSources()); + } catch (GeoLocationDataException ex) { + logger.log(Level.WARNING, "Exception thrown while retrieving list of Tracks", ex); + } + + List completeList = createWaypointList(waypoints, tracks); + final Set pointSet = MapWaypoint.getWaypoints(completeList); + SwingUtilities.invokeLater(new Runnable() { @Override public void run() { // If the list is empty, tell the user and do not change // the visible waypoints. - if (waypoints == null || waypoints.isEmpty()) { + if (completeList == null || completeList.isEmpty()) { mapPanel.clearWaypoints(); JOptionPane.showMessageDialog(GeolocationTopComponent.this, Bundle.GeoTopComponent_no_waypoints_returned_Title(), @@ -473,11 +494,173 @@ public final class GeolocationTopComponent extends TopComponent { return; } mapPanel.clearWaypoints(); - mapPanel.setWaypoints(MapWaypoint.getWaypoints(waypoints)); + mapPanel.setWaypoints(pointSet); setWaypointLoading(false); geoFilterPanel.setEnabled(true); } }); } + + /** + * Returns a complete list of waypoints including the tracks. Takes into + * account the current filters and includes waypoints as approprate. + * + * @param waypoints List of waypoints + * @param tracks List of tracks + * + * @return A list of waypoints including the tracks based on the current + * filters. + */ + private List createWaypointList(List waypoints, List tracks) { + final List completeList = new ArrayList<>(); + + if (tracks != null) { + Long timeRangeEnd; + Long timeRangeStart; + if (!filters.showAllWaypoints()) { + // Figure out what the most recent time is given the filtered + // waypoints and the tracks. + timeRangeEnd = getMostRecent(waypoints, tracks); + timeRangeStart = timeRangeEnd - (86400 * filters.getMostRecentNumDays()); + + completeList.addAll(getWaypointsInRange(timeRangeStart, timeRangeEnd, waypoints)); + completeList.addAll(getTracksInRange(timeRangeStart, timeRangeEnd, tracks)); + + } else { + completeList.addAll(waypoints); + for (Track track : tracks) { + completeList.addAll(track.getPath()); + } + } + } else { + completeList.addAll(waypoints); + } + + return completeList; + } + + /** + * Return a list of waypoints that fall into the given time range. + * + * @param timeRangeStart start timestamp of range (seconds from java + * epoch) + * @param timeRangeEnd start timestamp of range (seconds from java + * epoch) + * @param waypoints List of waypoints to filter. + * + * @return A list of waypoints that fall into the time range. + */ + private List getWaypointsInRange(Long timeRangeStart, Long timeRangeEnd, List waypoints) { + List completeList = new ArrayList<>(); + // Add all of the waypoints that fix into the time range. + if (waypoints != null) { + for (Waypoint point : waypoints) { + Long time = point.getTimestamp(); + if ((time == null && filters.showWaypointsWithoutTimeStamp()) + || (time != null && (time >= timeRangeStart && time <= timeRangeEnd))) { + + completeList.add(point); + } + } + } + return completeList; + } + + /** + * Return a list of waypoints from the given tracks that fall into for + * tracks that fall into the given time range. The track start time will + * used for determining if the whole track falls into the range. + * + * @param timeRangeStart start timestamp of range (seconds from java + * epoch) + * @param timeRangeEnd start timestamp of range (seconds from java + * epoch) + * @param tracks Track list. + * + * @return A list of waypoints that that belong to tracks that fall into + * the time range. + */ + private List getTracksInRange(Long timeRangeStart, Long timeRangeEnd, List tracks) { + List completeList = new ArrayList<>(); + if (tracks != null) { + for (Track track : tracks) { + Long trackTime = track.getStartTime(); + + if ((trackTime == null && filters.showWaypointsWithoutTimeStamp()) + || (trackTime != null && (trackTime >= timeRangeStart && trackTime <= timeRangeEnd))) { + + completeList.addAll(track.getPath()); + } + } + } + return completeList; + } + + /** + * Find the latest time stamp in the given list of waypoints. + * + * @param points List of Waypoints, required. + * + * @return The latest time stamp (seconds from java epoch) + */ + private Long findMostRecentTimestamp(List points) { + + Long mostRecent = null; + + for (Waypoint point : points) { + if (mostRecent == null) { + mostRecent = point.getTimestamp(); + } else { + mostRecent = Math.max(mostRecent, point.getTimestamp()); + } + } + + return mostRecent; + } + + /** + * Find the latest time stamp in the given list of tracks. + * + * @param tracks List of Waypoints, required. + * + * @return The latest time stamp (seconds from java epoch) + */ + private Long findMostRecentTracks(List tracks) { + Long mostRecent = null; + + for (Track track : tracks) { + if (mostRecent == null) { + mostRecent = track.getStartTime(); + } else { + mostRecent = Math.max(mostRecent, track.getStartTime()); + } + } + + return mostRecent; + } + + /** + * Returns the "most recent" timestamp amount the list of waypoints and + * track points. + * + * @param points List of Waypoints + * @param tracks List of Tracks + * + * @return Latest time stamp (seconds from java epoch) + */ + private Long getMostRecent(List points, List tracks) { + Long waypointMostRecent = findMostRecentTimestamp(points); + Long trackMostRecent = findMostRecentTracks(tracks); + + if (waypointMostRecent != null && trackMostRecent != null) { + return Math.max(waypointMostRecent, trackMostRecent); + } else if (waypointMostRecent == null && trackMostRecent != null) { + return trackMostRecent; + } else if (waypointMostRecent != null && trackMostRecent == null) { + return waypointMostRecent; + } + + return null; + } } } diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/MapPanel.form b/Core/src/org/sleuthkit/autopsy/geolocation/MapPanel.form index bea654be40..e915904d42 100755 --- a/Core/src/org/sleuthkit/autopsy/geolocation/MapPanel.form +++ b/Core/src/org/sleuthkit/autopsy/geolocation/MapPanel.form @@ -14,7 +14,7 @@ - + @@ -46,26 +46,7 @@ - - - - - - - - - - - - - - - - - - - - + @@ -86,6 +67,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/MapPanel.java b/Core/src/org/sleuthkit/autopsy/geolocation/MapPanel.java index 7645662e28..4d286467b8 100755 --- a/Core/src/org/sleuthkit/autopsy/geolocation/MapPanel.java +++ b/Core/src/org/sleuthkit/autopsy/geolocation/MapPanel.java @@ -82,6 +82,7 @@ final public class MapPanel extends javax.swing.JPanel { private static final long serialVersionUID = 1L; private boolean zoomChanging; private KdTree waypointTree; + private Set waypointSet; private Popup currentPopup; private final PopupFactory popupFactory; @@ -195,26 +196,14 @@ final public class MapPanel extends javax.swing.JPanel { mapViewer.setCenterPosition(new GeoPosition(0, 0)); // Basic painters for the way points. - WaypointPainter waypointPainter = new WaypointPainter() { + WaypointPainter waypointPainter = new WaypointPainter() { @Override - public Set getWaypoints() { - //To assure that the currentlySelectedWaypoint is visible it needs - // to be painted last. LinkedHashSet has a predicable ordering. - Set set = new LinkedHashSet<>(); - if (waypointTree != null) { - Iterator iterator = waypointTree.iterator(); - while (iterator.hasNext()) { - MapWaypoint point = iterator.next(); - set.add(point); - } - // Add the currentlySelectedWaypoint to the end so that - // it will be painted last. - if (currentlySelectedWaypoint != null) { - set.remove(currentlySelectedWaypoint); - set.add(currentlySelectedWaypoint); - } + public Set getWaypoints() { + if (currentlySelectedWaypoint != null) { + waypointSet.remove(currentlySelectedWaypoint); + waypointSet.add(currentlySelectedWaypoint); } - return set; + return waypointSet; } }; @@ -222,7 +211,6 @@ final public class MapPanel extends javax.swing.JPanel { waypointPainter.setRenderer(new MapWaypointRenderer()); } catch (IOException ex) { logger.log(Level.WARNING, "Failed to load waypoint image resource, using DefaultWaypointRenderer", ex); - waypointPainter.setRenderer(new DefaultWaypointRenderer()); } mapViewer.setOverlayPainter(waypointPainter); @@ -305,13 +293,12 @@ final public class MapPanel extends javax.swing.JPanel { * * @param waypoints List of waypoints */ - void setWaypoints(List waypoints) { + void setWaypoints(Set waypoints) { waypointTree = new KdTree<>(); - + this.waypointSet = waypoints; for (MapWaypoint waypoint : waypoints) { waypointTree.add(waypoint); } - mapViewer.repaint(); } @@ -548,6 +535,8 @@ final public class MapPanel extends javax.swing.JPanel { mapViewer = new org.jxmapviewer.JXMapViewer(); zoomPanel = new javax.swing.JPanel(); zoomSlider = new javax.swing.JSlider(); + javax.swing.JButton zoomInBtn = new javax.swing.JButton(); + javax.swing.JButton zoomOutBtn = new javax.swing.JButton(); setFocusable(false); setLayout(new java.awt.BorderLayout()); @@ -573,6 +562,7 @@ final public class MapPanel extends javax.swing.JPanel { zoomPanel.setFocusable(false); zoomPanel.setOpaque(false); zoomPanel.setRequestFocusEnabled(false); + zoomPanel.setLayout(new java.awt.GridBagLayout()); zoomSlider.setMaximum(15); zoomSlider.setMinimum(10); @@ -588,23 +578,44 @@ final public class MapPanel extends javax.swing.JPanel { zoomSliderStateChanged(evt); } }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + zoomPanel.add(zoomSlider, gridBagConstraints); - javax.swing.GroupLayout zoomPanelLayout = new javax.swing.GroupLayout(zoomPanel); - zoomPanel.setLayout(zoomPanelLayout); - zoomPanelLayout.setHorizontalGroup( - zoomPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(zoomPanelLayout.createSequentialGroup() - .addGap(0, 0, 0) - .addComponent(zoomSlider, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) - ); - zoomPanelLayout.setVerticalGroup( - zoomPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, zoomPanelLayout.createSequentialGroup() - .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addComponent(zoomSlider, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(0, 0, 0)) - ); + zoomInBtn.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/plus-grey.png"))); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(zoomInBtn, org.openide.util.NbBundle.getMessage(MapPanel.class, "MapPanel.zoomInBtn.text")); // NOI18N + zoomInBtn.setBorder(null); + zoomInBtn.setBorderPainted(false); + zoomInBtn.setFocusPainted(false); + zoomInBtn.setRequestFocusEnabled(false); + zoomInBtn.setRolloverEnabled(false); + zoomInBtn.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + zoomInBtnActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + zoomPanel.add(zoomInBtn, gridBagConstraints); + + zoomOutBtn.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/minus-grey.png"))); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(zoomOutBtn, org.openide.util.NbBundle.getMessage(MapPanel.class, "MapPanel.zoomOutBtn.text")); // NOI18N + zoomOutBtn.setBorder(null); + zoomOutBtn.setBorderPainted(false); + zoomOutBtn.setFocusPainted(false); + zoomOutBtn.setRequestFocusEnabled(false); + zoomOutBtn.setRolloverEnabled(false); + zoomOutBtn.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + zoomOutBtnActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + zoomPanel.add(zoomOutBtn, gridBagConstraints); gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.anchor = java.awt.GridBagConstraints.SOUTHWEST; @@ -652,6 +663,16 @@ final public class MapPanel extends javax.swing.JPanel { } }//GEN-LAST:event_mapViewerMouseClicked + private void zoomInBtnActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_zoomInBtnActionPerformed + int currentValue = mapViewer.getZoom(); + setZoom(currentValue-1); + }//GEN-LAST:event_zoomInBtnActionPerformed + + private void zoomOutBtnActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_zoomOutBtnActionPerformed + int currentValue = mapViewer.getZoom(); + setZoom(currentValue+1); + }//GEN-LAST:event_zoomOutBtnActionPerformed + // Variables declaration - do not modify//GEN-BEGIN:variables private org.jxmapviewer.JXMapViewer mapViewer; @@ -662,7 +683,7 @@ final public class MapPanel extends javax.swing.JPanel { /** * Renderer for the map waypoints. */ - private class MapWaypointRenderer implements WaypointRenderer { + private class MapWaypointRenderer implements WaypointRenderer { private final BufferedImage defaultWaypointImage; private final BufferedImage selectedWaypointImage; @@ -677,7 +698,7 @@ final public class MapPanel extends javax.swing.JPanel { } @Override - public void paintWaypoint(Graphics2D gd, JXMapViewer jxmv, Waypoint waypoint) { + public void paintWaypoint(Graphics2D gd, JXMapViewer jxmv, MapWaypoint waypoint) { Point2D point = jxmv.getTileFactory().geoToPixel(waypoint.getPosition(), jxmv.getZoom()); int x = (int)point.getX(); diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/MapWaypoint.java b/Core/src/org/sleuthkit/autopsy/geolocation/MapWaypoint.java index 61b4800d7a..2defebb97e 100755 --- a/Core/src/org/sleuthkit/autopsy/geolocation/MapWaypoint.java +++ b/Core/src/org/sleuthkit/autopsy/geolocation/MapWaypoint.java @@ -23,8 +23,10 @@ import java.awt.image.BufferedImage; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; +import java.util.Set; import java.util.logging.Level; import javax.swing.AbstractAction; import javax.swing.Action; @@ -45,11 +47,7 @@ import org.sleuthkit.autopsy.directorytree.ExternalViewerAction; import org.sleuthkit.autopsy.directorytree.ExternalViewerShortcutAction; import org.sleuthkit.autopsy.directorytree.ExtractAction; import org.sleuthkit.autopsy.directorytree.actionhelpers.ExtractActionHelper; -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; @@ -70,32 +68,6 @@ final class MapWaypoint extends KdTree.XYZPoint implements org.jxmapviewer.viewe private final Waypoint dataModelWaypoint; private final GeoPosition position; - /** - * Returns a list of waypoints for the currently open case. - * - * @param skCase Current case - * - * @return list of waypoints, list will be empty if no waypoints were found - * - * @throws GeoLocationDataException - */ - static List getWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { - List points = WaypointBuilder.getAllWaypoints(skCase); - - List routes = Route.getRoutes(skCase); - for (Route route : routes) { - points.addAll(route.getRoute()); - } - - List mapPoints = new ArrayList<>(); - - for (Waypoint point : points) { - mapPoints.add(new MapWaypoint(point)); - } - - return mapPoints; - } - /** * Returns a list of of MapWaypoint objects for the given list of * datamodel.Waypoint objects. @@ -105,8 +77,8 @@ final class MapWaypoint extends KdTree.XYZPoint implements org.jxmapviewer.viewe * @return List of MapWaypoint objects. List will be empty if dmWaypoints * was empty or null. */ - static List getWaypoints(List dmWaypoints) { - List mapPoints = new ArrayList<>(); + static Set getWaypoints(List dmWaypoints) { + Set mapPoints = new LinkedHashSet<>(); if (dmWaypoints != null) { diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/GeoPath.java b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/GeoPath.java new file mode 100755 index 0000000000..366aed8c08 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/GeoPath.java @@ -0,0 +1,141 @@ +/* + * + * Autopsy Forensic Browser + * + * Copyright 2020 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.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Class representing a series of waypoints that form a path. + */ +public class GeoPath { + + private final List waypointList; + private final String pathName; + private final BlackboardArtifact artifact; + + /** + * Gets the list of Routes from the TSK_GPS_ROUTE artifacts. + * + * @param skCase Currently open SleuthkitCase + * + * @return List of Route objects, empty list will be returned if no Routes + * were found + * + * @throws GeoLocationDataException + */ + static public List getRoutes(SleuthkitCase skCase) throws GeoLocationDataException { + List artifacts = null; + try { + artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_ROUTE); + } catch (TskCoreException ex) { + throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_BOOKMARK", ex); + } + + List routes = new ArrayList<>(); + for (BlackboardArtifact artifact : artifacts) { + Route route = new Route(artifact); + routes.add(route); + } + return routes; + } + + /** + * Gets the list of Routes from the TSK_GPS_TRACK artifacts. + * + * @param skCase Currently open SleuthkitCase + * @param sourceList List of source to return tracks from, maybe null to + * return tracks from all sources + * + * @return List of Route objects, empty list will be returned if no Routes + * were found + * + * @throws GeoLocationDataException + */ + static public List getTracks(SleuthkitCase skCase, List sourceList) throws GeoLocationDataException { + List artifacts = null; + List tracks = new ArrayList<>(); + try { + artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_TRACK); + for (BlackboardArtifact artifact : artifacts) { + if (sourceList == null || sourceList.contains(artifact.getDataSource())) { + Track route = new Track(artifact); + tracks.add(route); + } + } + } catch (TskCoreException ex) { + throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_BOOKMARK", ex); + } + return tracks; + } + + /** + * Path constructor. + * + * @param artifact BlackboardARtifact that this path represents, required + * @param pathName Name for this path, maybe null or empty string. + */ + GeoPath(BlackboardArtifact artifact, String pathName) { + this.waypointList = new ArrayList<>(); + this.pathName = pathName; + this.artifact = artifact; + } + + /** + * Adds a Waypoint to the path. + * + * @param point + */ + final void addToPath(Waypoint point) { + waypointList.add(point); + } + + /** + * Get the list of way points for this route; + * + * @return List an unmodifiableList of ArtifactWaypoints for this route + */ + final public List getPath() { + return Collections.unmodifiableList(waypointList); + } + + /** + * Returns the BlackboardARtifact that this path represents. + * + * @return + */ + final BlackboardArtifact getArtifact() { + return artifact; + } + + /** + * Returns the label\display name for this path. + * + * @return GeoPath label, empty string + */ + public String getLabel() { + return pathName != null ? pathName : ""; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Route.java b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Route.java index acc9c9d86a..c08d2637ba 100755 --- a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Route.java +++ b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Route.java @@ -19,15 +19,12 @@ */ package org.sleuthkit.autopsy.geolocation.datamodel; -import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import org.openide.util.NbBundle.Messages; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardAttribute; -import org.sleuthkit.datamodel.SleuthkitCase; -import org.sleuthkit.datamodel.TskCoreException; /** * A Route represents a TSK_GPS_ROUTE artifact which has a start and end point @@ -35,56 +32,35 @@ import org.sleuthkit.datamodel.TskCoreException; * more that two points. * */ -public final class Route { - private final List points; +public class Route extends GeoPath{ private final Long timestamp; // This list is not expected to change after construction so the // constructor will take care of creating an unmodifiable List - private final List immutablePropertiesList; - - /** - * Gets the list of Routes from the TSK_GPS_ROUTE artifacts. - * - * @param skCase Currently open SleuthkitCase - * - * @return List of Route objects, empty list will be returned if no Routes - * were found - * - * @throws GeoLocationDataException - */ - static public List getRoutes(SleuthkitCase skCase) throws GeoLocationDataException { - List artifacts = null; - try { - artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_ROUTE); - } catch (TskCoreException ex) { - throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_BOOKMARK", ex); - } - - List routes = new ArrayList<>(); - for (BlackboardArtifact artifact : artifacts) { - Route route = new Route(artifact); - routes.add(route); - } - return routes; - } + private final List propertiesList; /** * Construct a route for the given artifact. * * @param artifact TSK_GPS_ROUTE artifact object */ + @Messages({ + // This is the original static hardcoded label from the + // original kml-report code + "Route_Label=As-the-crow-flies Route" + }) Route(BlackboardArtifact artifact) throws GeoLocationDataException { - points = new ArrayList<>(); - - Map attributeMap = Waypoint.getAttributesFromArtifactAsMap(artifact); - points.add(getRouteStartPoint(artifact, attributeMap)); - points.add(getRouteEndPoint(artifact, attributeMap)); + super(artifact, Bundle.Route_Label()); + + Map attributeMap = Waypoint.getAttributesFromArtifactAsMap(artifact); + + addToPath(getRouteStartPoint(artifact, attributeMap)); + addToPath(getRouteEndPoint(artifact, attributeMap)); BlackboardAttribute attribute = attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME); timestamp = attribute != null ? attribute.getValueLong() : null; - immutablePropertiesList = Collections.unmodifiableList(Waypoint.createGeolocationProperties(attributeMap)); + propertiesList = Waypoint.createGeolocationProperties(attributeMap); } /** @@ -93,7 +69,7 @@ public final class Route { * @return List an unmodifiableList of ArtifactWaypoints for this route */ public List getRoute() { - return Collections.unmodifiableList(points); + return getPath(); } /** @@ -103,21 +79,9 @@ public final class Route { * @return Map of key, value pairs. */ public List getOtherProperties() { - return immutablePropertiesList; + return Collections.unmodifiableList(propertiesList); } - /** - * Get the route label. - */ - @Messages({ - // This is the original static hardcoded label from the - // original kml-report code - "Route_Label=As-the-crow-flies Route" - }) - public String getLabel() { - return Bundle.Route_Label(); - } - public Long getTimestamp() { return timestamp; } @@ -171,7 +135,7 @@ public final class Route { @Messages({ "Route_End_Label=End" }) - Waypoint getRouteEndPoint(BlackboardArtifact artifact, Map attributeMap) throws GeoLocationDataException { + private Waypoint getRouteEndPoint(BlackboardArtifact artifact, Map attributeMap) throws GeoLocationDataException { BlackboardAttribute latitude = attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_END); BlackboardAttribute longitude = attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE_END); BlackboardAttribute altitude = attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_ALTITUDE); diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Track.java b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Track.java new file mode 100755 index 0000000000..aed8b76848 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Track.java @@ -0,0 +1,243 @@ +/* + * + * Autopsy Forensic Browser + * + * Copyright 2020 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.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.blackboardutils.attributes.GeoTrackPoints; +import org.sleuthkit.datamodel.blackboardutils.attributes.GeoWaypoint.GeoTrackPoint; + +/** + * A GPS track with which wraps the TSK_GPS_TRACK artifact. + */ +public final class Track extends GeoPath{ + + private final Long startTimestamp; + private final Long endTimeStamp; + + /** + * Construct a new Track for the given artifact. + * + * @param artifact + * + * @throws GeoLocationDataException + */ + public Track(BlackboardArtifact artifact) throws GeoLocationDataException { + this(artifact, Waypoint.getAttributesFromArtifactAsMap(artifact)); + } + + /** + * Construct a Track for the given artifact and attributeMap. + * + * @param artifact TSK_GPD_TRACK artifact + * @param attributeMap Map of the artifact attributes + * + * @throws GeoLocationDataException + */ + private Track(BlackboardArtifact artifact, Map attributeMap) throws GeoLocationDataException { + super(artifact, getTrackName(attributeMap)); + + List points = getPointsList(attributeMap); + buildPath(points); + + startTimestamp = findStartTime(points); + endTimeStamp = findEndTime(points); + } + + /** + * Returns the start time of this track. + * + * @return Earliest time, or null if none was available. + * (seconds from java epoch) + */ + public Long getStartTime() { + return startTimestamp; + } + + /** + * Returns the end time of this track. + * + * @return Earliest timestamp, or null if none was available. + * (seconds from java epoch) + */ + public Long getEndTime() { + return endTimeStamp; + } + + /** + * Return the name of the track from the attributeMap. + * Track name is stored in the attribute TSK_NAME + * + * @param attributeMap + + * @return Track name or empty string if none was available. + */ + private static String getTrackName(Map attributeMap) { + BlackboardAttribute attribute = attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME); + + return attribute != null ? attribute.getValueString() : ""; + } + + /** + * Create the list of TrackWaypoints from the GeoTrackPoint list. + * + * @param points List of GeoTrackPoints + * + * @throws GeoLocationDataException + */ + private void buildPath(List points) throws GeoLocationDataException { + for (GeoTrackPoint point : points) { + addToPath(new TrackWaypoint(point)); + } + } + + /** + * Returns the list of GeoTrackPoints from the attributeMap. Creates the + * GeoTrackPoint list from the TSK_GEO_TRACKPOINTS attribute. + * + * @param attributeMap Map of artifact attributes. + * + * @return GeoTrackPoint list empty list if the attribute was not found. + */ + private List getPointsList(Map attributeMap) { + BlackboardAttribute attribute = attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_TRACKPOINTS); + if (attribute != null) { + String value = attribute.getValueString(); + return GeoTrackPoints.deserializePoints(value); + } + + return new ArrayList<>(); + } + + /** + * Return the start time for the track. Assumes the points are in time + * order. + * + * @param points List of GeoTrackPoints. + * + * @return First non-null time stamp or null, if one was not found. + */ + private Long findStartTime(List points) { + if (points != null) { + for (GeoTrackPoint point : points) { + if (point.getTimeStamp() != null) { + return point.getTimeStamp(); + } + } + } + return null; + } + + /** + * Return the ends time for the track. Assumes the points are in time + * order. + * + * @param points List of GeoTrackPoints. + * + * @return First non-null time stamp or null, if one was not found. + */ + private Long findEndTime(List points) { + if (points != null) { + for (int index = points.size() - 1; index >= 0; index--) { + GeoTrackPoint point = points.get(index); + if (point.getTimeStamp() != null) { + return point.getTimeStamp(); + } + } + } + return null; + } + + /** + * A Waypoint subclass for the points of a track. + */ + final class TrackWaypoint extends Waypoint { + + private final List propertyList; + + /** + * Construct a TrackWaypoint. + * + * @param point GeoTrackPoint + * + * @throws GeoLocationDataException + */ + TrackWaypoint(GeoTrackPoint point) throws GeoLocationDataException { + super(null, "", + point.getTimeStamp(), + point.getLatitude(), + point.getLongitude(), + point.getAltitude(), + null, + null, + Track.this); + + propertyList = createPropertyList(point); + } + + /** + * Overloaded to return a property list that is generated from + * the GeoTrackPoint instead of an artifact. + * + * @return unmodifiable list of Waypoint.Property + */ + @Override + public List getOtherProperties() { + return Collections.unmodifiableList(propertyList); + } + + /** + * Create a propertyList specific to GeoTrackPoints. + * + * @param point GeoTrackPoint to get values from. + * + * @return A list of Waypoint.properies. + */ + private List createPropertyList(GeoTrackPoint point) { + List list = new ArrayList<>(); + + Long timestamp = point.getTimeStamp(); + if (timestamp != null) { + list.add(new Property(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME.getDisplayName(), timestamp.toString())); + } + + Double value = point.getVelocity(); + if (value != null) { + list.add(new Property(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_VELOCITY.getDisplayName(), value.toString())); + } + + value = point.getDistanceTraveled(); + if (value != null) { + list.add(new Property(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_DISTANCE_TRAVELED.getDisplayName(), value.toString())); + } + + value = point.getDistanceFromHP(); + if (value != null) { + list.add(new Property(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_DISTANCE_FROM_HOME_POINT.getDisplayName(), value.toString())); + } + + return list; + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/TrackpointWaypoint.java b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/TrackpointWaypoint.java index 7009aa63a1..e078037bfb 100755 --- a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/TrackpointWaypoint.java +++ b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/TrackpointWaypoint.java @@ -35,7 +35,7 @@ final class TrackpointWaypoint extends Waypoint { TrackpointWaypoint(BlackboardArtifact artifact) throws GeoLocationDataException { this(artifact, getAttributesFromArtifactAsMap(artifact)); } - + private TrackpointWaypoint(BlackboardArtifact artifact, Map attributeMap) throws GeoLocationDataException { super(artifact, getLabelFromArtifact(attributeMap), diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Waypoint.java b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Waypoint.java index a63087f413..acc9e89352 100755 --- a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Waypoint.java +++ b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Waypoint.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2019 Basis Technology Corp. + * Copyright 2019-2020 Basis Technology Corp. * contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -43,17 +43,15 @@ public class Waypoint { final private String label; final private AbstractFile image; final private BlackboardArtifact artifact; - final private Route route; + final private GeoPath path; - // This list is not expected to change after construction. The - // constructor will take care of making an unmodifiable List - final private List immutablePropertiesList; + final private List propertiesList; /** * This is a list of attributes that are already being handled by the by * getter functions. */ - static final private BlackboardAttribute.ATTRIBUTE_TYPE[] ALREADY_HANDLED_ATTRIBUTES = { + static final BlackboardAttribute.ATTRIBUTE_TYPE[] ALREADY_HANDLED_ATTRIBUTES = { BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE, @@ -80,7 +78,7 @@ public class Waypoint { * @throws GeoLocationDataException Exception will be thrown if artifact did * not have a valid longitude and latitude. */ - Waypoint(BlackboardArtifact artifact, String label, Long timestamp, Double latitude, Double longitude, Double altitude, AbstractFile image, Map attributeMap, Route route) throws GeoLocationDataException { + Waypoint(BlackboardArtifact artifact, String label, Long timestamp, Double latitude, Double longitude, Double altitude, AbstractFile image, Map attributeMap, GeoPath path) throws GeoLocationDataException { if (longitude == null || latitude == null) { throw new GeoLocationDataException("Invalid waypoint, null value passed for longitude or latitude"); } @@ -92,9 +90,9 @@ public class Waypoint { this.longitude = longitude; this.latitude = latitude; this.altitude = altitude; - this.route = null; + this.path = path; - immutablePropertiesList = Collections.unmodifiableList(createGeolocationProperties(attributeMap)); + propertiesList = createGeolocationProperties(attributeMap); } /** @@ -171,7 +169,7 @@ public class Waypoint { * @return A List of waypoint properties */ public List getOtherProperties() { - return immutablePropertiesList; + return Collections.unmodifiableList(propertiesList); } /** @@ -180,25 +178,8 @@ public class Waypoint { * @return The waypoint route or null if the waypoint is not apart of a * route. */ - public Route getRoute() { - return route; - } - - /** - * Gets the label for this waypoint. - * - * @param attributeMap Attributes for waypoint - * - * @return Returns a label for the waypoint, or empty string if no label was - * found. - */ - private static String getLabelFromArtifact(Map attributeMap) { - BlackboardAttribute attribute = attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME); - if (attribute != null) { - return attribute.getDisplayString(); - } - - return ""; + public GeoPath getPath() { + return path; } /** @@ -238,29 +219,32 @@ public class Waypoint { * * @throws GeoLocationDataException */ - static public List createGeolocationProperties(Map attributeMap) throws GeoLocationDataException { + static List createGeolocationProperties(Map attributeMap) throws GeoLocationDataException { List list = new ArrayList<>(); + + if(attributeMap != null) { - Set keys = new HashSet<>(attributeMap.keySet()); + Set keys = new HashSet<>(attributeMap.keySet()); - for (BlackboardAttribute.ATTRIBUTE_TYPE type : ALREADY_HANDLED_ATTRIBUTES) { - keys.remove(type); - } + for (BlackboardAttribute.ATTRIBUTE_TYPE type : ALREADY_HANDLED_ATTRIBUTES) { + keys.remove(type); + } - for (BlackboardAttribute.ATTRIBUTE_TYPE type : keys) { - String key = type.getDisplayName(); - String value = attributeMap.get(type).getDisplayString(); + for (BlackboardAttribute.ATTRIBUTE_TYPE type : keys) { + String key = type.getDisplayName(); + String value = attributeMap.get(type).getDisplayString(); - list.add(new Waypoint.Property(key, value)); + list.add(new Waypoint.Property(key, value)); + } } return list; } - + /** * Simple property class for waypoint properties that a purely * informational. */ - public static final class Property { + public final static class Property { private final String displayName; private final String value; @@ -272,7 +256,7 @@ public class Waypoint { * or empty string. * @param value String value for property. Can be null. */ - private Property(String displayName, String value) { + Property(String displayName, String value) { this.displayName = displayName; this.value = value; } diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/WaypointBuilder.java b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/WaypointBuilder.java index 24b14dc81c..708b3a4803 100755 --- a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/WaypointBuilder.java +++ b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/WaypointBuilder.java @@ -132,15 +132,40 @@ public final class WaypointBuilder { public static List getRoutes(List waypoints) { List routeList = new ArrayList<>(); for (Waypoint point : waypoints) { - Route route = point.getRoute(); - if (route != null && !routeList.contains(route)) { - routeList.add(route); + GeoPath path = point.getPath(); + if (path instanceof Route) { + Route route = (Route) path; + if (!routeList.contains(route)) { + routeList.add(route); + } } } return routeList; } + /** + * Returns a list of tracks from the given list of waypoints. + * + * @param waypoints A list of waypoints + * + * @return A list of track or an empty list if none were found. + */ + public static List getTracks(List waypoints) { + List trackList = new ArrayList<>(); + for (Waypoint point : waypoints) { + GeoPath path = point.getPath(); + if (path instanceof Track) { + Track route = (Track) path; + if (!trackList.contains(route)) { + trackList.add(route); + } + } + } + + return trackList; + } + /** * Gets a list of Waypoints for TSK_GPS_TRACKPOINT artifacts. * @@ -470,18 +495,16 @@ public final class WaypointBuilder { * @return SQL SELECT statement */ static private String buildQueryForWaypointsWOTimeStamps(List dataSources) { - + // SELECT_WO_TIMESTAMP // SELECT DISTINCT artifact_id, artifact_type_id // FROM blackboard_attributes // WHERE artifact_id NOT IN (%s) // AND artifact_id IN (%s) - // GEO_ARTIFACT_QUERY_ID_ONLY // SELECT artifact_id // FROM blackboard_attributes // WHERE attribute_type_id IN (%d, %d) - return String.format(SELECT_WO_TIMESTAMP, String.format(GEO_ARTIFACT_QUERY_ID_ONLY, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME.getTypeID(), @@ -523,12 +546,12 @@ public final class WaypointBuilder { // IN ( %s ) // 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) - )); + String.format(MOST_RECENT_TIME, + cntDaysFromRecent, + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME.getTypeID(), + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_CREATED.getTypeID(), + getWaypointListQuery(dataSources) + )); } // GEO_ARTIFACT_QUERY @@ -622,8 +645,12 @@ public final class WaypointBuilder { case TSK_GPS_LAST_KNOWN_LOCATION: waypoints.add(new LastKnownWaypoint(artifact)); break; + case TSK_GPS_TRACK: + Track track = new Track(artifact); + waypoints.addAll(track.getPath()); + break; default: - waypoints.add(new CustomArtifactWaypoint(artifact)); + waypoints.add(new CustomArtifactWaypoint(artifact)); } return waypoints; diff --git a/Core/src/org/sleuthkit/autopsy/images/minus-grey.png b/Core/src/org/sleuthkit/autopsy/images/minus-grey.png new file mode 100755 index 0000000000..f30347473e Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/images/minus-grey.png differ diff --git a/Core/src/org/sleuthkit/autopsy/images/plus-grey.png b/Core/src/org/sleuthkit/autopsy/images/plus-grey.png new file mode 100755 index 0000000000..52264ca1d5 Binary files /dev/null and b/Core/src/org/sleuthkit/autopsy/images/plus-grey.png differ diff --git a/Core/src/org/sleuthkit/autopsy/modules/drones/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/modules/drones/Bundle.properties-MERGED new file mode 100755 index 0000000000..23f2a02775 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/modules/drones/Bundle.properties-MERGED @@ -0,0 +1,6 @@ +DATExtractor_process_message=Processing DJI DAT file: %s +DATFileExtractor_Extractor_Name=DAT File Extractor +DroneIngestModule_Description=Description +DroneIngestModule_Name=Drone +# {0} - AbstractFileName +DroneIngestModule_process_start=Started {0} diff --git a/Core/src/org/sleuthkit/autopsy/modules/drones/DATDumper.java b/Core/src/org/sleuthkit/autopsy/modules/drones/DATDumper.java new file mode 100755 index 0000000000..1293cf190e --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/modules/drones/DATDumper.java @@ -0,0 +1,149 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.modules.drones; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import src.Files.ConvertDat; +import src.Files.CsvWriter; +import src.Files.DatFile; +import src.Files.DJIAssistantFile; +import src.Files.Exception.FileEnd; +import src.Files.Exception.NotDatFile; + +/** + * Dump DJI DAT files to csv file using the DatCon.jar. + * + */ +final class DATDumper { + + /** + * Construct a DATDumper. + */ + DATDumper() { + // This constructor is intentionally empty. Nothing special is needed here. + } + + /** + * Dump the DJI DAT file to a csv file. + * + * @param datFilePath Path to input DAT file + * @param outputFilePath Output file path + * @param overWriteExisting True to overwrite an existing csv file with + * outputFilePath + * + * @throws DroneIngestException + */ + void dumpDATFile(String datFilePath, String outputFilePath, boolean overWriteExisting) throws DroneIngestException { + // Validate the input and output file paths. + validateOutputFile(outputFilePath, overWriteExisting); + if (!isDATFile(datFilePath)) { + throw new DroneIngestException(String.format("Not a DAT file! DAT = %s", datFilePath)); //NON-NLS + } + + DatFile datFile = null; + try (CsvWriter writer = new CsvWriter(outputFilePath)) { + // Creates a version specific DatFile object + datFile = DatFile.createDatFile(datFilePath); + datFile.reset(); + // preAnalyze does an inital pass of the DAT file to gather some + // information about the file. + datFile.preAnalyze(); + + // Creates a version specific ConvertDat object + ConvertDat convertDat = datFile.createConVertDat(); + + // The lower the sample rate the smaller the output csv file will be + // however the date will be less precise. For our purposes we are going + // a sample rate of 1. + convertDat.sampleRate = 1; + + // Setting the tickRangeLower and upper values reduces some of the + // noise invalid data in the output file. + if (datFile.gpsLockTick != -1) { + convertDat.tickRangeLower = datFile.gpsLockTick; + } + + if (datFile.lastMotorStopTick != -1) { + convertDat.tickRangeUpper = datFile.lastMotorStopTick; + } + + convertDat.setCsvWriter(writer); + convertDat.createRecordParsers(); + datFile.reset(); + + // Analyze does the work of parsing the data, everything prior was + // setup + convertDat.analyze(true); + + } catch (IOException | NotDatFile | FileEnd ex) { + throw new DroneIngestException(String.format("Failed to dump DAT file to csv. DAT = %s, CSV = %s", datFilePath, outputFilePath), ex); //NON-NLS + } finally { + if (datFile != null) { + datFile.close(); + } + } + } + + /** + * Validate that if the given csv file exists that the overWriteExsiting + * param is true. Throws an exception if the file exists and + * overWriteExisting is false. + * + * @param outputFileName Absolute path for the output csv file + * @param overWriteExisting True to over write an existing file. + * + * @throws DroneIngestException Throws exception if overWriteExisting is + * true and outputFileName exists + */ + private void validateOutputFile(String outputFileName, boolean overWriteExisting) throws DroneIngestException { + File csvFile = new File(outputFileName); + + if (csvFile.exists()) { + if (overWriteExisting) { + csvFile.delete(); + } else { + throw new DroneIngestException(String.format("Unable to dump DAT file. overWriteExsiting is false and DAT output csv file exists: %s", outputFileName)); //NON-NLS + } + } + } + + /** + * Validate that the DAT file exists and it is in a known format that can be + * dumped. + * + * @param datFilePath Absolute path to DAT file + * + * @throws DroneIngestException + */ + public boolean isDATFile(String datFilePath) throws DroneIngestException { + File datFile = new File(datFilePath); + + if (!datFile.exists()) { + throw new DroneIngestException(String.format("Unable to dump DAT file DAT file does not exist: %s", datFilePath)); //NON-NLS + } + + try { + return DatFile.isDatFile(datFilePath) || DJIAssistantFile.isDJIDat(datFile); + } catch (FileNotFoundException ex) { + throw new DroneIngestException(String.format("Unable to dump DAT file. File not found %s", datFilePath), ex); //NON-NLS + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/modules/drones/DATExtractor.java b/Core/src/org/sleuthkit/autopsy/modules/drones/DATExtractor.java new file mode 100755 index 0000000000..256ee59509 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/modules/drones/DATExtractor.java @@ -0,0 +1,322 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.modules.drones; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.nio.file.Paths; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.openide.util.NbBundle; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.casemodule.services.FileManager; +import org.sleuthkit.autopsy.coreutils.FileUtil; +import org.sleuthkit.autopsy.ingest.DataSourceIngestModuleProgress; +import org.sleuthkit.autopsy.ingest.IngestJobContext; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.blackboardutils.attributes.GeoWaypoint.GeoTrackPoint; +import org.sleuthkit.datamodel.blackboardutils.GeoArtifactsHelper; +import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.Blackboard.BlackboardException; + +/** + * Extract drone position data from DJI Phantom drones. + * + * Module uses DatCon.jar to dump FLYXXX.DAT file to a CSV file which is stored + * in the case temp directory. Artifacts are created by parsing the csv file. + * + */ +final class DATExtractor extends DroneExtractor { + + private static final Logger logger = Logger.getLogger(DATExtractor.class.getName()); + + private static final String HEADER_LONG = "IMU_ATTI(0):Longitude"; //NON-NLS + private static final String HEADER_LAT = "IMU_ATTI(0):Latitude"; //NON-NLS + private static final String HEADER_VELOCITY = "IMU_ATTI(0):velComposite"; //NON-NLS + private static final String HEADER_DATETILE = "GPS:dateTimeStamp"; //NON-NLS + private static final String HEADER_ALTITUDE = "GPS(0):heightMSL"; //NON-NLS + private static final String HEADER_DISTANCE_FROM_HP = "IMU_ATTI(0):distanceHP"; //NON-NLS + private static final String HEADER_DISTANCE_TRAVELED = "IMU_ATTI(0):distanceTravelled"; //NON-NLS + + /** + * Construct a DATExtractor. + * + * @throws DroneIngestException + */ + DATExtractor() throws DroneIngestException { + super(); + } + + @Messages({ + "DATExtractor_process_message=Processing DJI DAT file: %s" + }) + @Override + void process(Content dataSource, IngestJobContext context, DataSourceIngestModuleProgress progressBar) throws DroneIngestException { + List datFiles = findDATFiles(dataSource); + + DATDumper dumper = new DATDumper(); + + try { + for (AbstractFile DATFile : datFiles) { + if (context.dataSourceIngestIsCancelled()) { + break; + } + + progressBar.progress(String.format(Bundle.DATExtractor_process_message(), DATFile.getName())); + + // Copy the DAT file into the case temp folder + File tempDATFile = getTemporaryFile(context, DATFile); + + // Create a path for the csv file + String csvFilePath = getCSVPathForDAT(DATFile); + + try { + if (!dumper.isDATFile(tempDATFile.getAbsolutePath())) { + logger.log(Level.WARNING, String.format("%s is not a valid DAT file", DATFile.getName())); //NON-NLS + continue; + } + // Dump the DAT file to a csv file + dumper.dumpDATFile(tempDATFile.getAbsolutePath(), csvFilePath, true); + + if (context.dataSourceIngestIsCancelled()) { + break; + } + + // Process the csv file + List trackPoints = processCSVFile(context, DATFile, csvFilePath); + + if (trackPoints != null && !trackPoints.isEmpty()) { + (new GeoArtifactsHelper(getSleuthkitCase(), getName(), DATFile)).addTrack(DATFile.getName(), trackPoints); + } else { + logger.log(Level.INFO, String.format("No trackpoints with valid longitude or latitude found in %s", DATFile.getName())); //NON-NLS + } + + } catch (TskCoreException | BlackboardException ex) { + logger.log(Level.WARNING, String.format("Exception thrown while processing DAT file %s", DATFile.getName()), ex); //NON-NLS + } finally { + tempDATFile.delete(); + (new File(csvFilePath)).delete(); + } + } + } finally { + FileUtil.deleteDir(getExtractorTempPath().toFile()); + } + } + + @NbBundle.Messages({ + "DATFileExtractor_Extractor_Name=DAT File Extractor" + }) + + @Override + String getName() { + return Bundle.DATFileExtractor_Extractor_Name(); + } + + /** + * Find any files that have the file name FLYXXX.DAT where the X are digit + * characters. + * + * @param dataSource Data source to search + * + * @return List of found files or empty list if none where found + * + * @throws DroneIngestException + */ + private List findDATFiles(Content dataSource) throws DroneIngestException { + List fileList = new ArrayList<>(); + + FileManager fileManager = getCurrentCase().getServices().getFileManager(); + + // findFiles use the SQL wildcard # in the file name + try { + fileList = fileManager.findFiles(dataSource, "FLY___.DAT"); //NON-NLS + } catch (TskCoreException ex) { + throw new DroneIngestException("Unable to find drone DAT files.", ex); //NON-NLS + } + + return fileList; + } + + /** + * Return an absolute path for the given DAT file. + * + * @param file DAT file + * + * @return Absolute csv file path + */ + private String getCSVPathForDAT(AbstractFile file) { + String tempFileName = file.getName() + file.getId() + ".csv"; //NON-NLS + return Paths.get(getExtractorTempPath().toString(), tempFileName).toString(); + } + + /** + * Process the csv dump of the drone DAT file. + * + * Create artifacts for all rows that have a valid longitude and latitude. + * + * @param context current case job context + * @param DATFile Original DAT file + * @param csvFilePath Path of csv file to process + * + * @throws DroneIngestException + */ + private List processCSVFile(IngestJobContext context, AbstractFile DATFile, String csvFilePath) throws DroneIngestException { + List trackPoints = new ArrayList<>(); + try (BufferedReader reader = new BufferedReader(new FileReader(new File(csvFilePath)))) { + // First read in the header line and process + String line = reader.readLine(); + Map headerMap = makeHeaderMap(line.split(",")); //NON-NLS + + while ((line = reader.readLine()) != null) { + if (context.dataSourceIngestIsCancelled()) { + return null; + } + + String[] values = line.split(","); //NON-NLS + GeoTrackPoint point = createTrackPoint(headerMap, values); + if (point != null) { + trackPoints.add(point); + } + } + + } catch (IOException ex) { + throw new DroneIngestException(String.format("Failed to read DAT csvFile %s created for AbstractFile: %s", csvFilePath, DATFile.getId()), ex); //NON-NLS + } + + return trackPoints; + } + + /** + * Create a lookup to quickly match the column header with its array index + * + * @param headers + * + * @return Map of column names with the column index + */ + private Map makeHeaderMap(String[] headers) { + Map map = new HashMap<>(); + + for (int index = 0; index < headers.length; index++) { + map.put(headers[index], index); + + } + + return map; + } + + /** + * Returns a list of BlackboardAttributes generated from the String array. + * + * If longitude and latitude are not valid, assume we the row is not + * interesting and return a null collection. + * + * @param columnLookup column header lookup map + * @param values Row data + * + * @return Collection of BlackboardAttributes for row or null collection if + * longitude or latitude was not valid + * + * @throws DroneIngestException + */ + private GeoTrackPoint createTrackPoint(Map columnLookup, String[] values) throws DroneIngestException { + + Double latitude = getDoubleValue(columnLookup.get(HEADER_LAT), values); + Double longitude = getDoubleValue(columnLookup.get(HEADER_LONG), values); + + if (longitude == null || latitude == null) { + // Assume the row is not valid\has junk + return null; + } + + return new GeoTrackPoint(latitude, + longitude, + getDoubleValue(columnLookup.get(HEADER_ALTITUDE), values), + getDoubleValue(columnLookup.get(HEADER_VELOCITY), values), + getDoubleValue(columnLookup.get(HEADER_DISTANCE_FROM_HP), values), + getDoubleValue(columnLookup.get(HEADER_DISTANCE_TRAVELED), values), + getDateTimeValue(columnLookup, values)); + } + + /** + * Returns the waypoint timestamp in java/unix epoch seconds. + * + * The format of the date time string is 2016-09-26T18:26:19Z. + * + * @param headerMap + * @param values + * + * @return Epoch seconds or null if no dateTime value was found + */ + private Long getDateTimeValue(Map headerMap, String[] values) { + Integer index = headerMap.get(HEADER_DATETILE); + if (index == null || index == -1 || index > values.length) { + return null; + } + + String value = values[index]; + if (value == null || value.isEmpty()) { + return null; + } + + try { + ZonedDateTime zdt = ZonedDateTime.parse(value, DateTimeFormatter.ISO_DATE_TIME); + return zdt.toLocalDateTime().toEpochSecond(ZoneOffset.UTC); + } catch (DateTimeParseException ex) { + return null; + } + } + + /** + * Returns the string value at the given index parsed as a double. + * + * @param index Index to string array + * @param values Array of string values + * + * @return Double value or null if the index is out of bounds of the string + * array or the string value at index was not a double. + */ + private Double getDoubleValue(Integer index, String[] values) { + if (index == null || index == -1 || index > values.length) { + return null; + } + + String value = values[index]; + if (value == null || value.isEmpty()) { + return null; + } + + try { + return Double.parseDouble(value); + } catch (NumberFormatException ex) { + return null; + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/modules/drones/DroneExtractor.java b/Core/src/org/sleuthkit/autopsy/modules/drones/DroneExtractor.java new file mode 100755 index 0000000000..aaee376bf7 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/modules/drones/DroneExtractor.java @@ -0,0 +1,120 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.modules.drones; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.datamodel.ContentUtils; +import org.sleuthkit.autopsy.ingest.DataSourceIngestModuleProgress; +import org.sleuthkit.autopsy.ingest.IngestJobContext; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.SleuthkitCase; + +/** + * Abstract base class for all Drone file extractors. + */ +abstract class DroneExtractor { + + static private final String TEMP_FOLDER_NAME = "DroneExtractor"; //NON-NLS + private final Case currentCase; + + /** + * Common constructor. Subclasses should call super in their constructor. + * + * @throws DroneIngestException + */ + protected DroneExtractor() throws DroneIngestException { + try { + currentCase = Case.getCurrentCaseThrows(); + } catch (NoCurrentCaseException ex) { + throw new DroneIngestException("Unable to create drone extractor, no open case.", ex); //NON-NLS + } + } + + abstract void process(Content dataSource, IngestJobContext context, DataSourceIngestModuleProgress progressBar) throws DroneIngestException; + + abstract String getName(); + + /** + * Return the current case object. + * + * @return Current case + */ + final protected Case getCurrentCase() { + return currentCase; + } + + /** + * Return the current SleuthkitCase. + * + * @return Current sleuthkit case + */ + final protected SleuthkitCase getSleuthkitCase() { + return currentCase.getSleuthkitCase(); + } + + /** + * Build the temp path and create the directory if it does not currently + * exist. + * + * @param currentCase Currently open case + * @param extractorName Name of extractor + * + * @return Path of the temp directory for this module + */ + protected Path getExtractorTempPath() { + Path path = Paths.get(currentCase.getTempDirectory(), TEMP_FOLDER_NAME, this.getClass().getCanonicalName()); + File dir = path.toFile(); + if (!dir.exists()) { + dir.mkdirs(); + } + + return path; + } + + /** + * Create a copy of file in the case temp directory. + * + * @param context Current ingest context + * @param file File to be copied + * + * @return File copy. + * + * @throws DroneIngestException + */ + protected File getTemporaryFile(IngestJobContext context, AbstractFile file) throws DroneIngestException { + String tempFileName = file.getName() + file.getId() + file.getNameExtension(); + + Path tempFilePath = Paths.get(getExtractorTempPath().toString(), tempFileName); + + try { + ContentUtils.writeToFile(file, tempFilePath.toFile(), context::dataSourceIngestIsCancelled); + } catch (IOException ex) { + throw new DroneIngestException(String.format("Unable to create temp file %s for abstract file %s objectID: %d", tempFilePath.toString(), file.getName(), file.getId()), ex); //NON-NLS + } + + return tempFilePath.toFile(); + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/modules/drones/DroneIngestException.java b/Core/src/org/sleuthkit/autopsy/modules/drones/DroneIngestException.java new file mode 100755 index 0000000000..8807fe78db --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/modules/drones/DroneIngestException.java @@ -0,0 +1,48 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.modules.drones; + +import org.sleuthkit.autopsy.ingest.IngestModule.IngestModuleException; + +/** + * IngestModuleException wrapper class. + */ +public class DroneIngestException extends IngestModuleException { + + private static final long serialVersionUID = 1L; + + /** + * Create exception containing the error message + * + * @param msg the message + */ + public DroneIngestException(String msg) { + super(msg); + } + + /** + * Create exception containing the error message and cause exception + * + * @param msg the message + * @param ex cause exception + */ + public DroneIngestException(String msg, Exception ex) { + super(msg, ex); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/modules/drones/DroneIngestModule.java b/Core/src/org/sleuthkit/autopsy/modules/drones/DroneIngestModule.java new file mode 100755 index 0000000000..d126761e75 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/modules/drones/DroneIngestModule.java @@ -0,0 +1,94 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.modules.drones; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.ingest.DataSourceIngestModule; +import org.sleuthkit.autopsy.ingest.DataSourceIngestModuleProgress; +import org.sleuthkit.autopsy.ingest.IngestJobContext; +import org.sleuthkit.autopsy.ingest.IngestMessage; +import org.sleuthkit.autopsy.ingest.IngestServices; +import org.sleuthkit.datamodel.Content; + +/** + * Drone file ingest module. + * + */ +public final class DroneIngestModule implements DataSourceIngestModule { + + private static final Logger logger = Logger.getLogger(DroneIngestModule.class.getName()); + final private List extractors; + + private IngestJobContext context; + + /** + * Construct a new drone ingest module. + */ + DroneIngestModule() { + extractors = new ArrayList<>(); + } + + @Override + public void startUp(IngestJobContext context) throws IngestModuleException { + this.context = context; + + extractors.add(new DATExtractor()); + } + + @Messages({ + "# {0} - AbstractFileName", + "DroneIngestModule_process_start=Started {0}" + }) + @Override + public ProcessResult process(Content dataSource, DataSourceIngestModuleProgress progressBar) { + ProcessResult processResult = ProcessResult.OK; + + IngestServices services = IngestServices.getInstance(); + + services.postMessage(IngestMessage.createMessage( + IngestMessage.MessageType.INFO, + DroneIngestModuleFactory.getModuleName(), + Bundle.DroneIngestModule_process_start(dataSource.getName()))); + + progressBar.switchToIndeterminate(); + + for (DroneExtractor extractor : extractors) { + + if (context.dataSourceIngestIsCancelled()) { + logger.log(Level.INFO, "Drone ingest has been canceled, quitting before {0}", extractor.getName()); //NON-NLS + break; + } + + progressBar.progress(extractor.getName()); + + try { + extractor.process(dataSource, context, progressBar); + } catch (DroneIngestException ex) { + logger.log(Level.SEVERE, String.format("Exception thrown from drone extractor %s", extractor.getName()), ex); + } + } + + return processResult; + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/modules/drones/DroneIngestModuleFactory.java b/Core/src/org/sleuthkit/autopsy/modules/drones/DroneIngestModuleFactory.java new file mode 100755 index 0000000000..ca82222ee4 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/modules/drones/DroneIngestModuleFactory.java @@ -0,0 +1,72 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.modules.drones; + +import org.openide.util.NbBundle.Messages; +import org.openide.util.lookup.ServiceProvider; +import org.sleuthkit.autopsy.coreutils.Version; +import org.sleuthkit.autopsy.ingest.DataSourceIngestModule; +import org.sleuthkit.autopsy.ingest.IngestModuleFactory; +import org.sleuthkit.autopsy.ingest.IngestModuleFactoryAdapter; +import org.sleuthkit.autopsy.ingest.IngestModuleIngestJobSettings; + +/** + * Drone file ingest module factory. + */ +@ServiceProvider(service = IngestModuleFactory.class) +public class DroneIngestModuleFactory extends IngestModuleFactoryAdapter { + + @Messages({ + "DroneIngestModule_Name=Drone", + "DroneIngestModule_Description=Description" + }) + + /** + * Helper function for returning the name of this module. + */ + static String getModuleName() { + return Bundle.DroneIngestModule_Name(); + } + + @Override + public String getModuleDisplayName() { + return getModuleName(); + } + + @Override + public String getModuleDescription() { + return Bundle.DroneIngestModule_Description(); + } + + @Override + public String getModuleVersionNumber() { + return Version.getVersion(); + } + + @Override + public boolean isDataSourceIngestModuleFactory() { + return true; + } + + @Override + public DataSourceIngestModule createDataSourceIngestModule(IngestModuleIngestJobSettings ingestJobOptions) { + return new DroneIngestModule(); + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/report/modules/kml/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/report/modules/kml/Bundle.properties-MERGED index 3cbb7f6506..b97907cce9 100755 --- a/Core/src/org/sleuthkit/autopsy/report/modules/kml/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/report/modules/kml/Bundle.properties-MERGED @@ -14,6 +14,7 @@ KMLReport.trackpointDatabaseError=Could not get GPS Trackpoints from database. KMLReport.trackpointError=Could not extract Trackpoint information. KMLReport.unableToExtractPhotos=Could not extract photo information. KMLReport.unableToOpenCase=Exception while getting open case. +ReportBodyFile.ingestWarning.text=Ingest Warning message ReportKML.progress.querying=Querying files... ReportKML.progress.loading=Loading files... ReportKML.getName.text=Google Earth KML @@ -29,4 +30,6 @@ Waypoint_EXIF_Display_String=EXIF Metadata With Location Waypoint_Last_Known_Display_String=GPS Last Known Location Waypoint_Route_Point_Display_String=GPS Individual Route Point Waypoint_Search_Display_String=GPS Search +Waypoint_Track_Display_String=GPS Track +Waypoint_Track_Point_Display_String=GPS Individual Track Point Waypoint_Trackpoint_Display_String=GPS Trackpoint 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 8db758e53a..c13f385ba3 100644 --- a/Core/src/org/sleuthkit/autopsy/report/modules/kml/KMLReport.java +++ b/Core/src/org/sleuthkit/autopsy/report/modules/kml/KMLReport.java @@ -2,7 +2,7 @@ * * Autopsy Forensic Browser * - * Copyright 2014-2018 Basis Technology Corp. + * Copyright 2014-2020 Basis Technology Corp. * contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -33,7 +33,6 @@ import java.io.OutputStream; import java.nio.file.Path; import java.nio.file.Paths; import java.text.SimpleDateFormat; -import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import org.jdom2.Document; @@ -48,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.Track; import org.sleuthkit.autopsy.geolocation.datamodel.WaypointBuilder; import org.sleuthkit.autopsy.report.ReportBranding; import org.sleuthkit.autopsy.report.ReportProgressPanel; @@ -79,6 +79,7 @@ public final class KMLReport implements GeneralReportModule { private Element gpsRouteFolder; private Element gpsSearchesFolder; private Element gpsTrackpointsFolder; + private Element gpsTracksFolder; private List waypointList = null; @@ -142,7 +143,10 @@ public final class KMLReport implements GeneralReportModule { "Waypoint_Route_Point_Display_String=GPS Individual Route Point", "Waypoint_Search_Display_String=GPS Search", "Waypoint_Trackpoint_Display_String=GPS Trackpoint", - "Route_Details_Header=GPS Route" + "Waypoint_Track_Display_String=GPS Track", + "Route_Details_Header=GPS Route", + "ReportBodyFile.ingestWarning.text=Ingest Warning message", + "Waypoint_Track_Point_Display_String=GPS Individual Track Point" }) public void generateReport(String baseReportDir, ReportProgressPanel progressPanel, List waypointList) { @@ -175,6 +179,7 @@ public final class KMLReport implements GeneralReportModule { try { makeRoutes(skCase); + makeTracks(skCase); addLocationsToReport(skCase, baseReportDir); } catch (GeoLocationDataException | IOException ex) { errorMessage = "Failed to complete report."; @@ -280,12 +285,18 @@ public final class KMLReport implements GeneralReportModule { Element hrefTrackpoints = new Element("href", ns).addContent(cdataTrackpoints); //NON-NLS gpsTrackpointsFolder.addContent(new Element("Icon", ns).addContent(hrefTrackpoints)); //NON-NLS + gpsTracksFolder = new Element("Folder", ns); //NON-NLS + CDATA cdataTrack = new CDATA("https://raw.githubusercontent.com/sleuthkit/autopsy/develop/Core/src/org/sleuthkit/autopsy/images/gps-trackpoint.png"); //NON-NLS + Element hrefTrack = new Element("href", ns).addContent(cdataTrack); //NON-NLS + gpsTracksFolder.addContent(new Element("Icon", ns).addContent(hrefTrack)); //NON-NLS + gpsExifMetadataFolder.addContent(new Element("name", ns).addContent("EXIF Metadata")); //NON-NLS gpsBookmarksFolder.addContent(new Element("name", ns).addContent("GPS Bookmarks")); //NON-NLS gpsLastKnownLocationFolder.addContent(new Element("name", ns).addContent("GPS Last Known Location")); //NON-NLS gpsRouteFolder.addContent(new Element("name", ns).addContent("GPS Routes")); //NON-NLS gpsSearchesFolder.addContent(new Element("name", ns).addContent("GPS Searches")); //NON-NLS gpsTrackpointsFolder.addContent(new Element("name", ns).addContent("GPS Trackpoints")); //NON-NLS + gpsTracksFolder.addContent(new Element("name", ns).addContent("GPS Tracks")); //NON-NLS document.addContent(gpsExifMetadataFolder); document.addContent(gpsBookmarksFolder); @@ -293,6 +304,7 @@ public final class KMLReport implements GeneralReportModule { document.addContent(gpsRouteFolder); document.addContent(gpsSearchesFolder); document.addContent(gpsTrackpointsFolder); + document.addContent(gpsTracksFolder); return kmlDocument; } @@ -402,7 +414,7 @@ public final class KMLReport implements GeneralReportModule { if (waypointList == null) { routes = Route.getRoutes(skCase); } else { - routes = new ArrayList<>(); + routes = WaypointBuilder.getRoutes(waypointList); } for (Route route : routes) { @@ -410,7 +422,12 @@ public final class KMLReport implements GeneralReportModule { } } - void addRouteToReport(Route route) { + /** + * Add the given route to the KML report. + * + * @param route + */ + private void addRouteToReport(Route route) { List routePoints = route.getRoute(); Waypoint start = null; Waypoint end = null; @@ -455,6 +472,49 @@ public final class KMLReport implements GeneralReportModule { } } + /** + * Add the track to the track folder in the document. + * + * @param skCase Currently open case. + * + * @throws TskCoreException + */ + void makeTracks(SleuthkitCase skCase) throws GeoLocationDataException { + List tracks = null; + + if (waypointList == null) { + tracks = Track.getTracks(skCase, null); + } else { + tracks = WaypointBuilder.getTracks(waypointList); + } + + for (Track track : tracks) { + addTrackToReport(track); + } + } + + /** + * Add a track to the KML report. + * + * @param track + */ + private void addTrackToReport(Track track) { + List trackPoints = track.getPath(); + + // Adding a folder with the track name so that all of the + // tracks waypoints with be grouped together. + Element trackFolder = new Element("Folder", ns); //NON-NLS + trackFolder.addContent(new Element("name", ns).addContent(track.getLabel())); //NON-NLS + gpsTracksFolder.addContent(trackFolder); + + for (Waypoint point : trackPoints) { + Element element = makePoint(point.getLatitude(), point.getLongitude(), point.getAltitude()); + trackFolder.addContent(makePlacemark("", + FeatureColor.GREEN, getFormattedDetails(point, Bundle.Waypoint_Track_Point_Display_String()), + point.getTimestamp(), element, formattedCoordinates(point.getLatitude(), point.getLongitude()))); //NON-NLS + } + } + /** * Format a point time stamp (in seconds) to the report format. * @@ -739,6 +799,14 @@ public final class KMLReport implements GeneralReportModule { return result.toString(); } + /** + * Returns a HTML formatted string with the given title and value. + * + * @param title + * @param value + * + * @return HTML formatted string + */ private String formatAttribute(String title, String value) { return String.format(HTML_PROP_FORMAT, title, value); } diff --git a/CoreLibs/ivysettings.xml b/CoreLibs/ivysettings.xml index e3e086637b..dc6cdfd94f 100644 --- a/CoreLibs/ivysettings.xml +++ b/CoreLibs/ivysettings.xml @@ -2,7 +2,7 @@ - + diff --git a/Experimental/ivysettings.xml b/Experimental/ivysettings.xml index e3e086637b..68919d4dd4 100644 --- a/Experimental/ivysettings.xml +++ b/Experimental/ivysettings.xml @@ -2,7 +2,7 @@ - + diff --git a/KeywordSearch/ivysettings.xml b/KeywordSearch/ivysettings.xml index c27d905255..8209151a1c 100644 --- a/KeywordSearch/ivysettings.xml +++ b/KeywordSearch/ivysettings.xml @@ -2,7 +2,7 @@ - + diff --git a/RecentActivity/ivysettings.xml b/RecentActivity/ivysettings.xml index c27d905255..8209151a1c 100644 --- a/RecentActivity/ivysettings.xml +++ b/RecentActivity/ivysettings.xml @@ -2,7 +2,7 @@ - + diff --git a/docs/doxygen-user/extension_mismatch.dox b/docs/doxygen-user/extension_mismatch.dox index 25bac5e3fe..34ec2ac0df 100644 --- a/docs/doxygen-user/extension_mismatch.dox +++ b/docs/doxygen-user/extension_mismatch.dox @@ -14,8 +14,8 @@ One can add and remove MIME types in the "Tools", "Options", "File Extension Mis \image html extension-mismatch-detected-configuration.PNG
-If you'd like to contribute your changes back to the community, then you'll need to upload your updated %APPDATA%\autopsy\dev\config\mismatch_config.xml file by either: -- Make a fork of the Github Autopsy repository, copy the new file into the src\org\sleuthkit\autopsy\fileextmismatch folder and submit a pull request +If you'd like to contribute your changes back to the community, then you'll need to upload your updated %APPDATA%\\autopsy\\dev\\config\\mismatch_config.xml file by either: +- Make a fork of the Github Autopsy repository, copy the new file into the src\\org\\sleuthkit\\autopsy\\fileextmismatch folder and submit a pull request - Attach the entire mismatch_config.xml file to a github issue. Using the Module diff --git a/thirdparty/DatCon/3.6.9/DatCon.jar b/thirdparty/DatCon/3.6.9/DatCon.jar new file mode 100755 index 0000000000..3bcdca1cd9 Binary files /dev/null and b/thirdparty/DatCon/3.6.9/DatCon.jar differ diff --git a/thunderbirdparser/ivysettings.xml b/thunderbirdparser/ivysettings.xml index c27d905255..8209151a1c 100644 --- a/thunderbirdparser/ivysettings.xml +++ b/thunderbirdparser/ivysettings.xml @@ -2,7 +2,7 @@ - +