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/nbproject/project.properties b/Core/nbproject/project.properties
index c269acaa98..0f00b980af 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 b2e17a7e32..476beadec8 100644
--- a/Core/nbproject/project.xml
+++ b/Core/nbproject/project.xml
@@ -802,6 +802,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/modules/drones/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/modules/drones/Bundle.properties-MERGED
new file mode 100755
index 0000000000..f0e1fe823c
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/modules/drones/Bundle.properties-MERGED
@@ -0,0 +1,5 @@
+DATExtractor_process_message=Processing DJI DAT file: %s
+DATFileExtractor_Extractor_Name=DAT File Extractor
+DroneIngestModule_Description=Description
+DroneIngestModule_Name=Drone
+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..37d3c97920
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/modules/drones/DATDumper.java
@@ -0,0 +1,148 @@
+/*
+ * 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() {
+ }
+
+ /**
+ * 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);
+ validateDATFile(datFilePath);
+
+ 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. 30 was recommended in the
+ // DatCon documentation as a good default.
+ convertDat.sampleRate = 30;
+
+ // 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);
+ } 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));
+ }
+ }
+ }
+
+ /**
+ * 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
+ */
+ private void validateDATFile(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));
+ }
+
+ try {
+ if (!DatFile.isDatFile(datFilePath) && !DJIAssistantFile.isDJIDat(datFile)) {
+ throw new DroneIngestException(String.format("Unable to dump DAT file. File is not a DAT file: %s", datFilePath));
+ }
+ } catch (FileNotFoundException ex) {
+ throw new DroneIngestException(String.format("Unable to dump DAT file. File not found %s", datFilePath), ex);
+ }
+ }
+}
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..50db7723ba
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/modules/drones/DATExtractor.java
@@ -0,0 +1,318 @@
+/*
+ * 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.LocalDateTime;
+import java.time.ZoneOffset;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.TemporalAccessor;
+import java.util.ArrayList;
+import java.util.Collection;
+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.BlackboardArtifact;
+import org.sleuthkit.datamodel.BlackboardAttribute;
+import org.sleuthkit.datamodel.Content;
+import org.sleuthkit.datamodel.TskCoreException;
+
+/**
+ * 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";
+ private static final String HEADER_LAT = "IMU_ATTI(0):Latitude";
+ private static final String HEADER_VELOCITY = "IMU_ATTI(0):velComposite";
+ private static final String HEADER_DATETILE = "GPS:dateTimeStamp";
+ private static final String HEADER_ALTITUDE = "GPS(0):heightMSL";
+ private static final String HEADER_DISTANCE_FROM_HP = "IMU_ATTI(0):distanceHP";
+ private static final String HEADER_DISTANCE_TRAVELED = "IMU_ATTI(0):distanceTravelled";
+
+ /**
+ * 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 {
+ // Dump the DAT file to a csv file
+ dumper.dumpDATFile(tempDATFile.getAbsolutePath(), csvFilePath, true);
+
+ if (context.dataSourceIngestIsCancelled()) {
+ break;
+ }
+
+ // Process the csv file
+ processCSVFile(context, DATFile, csvFilePath);
+ } catch (DroneIngestException ex) {
+ logger.log(Level.WARNING, String.format("Exception thrown while processing DAT file %s", DATFile.getName()), ex);
+ } 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");
+ } catch (TskCoreException ex) {
+ throw new DroneIngestException("Unable to find drone DAT files.", ex);
+ }
+
+ 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";
+ 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 void processCSVFile(IngestJobContext context, AbstractFile DATFile, String csvFilePath) throws DroneIngestException {
+ 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(","));
+ Collection artifacts = new ArrayList<>();
+
+ while ((line = reader.readLine()) != null) {
+ if (context.dataSourceIngestIsCancelled()) {
+ break;
+ }
+
+ String[] values = line.split(",");
+ Collection attributes = buildAttributes(headerMap, values);
+ if (attributes != null) {
+ artifacts.add(makeWaypointArtifact(DATFile, attributes));
+ }
+ }
+
+ if (context.dataSourceIngestIsCancelled()) {
+ return;
+ }
+
+ postArtifacts(artifacts);
+
+ } catch (IOException ex) {
+ throw new DroneIngestException(String.format("Failed to read DAT csvFile %s", csvFilePath), ex);
+ }
+ }
+
+ /**
+ * 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 Collection buildAttributes(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 makeWaypointAttributes(latitude,
+ longitude,
+ getDoubleValue(columnLookup.get(HEADER_ALTITUDE), values),
+ getDateTimeValue(columnLookup, values),
+ getDoubleValue(columnLookup.get(HEADER_VELOCITY), values),
+ getDoubleValue(columnLookup.get(HEADER_DISTANCE_FROM_HP), values),
+ getDoubleValue(columnLookup.get(HEADER_DISTANCE_TRAVELED), 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..d6e314c03e
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/modules/drones/DroneExtractor.java
@@ -0,0 +1,232 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Collection;
+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.Blackboard;
+import org.sleuthkit.datamodel.BlackboardArtifact;
+import org.sleuthkit.datamodel.BlackboardAttribute;
+import org.sleuthkit.datamodel.Content;
+import org.sleuthkit.datamodel.SleuthkitCase;
+import org.sleuthkit.datamodel.TskCoreException;
+
+/**
+ * Abstract base class for all Drone file extractors.
+ */
+abstract class DroneExtractor {
+
+ static private final String TEMP_FOLDER_NAME = "DroneExtractor";
+ 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);
+ }
+ }
+
+ 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();
+ }
+
+ /**
+ * Return the Blackboard object.
+ *
+ * @return Current Case blackboard object.
+ */
+ final protected Blackboard getBlackboard() {
+ return currentCase.getSleuthkitCase().getBlackboard();
+ }
+
+ /**
+ * Method to post a list of BlackboardArtifacts to the blackboard.
+ *
+ * @param artifacts A list of artifacts. IF list is empty or null, the
+ * function will return.
+ */
+ void postArtifacts(Collection artifacts) throws DroneIngestException {
+ if (artifacts == null || artifacts.isEmpty()) {
+ return;
+ }
+
+ try {
+ getBlackboard().postArtifacts(artifacts, getName());
+ } catch (Blackboard.BlackboardException ex) {
+ throw new DroneIngestException(String.format("Failed to post Drone artifacts to blackboard."), ex);
+ }
+ }
+
+ /**
+ * Create a TSK_WAYPOINT artifact with the given list of attributes.
+ *
+ * @param DATFile DAT file
+ * @param attributes List of BlackboardAttributes
+ *
+ * @return TSK_WAYPOINT BlackboardArtifact
+ *
+ * @throws DroneIngestException
+ */
+ protected BlackboardArtifact makeWaypointArtifact(AbstractFile DATFile, Collection attributes) throws DroneIngestException {
+ try {
+ BlackboardArtifact artifact = DATFile.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_WAYPOINT);
+ artifact.addAttributes(attributes);
+ return artifact;
+ } catch (TskCoreException ex) {
+ throw new DroneIngestException(String.format("Failed to post Drone artifacts to blackboard."), ex);
+ }
+ }
+
+ /**
+ * Returns a list of BlackboardAttributes for the given parameters.
+ *
+ * Throws exception of longitude or latitude are null.
+ *
+ * @param latitude Waypoint latitude, must be non-null
+ * @param longitude waypoint longitude, must be non-null
+ * @param altitude Waypoint altitude\height
+ * @param dateTime Timestamp the waypoint was created (Java epoch
+ * seconds)
+ * @param velocity Velocity
+ * @param distanceHP Distance from home point
+ * @param distanceTraveled Total distance the drone has traveled
+ *
+ * @return Collection of BlackboardAttributes
+ *
+ * @throws DroneIngestException
+ */
+ protected Collection makeWaypointAttributes(Double latitude, Double longitude, Double altitude, Long dateTime, Double velocity, Double distanceHP, Double distanceTraveled) throws DroneIngestException {
+ Collection attributes = new ArrayList<>();
+
+ if (latitude == null || longitude == null) {
+ throw new DroneIngestException("Invalid list of waypoint attributes, longitude or latitude was null");
+ }
+
+ attributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE,
+ DroneIngestModuleFactory.getModuleName(), latitude));
+
+ attributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE,
+ DroneIngestModuleFactory.getModuleName(), longitude));
+
+ if (altitude != null) {
+ attributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_ALTITUDE,
+ DroneIngestModuleFactory.getModuleName(), altitude));
+ }
+
+ if (dateTime != null) {
+ attributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME,
+ DroneIngestModuleFactory.getModuleName(), dateTime));
+ }
+
+ if (velocity != null) {
+ attributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_VELOCITY,
+ DroneIngestModuleFactory.getModuleName(), velocity));
+ }
+
+ if (distanceHP != null) {
+ attributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DRONE_HP_DISTANCE,
+ DroneIngestModuleFactory.getModuleName(), velocity));
+ }
+
+ if (distanceTraveled != null) {
+ attributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DRONE_DISTANCE_TRAVELED,
+ DroneIngestModuleFactory.getModuleName(), velocity));
+ }
+
+ return attributes;
+ }
+
+ /**
+ * 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", tempFilePath.toString(), file.getName()), ex);
+ }
+
+ 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..3265c4de4c
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/modules/drones/DroneIngestModule.java
@@ -0,0 +1,93 @@
+/*
+ * 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({
+ "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/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