diff --git a/Core/build.xml b/Core/build.xml
index d2e9b00f169d395d418f76fd3b574fdbf0d2a68c..811ff30772a66858cc0d47872833740e7e5b1668 100644
--- a/Core/build.xml
+++ b/Core/build.xml
@@ -102,6 +102,7 @@
         <copy todir="${basedir}/release/yara" >
             <fileset dir="${thirdparty.dir}/yara/bin"/>
         </copy>
+        <copy file="${thirdparty.dir}/yara/bin/YaraJNIWrapper.jar" todir="${ext.dir}" />
     </target>
     
     
diff --git a/Core/nbproject/project.properties b/Core/nbproject/project.properties
index 2b8155b7b20bcd3409fda1ca5ea19b027e09b6aa..6986362a9056635565480fdbc837a90715e57d37 100644
--- a/Core/nbproject/project.properties
+++ b/Core/nbproject/project.properties
@@ -118,7 +118,7 @@ file.reference.StixLib.jar=release\\modules\\ext\\StixLib.jar
 file.reference.threetenbp-1.3.3.jar=release\\modules\\ext\\threetenbp-1.3.3.jar
 file.reference.webp-imageio-sejda-0.1.0.jar=release\\modules\\ext\\webp-imageio-sejda-0.1.0.jar
 file.reference.xmpcore-5.1.3.jar=release\\modules\\ext\\xmpcore-5.1.3.jar
-file.reference.YaraJNIWrapper.jar=release/modules/ext/YaraJNIWrapper.jar
+file.reference.YaraJNIWrapper.jar=release\\modules\\ext\\YaraJNIWrapper.jar
 file.reference.zookeeper-3.4.6.jar=release\\modules\\ext\\zookeeper-3.4.6.jar
 javac.source=1.8
 javac.compilerargs=-Xlint -Xlint:-serial
diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbManager.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbManager.java
index b0f288673bf3cb81f4913b808e8f5fca1c12b6bf..d3a0e61ecf14998448f4b3f4c349bab9b3703541 100755
--- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbManager.java
+++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDbManager.java
@@ -382,6 +382,31 @@ public void setupDefaultSqliteDb() throws CentralRepoException {
         CentralRepoDbUtil.setUseCentralRepo(true);
         saveNewCentralRepo();
     }
+    
+    /**
+     * Set up a PostgresDb using the settings for the given database choice
+     * enum.
+     * 
+     * @param choice Type of postgres DB to set up
+     * @throws CentralRepoException 
+     */
+    public void setupPostgresDb(CentralRepoDbChoice choice) throws CentralRepoException {        
+        selectedDbChoice = choice;
+        DatabaseTestResult curStatus = testStatus();
+        if (curStatus == DatabaseTestResult.DB_DOES_NOT_EXIST) {
+            createDb();
+            curStatus = testStatus();
+        }
+
+        // the only successful setup status is tested ok
+        if (curStatus != DatabaseTestResult.TESTED_OK) {
+            throw new CentralRepoException("Unable to successfully create postgres database. Test failed with: " + curStatus);
+        }
+
+        // if successfully got here, then save the settings
+        CentralRepoDbUtil.setUseCentralRepo(true);
+        saveNewCentralRepo();
+    }
 
     /**
      * This method returns if changes to the central repository configuration
diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PostgresCentralRepoSettings.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PostgresCentralRepoSettings.java
index 165d4ca6e083a8f4cd62a1f2b41eb69b118b5eec..dfb855a0b2b0468adf4b23f630e78af66df110bf 100644
--- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PostgresCentralRepoSettings.java
+++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/PostgresCentralRepoSettings.java
@@ -121,10 +121,12 @@ String getJDBCBaseURI() {
      * @return
      */
     String getConnectionURL(boolean usePostgresDb) {
-        StringBuilder url = new StringBuilder();
-        url.append(getJDBCBaseURI());
-        url.append(getHost());
-        url.append("/"); // NON-NLS
+        StringBuilder url = new StringBuilder()
+                .append(getJDBCBaseURI())
+                .append(getHost())
+                .append(":") // NON-NLS
+                .append(getPort())
+                .append("/"); // NON-NLS
         if (usePostgresDb) {
             url.append("postgres"); // NON-NLS
         } else {
@@ -153,7 +155,7 @@ Connection getEphemeralConnection(boolean usePostgresDb) {
         } catch (ClassNotFoundException | SQLException ex) {
             // TODO: Determine why a connection failure (ConnectionException) re-throws
             // the SQLException and does not print this log message?
-            LOGGER.log(Level.SEVERE, "Failed to acquire ephemeral connection to postgresql."); // NON-NLS
+            LOGGER.log(Level.SEVERE, "Failed to acquire ephemeral connection to postgresql.", ex); // NON-NLS
             conn = null;
         }
         return conn;
diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/Bundle.properties-MERGED
index d4cca6c407f6956bbff37299339a84b9c1fa8e43..e95a759c4fa87f103243551220454efa1e86aa0d 100755
--- a/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/Bundle.properties-MERGED
+++ b/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/Bundle.properties-MERGED
@@ -1,4 +1,10 @@
 caseeventlistener.evidencetag=Evidence
+CentralRepositoryNotificationDialog.bulletHeader=This data is used to:
+CentralRepositoryNotificationDialog.bulletOne=Ignore common items (files, domains, and accounts)
+CentralRepositoryNotificationDialog.bulletThree=Create personas that group accounts
+CentralRepositoryNotificationDialog.bulletTwo=Identify where an item was previously seen
+CentralRepositoryNotificationDialog.finalRemarks=To limit what is stored, use the Central Repository options panel.
+CentralRepositoryNotificationDialog.header=Autopsy stores data about each case in its Central Repository.
 IngestEventsListener.ingestmodule.name=Central Repository
 IngestEventsListener.prevCaseComment.text=Previous Case: 
 # {0} - typeName
@@ -7,6 +13,3 @@ IngestEventsListener.prevCount.text=Number of previous {0}: {1}
 IngestEventsListener.prevExists.text=Previously Seen Devices (Central Repository)
 IngestEventsListener.prevTaggedSet.text=Previously Tagged As Notable (Central Repository)
 Installer.centralRepoUpgradeFailed.title=Central repository disabled
-Installer.initialCreateSqlite.messageDesc=It will store information about all hashes and identifiers that you process. You can use this to ignore previously seen files and make connections between cases.
-Installer.initialCreateSqlite.messageHeader=The Central Repository is not enabled. Would you like to enable it?
-Installer.initialCreateSqlite.title=Enable Central Repository?
diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/CentralRepositoryNotificationDialog.java b/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/CentralRepositoryNotificationDialog.java
new file mode 100755
index 0000000000000000000000000000000000000000..883d979c908673d1861f06a74bc239a9e9bafb41
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/CentralRepositoryNotificationDialog.java
@@ -0,0 +1,73 @@
+/*
+ * Central Repository
+ *
+ * Copyright 2020 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.autopsy.centralrepository.eventlisteners;
+
+import org.openide.util.NbBundle;
+import org.sleuthkit.autopsy.core.RuntimeProperties;
+import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
+import org.sleuthkit.autopsy.coreutils.Version;
+
+/**
+ * Notifies new installations or old upgrades that the central repository will
+ * be enabled by default.
+ */
+public class CentralRepositoryNotificationDialog {
+
+    /**
+     * This dialog should display iff the mode is RELEASE and the
+     * application is running with a GUI.
+     */
+    static boolean shouldDisplay() {
+        return Version.getBuildType() == Version.Type.RELEASE 
+                && RuntimeProperties.runningWithGUI();
+    }
+
+    /**
+     * Displays an informational modal dialog to the user, which is dismissed by
+     * pressing 'OK'.
+     */
+    @NbBundle.Messages({
+        "CentralRepositoryNotificationDialog.header=Autopsy stores data about each case in its Central Repository.",
+        "CentralRepositoryNotificationDialog.bulletHeader=This data is used to:",
+        "CentralRepositoryNotificationDialog.bulletOne=Ignore common items (files, domains, and accounts)",
+        "CentralRepositoryNotificationDialog.bulletTwo=Identify where an item was previously seen",
+        "CentralRepositoryNotificationDialog.bulletThree=Create personas that group accounts",
+        "CentralRepositoryNotificationDialog.finalRemarks=To limit what is stored, use the Central Repository options panel."
+    })
+    static void display() {
+        assert shouldDisplay();
+
+        MessageNotifyUtil.Message.info(
+                "<html>"
+                + "<body>"
+                    + "<div>"
+                        + "<p>" + Bundle.CentralRepositoryNotificationDialog_header() + "</p>"
+                        + "<p>" + Bundle.CentralRepositoryNotificationDialog_bulletHeader() + "</p>"
+                        + "<ul>"
+                            + "<li>" + Bundle.CentralRepositoryNotificationDialog_bulletOne() + "</li>"
+                            + "<li>" + Bundle.CentralRepositoryNotificationDialog_bulletTwo() + "</li>"
+                            + "<li>" + Bundle.CentralRepositoryNotificationDialog_bulletThree() + "</li>"
+                        + "</ul>"
+                        + "<p>" + Bundle.CentralRepositoryNotificationDialog_finalRemarks() + "</p>"
+                    + "</div>"
+                + "</body>"
+                + "</html>"
+        );
+    }
+}
diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/Installer.java b/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/Installer.java
index d4f0253cd2f92b82f2113f5ce46b775ada31ff65..b2ef0d437ed05d3c81eb73c76228b667ea19af52 100644
--- a/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/Installer.java
+++ b/Core/src/org/sleuthkit/autopsy/centralrepository/eventlisteners/Installer.java
@@ -25,14 +25,13 @@
 import javax.swing.SwingUtilities;
 import org.openide.modules.ModuleInstall;
 import org.openide.util.NbBundle;
-import org.openide.windows.WindowManager;
-import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoDbChoice;
 import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoDbManager;
 import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoException;
 import org.sleuthkit.autopsy.core.RuntimeProperties;
+import org.sleuthkit.autopsy.core.UserPreferences;
 import org.sleuthkit.autopsy.coreutils.Logger;
 import org.sleuthkit.autopsy.coreutils.ModuleSettings;
-import org.sleuthkit.autopsy.coreutils.Version;
 
 /**
  * Adds/removes application event listeners responsible for adding data to the
@@ -81,19 +80,10 @@ private Installer() {
      * the org.sleuthkit.autopsy.core package when the already installed
      * Autopsy-Core module is restored (during application startup).
      */
-    @NbBundle.Messages({
-        "Installer.initialCreateSqlite.title=Enable Central Repository?",
-        "Installer.initialCreateSqlite.messageHeader=The Central Repository is not enabled. Would you like to enable it?",
-        "Installer.initialCreateSqlite.messageDesc=It will store information about all hashes and identifiers that you process. "
-        + "You can use this to ignore previously seen files and make connections between cases."
-    })
     @Override
     public void restored() {
         addApplicationEventListeners();
-
-        if (Version.getBuildType() == Version.Type.RELEASE) {
-            setupDefaultCentralRepository();
-        }
+        setupDefaultCentralRepository();
     }
 
     /**
@@ -107,9 +97,9 @@ private void addApplicationEventListeners() {
 
     /**
      * Checks if the central repository has been set up and configured. If not,
-     * either offers to perform set up (running with a GUI) or does the set up
-     * unconditionally (not running with a GUI, e.g., in an automated ingest
-     * node).
+     * does the set up unconditionally. If the application is running with a
+     * GUI, a notification will be displayed to the user if the mode is RELEASE
+     * (in other words, developers are exempt from seeing the notification).
      */
     private void setupDefaultCentralRepository() {
         Map<String, String> centralRepoSettings = ModuleSettings.getConfigSettings("CentralRepository");
@@ -127,62 +117,30 @@ private void setupDefaultCentralRepository() {
                 ModuleSettings.setConfigSetting("CentralRepository", "initialized", "true");
             }
         }
+        
+        if(initialized) {
+            return; // Nothing to do
+        }
 
-        // if central repository hasn't been previously initialized, initialize it
-        if (!initialized) {
-            // if running with a GUI, prompt the user
-            if (RuntimeProperties.runningWithGUI()) {
-                try {
-                    SwingUtilities.invokeAndWait(() -> {
-                        try {
-                            String dialogText
-                                    = "<html><body>"
-                                    + "<div style='width: 400px;'>"
-                                    + "<p>" + NbBundle.getMessage(this.getClass(), "Installer.initialCreateSqlite.messageHeader") + "</p>"
-                                    + "<p style='margin-top: 10px'>" + NbBundle.getMessage(this.getClass(), "Installer.initialCreateSqlite.messageDesc") + "</p>"
-                                    + "</div>"
-                                    + "</body></html>";
-
-                            if (JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(WindowManager.getDefault().getMainWindow(),
-                                    dialogText,
-                                    NbBundle.getMessage(this.getClass(), "Installer.initialCreateSqlite.title"),
-                                    JOptionPane.YES_NO_OPTION)) {
-
-                                setupDefaultSqliteCentralRepo();
-                            }
-                        } catch (CentralRepoException ex) {
-                            logger.log(Level.SEVERE, "There was an error while initializing the central repository database", ex);
-
-                            doMessageBoxIfRunningInGUI(ex);
-                        }
-                    });
-                } catch (InterruptedException | InvocationTargetException ex) {
-                    logger.log(Level.SEVERE, "There was an error while running the swing utility invoke later while creating the central repository database", ex);
-                }
-            } // if no GUI, just initialize
-            else {
-                try {
-                    setupDefaultSqliteCentralRepo();
-                } catch (CentralRepoException ex) {
-                    logger.log(Level.SEVERE, "There was an error while initializing the central repository database", ex);
-
-                    doMessageBoxIfRunningInGUI(ex);
-                }
+        if (CentralRepositoryNotificationDialog.shouldDisplay()) {
+            CentralRepositoryNotificationDialog.display();
+        }
+
+        try {
+            CentralRepoDbManager manager = new CentralRepoDbManager();
+            if (UserPreferences.getIsMultiUserModeEnabled()) {
+                // Set up using existing multi-user settings.
+                manager.setupPostgresDb(CentralRepoDbChoice.POSTGRESQL_MULTIUSER);
+            } else {
+                manager.setupDefaultSqliteDb();
             }
+        } catch (CentralRepoException ex) {
+            logger.log(Level.SEVERE, "There was an error while initializing the central repository database", ex);
 
-            ModuleSettings.setConfigSetting("CentralRepository", "initialized", "true");
+            doMessageBoxIfRunningInGUI(ex);
         }
-    }
 
-    /**
-     * Sets up a default single-user SQLite central repository.
-     *
-     * @throws CentralRepoException If there is an error setting up teh central
-     *                              repository.
-     */
-    private void setupDefaultSqliteCentralRepo() throws CentralRepoException {
-        CentralRepoDbManager manager = new CentralRepoDbManager();
-        manager.setupDefaultSqliteDb();
+        ModuleSettings.setConfigSetting("CentralRepository", "initialized", "true");
     }
 
     /**
diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/RecentFilesSummary.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/RecentFilesSummary.java
index 6c4601af88bb0ba46ad2e904f0b6484da334679c..651f5fe758ba697b5da9e6fe7d29f2a5fcff44fd 100755
--- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/RecentFilesSummary.java
+++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/RecentFilesSummary.java
@@ -28,10 +28,11 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
-import java.util.SortedMap;
-import java.util.TreeMap;
+import java.util.stream.Collectors;
+import org.apache.commons.lang.StringUtils;
 import org.sleuthkit.datamodel.AbstractFile;
 import org.sleuthkit.datamodel.BlackboardArtifact;
 import org.sleuthkit.datamodel.BlackboardAttribute;
@@ -93,16 +94,56 @@ public Set<Integer> getArtifactTypeIdsForRefresh() {
         return ARTIFACT_UPDATE_TYPE_IDS;
     }
 
+    /**
+     * Removes fileDetails entries with redundant paths, sorts by date
+     * descending and limits to the limit provided.
+     *
+     * @param fileDetails The file details list.
+     * @param limit The maximum number of entries to return.
+     * @return The sorted limited list with unique paths.
+     */
+    private <T extends RecentFileDetails> List<T> getSortedLimited(List<T> fileDetails, int limit) {
+        Map<String, T> fileDetailsMap = fileDetails.stream()
+                .filter(details -> details != null)
+                .collect(Collectors.toMap(
+                        d -> d.getPath().toUpperCase(),
+                        d -> d,
+                        (d1, d2) -> Long.compare(d1.getDateAsLong(), d2.getDateAsLong()) > 0 ? d1 : d2));
+
+        return fileDetailsMap.values().stream()
+                .sorted((a, b) -> -Long.compare(a.getDateAsLong(), b.getDateAsLong()))
+                .limit(limit)
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * Returns a RecentFileDetails object as derived from the recent document
+     * artifact or null if no appropriate object can be made.
+     *
+     * @param artifact The artifact.
+     * @return The derived object or null if artifact is invalid.
+     */
+    private RecentFileDetails getRecentlyOpenedDocument(BlackboardArtifact artifact) {
+        String path = DataSourceInfoUtilities.getStringOrNull(artifact, PATH_ATT);
+        Long lastOpened = DataSourceInfoUtilities.getLongOrNull(artifact, DATETIME_ATT);
+
+        if (StringUtils.isBlank(path) || lastOpened == null || lastOpened == 0) {
+            return null;
+        } else {
+            return new RecentFileDetails(path, lastOpened);
+        }
+    }
+
     /**
      * Return a list of the most recently opened documents based on the
      * TSK_RECENT_OBJECT artifact.
      *
      * @param dataSource The data source to query.
-     * @param maxCount   The maximum number of results to return, pass 0 to get
-     *                   a list of all results.
+     * @param maxCount The maximum number of results to return, pass 0 to get a
+     * list of all results.
      *
      * @return A list RecentFileDetails representing the most recently opened
-     *         documents or an empty list if none were found.
+     * documents or an empty list if none were found.
      *
      * @throws SleuthkitCaseProviderException
      * @throws TskCoreException
@@ -112,36 +153,45 @@ public List<RecentFileDetails> getRecentlyOpenedDocuments(DataSource dataSource,
             return Collections.emptyList();
         }
 
-        List<BlackboardArtifact> artifactList
-                = DataSourceInfoUtilities.getArtifacts(provider.get(),
-                        new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_RECENT_OBJECT),
-                        dataSource,
-                        DATETIME_ATT,
-                        DataSourceInfoUtilities.SortOrder.DESCENDING,
-                        maxCount);
-
-        List<RecentFileDetails> fileDetails = new ArrayList<>();
-        for (BlackboardArtifact artifact : artifactList) {
-            Long accessedTime = null;
-            String path = "";
-
-            // Get all the attributes in one call.
-            List<BlackboardAttribute> attributeList = artifact.getAttributes();
-            for (BlackboardAttribute attribute : attributeList) {
-
-                if (attribute.getAttributeType().equals(DATETIME_ATT)) {
-                    accessedTime = attribute.getValueLong();
-                } else if (attribute.getAttributeType().equals(PATH_ATT)) {
-                    path = attribute.getValueString();
-                }
-            }
+        throwOnNonPositiveCount(maxCount);
 
-            if (accessedTime != null && accessedTime != 0) {
-                fileDetails.add(new RecentFileDetails(path, accessedTime));
-            }
+        List<RecentFileDetails> details = provider.get().getBlackboard()
+                .getArtifacts(ARTIFACT_TYPE.TSK_RECENT_OBJECT.getTypeID(), dataSource.getId()).stream()
+                .map(art -> getRecentlyOpenedDocument(art))
+                .filter(d -> d != null)
+                .collect(Collectors.toList());
+
+        return getSortedLimited(details, maxCount);
+    }
+
+    /**
+     * Returns a RecentDownloadDetails object as derived from the recent
+     * download artifact or null if no appropriate object can be made.
+     *
+     * @param artifact The artifact.
+     * @return The derived object or null if artifact is invalid.
+     */
+    private RecentDownloadDetails getRecentDownload(BlackboardArtifact artifact) {
+        Long accessedTime = DataSourceInfoUtilities.getLongOrNull(artifact, DATETIME_ACCESSED_ATT);
+        String domain = DataSourceInfoUtilities.getStringOrNull(artifact, DOMAIN_ATT);
+        String path = DataSourceInfoUtilities.getStringOrNull(artifact, PATH_ATT);
+
+        if (StringUtils.isBlank(path) || accessedTime == null || accessedTime == 0) {
+            return null;
+        } else {
+            return new RecentDownloadDetails(path, accessedTime, domain);
         }
+    }
 
-        return fileDetails;
+    /**
+     * Throws an IllegalArgumentException if count is less than 1.
+     *
+     * @param count The count.
+     */
+    private void throwOnNonPositiveCount(int count) {
+        if (count < 1) {
+            throw new IllegalArgumentException("Invalid count: value must be greater than 0.");
+        }
     }
 
     /**
@@ -149,11 +199,11 @@ public List<RecentFileDetails> getRecentlyOpenedDocuments(DataSource dataSource,
      * artifact TSK_DATETIME_ACCESSED attribute.
      *
      * @param dataSource Data source to query.
-     * @param maxCount   Maximum number of results to return, passing 0 will
-     *                   return all results.
+     * @param maxCount Maximum number of results to return, passing 0 will
+     * return all results.
      *
      * @return A list of RecentFileDetails objects or empty list if none were
-     *         found.
+     * found.
      *
      * @throws TskCoreException
      * @throws SleuthkitCaseProviderException
@@ -163,46 +213,23 @@ public List<RecentDownloadDetails> getRecentDownloads(DataSource dataSource, int
             return Collections.emptyList();
         }
 
-        List<BlackboardArtifact> artifactList
-                = DataSourceInfoUtilities.getArtifacts(provider.get(),
-                        new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_WEB_DOWNLOAD),
-                        dataSource,
-                        DATETIME_ACCESSED_ATT,
-                        DataSourceInfoUtilities.SortOrder.DESCENDING,
-                        maxCount);
-
-        List<RecentDownloadDetails> fileDetails = new ArrayList<>();
-        for (BlackboardArtifact artifact : artifactList) {
-            // Get all the attributes in one call.
-            Long accessedTime = null;
-            String domain = "";
-            String path = "";
-
-            List<BlackboardAttribute> attributeList = artifact.getAttributes();
-            for (BlackboardAttribute attribute : attributeList) {
-
-                if (attribute.getAttributeType().equals(DATETIME_ACCESSED_ATT)) {
-                    accessedTime = attribute.getValueLong();
-                } else if (attribute.getAttributeType().equals(DOMAIN_ATT)) {
-                    domain = attribute.getValueString();
-                } else if (attribute.getAttributeType().equals(PATH_ATT)) {
-                    path = attribute.getValueString();
-                }
-            }
-            if (accessedTime != null && accessedTime != 0L) {
-                fileDetails.add(new RecentDownloadDetails(path, accessedTime, domain));
-            }
-        }
+        throwOnNonPositiveCount(maxCount);
+
+        List<RecentDownloadDetails> details = provider.get().getBlackboard()
+                .getArtifacts(ARTIFACT_TYPE.TSK_WEB_DOWNLOAD.getTypeID(), dataSource.getId()).stream()
+                .map(art -> getRecentDownload(art))
+                .filter(d -> d != null)
+                .collect(Collectors.toList());
 
-        return fileDetails;
+        return getSortedLimited(details, maxCount);
     }
 
     /**
      * Returns a list of the most recent message attachments.
      *
      * @param dataSource Data source to query.
-     * @param maxCount   Maximum number of results to return, passing 0 will
-     *                   return all results.
+     * @param maxCount Maximum number of results to return, passing 0 will
+     * return all results.
      *
      * @return A list of RecentFileDetails of the most recent attachments.
      *
@@ -214,115 +241,72 @@ public List<RecentAttachmentDetails> getRecentAttachments(DataSource dataSource,
             return Collections.emptyList();
         }
 
-        if (maxCount < 0) {
-            throw new IllegalArgumentException("Invalid maxCount passed to getRecentAttachments, value must be equal to or greater than 0");
-        }
-
-        return createListFromMap(buildAttachmentMap(dataSource), maxCount);
-    }
+        throwOnNonPositiveCount(maxCount);
 
-    /**
-     * Build a map of all of the message attachment sorted in date order.
-     *
-     * @param dataSource Data source to query.
-     *
-     * @return Returns a SortedMap of details objects returned in descending
-     *         order.
-     *
-     * @throws SleuthkitCaseProviderException
-     * @throws TskCoreException
-     */
-    private SortedMap<Long, List<RecentAttachmentDetails>> buildAttachmentMap(DataSource dataSource) throws SleuthkitCaseProviderException, TskCoreException {
         SleuthkitCase skCase = provider.get();
-        TreeMap<Long, List<RecentAttachmentDetails>> sortedMap = new TreeMap<>();
 
-        List<BlackboardArtifact> associatedArtifacts = skCase.getBlackboard().getArtifacts(ASSOCATED_OBJ_ART.getTypeID(), dataSource.getId());
+        List<BlackboardArtifact> associatedArtifacts = skCase.getBlackboard()
+                .getArtifacts(ASSOCATED_OBJ_ART.getTypeID(), dataSource.getId());
+
+        List<RecentAttachmentDetails> details = new ArrayList<>();
         for (BlackboardArtifact artifact : associatedArtifacts) {
-            BlackboardAttribute attribute = artifact.getAttribute(ASSOCATED_ATT);
-            if (attribute == null) {
-                continue;
-            }
+            RecentAttachmentDetails thisDetails = getRecentAttachment(artifact, skCase);
 
-            BlackboardArtifact messageArtifact = skCase.getBlackboardArtifact(attribute.getValueLong());
-            if (messageArtifact != null && isMessageArtifact(messageArtifact)) {
-                Content content = artifact.getParent();
-                if (content instanceof AbstractFile) {
-                    String sender;
-                    Long date = null;
-                    String path;
-
-                    BlackboardAttribute senderAttribute = messageArtifact.getAttribute(EMAIL_FROM_ATT);
-                    if (senderAttribute != null) {
-                        sender = senderAttribute.getValueString();
-                    } else {
-                        sender = "";
-                    }
-                    senderAttribute = messageArtifact.getAttribute(MSG_DATEIME_SENT_ATT);
-                    if (senderAttribute != null) {
-                        date = senderAttribute.getValueLong();
-                    }
-
-                    AbstractFile abstractFile = (AbstractFile) content;
-
-                    path = Paths.get(abstractFile.getParentPath(), abstractFile.getName()).toString();
-
-                    if (date != null && date != 0) {
-                        List<RecentAttachmentDetails> list = sortedMap.get(date);
-                        if (list == null) {
-                            list = new ArrayList<>();
-                            sortedMap.put(date, list);
-                        }
-                        RecentAttachmentDetails details = new RecentAttachmentDetails(path, date, sender);
-                        if (!list.contains(details)) {
-                            list.add(details);
-                        }
-                    }
-                }
+            if (thisDetails != null) {
+                details.add(thisDetails);
             }
         }
-        return sortedMap.descendingMap();
+
+        return getSortedLimited(details, maxCount);
     }
 
     /**
-     * Create a list of detail objects from the given sorted map of the max
-     * size.
+     * Creates a RecentAttachmentDetails object from the associated object
+     * artifact or null if no RecentAttachmentDetails object can be derived.
      *
-     * @param sortedMap A Map of attachment details sorted by date.
-     * @param maxCount  Maximum number of values to return.
-     *
-     * @return A list of the details of the most recent attachments or empty
-     *         list if none where found.
+     * @param artifact The associated object artifact.
+     * @param skCase The current case.
+     * @return The derived object or null.
+     * @throws TskCoreException
      */
-    private List<RecentAttachmentDetails> createListFromMap(SortedMap<Long, List<RecentAttachmentDetails>> sortedMap, int maxCount) {
-        List<RecentAttachmentDetails> fileList = new ArrayList<>();
-
-        for (List<RecentAttachmentDetails> mapList : sortedMap.values()) {
-            if (maxCount == 0 || fileList.size() + mapList.size() <= maxCount) {
-                fileList.addAll(mapList);
-                continue;
-            }
+    private RecentAttachmentDetails getRecentAttachment(BlackboardArtifact artifact, SleuthkitCase skCase) throws TskCoreException {
+        // get associated artifact or return no result
+        BlackboardAttribute attribute = artifact.getAttribute(ASSOCATED_ATT);
+        if (attribute == null) {
+            return null;
+        }
 
-            if (maxCount == fileList.size()) {
-                break;
-            }
+        // get associated message artifact if exists or return no result
+        BlackboardArtifact messageArtifact = skCase.getBlackboardArtifact(attribute.getValueLong());
+        if (messageArtifact == null || !isMessageArtifact(messageArtifact)) {
+            return null;
+        }
 
-            for (RecentAttachmentDetails details : mapList) {
-                if (fileList.size() < maxCount) {
-                    fileList.add(details);
-                } else {
-                    break;
-                }
-            }
+        // get abstract file if exists or return no result
+        Content content = artifact.getParent();
+        if (!(content instanceof AbstractFile)) {
+            return null;
         }
 
-        return fileList;
+        AbstractFile abstractFile = (AbstractFile) content;
+
+        // get the path, sender, and date
+        String path = Paths.get(abstractFile.getParentPath(), abstractFile.getName()).toString();
+        String sender = DataSourceInfoUtilities.getStringOrNull(messageArtifact, EMAIL_FROM_ATT);
+        Long date = DataSourceInfoUtilities.getLongOrNull(messageArtifact, MSG_DATEIME_SENT_ATT);
+
+        if (date == null || date == 0 || StringUtils.isBlank(path)) {
+            return null;
+        } else {
+            return new RecentAttachmentDetails(path, date, sender);
+        }
     }
 
     /**
      * Is the given artifact a message.
      *
      * @param nodeArtifact An artifact that might be a message. Must not be
-     *                     null.
+     * null.
      *
      * @return True if the given artifact is a message artifact
      */
@@ -330,6 +314,7 @@ private boolean isMessageArtifact(BlackboardArtifact nodeArtifact) {
         final int artifactTypeID = nodeArtifact.getArtifactTypeID();
         return artifactTypeID == ARTIFACT_TYPE.TSK_EMAIL_MSG.getTypeID()
                 || artifactTypeID == ARTIFACT_TYPE.TSK_MESSAGE.getTypeID();
+
     }
 
     /**
@@ -391,8 +376,8 @@ public static class RecentDownloadDetails extends RecentFileDetails {
         /**
          * Constructor for files with just a path and date.
          *
-         * @param path      File path.
-         * @param date      File access date\time in seconds with java epoch.
+         * @param path File path.
+         * @param date File access date\time in seconds with java epoch.
          * @param webDomain The webdomain from which the file was downloaded.
          */
         RecentDownloadDetails(String path, long date, String webDomain) {
@@ -404,7 +389,7 @@ public static class RecentDownloadDetails extends RecentFileDetails {
          * Returns the web domain.
          *
          * @return The web domain or empty string if not available or
-         *         applicable.
+         * applicable.
          */
         public String getWebDomain() {
             return webDomain;
@@ -422,10 +407,10 @@ public static class RecentAttachmentDetails extends RecentFileDetails {
          * Constructor for recent download files which have a path, date and
          * domain value.
          *
-         * @param path   File path.
-         * @param date   File crtime.
+         * @param path File path.
+         * @param date File crtime.
          * @param sender The sender of the message from which the file was
-         *               attached.
+         * attached.
          */
         RecentAttachmentDetails(String path, long date, String sender) {
             super(path, date);
@@ -436,7 +421,7 @@ public static class RecentAttachmentDetails extends RecentFileDetails {
          * Return the sender of the attached file.
          *
          * @return The sender of the attached file or empty string if not
-         *         available.
+         * available.
          */
         public String getSender() {
             return sender;
diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/UserActivitySummary.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/UserActivitySummary.java
index 16da6f5c4bf1f41b01d2198c80f2b52f680e387e..b7efda6e29c442ada5860182281ac5a218935c58 100644
--- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/UserActivitySummary.java
+++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/UserActivitySummary.java
@@ -137,7 +137,7 @@ public class UserActivitySummary implements DefaultArtifactUpdateGovernor {
         return (a.getProgramName() == null ? "" : a.getProgramName())
                 .compareToIgnoreCase((b.getProgramName() == null ? "" : b.getProgramName()));
     };
-    
+
     private static final Set<Integer> ARTIFACT_UPDATE_TYPE_IDS = new HashSet<>(Arrays.asList(
             ARTIFACT_TYPE.TSK_WEB_SEARCH_QUERY.getTypeID(),
             ARTIFACT_TYPE.TSK_MESSAGE.getTypeID(),
@@ -172,9 +172,9 @@ public UserActivitySummary() {
      * is designed with unit testing in mind since mocked dependencies can be
      * utilized.
      *
-     * @param provider           The object providing the current SleuthkitCase.
+     * @param provider The object providing the current SleuthkitCase.
      * @param translationService The translation service.
-     * @param logger             The logger to use.
+     * @param logger The logger to use.
      */
     public UserActivitySummary(
             SleuthkitCaseProvider provider,
@@ -206,7 +206,7 @@ private void assertValidCount(int count) {
      * Gets a list of recent domains based on the datasource.
      *
      * @param dataSource The datasource to query for recent domains.
-     * @param count      The max count of items to return.
+     * @param count The max count of items to return.
      *
      * @return The list of items retrieved from the database.
      *
@@ -242,12 +242,12 @@ public List<TopDomainsResult> getRecentDomains(DataSource dataSource, int count)
      * Creates a TopDomainsResult from data or null if no visit date exists
      * within DOMAIN_WINDOW_MS of mostRecentMs.
      *
-     * @param domain       The domain.
-     * @param visits       The number of visits.
+     * @param domain The domain.
+     * @param visits The number of visits.
      * @param mostRecentMs The most recent visit of any domain.
      *
      * @return The TopDomainsResult or null if no visits to this domain within
-     *         30 days of mostRecentMs.
+     * 30 days of mostRecentMs.
      */
     private TopDomainsResult getDomainsResult(String domain, List<Long> visits, long mostRecentMs) {
         long visitCount = 0;
@@ -280,9 +280,8 @@ private TopDomainsResult getDomainsResult(String domain, List<Long> visits, long
      * @param dataSource The datasource.
      *
      * @return A tuple where the first value is the latest web history accessed
-     *         date in milliseconds and the second value maps normalized
-     *         (lowercase; trimmed) domain names to when those domains were
-     *         visited.
+     * date in milliseconds and the second value maps normalized (lowercase;
+     * trimmed) domain names to when those domains were visited.
      *
      * @throws TskCoreException
      * @throws SleuthkitCaseProviderException
@@ -349,7 +348,7 @@ private static Long getMax(Long num1, Long num2) {
      * @param artifact The artifact.
      *
      * @return The TopWebSearchResult or null if the search string or date
-     *         accessed cannot be determined.
+     * accessed cannot be determined.
      */
     private static TopWebSearchResult getWebSearchResult(BlackboardArtifact artifact) {
         String searchString = DataSourceInfoUtilities.getStringOrNull(artifact, TYPE_TEXT);
@@ -364,11 +363,10 @@ private static TopWebSearchResult getWebSearchResult(BlackboardArtifact artifact
      * term.
      *
      * @param dataSource The data source.
-     * @param count      The maximum number of records to be shown (must be >
-     *                   0).
+     * @param count The maximum number of records to be shown (must be > 0).
      *
      * @return The list of most recent web searches where most recent search
-     *         appears first.
+     * appears first.
      *
      * @throws
      * org.sleuthkit.autopsy.datasourcesummary.datamodel.SleuthkitCaseProvider.SleuthkitCaseProviderException
@@ -386,21 +384,22 @@ public List<TopWebSearchResult> getMostRecentWebSearches(DataSource dataSource,
                 .getArtifacts(ARTIFACT_TYPE.TSK_WEB_SEARCH_QUERY.getTypeID(), dataSource.getId());
 
         // group by search string (case insensitive)
-        Collection<List<TopWebSearchResult>> resultGroups = webSearchArtifacts
+        Collection<TopWebSearchResult> resultGroups = webSearchArtifacts
                 .stream()
                 // get items where search string and date is not null
                 .map(UserActivitySummary::getWebSearchResult)
                 // remove null records
                 .filter(result -> result != null)
-                // get these messages grouped by search to string
-                .collect(Collectors.groupingBy((result) -> result.getSearchString().toUpperCase()))
+                // get the latest message for each search string
+                .collect(Collectors.toMap(
+                        (result) -> result.getSearchString().toUpperCase(),
+                        result -> result,
+                        (result1, result2) -> TOP_WEBSEARCH_RESULT_DATE_COMPARE.compare(result1, result2) >= 0 ? result1 : result2))
                 .values();
 
         // get the most recent date for each search term
         List<TopWebSearchResult> results = resultGroups
                 .stream()
-                // get the most recent access per search type
-                .map((list) -> list.stream().max(TOP_WEBSEARCH_RESULT_DATE_COMPARE).get())
                 // get most recent searches first
                 .sorted(TOP_WEBSEARCH_RESULT_DATE_COMPARE.reversed())
                 .limit(count)
@@ -424,7 +423,7 @@ public List<TopWebSearchResult> getMostRecentWebSearches(DataSource dataSource,
      * @param original The original text.
      *
      * @return The translated text or null if no translation can be determined
-     *         or exists.
+     * or exists.
      */
     private String getTranslationOrNull(String original) {
         if (!translationService.hasProvider() || StringUtils.isBlank(original)) {
@@ -448,15 +447,34 @@ private String getTranslationOrNull(String original) {
         return translated;
     }
 
+    /**
+     * Gives the most recent TopDeviceAttachedResult. If one is null, the other
+     * is returned.
+     *
+     * @param r1 A result.
+     * @param r2 Another result.
+     * @return The most recent one with a non-null date.
+     */
+    private TopDeviceAttachedResult getMostRecentDevice(TopDeviceAttachedResult r1, TopDeviceAttachedResult r2) {
+        if (r2.getDateAccessed() == null) {
+            return r1;
+        }
+
+        if (r1.getDateAccessed() == null) {
+            return r2;
+        }
+
+        return r1.getDateAccessed().compareTo(r2.getDateAccessed()) >= 0 ? r1 : r2;
+    }
+
     /**
      * Retrieves most recent devices used by most recent date attached.
      *
      * @param dataSource The data source.
-     * @param count      The maximum number of records to be shown (must be >
-     *                   0).
+     * @param count The maximum number of records to be shown (must be > 0).
      *
      * @return The list of most recent devices attached where most recent device
-     *         attached appears first.
+     * attached appears first.
      *
      * @throws
      * org.sleuthkit.autopsy.datasourcesummary.datamodel.SleuthkitCaseProvider.SleuthkitCaseProviderException
@@ -469,7 +487,7 @@ public List<TopDeviceAttachedResult> getRecentDevices(DataSource dataSource, int
             return Collections.emptyList();
         }
 
-        return DataSourceInfoUtilities.getArtifacts(caseProvider.get(), TYPE_DEVICE_ATTACHED,
+        Collection<TopDeviceAttachedResult> results = DataSourceInfoUtilities.getArtifacts(caseProvider.get(), TYPE_DEVICE_ATTACHED,
                 dataSource, TYPE_DATETIME, DataSourceInfoUtilities.SortOrder.DESCENDING, 0)
                 .stream()
                 .map(artifact -> {
@@ -482,9 +500,14 @@ public List<TopDeviceAttachedResult> getRecentDevices(DataSource dataSource, int
                 })
                 // remove Root Hub identifier
                 .filter(result -> {
-                    return result.getDeviceModel() == null
+                    return result.getDeviceId() == null
+                            || result.getDeviceModel() == null
                             || !DEVICE_EXCLUDE_LIST.contains(result.getDeviceModel().trim().toUpperCase());
                 })
+                .collect(Collectors.toMap(result -> result.getDeviceId(), result -> result, (r1, r2) -> getMostRecentDevice(r1, r2)))
+                .values();
+
+        return results.stream()
                 .limit(count)
                 .collect(Collectors.toList());
     }
@@ -495,7 +518,7 @@ public List<TopDeviceAttachedResult> getRecentDevices(DataSource dataSource, int
      * @param artifact The artifact.
      *
      * @return The TopAccountResult or null if the account type or message date
-     *         cannot be determined.
+     * cannot be determined.
      */
     private static TopAccountResult getMessageAccountResult(BlackboardArtifact artifact) {
         String type = DataSourceInfoUtilities.getStringOrNull(artifact, TYPE_MESSAGE_TYPE);
@@ -509,12 +532,12 @@ private static TopAccountResult getMessageAccountResult(BlackboardArtifact artif
      * Obtains a TopAccountResult from a blackboard artifact. The date is
      * maximum of any found dates for attribute types provided.
      *
-     * @param artifact    The artifact.
+     * @param artifact The artifact.
      * @param messageType The type of message this is.
-     * @param dateAttrs   The date attribute types.
+     * @param dateAttrs The date attribute types.
      *
      * @return The TopAccountResult or null if the account type or max date are
-     *         not provided.
+     * not provided.
      */
     private static TopAccountResult getAccountResult(BlackboardArtifact artifact, String messageType, BlackboardAttribute.Type... dateAttrs) {
         String type = messageType;
@@ -538,11 +561,10 @@ private static TopAccountResult getAccountResult(BlackboardArtifact artifact, St
      * sent.
      *
      * @param dataSource The data source.
-     * @param count      The maximum number of records to be shown (must be >
-     *                   0).
+     * @param count The maximum number of records to be shown (must be > 0).
      *
      * @return The list of most recent accounts used where the most recent
-     *         account by last message sent occurs first.
+     * account by last message sent occurs first.
      *
      * @throws
      * org.sleuthkit.autopsy.datasourcesummary.datamodel.SleuthkitCaseProvider.SleuthkitCaseProviderException
@@ -585,18 +607,19 @@ public List<TopAccountResult> getRecentAccounts(DataSource dataSource, int count
         Stream<TopAccountResult> allResults = Stream.concat(messageResults, Stream.concat(emailResults, calllogResults));
 
         // get them grouped by account type        
-        Collection<List<TopAccountResult>> groupedResults = allResults
+        Collection<TopAccountResult> groupedResults = allResults
                 // remove null records
                 .filter(result -> result != null)
-                // get these messages grouped by account type
-                .collect(Collectors.groupingBy(TopAccountResult::getAccountType))
+                // get these messages grouped by account type and get the most recent of each type
+                .collect(Collectors.toMap(
+                        result -> result.getAccountType(),
+                        result -> result,
+                        (result1, result2) -> TOP_ACCOUNT_RESULT_DATE_COMPARE.compare(result1, result2) >= 0 ? result1 : result2))
                 .values();
 
         // get account type sorted by most recent date
         return groupedResults
                 .stream()
-                // get the most recent access per account type
-                .map((accountGroup) -> accountGroup.stream().max(TOP_ACCOUNT_RESULT_DATE_COMPARE).get())
                 // get most recent accounts accessed
                 .sorted(TOP_ACCOUNT_RESULT_DATE_COMPARE.reversed())
                 // limit to count
@@ -608,7 +631,7 @@ public List<TopAccountResult> getRecentAccounts(DataSource dataSource, int count
     /**
      * Determines a short folder name if any. Otherwise, returns empty string.
      *
-     * @param strPath         The string path.
+     * @param strPath The string path.
      * @param applicationName The application name.
      *
      * @return The short folder name or empty string if not found.
@@ -659,7 +682,7 @@ private TopProgramsResult getTopProgramsResult(BlackboardArtifact artifact) {
         if (StringUtils.startsWithIgnoreCase(path, WINDOWS_PREFIX)) {
             return null;
         }
-        
+
         Integer count = DataSourceInfoUtilities.getIntOrNull(artifact, TYPE_COUNT);
         Long longCount = (count == null) ? null : (long) count;
 
@@ -696,7 +719,7 @@ private static Date getMax(Date date1, Date date2) {
      * @param long2 Second possibly null long.
      *
      * @return Returns the compare value: 1,0,-1 favoring the higher non-null
-     *         value.
+     * value.
      */
     private static int nullableCompare(Long long1, Long long2) {
         if (long1 == null && long2 == null) {
@@ -721,7 +744,6 @@ private static boolean isPositiveNum(Long longNum) {
         return longNum != null && longNum > 0;
     }
 
-
     /**
      * Retrieves the top programs results for the given data source limited to
      * the count provided as a parameter. The highest run times are at the top
@@ -731,12 +753,12 @@ private static boolean isPositiveNum(Long longNum) {
      * be ignored and all items will be returned.
      *
      * @param dataSource The datasource. If the datasource is null, an empty
-     *                   list will be returned.
-     * @param count      The number of results to return. This value must be > 0
-     *                   or an IllegalArgumentException will be thrown.
+     * list will be returned.
+     * @param count The number of results to return. This value must be > 0 or
+     * an IllegalArgumentException will be thrown.
      *
      * @return The sorted list and limited to the count if last run or run count
-     *         information is available on any item.
+     * information is available on any item.
      *
      * @throws SleuthkitCaseProviderException
      * @throws TskCoreException
@@ -759,7 +781,9 @@ public List<TopProgramsResult> getTopPrograms(DataSource dataSource, int count)
                 // The value will be a TopProgramsResult with the max run times 
                 // and most recent last run date for each program name / program path pair.
                 .collect(Collectors.toMap(
-                        res -> Pair.of(res.getProgramName(), res.getProgramPath()),
+                        res -> Pair.of(
+                                res.getProgramName() == null ? null : res.getProgramName().toUpperCase(),
+                                res.getProgramPath() == null ? null : res.getProgramPath().toUpperCase()),
                         res -> res,
                         (res1, res2) -> {
                             return new TopProgramsResult(
@@ -852,10 +876,10 @@ public static class TopDeviceAttachedResult {
         /**
          * Main constructor.
          *
-         * @param deviceId     The device id.
+         * @param deviceId The device id.
          * @param dateAccessed The date last attached.
-         * @param deviceMake   The device make.
-         * @param deviceModel  The device model.
+         * @param deviceMake The device make.
+         * @param deviceModel The device model.
          */
         public TopDeviceAttachedResult(String deviceId, Date dateAccessed, String deviceMake, String deviceModel) {
             this.deviceId = deviceId;
@@ -906,7 +930,7 @@ public static class TopAccountResult {
          * Main constructor.
          *
          * @param accountType The account type.
-         * @param lastAccess  The date the account was last accessed.
+         * @param lastAccess The date the account was last accessed.
          */
         public TopAccountResult(String accountType, Date lastAccess) {
             this.accountType = accountType;
@@ -940,9 +964,9 @@ public static class TopDomainsResult {
         /**
          * Describes a top domain result.
          *
-         * @param domain     The domain.
+         * @param domain The domain.
          * @param visitTimes The number of times it was visited.
-         * @param lastVisit  The date of the last visit.
+         * @param lastVisit The date of the last visit.
          */
         public TopDomainsResult(String domain, Long visitTimes, Date lastVisit) {
             this.domain = domain;
@@ -987,7 +1011,7 @@ public static class TopProgramsResult {
          *
          * @param programName The name of the program.
          * @param programPath The path of the program.
-         * @param runTimes    The number of runs.
+         * @param runTimes The number of runs.
          */
         TopProgramsResult(String programName, String programPath, Long runTimes, Date lastRun) {
             this.programName = programName;
diff --git a/Core/src/org/sleuthkit/autopsy/modules/drones/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/modules/drones/Bundle.properties-MERGED
index bf61ad9be0477e90eaf6c9c54fcc230c45d9f9a3..cd35320f717a67570b41cc3c7df9d8c0ffa3ed01 100755
--- a/Core/src/org/sleuthkit/autopsy/modules/drones/Bundle.properties-MERGED
+++ b/Core/src/org/sleuthkit/autopsy/modules/drones/Bundle.properties-MERGED
@@ -1,6 +1,6 @@
 DATExtractor_process_message=Processing DJI DAT file: %s
 DATFileExtractor_Extractor_Name=DAT File Extractor
-DroneIngestModule_Description=Analyzes files generated by drones.
-DroneIngestModule_Name=Drone Analyzer
+DroneIngestModule_Description=Analyzes files generated by some DJI drones.
+DroneIngestModule_Name=DJI Drone Analyzer
 # {0} - AbstractFileName
 DroneIngestModule_process_start=Started {0}
diff --git a/Core/src/org/sleuthkit/autopsy/modules/drones/DroneIngestModuleFactory.java b/Core/src/org/sleuthkit/autopsy/modules/drones/DroneIngestModuleFactory.java
index 1213ffedd5e8df1650661911cbd2acfd27632d04..0e8f7107a5e372734a2a43cc59b8bd2bc35d6c76 100755
--- a/Core/src/org/sleuthkit/autopsy/modules/drones/DroneIngestModuleFactory.java
+++ b/Core/src/org/sleuthkit/autopsy/modules/drones/DroneIngestModuleFactory.java
@@ -33,8 +33,8 @@
 public class DroneIngestModuleFactory extends IngestModuleFactoryAdapter {
 
     @Messages({
-        "DroneIngestModule_Name=Drone Analyzer",
-        "DroneIngestModule_Description=Analyzes files generated by drones."
+        "DroneIngestModule_Name=DJI Drone Analyzer",
+        "DroneIngestModule_Description=Analyzes files generated by some DJI drones."
     })
 
     /**
diff --git a/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/Bundle.properties b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/Bundle.properties
index 0036d4dd6ff5999a3866a5c73eba1f40c8c3c3de..15698d736c21d37e86e4f15d71d1a12ca9ef333a 100644
--- a/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/Bundle.properties
+++ b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/Bundle.properties
@@ -1,3 +1,4 @@
 ILeappAnalyzerIngestModule.init.exception.msg=Unable to find {0}.
 ILeappAnalyzerIngestModule.processing.file=Processing file {0}
-ILeappAnalyzerIngestModule.parsing.file=Parsing file {0}
\ No newline at end of file
+ILeappAnalyzerIngestModule.parsing.file=Parsing file {0}
+ILeappAnalyzerIngestModule.processing.filesystem=Processing filesystem
\ No newline at end of file
diff --git a/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/Bundle.properties-MERGED
index b4f350f478f63a91207ccc1b4b11f70ebe84ca02..b4e8226a91cffd9666b2d44f0030d8fe07275a57 100644
--- a/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/Bundle.properties-MERGED
+++ b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/Bundle.properties-MERGED
@@ -8,6 +8,7 @@ ILeappAnalyzerIngestModule.iLeapp.cancelled=iLeapp run was canceled
 ILeappAnalyzerIngestModule.init.exception.msg=Unable to find {0}.
 ILeappAnalyzerIngestModule.processing.file=Processing file {0}
 ILeappAnalyzerIngestModule.parsing.file=Parsing file {0}
+ILeappAnalyzerIngestModule.processing.filesystem=Processing filesystem
 ILeappAnalyzerIngestModule.report.name=iLeapp Html Report
 ILeappAnalyzerIngestModule.requires.windows=iLeapp module requires windows.
 ILeappAnalyzerIngestModule.running.iLeapp=Running iLeapp
diff --git a/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ILeappAnalyzerIngestModule.java b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ILeappAnalyzerIngestModule.java
index f859919bae7aa35e96fe3ca6ee9c935c7cbd5ea0..abec90e6c0eed909fa8512e1a5ce941aa099f132 100644
--- a/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ILeappAnalyzerIngestModule.java
+++ b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ILeappAnalyzerIngestModule.java
@@ -18,8 +18,10 @@
  */
 package org.sleuthkit.autopsy.modules.ileappanalyzer;
 
+import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileNotFoundException;
+import java.io.FileReader;
 import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.nio.file.Files;
@@ -32,14 +34,17 @@
 import java.util.logging.Level;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
+import org.apache.commons.io.FilenameUtils;
 import org.openide.modules.InstalledFileLocator;
 import org.openide.util.NbBundle;
 import org.sleuthkit.autopsy.casemodule.Case;
 import static org.sleuthkit.autopsy.casemodule.Case.getCurrentCase;
+import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
 import org.sleuthkit.autopsy.casemodule.services.FileManager;
 import org.sleuthkit.autopsy.coreutils.ExecUtil;
 import org.sleuthkit.autopsy.coreutils.Logger;
 import org.sleuthkit.autopsy.coreutils.PlatformUtil;
+import org.sleuthkit.autopsy.datamodel.ContentUtils;
 import org.sleuthkit.autopsy.ingest.DataSourceIngestModule;
 import org.sleuthkit.autopsy.ingest.DataSourceIngestModuleProcessTerminator;
 import org.sleuthkit.autopsy.ingest.DataSourceIngestModuleProgress;
@@ -50,6 +55,7 @@
 import org.sleuthkit.datamodel.AbstractFile;
 import org.sleuthkit.datamodel.Content;
 import org.sleuthkit.datamodel.LocalFilesDataSource;
+import org.sleuthkit.datamodel.ReadContentInputStream;
 import org.sleuthkit.datamodel.TskCoreException;
 
 /**
@@ -61,7 +67,9 @@ public class ILeappAnalyzerIngestModule implements DataSourceIngestModule {
     private static final String MODULE_NAME = ILeappAnalyzerModuleFactory.getModuleName();
 
     private static final String ILEAPP = "iLeapp"; //NON-NLS
+    private static final String ILEAPP_FS = "fs_"; //NON-NLS
     private static final String ILEAPP_EXECUTABLE = "ileapp.exe";//NON-NLS
+    private static final String ILEAPP_PATHS_FILE = "iLeapp_paths.txt"; //NON-NLS
 
     private File iLeappExecutable;
 
@@ -87,7 +95,7 @@ public void startUp(IngestJobContext context) throws IngestModuleException {
 
         try {
             iLeappFileProcessor = new ILeappFileProcessor();
-        } catch (IOException | IngestModuleException ex) {
+        } catch (IOException | IngestModuleException | NoCurrentCaseException ex) {
             throw new IngestModuleException(Bundle.ILeappAnalyzerIngestModule_error_ileapp_file_processor_init(), ex);
         }
 
@@ -112,65 +120,149 @@ public void startUp(IngestJobContext context) throws IngestModuleException {
     @Override
     public ProcessResult process(Content dataSource, DataSourceIngestModuleProgress statusHelper) {
 
-        if (!(context.getDataSource() instanceof LocalFilesDataSource)) {
-            return ProcessResult.OK;
+        Case currentCase = Case.getCurrentCase();
+        Path tempOutputPath = Paths.get(currentCase.getTempDirectory(), ILEAPP, ILEAPP_FS + dataSource.getId());
+        try {
+            Files.createDirectories(tempOutputPath);
+        } catch (IOException ex) {
+            logger.log(Level.SEVERE, String.format("Error creating iLeapp output directory %s", tempOutputPath.toString()), ex);
+            return ProcessResult.ERROR;
+        }
+
+        List<String> iLeappPathsToProcess = new ArrayList<>();
+        ProcessBuilder iLeappCommand = buildiLeappListCommand(tempOutputPath);
+        try {
+            int result = ExecUtil.execute(iLeappCommand, new DataSourceIngestModuleProcessTerminator(context, true));
+            if (result != 0) {
+                logger.log(Level.SEVERE, String.format("Error when trying to execute iLeapp program getting file paths to search for result is %d", result));
+                return ProcessResult.ERROR;
+            }
+            iLeappPathsToProcess = loadIleappPathFile(tempOutputPath);
+        } catch (IOException ex) {
+            logger.log(Level.SEVERE, String.format("Error when trying to execute iLeapp program getting file paths to search"), ex);
+            return ProcessResult.ERROR;
         }
 
         statusHelper.progress(Bundle.ILeappAnalyzerIngestModule_starting_iLeapp(), 0);
 
-        List<AbstractFile> iLeappFilesToProcess = findiLeappFilesToProcess(dataSource);
+        List<AbstractFile> iLeappFilesToProcess = new ArrayList<>();
 
-        statusHelper.switchToDeterminate(iLeappFilesToProcess.size());
+        if (!(context.getDataSource() instanceof LocalFilesDataSource)) {
+            extractFilesFromImage(dataSource, iLeappPathsToProcess, tempOutputPath);
+            statusHelper.switchToDeterminate(iLeappFilesToProcess.size());
+            processILeappFs(dataSource, currentCase, statusHelper, tempOutputPath.toString());
+        } else {
+            iLeappFilesToProcess = findiLeappFilesToProcess(dataSource);
+            statusHelper.switchToDeterminate(iLeappFilesToProcess.size());
+
+            Integer filesProcessedCount = 0;
+            for (AbstractFile iLeappFile : iLeappFilesToProcess) {
+                processILeappFile(dataSource, currentCase, statusHelper, filesProcessedCount, iLeappFile);
+                filesProcessedCount++;
+            }
+            // Process the logical image as a fs in iLeapp to make sure this is not a logical fs that was added
+            extractFilesFromImage(dataSource, iLeappPathsToProcess, tempOutputPath);
+            processILeappFs(dataSource, currentCase, statusHelper, tempOutputPath.toString());
+        }
 
-        Integer filesProcessedCount = 0;
+        IngestMessage message = IngestMessage.createMessage(IngestMessage.MessageType.DATA,
+                Bundle.ILeappAnalyzerIngestModule_has_run(),
+                Bundle.ILeappAnalyzerIngestModule_completed());
+        IngestServices.getInstance().postMessage(message);
+        return ProcessResult.OK;
+    }
 
-        Case currentCase = Case.getCurrentCase();
-        for (AbstractFile iLeappFile : iLeappFilesToProcess) {
+    /**
+     * Process each tar/zip file that is found in a logical image that contains xLeapp data
+     * @param dataSource Datasource where the file has been found
+     * @param currentCase current case
+     * @param statusHelper Progress bar for messages to show user
+     * @param filesProcessedCount count that is incremented for progress bar
+     * @param iLeappFile abstract file that will be processed
+     */
+    private void processILeappFile(Content dataSource, Case currentCase, DataSourceIngestModuleProgress statusHelper, int filesProcessedCount,
+            AbstractFile iLeappFile) {
+        String currentTime = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss z", Locale.US).format(System.currentTimeMillis());//NON-NLS
+        Path moduleOutputPath = Paths.get(currentCase.getModuleDirectory(), ILEAPP, currentTime);
+        try {
+            Files.createDirectories(moduleOutputPath);
+        } catch (IOException ex) {
+            logger.log(Level.SEVERE, String.format("Error creating iLeapp output directory %s", moduleOutputPath.toString()), ex);
+            return;
+        }
 
-            String currentTime = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss z", Locale.US).format(System.currentTimeMillis());//NON-NLS
-            Path moduleOutputPath = Paths.get(currentCase.getModuleDirectory(), ILEAPP, currentTime);
-            try {
-                Files.createDirectories(moduleOutputPath);
-            } catch (IOException ex) {
-                logger.log(Level.SEVERE, String.format("Error creating iLeapp output directory %s", moduleOutputPath.toString()), ex);
-                return ProcessResult.ERROR;
+        statusHelper.progress(NbBundle.getMessage(this.getClass(), "ILeappAnalyzerIngestModule.processing.file", iLeappFile.getName()), filesProcessedCount);
+        ProcessBuilder iLeappCommand = buildiLeappCommand(moduleOutputPath, iLeappFile.getLocalAbsPath(), iLeappFile.getNameExtension());
+        try {
+            int result = ExecUtil.execute(iLeappCommand, new DataSourceIngestModuleProcessTerminator(context, true));
+            if (result != 0) {
+                logger.log(Level.WARNING, String.format("Error when trying to execute iLeapp program getting file paths to search for result is %d", result));
+                return;
             }
 
-            statusHelper.progress(NbBundle.getMessage(this.getClass(), "ILeappAnalyzerIngestModule.processing.file", iLeappFile.getName()), filesProcessedCount);
-            ProcessBuilder iLeappCommand = buildiLeappCommand(moduleOutputPath, iLeappFile.getLocalAbsPath(), iLeappFile.getNameExtension());
-            try {
-                int result = ExecUtil.execute(iLeappCommand, new DataSourceIngestModuleProcessTerminator(context, true));
-                if (result != 0) {
-                    // ignore if there is an error and continue to try and process the next file if there is one
-                    continue;
-                }
+            addILeappReportToReports(moduleOutputPath, currentCase);
 
-                addILeappReportToReports(moduleOutputPath, currentCase);
+        } catch (IOException ex) {
+            logger.log(Level.SEVERE, String.format("Error when trying to execute iLeapp program against file %s", iLeappFile.getLocalAbsPath()), ex);
+            return;
+        }
 
-            } catch (IOException ex) {
-                logger.log(Level.SEVERE, String.format("Error when trying to execute iLeapp program against file %s", iLeappFile.getLocalAbsPath()), ex);
-                return ProcessResult.ERROR;
-            }
+        if (context.dataSourceIngestIsCancelled()) {
+            logger.log(Level.INFO, "ILeapp Analyser ingest module run was canceled"); //NON-NLS
+            return;
+        }
 
-            if (context.dataSourceIngestIsCancelled()) {
-                logger.log(Level.INFO, "ILeapp Analyser ingest module run was canceled"); //NON-NLS
-                return ProcessResult.OK;
-            }
+        ProcessResult fileProcessorResult = iLeappFileProcessor.processFiles(dataSource, moduleOutputPath, iLeappFile);
 
-            ProcessResult fileProcessorResult = iLeappFileProcessor.processFiles(dataSource, moduleOutputPath, iLeappFile);
+        if (fileProcessorResult == ProcessResult.ERROR) {
+            return;
+        }
+    }
 
-            if (fileProcessorResult == ProcessResult.ERROR) {
-                return ProcessResult.ERROR;
+    /**
+     * Process extracted files from a disk image using xLeapp
+     * @param dataSource Datasource where the file has been found
+     * @param currentCase current case
+     * @param statusHelper Progress bar for messages to show user
+     * @param directoryToProcess 
+     */
+    private void processILeappFs(Content dataSource, Case currentCase, DataSourceIngestModuleProgress statusHelper, String directoryToProcess) {
+        String currentTime = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss z", Locale.US).format(System.currentTimeMillis());//NON-NLS
+        Path moduleOutputPath = Paths.get(currentCase.getModuleDirectory(), ILEAPP, currentTime);
+        try {
+            Files.createDirectories(moduleOutputPath);
+        } catch (IOException ex) {
+            logger.log(Level.SEVERE, String.format("Error creating iLeapp output directory %s", moduleOutputPath.toString()), ex);
+            return;
+        }
+
+        statusHelper.progress(NbBundle.getMessage(this.getClass(), "ILeappAnalyzerIngestModule.processing.filesystem"));
+        ProcessBuilder iLeappCommand = buildiLeappCommand(moduleOutputPath, directoryToProcess, "fs");
+        try {
+            int result = ExecUtil.execute(iLeappCommand, new DataSourceIngestModuleProcessTerminator(context, true));
+            if (result != 0) {
+                logger.log(Level.WARNING, String.format("Error when trying to execute iLeapp program getting file paths to search for result is %d", result));
+                return;
             }
 
-            filesProcessedCount++;
+            addILeappReportToReports(moduleOutputPath, currentCase);
+
+        } catch (IOException ex) {
+            logger.log(Level.SEVERE, String.format("Error when trying to execute iLeapp program against file system"), ex);
+            return;
+        }
+
+        if (context.dataSourceIngestIsCancelled()) {
+            logger.log(Level.INFO, "ILeapp Analyser ingest module run was canceled"); //NON-NLS
+            return;
+        }
+
+        ProcessResult fileProcessorResult = iLeappFileProcessor.processFileSystem(dataSource, moduleOutputPath);
+
+        if (fileProcessorResult == ProcessResult.ERROR) {
+            return;
         }
 
-        IngestMessage message = IngestMessage.createMessage(IngestMessage.MessageType.DATA,
-                Bundle.ILeappAnalyzerIngestModule_has_run(),
-                Bundle.ILeappAnalyzerIngestModule_completed());
-        IngestServices.getInstance().postMessage(message);
-        return ProcessResult.OK;
     }
 
     /**
@@ -197,17 +289,24 @@ private List<AbstractFile> findiLeappFilesToProcess(Content dataSource) {
         List<AbstractFile> iLeappFilesToProcess = new ArrayList<>();
         for (AbstractFile iLeappFile : iLeappFiles) {
             if (((iLeappFile.getLocalAbsPath() != null)
-                    && (!iLeappFile.getNameExtension().isEmpty() && (!iLeappFile.isVirtual()))) 
-                && ((iLeappFile.getName().toLowerCase().contains(".zip") || (iLeappFile.getName().toLowerCase().contains(".tar")))
-                        || iLeappFile.getName().toLowerCase().contains(".tgz"))) {
-                    iLeappFilesToProcess.add(iLeappFile);
-                
+                    && (!iLeappFile.getNameExtension().isEmpty() && (!iLeappFile.isVirtual())))
+                    && ((iLeappFile.getName().toLowerCase().contains(".zip") || (iLeappFile.getName().toLowerCase().contains(".tar")))
+                    || iLeappFile.getName().toLowerCase().contains(".tgz"))) {
+                iLeappFilesToProcess.add(iLeappFile);
+
             }
         }
 
         return iLeappFilesToProcess;
     }
 
+    /**
+     * Build the command to run xLeapp
+     * @param moduleOutputPath output path for xLeapp
+     * @param sourceFilePath path where the xLeapp file is
+     * @param iLeappFileSystem type of file to process tar/zip/fs
+     * @return process to run
+     */
     private ProcessBuilder buildiLeappCommand(Path moduleOutputPath, String sourceFilePath, String iLeappFileSystemType) {
 
         ProcessBuilder processBuilder = buildProcessWithRunAsInvoker(
@@ -221,10 +320,26 @@ private ProcessBuilder buildiLeappCommand(Path moduleOutputPath, String sourceFi
         return processBuilder;
     }
 
+    /**
+     * Command to run xLeapp using the path option
+     * @param moduleOutputPath path where the file paths output will reside
+     * @return process to run
+     */
+    private ProcessBuilder buildiLeappListCommand(Path moduleOutputPath) {
+
+        ProcessBuilder processBuilder = buildProcessWithRunAsInvoker(
+                "\"" + iLeappExecutable + "\"", //NON-NLS
+                "-p"
+        );
+        processBuilder.redirectError(moduleOutputPath.resolve("iLeapp_paths_error.txt").toFile());  //NON-NLS
+        processBuilder.redirectOutput(moduleOutputPath.resolve("iLeapp_paths.txt").toFile());  //NON-NLS
+        return processBuilder;
+    }
+
     static private ProcessBuilder buildProcessWithRunAsInvoker(String... commandLine) {
         ProcessBuilder processBuilder = new ProcessBuilder(commandLine);
         /*
-         * Add an environment variable to force log2timeline/psort to run with
+         * Add an environment variable to force iLeapp to run with
          * the same permissions Autopsy uses.
          */
         processBuilder.environment().put("__COMPAT_LAYER", "RunAsInvoker"); //NON-NLS
@@ -248,13 +363,18 @@ private static File locateExecutable(String executableName) throws FileNotFoundE
     private void addILeappReportToReports(Path iLeappOutputDir, Case currentCase) {
         List<String> allIndexFiles = new ArrayList<>();
 
-        try (Stream<Path> walk = Files.walk(iLeappOutputDir)) {
+        try (Stream<Path> walk = Files.walk(iLeappOutputDir)) { 
 
             allIndexFiles = walk.map(x -> x.toString())
                     .filter(f -> f.toLowerCase().endsWith("index.html")).collect(Collectors.toList());
 
             if (!allIndexFiles.isEmpty()) {
-                currentCase.addReport(allIndexFiles.get(0), MODULE_NAME, Bundle.ILeappAnalyzerIngestModule_report_name());
+                // Check for existance of directory that holds report data if does not exist then report contains no data
+                String filePath = FilenameUtils.getFullPathNoEndSeparator(allIndexFiles.get(0));
+                File dataFilesDir = new File(Paths.get(filePath, "_TSV Exports").toString());
+                if (dataFilesDir.exists()) {
+                    currentCase.addReport(allIndexFiles.get(0), MODULE_NAME, Bundle.ILeappAnalyzerIngestModule_report_name());
+                }
             }
 
         } catch (IOException | UncheckedIOException | TskCoreException ex) {
@@ -264,4 +384,129 @@ private void addILeappReportToReports(Path iLeappOutputDir, Case currentCase) {
 
     }
 
+    /*
+     * Reads the iLeapp paths file to get the paths that we want to extract
+     *
+     * @param moduleOutputPath path where the file paths output will reside
+     */
+    private List<String> loadIleappPathFile(Path moduleOutputPath) throws FileNotFoundException, IOException {
+        List<String> iLeappPathsToProcess = new ArrayList<>();
+
+        Path filePath = Paths.get(moduleOutputPath.toString(), ILEAPP_PATHS_FILE);
+
+        try (BufferedReader reader = new BufferedReader(new FileReader(filePath.toString()))) {
+            String line = reader.readLine();
+            while (line != null) {
+                if (line.contains("path list generation") || line.length() < 2) {
+                    line = reader.readLine();
+                    continue;
+                }
+                iLeappPathsToProcess.add(line.trim());
+                line = reader.readLine();
+            }
+        }
+
+        return iLeappPathsToProcess;
+    }
+
+    /**
+     * Extract files from a disk image to process with xLeapp
+     * @param dataSource Datasource of the image
+     * @param iLeappPathsToProcess List of paths to extract content from 
+     * @param moduleOutputPath path to write content to
+     */
+    private void extractFilesFromImage(Content dataSource, List<String> iLeappPathsToProcess, Path moduleOutputPath) {
+        FileManager fileManager = getCurrentCase().getServices().getFileManager();
+
+        for (String fullFilePath : iLeappPathsToProcess) {
+
+            if (context.dataSourceIngestIsCancelled()) {
+                logger.log(Level.INFO, "ILeapp Analyser ingest module run was canceled"); //NON-NLS
+                break;
+            }
+
+            String ffp = fullFilePath.replaceAll("\\*", "%");
+            ffp = FilenameUtils.normalize(ffp, true);
+            String fileName = FilenameUtils.getName(ffp);
+            String filePath = FilenameUtils.getPath(ffp);
+
+            List<AbstractFile> iLeappFiles = new ArrayList<>();
+            try {
+                if (filePath.isEmpty()) {
+                    iLeappFiles = fileManager.findFiles(dataSource, fileName); //NON-NLS                
+                } else {
+                    iLeappFiles = fileManager.findFiles(dataSource, fileName, filePath); //NON-NLS
+                }
+            } catch (TskCoreException ex) {
+                logger.log(Level.WARNING, "No files found to process"); //NON-NLS
+                return;
+            }
+
+            for (AbstractFile iLeappFile : iLeappFiles) {
+                Path parentPath = Paths.get(moduleOutputPath.toString(), iLeappFile.getParentPath());
+                File fileParentPath = new File(parentPath.toString());
+
+                extractFileToOutput(dataSource, iLeappFile, fileParentPath, parentPath);
+            }
+        }
+    }
+
+    /**
+     * Create path and file from datasource in temp
+     * @param dataSource datasource of the image
+     * @param iLeappFile abstract file to write out
+     * @param fileParentPath parent file path
+     * @param parentPath parent file
+     */
+    private void extractFileToOutput(Content dataSource, AbstractFile iLeappFile, File fileParentPath, Path parentPath) {
+        if (fileParentPath.exists()) {
+                    if (!iLeappFile.isDir()) {
+                        writeiLeappFile(dataSource, iLeappFile, fileParentPath.toString());
+                    } else {
+                        try {
+                            Files.createDirectories(Paths.get(parentPath.toString(), iLeappFile.getName()));
+                        } catch (IOException ex) {
+                            logger.log(Level.INFO, String.format("Error creating iLeapp output directory %s", parentPath.toString()), ex);
+                        }
+                    }
+                } else {
+                    try {
+                        Files.createDirectories(parentPath);
+                    } catch (IOException ex) {
+                        logger.log(Level.INFO, String.format("Error creating iLeapp output directory %s", parentPath.toString()), ex);
+                    }
+                    if (!iLeappFile.isDir()) {
+                        writeiLeappFile(dataSource, iLeappFile, fileParentPath.toString());
+                    } else {
+                        try {
+                            Files.createDirectories(Paths.get(parentPath.toString(), iLeappFile.getName()));
+                        } catch (IOException ex) {
+                            logger.log(Level.INFO, String.format("Error creating iLeapp output directory %s", parentPath.toString()), ex);
+                        }
+                    }
+                }
+    }
+    
+    /**
+     * Write out file to output
+     * @param dataSource datasource of disk image
+     * @param iLeappFile acstract file to write out
+     * @param parentPath path to write file to
+     */
+    private void writeiLeappFile(Content dataSource, AbstractFile iLeappFile, String parentPath) {
+        String fileName = iLeappFile.getName().replace(":", "-");
+        if (!fileName.matches(".") && !fileName.matches("..") && !fileName.toLowerCase().endsWith("-slack")) {
+            Path filePath = Paths.get(parentPath, fileName);
+            File localFile = new File(filePath.toString());
+            try {
+                ContentUtils.writeToFile(iLeappFile, localFile, context::dataSourceIngestIsCancelled);
+            } catch (ReadContentInputStream.ReadContentInputStreamException ex) {
+                logger.log(Level.WARNING, String.format("Error reading file '%s' (id=%d).",
+                        iLeappFile.getName(), iLeappFile.getId()), ex); //NON-NLS
+            } catch (IOException ex) {
+                logger.log(Level.WARNING, String.format("Error writing file local file '%s' (id=%d).",
+                        filePath.toString(), iLeappFile.getId()), ex); //NON-NLS
+            }
+        }
+    }
 }
diff --git a/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ILeappFileProcessor.java b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ILeappFileProcessor.java
index cb5ab516a66ac386625e81f1051dfcc5247228eb..058b0aa28d23d6449cb16eeaf9b5a7d052ee0697 100644
--- a/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ILeappFileProcessor.java
+++ b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ILeappFileProcessor.java
@@ -44,6 +44,7 @@
 import org.apache.commons.io.FilenameUtils;
 import org.openide.util.NbBundle;
 import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
 import org.sleuthkit.autopsy.coreutils.Logger;
 import org.sleuthkit.autopsy.coreutils.PlatformUtil;
 import org.sleuthkit.autopsy.ingest.IngestModule.IngestModuleException;
@@ -76,12 +77,16 @@ public final class ILeappFileProcessor {
     private final Map<String, String> tsvFileArtifactComments;
     private final Map<String, List<List<String>>> tsvFileAttributes;
 
-    public ILeappFileProcessor() throws IOException, IngestModuleException {
+    Blackboard blkBoard;
+
+    public ILeappFileProcessor() throws IOException, IngestModuleException, NoCurrentCaseException {
         this.tsvFiles = new HashMap<>();
         this.tsvFileArtifacts = new HashMap<>();
         this.tsvFileArtifactComments = new HashMap<>();
         this.tsvFileAttributes = new HashMap<>();
 
+        blkBoard = Case.getCurrentCaseThrows().getSleuthkitCase().getBlackboard();
+
         configExtractor();
         loadConfigFile();
 
@@ -110,6 +115,19 @@ public ProcessResult processFiles(Content dataSource, Path moduleOutputPath, Abs
         return ProcessResult.OK;
     }
 
+    public ProcessResult processFileSystem(Content dataSource, Path moduleOutputPath) {
+
+        try {
+            List<String> iLeappTsvOutputFiles = findTsvFiles(moduleOutputPath);
+            processiLeappFiles(iLeappTsvOutputFiles, dataSource);
+        } catch (IOException | IngestModuleException ex) {
+            logger.log(Level.SEVERE, String.format("Error trying to process iLeapp output files in directory %s. ", moduleOutputPath.toString()), ex); //NON-NLS
+            return ProcessResult.ERROR;
+        }
+
+        return ProcessResult.OK;
+    }
+
     /**
      * Find the tsv files in the iLeapp output directory and match them to files
      * we know we want to process and return the list to process those files.
@@ -124,7 +142,7 @@ private List<String> findTsvFiles(Path iLeappOutputDir) throws IngestModuleExcep
                     .filter(f -> f.toLowerCase().endsWith(".tsv")).collect(Collectors.toList());
 
             for (String tsvFile : allTsvFiles) {
-                if (tsvFiles.containsKey(FilenameUtils.getName(tsvFile))) {
+                if (tsvFiles.containsKey(FilenameUtils.getName(tsvFile.toLowerCase()))) {
                     foundTsvFiles.add(tsvFile);
                 }
             }
@@ -160,7 +178,41 @@ private void processiLeappFiles(List<String> iLeappFilesToProcess, AbstractFile
                     processFile(iLeappFile, attrList, fileName, artifactType, bbartifacts, iLeappImageFile);
 
                 } catch (TskCoreException ex) {
-                    // check this
+                    throw new IngestModuleException(String.format("Error getting Blackboard Artifact Type for %s", tsvFileArtifacts.get(fileName)), ex);
+                }
+            }
+
+        }
+
+        if (!bbartifacts.isEmpty()) {
+            postArtifacts(bbartifacts);
+        }
+
+    }
+
+    /**
+     * Process the iLeapp files that were found that match the xml mapping file
+     *
+     * @param iLeappFilesToProcess List of files to process
+     * @param iLeappImageFile      Abstract file to create artifact for
+     *
+     * @throws FileNotFoundException
+     * @throws IOException
+     */
+    private void processiLeappFiles(List<String> iLeappFilesToProcess, Content dataSource) throws FileNotFoundException, IOException, IngestModuleException {
+        List<BlackboardArtifact> bbartifacts = new ArrayList<>();
+
+        for (String iLeappFileName : iLeappFilesToProcess) {
+            String fileName = FilenameUtils.getName(iLeappFileName);
+            File iLeappFile = new File(iLeappFileName);
+            if (tsvFileAttributes.containsKey(fileName)) {
+                List<List<String>> attrList = tsvFileAttributes.get(fileName);
+                try {
+                    BlackboardArtifact.Type artifactType = Case.getCurrentCase().getSleuthkitCase().getArtifactType(tsvFileArtifacts.get(fileName));
+
+                    processFile(iLeappFile, attrList, fileName, artifactType, bbartifacts, dataSource);
+
+                } catch (TskCoreException ex) {
                     throw new IngestModuleException(String.format("Error getting Blackboard Artifact Type for %s", tsvFileArtifacts.get(fileName)), ex);
                 }
             }
@@ -174,7 +226,8 @@ private void processiLeappFiles(List<String> iLeappFilesToProcess, AbstractFile
     }
 
     private void processFile(File iLeappFile, List<List<String>> attrList, String fileName, BlackboardArtifact.Type artifactType,
-        List<BlackboardArtifact> bbartifacts, AbstractFile iLeappImageFile) throws FileNotFoundException, IOException, IngestModuleException {
+            List<BlackboardArtifact> bbartifacts, Content dataSource) throws FileNotFoundException, IOException, IngestModuleException,
+            TskCoreException {
         try (BufferedReader reader = new BufferedReader(new FileReader(iLeappFile))) {
             String line = reader.readLine();
             // Check first line, if it is null then no heading so nothing to match to, close and go to next file.
@@ -183,8 +236,8 @@ private void processFile(File iLeappFile, List<List<String>> attrList, String fi
                 line = reader.readLine();
                 while (line != null) {
                     Collection<BlackboardAttribute> bbattributes = processReadLine(line, columnNumberToProcess, fileName);
-                    if (!bbattributes.isEmpty()) {
-                        BlackboardArtifact bbartifact = createArtifactWithAttributes(artifactType.getTypeID(), iLeappImageFile, bbattributes);
+                    if (!bbattributes.isEmpty() && !blkBoard.artifactExists(dataSource, BlackboardArtifact.ARTIFACT_TYPE.fromID(artifactType.getTypeID()), bbattributes)) {
+                        BlackboardArtifact bbartifact = createArtifactWithAttributes(artifactType.getTypeID(), dataSource, bbattributes);
                         if (bbartifact != null) {
                             bbartifacts.add(bbartifact);
                         }
@@ -234,8 +287,8 @@ private Collection<BlackboardAttribute> processReadLine(String line, Map<Integer
 
     }
 
-    private void checkAttributeType(Collection<BlackboardAttribute> bbattributes, String attrType, String[] columnValues, Integer columnNumber, BlackboardAttribute.Type attributeType, 
-                                    String fileName) {
+    private void checkAttributeType(Collection<BlackboardAttribute> bbattributes, String attrType, String[] columnValues, Integer columnNumber, BlackboardAttribute.Type attributeType,
+            String fileName) {
         if (attrType.matches("STRING")) {
             bbattributes.add(new BlackboardAttribute(attributeType, MODULE_NAME, columnValues[columnNumber]));
         } else if (attrType.matches("INTEGER")) {
@@ -340,7 +393,7 @@ private void getFileNode(Document xmlinput) {
 
         for (int i = 0; i < nlist.getLength(); i++) {
             NamedNodeMap nnm = nlist.item(i).getAttributes();
-            tsvFiles.put(nnm.getNamedItem("filename").getNodeValue(), nnm.getNamedItem("description").getNodeValue());
+            tsvFiles.put(nnm.getNamedItem("filename").getNodeValue().toLowerCase(), nnm.getNamedItem("description").getNodeValue());
 
         }
 
@@ -393,19 +446,20 @@ private void getAttributeNodes(Document xmlinput) {
 
         }
     }
-        /**
-         * Generic method for creating a blackboard artifact with attributes
-         *
-         * @param type         is a blackboard.artifact_type enum to determine
-         *                     which type the artifact should be
-         * @param abstractFile is the AbstractFile object that needs to have the
-         *                     artifact added for it
-         * @param bbattributes is the collection of blackboard attributes that
-         *                     need to be added to the artifact after the
-         *                     artifact has been created
-         *
-         * @return The newly-created artifact, or null on error
-         */
+
+    /**
+     * Generic method for creating a blackboard artifact with attributes
+     *
+     * @param type         is a blackboard.artifact_type enum to determine which
+     *                     type the artifact should be
+     * @param abstractFile is the AbstractFile object that needs to have the
+     *                     artifact added for it
+     * @param bbattributes is the collection of blackboard attributes that need
+     *                     to be added to the artifact after the artifact has
+     *                     been created
+     *
+     * @return The newly-created artifact, or null on error
+     */
     private BlackboardArtifact createArtifactWithAttributes(int type, AbstractFile abstractFile, Collection<BlackboardAttribute> bbattributes) {
         try {
             BlackboardArtifact bbart = abstractFile.newArtifact(type);
@@ -417,6 +471,30 @@ private BlackboardArtifact createArtifactWithAttributes(int type, AbstractFile a
         return null;
     }
 
+    /**
+     * Generic method for creating a blackboard artifact with attributes
+     *
+     * @param type         is a blackboard.artifact_type enum to determine which
+     *                     type the artifact should be
+     * @param datasource   is the Content object that needs to have the artifact
+     *                     added for it
+     * @param bbattributes is the collection of blackboard attributes that need
+     *                     to be added to the artifact after the artifact has
+     *                     been created
+     *
+     * @return The newly-created artifact, or null on error
+     */
+    private BlackboardArtifact createArtifactWithAttributes(int type, Content dataSource, Collection<BlackboardAttribute> bbattributes) {
+        try {
+            BlackboardArtifact bbart = dataSource.newArtifact(type);
+            bbart.addAttributes(bbattributes);
+            return bbart;
+        } catch (TskException ex) {
+            logger.log(Level.WARNING, Bundle.ILeappFileProcessor_error_creating_new_artifacts(), ex); //NON-NLS
+        }
+        return null;
+    }
+
     /**
      * Method to post a list of BlackboardArtifacts to the blackboard.
      *
diff --git a/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ileap-artifact-attribute-reference.xml b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ileap-artifact-attribute-reference.xml
index 0959cbdcc48f85d5034efaa5a7a380c794ab44f7..a4169395aae21cce3b9cb1360d4bca0d14a11200 100644
--- a/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ileap-artifact-attribute-reference.xml
+++ b/Core/src/org/sleuthkit/autopsy/modules/ileappanalyzer/ileap-artifact-attribute-reference.xml
@@ -47,6 +47,15 @@
             </ArtifactName>
         </FileName>
 
+        <FileName filename="App Snapshots.tsv" description="App Snapshots (screenshots)">
+            <ArtifactName artifactname="TSK_SCREEN_SHOTS" comment="null">
+                <AttributeName attributename="TSK_PROG_NAME" columnName="App Name" required="yes" />
+                <AttributeName attributename="TSK_PATH" columnName="SOurce Path" required="yes" />
+                <AttributeName attributename="TSK_DATETIME" columnName="Date Modified" required="yes" />
+                <AttributeName attributename="null" columnName="Source File Located" required="no" />
+            </ArtifactName>
+        </FileName>
+
         <FileName filename="Bluetooth Other.tsv" description="Bluetooth Other">
             <ArtifactName artifactname="TSK_BLUETOOTH_ADAPTER" comment="Bluetooth Other">
                 <AttributeName attributename="TSK_NAME" columnName="Name" required="yes" />
@@ -120,6 +129,13 @@
             </ArtifactName>
         </FileName>
 
+        <FileName filename="DHCP Received List.tsv" description="DHCP Received List" >
+            <ArtifactName artifactname="TSK_IP_DHCP" comment="null">
+                <AttributeName attributename="TSK_NAME" columnName="Key" required="yes" />
+                <AttributeName attributename="TSK_VALUE" columnName="Value" required="yes" />
+            </ArtifactName>
+        </FileName>
+
         <FileName filename="KnowledgeC App Activity.tsv" description="KnowledgeC App Activity">
             <ArtifactName artifactname="TSK_PROG_RUN" comment="KnowledgeC App Activity">
                 <AttributeName attributename="TSK_DATETIME" columnName="Entry Creation" required="yes" />
@@ -189,6 +205,36 @@
             </ArtifactName>
         </FileName>
 
+        <FileName filename="KnowledgeC Device is Backlit.tsv" description="KnowledgeC Device is Backlit">
+            <ArtifactName artifactname="TSK_USER_DEVICE_EVENT" comment="Device Backlit">
+                <AttributeName attributename="TSK_DATETIME_START" columnName="Start" required="yes" />
+                <AttributeName attributename="TSK_DATETIME_END" columnName="End" required="yes" />
+                <AttributeName attributename="TSK_USER_DEVICE_EVENT_TYPE" columnName="Screen is Backlit" required="yes" />
+                <AttributeName attributename="null" columnName="Usage in Seconds" required="no" />
+                <AttributeName attributename="null" columnName="Usage in Minutes" required="no" />
+                <AttributeName attributename="null" columnName="Day of Week" required="no" />
+                <AttributeName attributename="null" columnName="GMT Offset" required="no" />
+                <AttributeName attributename="null" columnName="Start" required="no" />
+                <AttributeName attributename="null" columnName="End" required="no" />
+                <AttributeName attributename="null" columnName="Entry Creation" required="no" />
+                <AttributeName attributename="null" columnName="UUID" required="no" />
+                <AttributeName attributename="null" columnName="ZOBJECT Table ID" required="no" />
+            </ArtifactName>
+        </FileName>
+
+        <FileName filename="KnowledgeC Battery Level.tsv" description="KnowledgeC Battery Level">
+            <ArtifactName artifactname="TSK_USER_DEVICE_EVENT" comment="Battery Level">
+                <AttributeName attributename="TSK_DATETIME_START" columnName="Start" required="yes" />
+                <AttributeName attributename="TSK_DATETIME_END" columnName="End" required="yes" />
+                <AttributeName attributename="TSK_USER_DEVICE_EVENT_TYPE" columnName="Battery Level" required="yes" />
+                <AttributeName attributename="null" columnName="Usage in Seconds" required="no" />
+                <AttributeName attributename="null" columnName="Day of the Week" required="no" />
+                <AttributeName attributename="null" columnName="GMT Offset" required="no" />
+                <AttributeName attributename="null" columnName="Entry Creation" required="no" />
+                <AttributeName attributename="null" columnName=" ZOBJECT Table ID" required="no" />
+            </ArtifactName>
+        </FileName>
+
         <FileName filename="KnowledgeC Bluetooth Connections.tsv" description="KnowledgeC Bluetooth Connections">
             <ArtifactName artifactname="TSK_BLUETOOTH_PAIRING" comment="KnowledgeC Bluetooth Connections">
                 <AttributeName attributename="TSK_DATETIME_START" columnName="Start" required="yes" />
@@ -207,15 +253,61 @@
 
         <FileName filename="KnowledgeC Car Play Connections.tsv" description="KnowledgeC Car Play Connections">
             <ArtifactName artifactname="TSK_DEVICE_INFO" comment="KnowledgeC Car Play Connections">
-                <AttributeName attributename="TSK_DATETIME" columnName="Start" required="no" />
+                <AttributeName attributename="TSK_DATETIME" columnName="Start" required="yes" />
                 <AttributeName attributename="null" columnName="End" required="no" />
-                <AttributeName attributename="null" columnName="Car Play Connected" required="no" />
+                <AttributeName attributename="TSK_USER_DEVICE_EVENT_TYPE" columnName="Car Play Connected" required="yes" />
                 <AttributeName attributename="null" columnName="Usage in Seconds" required="no" />
                 <AttributeName attributename="null" columnName="Usage in Minutes" required="no" />
                 <AttributeName attributename="null" columnName="Day of Week" required="no" />
                 <AttributeName attributename="null" columnName="GMT Offset" required="no" />
                 <AttributeName attributename="null" columnName="Entry Creation" required="no" />
-                <AttributeName attributename="TSK_DEVICE_ID" columnName="UUID" required="no" />
+                <AttributeName attributename="TSK_DEVICE_ID" columnName="UUID" required="yes" />
+                <AttributeName attributename="null" columnName="Zobject Table ID" required="no" />
+            </ArtifactName>
+        </FileName>
+
+        <FileName filename="KnowledgeC Disk Subsystem Access.tsv" description="KnowledgeC Disk Subsystem Access">
+            <ArtifactName artifactname="TSK_USER_DEVICE_EVENT" comment="disk Subsystem">
+                <AttributeName attributename="TSK_DATETIME_START" columnName="Start" required="yes" />
+                <AttributeName attributename="TSK_DATETIME_END" columnName="End" required="yes" />
+                <AttributeName attributename="TSK_PROG_NAME" columnName="Bundle ID" required="yes" />
+                <AttributeName attributename="TSK_USER_DEVICE_EVENT_TYPE" columnName="Value String" required="yes" />
+                <AttributeName attributename="null" columnName="Usage in Seconds" required="no" />
+                <AttributeName attributename="null" columnName="Usage in Minutes" required="no" />
+                <AttributeName attributename="null" columnName="Day of Week" required="no" />
+                <AttributeName attributename="null" columnName="GMT Offset" required="no" />
+                <AttributeName attributename="null" columnName="Entry Creation" required="no" />
+                <AttributeName attributename="null" columnName="UUID" required="no" />
+                <AttributeName attributename="null" columnName="Zobject Table ID" required="no" />
+            </ArtifactName>
+        </FileName>
+                           
+        <FileName filename="KnowledgeC Do Not Disturb.tsv" description="KnowledgeC Do Not Disturb">
+            <ArtifactName artifactname="TSK_USER_DEVICE_EVENT" comment="Do Not Disturb">
+                <AttributeName attributename="TSK_DATETIME_START" columnName="Start" required="yes" />
+                <AttributeName attributename="TSK_DATETIME_END" columnName="End" required="yes" />
+                <AttributeName attributename="TSK_USER_DEVICE_EVENT_TYPE" columnName="Value" required="yes" />
+                <AttributeName attributename="null" columnName="Usage in Seconds" required="no" />
+                <AttributeName attributename="null" columnName="Usage in Minutes" required="no" />
+                <AttributeName attributename="null" columnName="Day of Week" required="no" />
+                <AttributeName attributename="null" columnName="GMT Offset" required="no" />
+                <AttributeName attributename="null" columnName="Entry Creation" required="no" />
+                <AttributeName attributename="null" columnName="UUID" required="no" />
+                <AttributeName attributename="null" columnName="Zobject Table ID" required="no" />
+            </ArtifactName>
+        </FileName>
+
+        <FileName filename="KnowledgeC Inferred Motion.tsv" description="KnowledgeC Inferred Motion">
+            <ArtifactName artifactname="TSK_USER_DEVICE_EVENT" comment="Inferred Motion">
+                <AttributeName attributename="TSK_DATETIME_START" columnName="Start" required="yes" />
+                <AttributeName attributename="TSK_DATETIME_END" columnName="End" required="yes" />
+                <AttributeName attributename="TSK_USER_DEVICE_EVENT_TYPE" columnName="Value" required="yes" />
+                <AttributeName attributename="null" columnName="Usage in Seconds" required="no" />
+                <AttributeName attributename="null" columnName="Usage in Minutes" required="no" />
+                <AttributeName attributename="null" columnName="Day of Week" required="no" />
+                <AttributeName attributename="null" columnName="GMT Offset" required="no" />
+                <AttributeName attributename="null" columnName="Entry Creation" required="no" />
+                <AttributeName attributename="null" columnName="UUID" required="no" />
                 <AttributeName attributename="null" columnName="Zobject Table ID" required="no" />
             </ArtifactName>
         </FileName>
@@ -248,6 +340,19 @@
             </ArtifactName>
         </FileName>
 
+        <FileName filename="KnowledgeC Device Locked.tsv" description="KnowledgeC Device Locked">
+            <ArtifactName artifactname="TSK_USER_DEVICE_EVENT" comment="Device Locked">
+                <AttributeName attributename="TSK_DATETIME_START" columnName="Start" required="yes" />
+                <AttributeName attributename="TSK_DATETIME_END" columnName="End" required="yes" />
+                <AttributeName attributename="TSK_USER_DEVICE_EVENT_TYPE" columnName="Is Locked?" required="yes" />
+                <AttributeName attributename="null" columnName="Usage in Seconds" required="no" />
+                <AttributeName attributename="null" columnName="Day of the Week" required="no" />
+                <AttributeName attributename="null" columnName="GMT Offset" required="no" />
+                <AttributeName attributename="null" columnName="Entry Creation" required="no" />
+                <AttributeName attributename="null" columnName=" ZOBJECT Table ID" required="no" />
+            </ArtifactName>
+        </FileName>
+
         <FileName filename="Media Playing.tsv" description="KnowledgeC Media Playing">
             <ArtifactName artifactname="TSK_RECENT_OBJ" comment="KnowledgeC Media Playing">
                 <AttributeName attributename="TSK_DATETIME_ACCESSED" columnName="Start" required="yes" />
@@ -288,6 +393,36 @@
             </ArtifactName>
         </FileName>
 
+        <FileName filename="KnowledgeC Screen Orientation.tsv" description="KnowledgeC Screen Orientation">
+            <ArtifactName artifactname="TSK_USER_DEVICE_EVENT" comment="Screen Orientation">
+                <AttributeName attributename="TSK_DATETIME_START" columnName="Start" required="yes" />
+                <AttributeName attributename="TSK_DATETIME_END" columnName="End" required="yes" />
+                <AttributeName attributename="TSK_USER_DEVICE_EVENT_TYPE" columnName="Orientation" required="yes" />
+                <AttributeName attributename="null" columnName="Usage in Seconds" required="no" />
+                <AttributeName attributename="null" columnName="Usage in Minutes" required="no" />
+                <AttributeName attributename="null" columnName="Day of Week" required="no" />
+                <AttributeName attributename="null" columnName="GMT Offset" required="no" />
+                <AttributeName attributename="null" columnName="Entry Creation" required="no" />
+                <AttributeName attributename="null" columnName="UUID" required="no" />
+                <AttributeName attributename="null" columnName="Zobject Table ID" required="no" />
+            </ArtifactName>
+        </FileName>
+
+        <FileName filename="KnowledgeC Plugged In.tsv" description="KnowledgeC Plugged In">
+            <ArtifactName artifactname="TSK_USER_DEVICE_EVENT" comment="Plugged In">
+                <AttributeName attributename="TSK_DATETIME_START" columnName="Start" required="yes" />
+                <AttributeName attributename="TSK_DATETIME_END" columnName="End" required="yes" />
+                <AttributeName attributename="TSK_USER_DEVICE_EVENT_TYPE" columnName="Is Plugged In?" required="yes" />
+                <AttributeName attributename="null" columnName="Usage in Seconds" required="no" />
+                <AttributeName attributename="null" columnName="Day of the Week" required="no" />
+                <AttributeName attributename="null" columnName="GMT Offset" required="no" />
+                <AttributeName attributename="null" columnName="Start" required="no" />
+                <AttributeName attributename="null" columnName="End" required="no" />
+                <AttributeName attributename="null" columnName="Entry Creation" required="no" />
+                <AttributeName attributename="null" columnName=" ZOBJECT Table ID" required="no" />
+            </ArtifactName>
+        </FileName>
+
         <FileName filename="KnowledgeC Safari Browsing.tsv" description="KnowledgeC Safari Browsing">
             <ArtifactName artifactname="TSK_WEB_HISTORY" comment="KnowledgeC Safari Browsing">
                 <AttributeName attributename="TSK_DATETIME_ACCESSED" columnName="Start" required="yes" />
@@ -302,6 +437,18 @@
             </ArtifactName>
         </FileName>
 
+        <FileName filename="KnowledgeC Siri Usage.tsv" description="KnowledgeC Siri Usage">
+            <ArtifactName artifactname="TSK_USER_DEVICE_EVENT" comment="Siri Usage">
+                <AttributeName attributename="TSK_DATETIME_START" columnName="Start" required="yes" />
+                <AttributeName attributename="TSK_PROG_NAME" columnName="App Name" required="yes" />
+                <AttributeName attributename="null" columnName="Weekday" required="no" />
+                <AttributeName attributename="null" columnName="GMT Offset" required="no" />
+                <AttributeName attributename="null" columnName="Entry Creation" required="no" />
+                <AttributeName attributename="null" columnName="UUID" required="no" />
+                <AttributeName attributename="null" columnName="ZOBJECT Table ID" required="no" />
+            </ArtifactName>
+        </FileName>
+
         <FileName filename="KnowledgeC App Usage.tsv" description="KnowledgeC App Usage">
             <ArtifactName artifactname="TSK_PROG_RUN" comment="KnowledgeC App Usage">
                 <AttributeName attributename="TSK_DATETIME" columnName="Start" required="yes" />
@@ -317,6 +464,18 @@
                 <AttributeName attributename="null" columnName="Zobject Table ID"	 required="no" />
             </ArtifactName>
         </FileName>
+                           
+        <FileName filename="KnowledgeC User Waking Events.tsv" description="KnowledgeC User Waking Event">
+            <ArtifactName artifactname="TSK_USER_DEVICE_EVENT" comment="User Waking">
+                <AttributeName attributename="TSK_DATETIME_START" columnName="Start" required="yes" />
+                <AttributeName attributename="TSK_DATETIME_END" columnName="End" required="yes" />
+                <AttributeName attributename="null" columnName="Day of Week" required="no" />
+                <AttributeName attributename="null" columnName="GMT Offset" required="no" />
+                <AttributeName attributename="null" columnName="Entry Creation" required="no" />
+                <AttributeName attributename="null" columnName="UUID" required="no" />
+                <AttributeName attributename="null" columnName="Zobject Table ID" required="no" />
+            </ArtifactName>
+        </FileName>
 
         <FileName filename="KnowledgeC Web Usage.tsv" description="KnowledgeC Web Usage">
             <ArtifactName artifactname="TSK_WEB_HISTORY" comment="KnowledgeC Web Usage">
@@ -433,6 +592,102 @@
             </ArtifactName>
         </FileName>
 -->
+
+        <FileName filename="Notifications.tsv" description="iOS Notificatons">
+            <ArtifactName artifactname="TSK_PROG_NOTIFICATIONS" comment="iOS Notificatons">
+                <AttributeName attributename="TSK_DATETIME" columnName="Creation Time" required="yes" />
+                <AttributeName attributename="TSK_PROG_NAME" columnName=" Bundle" required="yes" />
+                <AttributeName attributename="TSK_TITLE" columnName=" Title[Subtitle]" required="yes" />
+                <AttributeName attributename="TSK_VALUE" columnName=" Message" required="yes" />
+                <AttributeName attributename="null" columnName=" Other Details" required="no" />
+            </ArtifactName>
+        </FileName>
+
+        <FileName filename="Powerlog Agg Bulletins.tsv" description="Powerlog Aggregate Bulletins">
+            <ArtifactName artifactname="TSK_PROG_NOTIFICATIONS" comment="Powerlog Aggregate Bulletins">
+                <AttributeName attributename="TSK_DATETIME" columnName="Timestamp" required="yes" />
+                <AttributeName attributename="TSK_PROG_NAME" columnName="Bulletin Bundle ID" required="yes" />
+                <AttributeName attributename="null" columnName="Time Interval in Seconds" required="no" />
+                <AttributeName attributename="null" columnName="Count" required="no" />
+                <AttributeName attributename="null" columnName="Post Type" required="no" />
+                <AttributeName attributename="null" columnName="Aggregate Table ID" required="no" />
+                </ArtifactName>
+            </FileName>
+
+        <FileName filename="Powerlog Agg Notifications.tsv" description="Powerlog Aggregate Notifications">
+            <ArtifactName artifactname="TSK_PROG_NOTIFICATIONS" comment="Powerlog Aggregate Notifications">
+                <AttributeName attributename="TSK_DATETIME" columnName="Timestamp" required="yes" />
+                <AttributeName attributename="TSK_PROG_NAME" columnName="Notification Bundle ID" required="yes" />
+                <AttributeName attributename="null" columnName="Time Interval in Seconds" required="no" />
+                <AttributeName attributename="null" columnName="Count" required="no" />
+                <AttributeName attributename="null" columnName="Notification Type" required="no" />
+                <AttributeName attributename="null" columnName="Aggregate Table ID" required="no" />
+                </ArtifactName>
+            </FileName>
+
+        <FileName filename="Powerlog Backup Info.tsv" description="Powerlog Backup Info">
+            <ArtifactName artifactname="TSK_BACKUP_EVENT" comment="null">
+                <AttributeName attributename="TSK_DATETIME" columnName="Timestamp" required="yes" />
+                <AttributeName attributename="TSK_DATETIME_START" columnName="Start" required="yes" />
+                <AttributeName attributename="TSK_DATETIME_END" columnName="End" required="yes" />
+                <AttributeName attributename="null" columnName="State" required="no" />
+                <AttributeName attributename="null" columnName="Finished" required="no" />
+                <AttributeName attributename="null" columnName="Has error" required="no" />
+                <AttributeName attributename="null" columnName="Table ID" required="no" />
+                </ArtifactName>
+            </FileName>
+
+        <FileName filename="Powerlog Deleted Apps.tsv" description="Powerlog Deleted Apps">
+            <ArtifactName artifactname="TSK_DELETED_PROG" comment="Powerlog Deleted Apps">
+                <AttributeName attributename="TSK_DATETIME_DELETED" columnName="App Deleted Date" required="yes" />
+                <AttributeName attributename="TSK_DATETIME" columnName="Timestamp" required="yes" />
+                <AttributeName attributename="TSK_PROG_NAME" columnName="App Name" required="yes" />
+                <AttributeName attributename="null" columnName="App Executable Name" required="no" />
+                <AttributeName attributename="TSK_PATH" columnName="Bundle ID" required="yes" />
+                <AttributeName attributename="null" columnName="Table ID" required="no" />
+                </ArtifactName>
+            </FileName>
+
+        <FileName filename="Powerlog Lightning Connector.tsv" description="Powerlog Lightning Connector Status">
+            <ArtifactName artifactname="TSK_USER_DEVICE_EVENT" comment="Powerlog Lightning Connector Status">
+                <AttributeName attributename="TSK_DATETIME" columnName="Adjusted Timestamp" required="yes" />
+                <AttributeName attributename="TSK_USER_DEVICE_EVENT_TYPE" columnName="Accesory Power Mode" required="yes" />
+                <AttributeName attributename="null" columnName="Original Lightnint Connector Timestamp" required="no" />
+                <AttributeName attributename="null" columnName="Offset Timestamp" required="no" />
+                <AttributeName attributename="null" columnName="Table ID" required="no" />
+                </ArtifactName>
+            </FileName>
+
+        <FileName filename="Powerlog Push Message Received.tsv" description="Powerlog Push Message Received">
+            <ArtifactName artifactname="TSK_USER_DEVICE_EVENT" comment="Powerlog Push Message Received">
+                <AttributeName attributename="TSK_DATETIME" columnName="Adjusted Timestamp" required="yes" />
+                <AttributeName attributename="TSK_PROG_NAME" columnName="Bundle ID" required="yes" />
+                <AttributeName attributename="TSK_VALUE" columnName="Connection Type" required="yes" />
+                <AttributeName attributename="null" columnName="Is Dropped" required="no" />
+                <AttributeName attributename="null" columnName="Link Quality" required="no" />
+                <AttributeName attributename="null" columnName="Priority" required="no" />
+                <AttributeName attributename="null" columnName="Topic" required="no" />
+                <AttributeName attributename="null" columnName="Server Hostname" required="no" />
+                <AttributeName attributename="null" columnName="Server IP" required="no" />
+                <AttributeName attributename="null" columnName="Original Timestamp" required="no" />
+                <AttributeName attributename="null" columnName="Offset Timestamp" required="no" />
+                <AttributeName attributename="null" columnName="Time Offset" required="no" />
+                <AttributeName attributename="null" columnName="Aggregate Table ID" required="no" />
+                </ArtifactName>
+            </FileName>
+
+        <FileName filename="Powerlog Torch.tsv" description="Powerlog Torch">
+            <ArtifactName artifactname="TSK_USER_DEVICE_EVENT" comment="Powerlog Torch">
+                <AttributeName attributename="TSK_DATETIME" columnName="Adjusted Timestamp" required="yes" />
+                <AttributeName attributename="null" columnName="Bundle ID" required="no" />
+                <AttributeName attributename="TSK_USER_DEVICE_EVENT_TYPE" columnName="Status" required="yes" />
+                <AttributeName attributename="null" columnName="Original Torch Timestamp" required="no" />
+                <AttributeName attributename="null" columnName="Offset Timestamp" required="no" />
+                <AttributeName attributename="null" columnName="Time Offset" required="no" />
+                <AttributeName attributename="null" columnName="Torch ID" required="no" />
+                </ArtifactName>
+            </FileName>
+
         <FileName filename="Powerlog Wifi Network Connections.tsv" description="Powerlog WiFi Network Connections">
             <ArtifactName artifactname="TSK_WIFI_NETWORK" comment="Powerlog WiFi Network Connections">
                 <AttributeName attributename="TSK_DATETIME" columnName="Adjusted Timestamp" required="yes" />
diff --git a/Core/src/org/sleuthkit/autopsy/report/modules/portablecase/PortableCaseReportModule.java b/Core/src/org/sleuthkit/autopsy/report/modules/portablecase/PortableCaseReportModule.java
index 67b253a1fa66b6e420ecd4742aaa8af73a582838..7221ee4a5537b459be3c804234c642f2f2301876 100644
--- a/Core/src/org/sleuthkit/autopsy/report/modules/portablecase/PortableCaseReportModule.java
+++ b/Core/src/org/sleuthkit/autopsy/report/modules/portablecase/PortableCaseReportModule.java
@@ -60,6 +60,7 @@
 import org.sleuthkit.autopsy.report.ReportProgressPanel;
 import org.sleuthkit.caseuco.CaseUcoExporter;
 import org.sleuthkit.datamodel.AbstractFile;
+import org.sleuthkit.datamodel.Blackboard.BlackboardException;
 import org.sleuthkit.datamodel.BlackboardArtifact;
 import org.sleuthkit.datamodel.BlackboardArtifactTag;
 import org.sleuthkit.datamodel.BlackboardAttribute;
@@ -983,10 +984,10 @@ private int getNewArtifactTypeId(BlackboardArtifact oldArtifact) throws TskCoreE
 
         BlackboardArtifact.Type oldCustomType = currentCase.getSleuthkitCase().getArtifactType(oldArtifact.getArtifactTypeName());
         try {
-            BlackboardArtifact.Type newCustomType = portableSkCase.addBlackboardArtifactType(oldCustomType.getTypeName(), oldCustomType.getDisplayName());
+            BlackboardArtifact.Type newCustomType = portableSkCase.getBlackboard().getOrAddArtifactType(oldCustomType.getTypeName(), oldCustomType.getDisplayName());
             oldArtTypeIdToNewArtTypeId.put(oldArtifact.getArtifactTypeID(), newCustomType.getTypeID());
             return newCustomType.getTypeID();
-        } catch (TskDataException ex) {
+        } catch (BlackboardException ex) {
             throw new TskCoreException("Error creating new artifact type " + oldCustomType.getTypeName(), ex); // NON-NLS
         }
     }
@@ -1007,11 +1008,11 @@ private BlackboardAttribute.Type getNewAttributeType(BlackboardAttribute oldAttr
         }
 
         try {
-            BlackboardAttribute.Type newCustomType = portableSkCase.addArtifactAttributeType(oldAttrType.getTypeName(),
+            BlackboardAttribute.Type newCustomType = portableSkCase.getBlackboard().getOrAddAttributeType(oldAttrType.getTypeName(),
                     oldAttrType.getValueType(), oldAttrType.getDisplayName());
             oldAttrTypeIdToNewAttrType.put(oldAttribute.getAttributeType().getTypeID(), newCustomType);
             return newCustomType;
-        } catch (TskDataException ex) {
+        } catch (BlackboardException ex) {
             throw new TskCoreException("Error creating new attribute type " + oldAttrType.getTypeName(), ex); // NON-NLS
         }
     }
diff --git a/Core/test/unit/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/RecentFilesSummaryTest.java b/Core/test/unit/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/RecentFilesSummaryTest.java
index 068f0c3904f8e517842d60500d4f12de018cb4ce..ff73c428b9689ee23e1d167f9551dab72cc2789e 100644
--- a/Core/test/unit/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/RecentFilesSummaryTest.java
+++ b/Core/test/unit/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/RecentFilesSummaryTest.java
@@ -71,8 +71,8 @@ private interface RecentFilesMethod<T> {
          * Means of acquiring data from a method in RecentFilesSummary.
          *
          * @param recentFilesSummary The RecentFilesSummary object.
-         * @param dataSource         The datasource.
-         * @param count              The number of items to retrieve.
+         * @param dataSource The datasource.
+         * @param count The number of items to retrieve.
          *
          * @return The method's return data.
          *
@@ -95,7 +95,7 @@ List<T> fetch(RecentFilesSummary recentFilesSummary, DataSource dataSource, int
     /**
      * If -1 count passed to method, should throw IllegalArgumentException.
      *
-     * @param method     The method to call.
+     * @param method The method to call.
      * @param methodName The name of the metho
      *
      * @throws TskCoreException
@@ -137,7 +137,7 @@ public void getRecentAttachments_nonPositiveCount_ThrowsError() throws TskCoreEx
      * SleuthkitCase isn't called.
      *
      * @param recentFilesMethod The method to call.
-     * @param methodName        The name of the method
+     * @param methodName The name of the method
      *
      * @throws SleuthkitCaseProviderException
      * @throws TskCoreException
@@ -175,7 +175,7 @@ public void getRecentAttachments_noDataSource_ReturnsEmptyList() throws TskCoreE
      * If SleuthkitCase returns no results, an empty list is returned.
      *
      * @param recentFilesMethod The method to call.
-     * @param methodName        The name of the method.
+     * @param methodName The name of the method.
      *
      * @throws SleuthkitCaseProviderException
      * @throws TskCoreException
@@ -220,11 +220,11 @@ public void getRecentAttachments_testNoDataSource_ReturnsEmptyList() throws TskC
     /**
      * Gets a mock BlackboardArtifact.
      *
-     * @param ds            The data source to which the artifact belongs.
-     * @param artifactId    The artifact id.
-     * @param artType       The artifact type.
+     * @param ds The data source to which the artifact belongs.
+     * @param artifactId The artifact id.
+     * @param artType The artifact type.
      * @param attributeArgs The mapping of attribute type to value for each
-     *                      attribute in the artifact.
+     * attribute in the artifact.
      *
      * @return The mock artifact.
      */
@@ -247,10 +247,10 @@ private BlackboardArtifact getArtifact(DataSource ds, long artifactId, ARTIFACT_
     /**
      * Returns a mock artifact for getRecentlyOpenedDocuments.
      *
-     * @param ds         The datasource for the artifact.
+     * @param ds The datasource for the artifact.
      * @param artifactId The artifact id.
-     * @param dateTime   The time in seconds from epoch.
-     * @param path       The path for the document.
+     * @param dateTime The time in seconds from epoch.
+     * @param path The path for the document.
      *
      * @return The mock artifact with pertinent attributes.
      */
@@ -292,13 +292,33 @@ public void getRecentlyOpenedDocuments_sortedByDateTimeAndLimited() throws Sleut
         }
     }
 
+    @Test
+    public void getRecentlyOpenedDocuments_uniquePaths() throws SleuthkitCaseProviderException, TskCoreException {
+        DataSource dataSource = TskMockUtils.getDataSource(1);
+
+        BlackboardArtifact item1 = getRecentDocumentArtifact(dataSource, 1001, DAY_SECONDS, "/a/path");
+        BlackboardArtifact item2 = getRecentDocumentArtifact(dataSource, 1002, DAY_SECONDS + 1, "/a/path");
+        BlackboardArtifact item3 = getRecentDocumentArtifact(dataSource, 1003, DAY_SECONDS + 2, "/a/path");
+        List<BlackboardArtifact> artifacts = Arrays.asList(item2, item3, item1);
+
+        Pair<SleuthkitCase, Blackboard> casePair = DataSourceSummaryMockUtils.getArtifactsTSKMock(RandomizationUtils.getMixedUp(artifacts));
+        RecentFilesSummary summary = new RecentFilesSummary(() -> casePair.getLeft());
+        List<RecentFileDetails> results = summary.getRecentlyOpenedDocuments(dataSource, 10);
+
+        // verify results (only successItem)
+        Assert.assertNotNull(results);
+        Assert.assertEquals(1, results.size());
+        Assert.assertEquals((Long) (DAY_SECONDS + 2), results.get(0).getDateAsLong());
+        Assert.assertTrue("/a/path".equalsIgnoreCase(results.get(0).getPath()));
+    }
+
     @Test
     public void getRecentlyOpenedDocuments_filtersMissingData() throws SleuthkitCaseProviderException, TskCoreException {
         DataSource dataSource = TskMockUtils.getDataSource(1);
 
         BlackboardArtifact successItem = getRecentDocumentArtifact(dataSource, 1001, DAY_SECONDS, "/a/path");
         BlackboardArtifact nullTime = getRecentDocumentArtifact(dataSource, 1002, null, "/a/path2");
-        BlackboardArtifact zeroTime = getRecentDocumentArtifact(dataSource, 10021, 0L, "/a/path2a");      
+        BlackboardArtifact zeroTime = getRecentDocumentArtifact(dataSource, 10021, 0L, "/a/path2a");
         List<BlackboardArtifact> artifacts = Arrays.asList(nullTime, zeroTime, successItem);
 
         Pair<SleuthkitCase, Blackboard> casePair = DataSourceSummaryMockUtils.getArtifactsTSKMock(RandomizationUtils.getMixedUp(artifacts));
@@ -315,11 +335,11 @@ public void getRecentlyOpenedDocuments_filtersMissingData() throws SleuthkitCase
     /**
      * Creates a mock blackboard artifact for getRecentDownloads.
      *
-     * @param ds         The datasource.
+     * @param ds The datasource.
      * @param artifactId The artifact id.
-     * @param dateTime   The time in seconds from epoch.
-     * @param domain     The domain.
-     * @param path       The path for the download.
+     * @param dateTime The time in seconds from epoch.
+     * @param domain The domain.
+     * @param path The path for the download.
      *
      * @return The mock artifact.
      */
@@ -368,6 +388,30 @@ public void getRecentDownloads_sortedByDateTimeAndLimited() throws SleuthkitCase
         }
     }
 
+    @Test
+    public void getRecentDownloads_uniquePaths() throws SleuthkitCaseProviderException, TskCoreException {
+        DataSource dataSource = TskMockUtils.getDataSource(1);
+
+        BlackboardArtifact item1 = getRecentDownloadArtifact(dataSource, 1001, DAY_SECONDS, "domain1.com", "/a/path1");
+        BlackboardArtifact item1a = getRecentDownloadArtifact(dataSource, 10011, DAY_SECONDS + 1, "domain1.com", "/a/path1");
+        BlackboardArtifact item2 = getRecentDownloadArtifact(dataSource, 1002, DAY_SECONDS + 2, "domain2.com", "/a/path1");
+        BlackboardArtifact item3 = getRecentDownloadArtifact(dataSource, 1003, DAY_SECONDS + 3, "domain2a.com", "/a/path1");
+        List<BlackboardArtifact> artifacts = Arrays.asList(item2, item3, item1);
+
+        Pair<SleuthkitCase, Blackboard> casePair = DataSourceSummaryMockUtils.getArtifactsTSKMock(RandomizationUtils.getMixedUp(artifacts));
+        RecentFilesSummary summary = new RecentFilesSummary(() -> casePair.getLeft());
+
+        // call method
+        List<RecentDownloadDetails> results = summary.getRecentDownloads(dataSource, 10);
+
+        // verify results
+        Assert.assertNotNull(results);
+        Assert.assertEquals(1, results.size());
+        Assert.assertEquals((Long) (DAY_SECONDS + 3), results.get(0).getDateAsLong());
+        Assert.assertTrue("/a/path1".equalsIgnoreCase(results.get(0).getPath()));
+        Assert.assertTrue("domain2a.com".equalsIgnoreCase(results.get(0).getWebDomain()));
+    }
+
     @Test
     public void getRecentDownloads_filtersMissingData() throws SleuthkitCaseProviderException, TskCoreException {
         DataSource dataSource = TskMockUtils.getDataSource(1);
@@ -409,19 +453,17 @@ private class AttachmentArtifactItem {
          * Constructor with all parameters.
          *
          * @param messageArtifactTypeId The type id for the artifact or null if
-         *                              no message artifact to be created.
-         * @param emailFrom             Who the message is from or null not to
-         *                              include attribute.
-         * @param messageTime           Time in seconds from epoch or null not
-         *                              to include attribute.
-         * @param fileParentPath        The parent AbstractFile's path value.
-         * @param fileName              The parent AbstractFile's filename
-         *                              value.
-         * @param associatedAttrFormed  If false, the TSK_ASSOCIATED_OBJECT
-         *                              artifact has no attribute (even though
-         *                              it is required).
-         * @param hasParent             Whether or not the artifact has a parent
-         *                              AbstractFile.
+         * no message artifact to be created.
+         * @param emailFrom Who the message is from or null not to include
+         * attribute.
+         * @param messageTime Time in seconds from epoch or null not to include
+         * attribute.
+         * @param fileParentPath The parent AbstractFile's path value.
+         * @param fileName The parent AbstractFile's filename value.
+         * @param associatedAttrFormed If false, the TSK_ASSOCIATED_OBJECT
+         * artifact has no attribute (even though it is required).
+         * @param hasParent Whether or not the artifact has a parent
+         * AbstractFile.
          */
         AttachmentArtifactItem(Integer messageArtifactTypeId, String emailFrom, Long messageTime,
                 String fileParentPath, String fileName,
@@ -441,14 +483,13 @@ private class AttachmentArtifactItem {
          * SleuthkitCase assumed.
          *
          * @param messageArtifactTypeId The type id for the artifact or null if
-         *                              no message artifact to be created.
-         * @param emailFrom             Who the message is from or null not to
-         *                              include attribute.
-         * @param messageTime           Time in seconds from epoch or null not
-         *                              to include attribute.
-         * @param fileParentPath        The parent AbstractFile's path value.
-         * @param fileName              The parent AbstractFile's filename
-         *                              value.
+         * no message artifact to be created.
+         * @param emailFrom Who the message is from or null not to include
+         * attribute.
+         * @param messageTime Time in seconds from epoch or null not to include
+         * attribute.
+         * @param fileParentPath The parent AbstractFile's path value.
+         * @param fileName The parent AbstractFile's filename value.
          */
         AttachmentArtifactItem(Integer messageArtifactTypeId, String emailFrom, Long messageTime, String fileParentPath, String fileName) {
             this(messageArtifactTypeId, emailFrom, messageTime, fileParentPath, fileName, true, true);
@@ -486,11 +527,11 @@ Integer getMessageArtifactTypeId() {
     /**
      * Sets up the associated artifact message for the TSK_ASSOCIATED_OBJECT.
      *
-     * @param artifacts    The mapping of artifact id to artifact.
-     * @param item         The record to setup.
-     * @param dataSource   The datasource.
+     * @param artifacts The mapping of artifact id to artifact.
+     * @param item The record to setup.
+     * @param dataSource The datasource.
      * @param associatedId The associated attribute id.
-     * @param artifactId   The artifact id.
+     * @param artifactId The artifact id.
      *
      * @return The associated Artifact blackboard attribute.
      *
@@ -504,7 +545,7 @@ private BlackboardAttribute setupAssociatedMessage(Map<Long, BlackboardArtifact>
         if (item.getMessageArtifactTypeId() == null) {
             return associatedAttr;
         }
-        
+
         // find the artifact type or null if not found
         ARTIFACT_TYPE messageType = Stream.of(ARTIFACT_TYPE.values())
                 .filter((artType) -> artType.getTypeID() == item.getMessageArtifactTypeId())
@@ -534,7 +575,7 @@ private BlackboardAttribute setupAssociatedMessage(Map<Long, BlackboardArtifact>
      * to return pertinent data.
      *
      * @param items Each attachment item where each item could represent a
-     *              return result if fully formed.
+     * return result if fully formed.
      *
      * @return The mock SleuthkitCase and Blackboard.
      */
@@ -678,4 +719,34 @@ public void getRecentAttachments_filterData() throws SleuthkitCaseProviderExcept
                 .toString().equalsIgnoreCase(successItem2Details.getPath()));
         Assert.assertTrue(successItem2.getEmailFrom().equalsIgnoreCase(successItem2Details.getSender()));
     }
+
+    @Test
+    public void getRecentAttachments_uniquePath() throws SleuthkitCaseProviderException, TskCoreException {
+        // setup data
+        DataSource dataSource = TskMockUtils.getDataSource(1);
+
+        AttachmentArtifactItem item1 = new AttachmentArtifactItem(ARTIFACT_TYPE.TSK_EMAIL_MSG.getTypeID(),
+                "person@sleuthkit.com", DAY_SECONDS, "/parent/path", "msg.pdf");
+        AttachmentArtifactItem item2 = new AttachmentArtifactItem(ARTIFACT_TYPE.TSK_MESSAGE.getTypeID(),
+                "person_on_skype", DAY_SECONDS + 1, "/parent/path", "msg.pdf");
+        AttachmentArtifactItem item3 = new AttachmentArtifactItem(ARTIFACT_TYPE.TSK_EMAIL_MSG.getTypeID(),
+                "person2@sleuthkit.com", DAY_SECONDS + 2, "/parent/path", "msg.pdf");
+
+        List<AttachmentArtifactItem> items = Arrays.asList(item1, item2, item3);
+
+        Pair<SleuthkitCase, Blackboard> casePair = getRecentAttachmentArtifactCase(items);
+        RecentFilesSummary summary = new RecentFilesSummary(() -> casePair.getLeft());
+
+        // get data
+        List<RecentAttachmentDetails> results = summary.getRecentAttachments(dataSource, 10);
+
+        // verify results
+        Assert.assertNotNull(results);
+        Assert.assertEquals(1, results.size());
+
+        Assert.assertEquals(results.get(0).getDateAsLong(), (Long) (DAY_SECONDS + 2));
+        Assert.assertTrue(Paths.get(item3.getFileParentPath(), item3.getFileName())
+                .toString().equalsIgnoreCase(results.get(0).getPath()));
+        Assert.assertTrue(results.get(0).getSender().equalsIgnoreCase(item3.getEmailFrom()));
+    }
 }
diff --git a/Core/test/unit/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/UserActivitySummaryTest.java b/Core/test/unit/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/UserActivitySummaryTest.java
index 0aff8a56b5b2c8c3d41ec828ab757c8e97933166..c7f3246a4db626630f0f83ac60f32c27750fc4a5 100644
--- a/Core/test/unit/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/UserActivitySummaryTest.java
+++ b/Core/test/unit/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/UserActivitySummaryTest.java
@@ -66,18 +66,22 @@
  * Tests for UserActivitySummary.
  */
 public class UserActivitySummaryTest {
+
     /**
-     *  Function to retrieve data from UserActivitySummary with the provided arguments.
+     * Function to retrieve data from UserActivitySummary with the provided
+     * arguments.
      */
     private interface DataFunction<T> {
+
         /**
          * A UserActivitySummary method encapsulated in a uniform manner.
+         *
          * @param userActivitySummary The UserActivitySummary class to use.
          * @param datasource The data source.
          * @param count The count.
          * @return The list of objects to return.
          * @throws SleuthkitCaseProviderException
-         * @throws TskCoreException 
+         * @throws TskCoreException
          */
         List<T> retrieve(UserActivitySummary userActivitySummary, DataSource datasource, int count) throws
                 SleuthkitCaseProviderException, TskCoreException;
@@ -117,8 +121,8 @@ private static void verifyCalled(Blackboard mockBlackboard, int artifactType, lo
     /**
      * Gets a UserActivitySummary class to test.
      *
-     * @param tskCase           The SleuthkitCase.
-     * @param hasTranslation    Whether the translation service is functional.
+     * @param tskCase The SleuthkitCase.
+     * @param hasTranslation Whether the translation service is functional.
      * @param translateFunction Function for translation.
      *
      * @return The UserActivitySummary class to use for testing.
@@ -333,6 +337,28 @@ public void getRecentDevices_limitedToCount()
         }
     }
 
+    @Test
+    public void getRecentDevices_uniqueByDeviceId()
+            throws TskCoreException, NoServiceProviderException, SleuthkitCaseProviderException, TskCoreException, TranslationException {
+
+        long dataSourceId = 1L;
+        DataSource dataSource = TskMockUtils.getDataSource(dataSourceId);
+        BlackboardArtifact item1 = getRecentDeviceArtifact(1001, dataSource, "ID1", "MAKE1", "MODEL1", DAY_SECONDS);
+        BlackboardArtifact item2 = getRecentDeviceArtifact(1002, dataSource, "ID1", "MAKE1", "MODEL1", DAY_SECONDS + 1);
+        BlackboardArtifact item3 = getRecentDeviceArtifact(1003, dataSource, "ID1", "MAKE1", "MODEL1", DAY_SECONDS + 2);
+
+        Pair<SleuthkitCase, Blackboard> tskPair = getArtifactsTSKMock(Arrays.asList(item1, item2, item3));
+        UserActivitySummary summary = getTestClass(tskPair.getLeft(), false, null);
+
+        List<TopDeviceAttachedResult> results = summary.getRecentDevices(dataSource, 10);
+
+        Assert.assertEquals(1, results.size());
+        Assert.assertEquals((long) (DAY_SECONDS + 2), results.get(0).getDateAccessed().getTime() / 1000);
+        Assert.assertTrue("ID1".equalsIgnoreCase(results.get(0).getDeviceId()));
+        Assert.assertTrue("MAKE1".equalsIgnoreCase(results.get(0).getDeviceMake()));
+        Assert.assertTrue("MODEL1".equalsIgnoreCase(results.get(0).getDeviceModel()));
+    }
+
     private static BlackboardArtifact getWebSearchArtifact(long artifactId, DataSource dataSource, String query, Long date) {
         try {
             return TskMockUtils.getArtifact(new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_WEB_SEARCH_QUERY), artifactId, dataSource,
@@ -708,8 +734,8 @@ public void getRecentDomains_limitedAppropriately()
      *
      * @param artifactId The artifact id.
      * @param dataSource The datasource.
-     * @param dateRcvd   The date received in seconds or null to exclude.
-     * @param dateSent   The date sent in seconds or null to exclude.
+     * @param dateRcvd The date received in seconds or null to exclude.
+     * @param dateSent The date sent in seconds or null to exclude.
      *
      * @return The mock artifact.
      */
@@ -738,8 +764,8 @@ private static BlackboardArtifact getEmailArtifact(long artifactId, DataSource d
      *
      * @param artifactId The artifact id.
      * @param dataSource The datasource.
-     * @param dateStart  The date start in seconds or null to exclude.
-     * @param dateEnd    The date end in seconds or null to exclude.
+     * @param dateStart The date start in seconds or null to exclude.
+     * @param dateEnd The date end in seconds or null to exclude.
      *
      * @return The mock artifact.
      */
@@ -768,8 +794,8 @@ private static BlackboardArtifact getCallogArtifact(long artifactId, DataSource
      *
      * @param artifactId The artifact id.
      * @param dataSource The datasource.
-     * @param type       The account type.
-     * @param dateSent   The date of the message in seconds.
+     * @param type The account type.
+     * @param dateSent The date of the message in seconds.
      */
     private static BlackboardArtifact getMessageArtifact(long artifactId, DataSource dataSource, String type, Long dateTime) {
         List<BlackboardAttribute> attributes = new ArrayList<>();
@@ -794,11 +820,11 @@ private static BlackboardArtifact getMessageArtifact(long artifactId, DataSource
     /**
      * Performs a test on UserActivitySummary.getRecentAccounts.
      *
-     * @param dataSource      The datasource to use as parameter.
-     * @param count           The count to use as a parameter.
-     * @param retArtifacts    The artifacts to return from
-     *                        SleuthkitCase.getArtifacts. This method filters
-     *                        based on artifact type from the call.
+     * @param dataSource The datasource to use as parameter.
+     * @param count The count to use as a parameter.
+     * @param retArtifacts The artifacts to return from
+     * SleuthkitCase.getArtifacts. This method filters based on artifact type
+     * from the call.
      * @param expectedResults The expected results.
      *
      * @throws TskCoreException
diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Bundle.properties-MERGED b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Bundle.properties-MERGED
index a842d76f7431114977ca0191bc2c1145756ae8d5..7927d43415af28eb598a640d1d92ef09a4c308ba 100755
--- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Bundle.properties-MERGED
+++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Bundle.properties-MERGED
@@ -61,6 +61,7 @@ ExtractOS_progressMessage=Checking for OS
 ExtractPrefetch_errMsg_prefetchParsingFailed={0}: Error analyzing prefetch files
 ExtractPrefetch_module_name=Windows Prefetch Extractor
 ExtractRecycleBin_module_name=Recycle Bin
+ExtractRecycleBin_Recyle_Bin_Display_Name=Recycle Bin
 ExtractSafari_Error_Getting_History=An error occurred while processing Safari history files.
 ExtractSafari_Error_Parsing_Bookmark=An error occured while processing Safari Bookmark files
 ExtractSafari_Error_Parsing_Cookies=An error occured while processing Safari Cookies files
@@ -84,16 +85,9 @@ ExtractZone_progress_Msg=Extracting :Zone.Identifer files
 ExtractZone_Restricted=Restricted Sites Zone
 ExtractZone_Trusted=Trusted Sites Zone
 OpenIDE-Module-Display-Category=Ingest Module
-OpenIDE-Module-Long-Description=Recent Activity ingest module.\n\nThe module extracts useful information about the recent user activity on the disk image being ingested, such as:\n\n- Recently open documents,\n- Web activity (sites visited, stored cookies, book marked sites, search engine queries, file downloads),\n- Recently attached devices,\n- Installed programs.\n\nThe module currently supports Windows only disk images.\nThe plugin is also fully functional when deployed on Windows version of Autopsy.
+OpenIDE-Module-Long-Description=Recent Activity ingest module.\n\n\The module extracts useful information about the recent user activity on the disk image being ingested, such as:\n\n- Recently open documents,\n- Web activity (sites visited, stored cookies, book marked sites, search engine queries, file downloads),\n- Recently attached devices,\n- Installed programs.\n\nThe module currently supports Windows only disk images.\nThe plugin is also fully functional when deployed on Windows version of Autopsy.
 OpenIDE-Module-Name=RecentActivity
 OpenIDE-Module-Short-Description=Recent Activity finder ingest module
-Browser.name.Microsoft.Edge=Microsoft Edge
-Browser.name.Yandex=Yandex
-Browser.name.Opera=Opera
-Browser.name.SalamWeb=SalamWeb
-Browser.name.UC.Browser=UC Browser
-Browser.name.Brave=Brave
-Browser.name.Google.Chrome=Google Chrome
 Chrome.moduleName=Chromium
 Chrome.getHistory.errMsg.errGettingFiles=Error when trying to get Chrome history files.
 Chrome.getHistory.errMsg.couldntFindAnyFiles=Could not find any allocated Chrome history files.
diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractRecycleBin.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractRecycleBin.java
index adc71704826898f79781418c86e4791a47bb6267..175a47044ef731e9ca06d0ed33931ac398e5d769 100755
--- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractRecycleBin.java
+++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractRecycleBin.java
@@ -45,6 +45,7 @@
 import org.sleuthkit.autopsy.ingest.DataSourceIngestModuleProgress;
 import org.sleuthkit.autopsy.ingest.IngestJobContext;
 import org.sleuthkit.datamodel.AbstractFile;
+import org.sleuthkit.datamodel.Blackboard.BlackboardException;
 import org.sleuthkit.datamodel.BlackboardArtifact;
 import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_OS_ACCOUNT;
 import org.sleuthkit.datamodel.BlackboardAttribute;
@@ -415,6 +416,9 @@ private BlackboardAttribute getAttributeForArtifact(BlackboardArtifact artifact,
         return artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.fromID(type.getTypeID())));
     }
 
+    @Messages({
+        "ExtractRecycleBin_Recyle_Bin_Display_Name=Recycle Bin"
+    })
     /**
      * Create TSK_RECYCLE_BIN artifact type.
      *
@@ -422,9 +426,9 @@ private BlackboardAttribute getAttributeForArtifact(BlackboardArtifact artifact,
      */
     private void createRecycleBinArtifactType() throws TskCoreException {
         try {
-            tskCase.addBlackboardArtifactType(RECYCLE_BIN_ARTIFACT_NAME, "Recycle Bin"); //NON-NLS
-        } catch (TskDataException ex) {
-            logger.log(Level.INFO, String.format("%s may have already been defined for this case", RECYCLE_BIN_ARTIFACT_NAME));
+            tskCase.getBlackboard().getOrAddArtifactType(RECYCLE_BIN_ARTIFACT_NAME, Bundle.ExtractRecycleBin_Recyle_Bin_Display_Name()); //NON-NLS
+        } catch (BlackboardException ex) {
+            throw new TskCoreException(String.format("An exception was thrown while defining artifact type %s", RECYCLE_BIN_ARTIFACT_NAME), ex);
         }
 
     }
diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractRegistry.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractRegistry.java
index 9d232cdc841d16bd1b30fc1dadcacdc6baae70bb..13ede70c90c12537af17bd60384583b8f186fac4 100644
--- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractRegistry.java
+++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractRegistry.java
@@ -68,6 +68,7 @@
 import java.util.HashSet;
 import static java.util.Locale.US;
 import static java.util.TimeZone.getTimeZone;
+import org.openide.util.Exceptions;
 import org.openide.util.Lookup;
 import org.sleuthkit.autopsy.casemodule.Case;
 import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
@@ -77,6 +78,7 @@
 import org.sleuthkit.autopsy.recentactivity.ShellBagParser.ShellBag;
 import org.sleuthkit.datamodel.AbstractFile;
 import org.sleuthkit.datamodel.Account;
+import org.sleuthkit.datamodel.Blackboard.BlackboardException;
 import org.sleuthkit.datamodel.BlackboardArtifact;
 import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_ASSOCIATED_OBJECT;
 import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_OS_ACCOUNT;
@@ -1961,18 +1963,11 @@ void createShellBagArtifacts(AbstractFile regFile, List<ShellBag> shellbags) thr
      */
     private BlackboardArtifact.Type getShellBagArtifact() throws TskCoreException {
         if (shellBagArtifactType == null) {
-            shellBagArtifactType = tskCase.getArtifactType(SHELLBAG_ARTIFACT_NAME);
-
-            if (shellBagArtifactType == null) {
-                try {
-                    tskCase.addBlackboardArtifactType(SHELLBAG_ARTIFACT_NAME, Bundle.Shellbag_Artifact_Display_Name()); //NON-NLS
-                } catch (TskDataException ex) {
-                    // Artifact already exists
-                    logger.log(Level.INFO, String.format("%s may have already been defined for this case", SHELLBAG_ARTIFACT_NAME));
-                }
-
-                shellBagArtifactType = tskCase.getArtifactType(SHELLBAG_ARTIFACT_NAME);
-            }
+            try {
+                shellBagArtifactType = tskCase.getBlackboard().getOrAddArtifactType(SHELLBAG_ARTIFACT_NAME, Bundle.Shellbag_Artifact_Display_Name());
+            } catch (BlackboardException ex) {
+                throw new TskCoreException(String.format("Failed to get shell bag artifact type", SHELLBAG_ARTIFACT_NAME), ex);
+            }  
         }
 
         return shellBagArtifactType;
@@ -1989,12 +1984,12 @@ private BlackboardArtifact.Type getShellBagArtifact() throws TskCoreException {
     private BlackboardAttribute.Type getLastWriteAttribute() throws TskCoreException {
         if (shellBagLastWriteAttributeType == null) {
             try {
-                shellBagLastWriteAttributeType = tskCase.addArtifactAttributeType(SHELLBAG_ATTRIBUTE_LAST_WRITE,
+                shellBagLastWriteAttributeType = tskCase.getBlackboard().getOrAddAttributeType(SHELLBAG_ATTRIBUTE_LAST_WRITE,
                         BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DATETIME,
                         Bundle.Shellbag_Last_Write_Attribute_Display_Name());
-            } catch (TskDataException ex) {
+            } catch (BlackboardException ex) {
                 // Attribute already exists get it from the case
-                shellBagLastWriteAttributeType = tskCase.getAttributeType(SHELLBAG_ATTRIBUTE_LAST_WRITE);
+                throw new TskCoreException(String.format("Failed to get custom attribute %s", SHELLBAG_ATTRIBUTE_LAST_WRITE), ex);
             }
         }
         return shellBagLastWriteAttributeType;
@@ -2011,12 +2006,11 @@ private BlackboardAttribute.Type getLastWriteAttribute() throws TskCoreException
     private BlackboardAttribute.Type getKeyAttribute() throws TskCoreException {
         if (shellBagKeyAttributeType == null) {
             try {
-                shellBagKeyAttributeType = tskCase.addArtifactAttributeType(SHELLBAG_ATTRIBUTE_KEY,
+                shellBagKeyAttributeType = tskCase.getBlackboard().getOrAddAttributeType(SHELLBAG_ATTRIBUTE_KEY,
                         BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING,
                         Bundle.Shellbag_Key_Attribute_Display_Name());
-            } catch (TskDataException ex) {
-                // The attribute already exists get it from the case
-                shellBagKeyAttributeType = tskCase.getAttributeType(SHELLBAG_ATTRIBUTE_KEY);
+            } catch (BlackboardException ex) {
+                throw new TskCoreException(String.format("Failed to get key attribute %s", SHELLBAG_ATTRIBUTE_KEY), ex);
             }
         }
         return shellBagKeyAttributeType;
diff --git a/Testing/build.xml b/Testing/build.xml
index f0ba6531a4ad9122aa5bdbaf314f101246e7c27c..f8eff4b82111a8ca3b3ac036cb2699c9ef4b0eab 100644
--- a/Testing/build.xml
+++ b/Testing/build.xml
@@ -76,6 +76,10 @@
                 <sysproperty key="solrPort" value="${solrPort}"/>
                 <sysproperty key="messageServiceHost" value="${messageServiceHost}"/>
                 <sysproperty key="messageServicePort" value="${messageServicePort}"/>
+                <sysproperty key="crHost" value="${crHost}"/>
+                <sysproperty key="crPort" value="${crPort}"/>
+                <sysproperty key="crUserName" value="${crUserName}"/>
+                <sysproperty key="crPassword" value="${crPassword}"/>
                 <sysproperty key="isMultiUser" value="${isMultiUser}"/>
                 <!--needed to have tests NOT to steal focus when running, works in latest apple jdk update only.-->
                 <sysproperty key="apple.awt.UIElement" value="@{disable.apple.ui}"/>
diff --git a/Testing/test/qa-functional/src/org/sleuthkit/autopsy/testing/RegressionTest.java b/Testing/test/qa-functional/src/org/sleuthkit/autopsy/testing/RegressionTest.java
index 6f6e04d7bd5fab2951a84e2d4fcec2785cb2ba6e..f2aa476590858a56029587ada3b983fca55def08 100644
--- a/Testing/test/qa-functional/src/org/sleuthkit/autopsy/testing/RegressionTest.java
+++ b/Testing/test/qa-functional/src/org/sleuthkit/autopsy/testing/RegressionTest.java
@@ -1,7 +1,7 @@
 /*
  * Autopsy Forensic Browser
  *
- * Copyright 2011-2018 Basis Technology Corp.
+ * Copyright 2011-2020 Basis Technology Corp.
  * Contact: carrier <at> sleuthkit <dot> org
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -25,6 +25,9 @@
 import junit.framework.TestCase;
 import org.netbeans.jemmy.Timeouts;
 import org.netbeans.junit.NbModuleSuite;
+import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoDbChoice;
+import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoDbManager;
+import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoException;
 
 /**
  * This test expects the following system properties to be set: img_path: The
@@ -100,6 +103,21 @@ public static Test suite() {
     public void setUp() {
         logger.info("########  " + AutopsyTestCases.getEscapedPath(System.getProperty("img_path")) + "  #######");
         Timeouts.setDefault("ComponentOperator.WaitComponentTimeout", 1000000);
+        
+        try {
+            if (Boolean.parseBoolean(System.getProperty("isMultiUser"))) {
+                // Set up a custom postgres CR using the configuration passed
+                // to system properties.
+                CentralRepoDbManager manager = new CentralRepoDbManager();
+                manager.getDbSettingsPostgres().setHost(System.getProperty("crHost"));
+                manager.getDbSettingsPostgres().setPort(Integer.parseInt(System.getProperty("crPort")));
+                manager.getDbSettingsPostgres().setUserName(System.getProperty("crUserName"));
+                manager.getDbSettingsPostgres().setPassword(System.getProperty("crPassword"));
+                manager.setupPostgresDb(CentralRepoDbChoice.POSTGRESQL_CUSTOM);
+            }
+        } catch (CentralRepoException ex) {
+            throw new RuntimeException("Error setting up multi user CR", ex);
+        }
     }
 
     /**
diff --git a/docs/doxygen-user/multi-user/installPostgres.dox b/docs/doxygen-user/multi-user/installPostgres.dox
index fd7753281be7433c37a38f5b69b07cb1c94d3d7d..d56afe931b8b34e739e37f0ec96f116ad0f54533 100644
--- a/docs/doxygen-user/multi-user/installPostgres.dox
+++ b/docs/doxygen-user/multi-user/installPostgres.dox
@@ -13,7 +13,7 @@ You should ensure that the database folder is backed up.
 
 To install PostgreSQL, perform the following steps:
 
-1. Download a 64-bit PostgreSQL installer from  http://www.enterprisedb.com/products-services-training/pgdownload#windows  Choose the one that says _Win X86-64_. Autopsy has been tested with PostgreSQL version 9.5.
+1. Download a 64-bit PostgreSQL installer from  https://www.enterprisedb.com/downloads/postgres-postgresql-downloads  Choose one under Windows x86-64. Autopsy has been tested with PostgreSQL version 9.5.
 
 2. Run the installer. The name will be similar to _postgresql-9.5.3-1-windows-x64.exe_.
 
diff --git a/test/script/regression.py b/test/script/regression.py
index 9999a5c5635604f938bca935a145c75c476783de..94b50c244f6be0a70b2a0ff6d67fbfa1f74f24f8 100644
--- a/test/script/regression.py
+++ b/test/script/regression.py
@@ -471,6 +471,10 @@ def _run_ant(test_data):
         test_data.ant.append("-DsolrPort=" + str(test_config.solrPort))
         test_data.ant.append("-DmessageServiceHost=" + test_config.messageServiceHost)
         test_data.ant.append("-DmessageServicePort=" + str(test_config.messageServicePort))
+        test_data.ant.append("-DcrHost=" + str(test_config.crHost))
+        test_data.ant.append("-DcrPort=" + str(test_config.crPort))
+        test_data.ant.append("-DcrUserName=" + str(test_config.crUserName))
+        test_data.ant.append("-DcrPassword=" + str(test_config.crPassword))
         if test_data.isMultiUser:
             test_data.ant.append("-DisMultiUser=true")
         # Note: test_data has autopys_version attribute, but we couldn't see it from here. It's set after run ingest.
@@ -854,6 +858,14 @@ def _load_config_file(self, config_file):
                 self.messageServicePort = parsed_config.getElementsByTagName("messageServicePort")[0].getAttribute("value").encode().decode("utf_8")
             if parsed_config.getElementsByTagName("multiUser_outdir"):
                 self.multiUser_outdir = parsed_config.getElementsByTagName("multiUser_outdir")[0].getAttribute("value").encode().decode("utf_8")
+            if parsed_config.getElementsByTagName("crHost"):
+                self.crHost = parsed_config.getElementsByTagName("crHost")[0].getAttribute("value").encode().decode("utf_8")
+            if parsed_config.getElementsByTagName("crPort"):
+                self.crPort = parsed_config.getElementsByTagName("crPort")[0].getAttribute("value").encode().decode("utf_8")
+            if parsed_config.getElementsByTagName("crUserName"):
+                self.crUserName = parsed_config.getElementsByTagName("crUserName")[0].getAttribute("value").encode().decode("utf_8")
+            if parsed_config.getElementsByTagName("crPassword"):
+                self.crPassword = parsed_config.getElementsByTagName("crPassword")[0].getAttribute("value").encode().decode("utf_8")
             self._init_imgs(parsed_config)
             self._init_build_info(parsed_config)
 
diff --git a/test/script/regression_utils.py b/test/script/regression_utils.py
index 51fa3eb1c4a2a3f2c9296368af25e2ea8fe8b132..0c0229beb24a3ecf96c94f188d830144aacc012f 100644
--- a/test/script/regression_utils.py
+++ b/test/script/regression_utils.py
@@ -27,7 +27,7 @@ def make_os_path(platform, *dirs):
             path += str(dir).replace('\\', '/') + '/'
         return path_fix(path)
     elif platform == "win32":
-        return make_path(dirs)
+        return make_path(*dirs)
     else:
         print("Couldn't make path, because we only support Windows and Cygwin at this time.")
         sys.exit(1)
diff --git a/thirdparty/iLeapp/ileapp.exe b/thirdparty/iLeapp/ileapp.exe
index 8176b4f6793a424a0e9d94a3b0796f9497f59f9d..d17ab28f4d1018e2ebc1e08f15a4427b14015b7c 100644
Binary files a/thirdparty/iLeapp/ileapp.exe and b/thirdparty/iLeapp/ileapp.exe differ
diff --git a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/VcardParser.java b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/VcardParser.java
index d662c5f5e65b3158d349d518a00ed47a0c893a4a..886a3bc41f5a5069969093cd6492641a55a2cb68 100755
--- a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/VcardParser.java
+++ b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/VcardParser.java
@@ -53,6 +53,7 @@
 import org.sleuthkit.datamodel.Account;
 import org.sleuthkit.datamodel.AccountFileInstance;
 import org.sleuthkit.datamodel.Blackboard;
+import org.sleuthkit.datamodel.Blackboard.BlackboardException;
 import org.sleuthkit.datamodel.BlackboardArtifact;
 import org.sleuthkit.datamodel.BlackboardAttribute;
 import org.sleuthkit.datamodel.Content;
@@ -421,14 +422,16 @@ private void addPhoneAttributes(Telephone telephone, AbstractFile abstractFile,
                     if (attributeType == null) {
                         try{
                             // Add this attribute type to the case database.
-                            attributeType = tskCase.addArtifactAttributeType(attributeTypeName,
+                            attributeType = tskCase.getBlackboard().getOrAddAttributeType(attributeTypeName,
                                     BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING,
                                     String.format("Phone Number (%s)", StringUtils.capitalize(splitType.toLowerCase())));
-                        }catch (TskDataException ex) {
-                            attributeType = tskCase.getAttributeType(attributeTypeName);
+                            
+                            ThunderbirdMboxFileIngestModule.addArtifactAttribute(telephoneText, attributeType, attributes);
+                        }catch (BlackboardException ex) {
+                            logger.log(Level.WARNING, String.format("Unable to retrieve attribute type '%s' for file '%s' (id=%d).", attributeTypeName, abstractFile.getName(), abstractFile.getId()), ex);
                         }
                     }
-                    ThunderbirdMboxFileIngestModule.addArtifactAttribute(telephoneText, attributeType, attributes);
+                    
                 } catch (TskCoreException ex) {
                     logger.log(Level.WARNING, String.format("Unable to retrieve attribute type '%s' for file '%s' (id=%d).", attributeTypeName, abstractFile.getName(), abstractFile.getId()), ex);
                 }
@@ -474,14 +477,14 @@ private void addEmailAttributes(Email email, AbstractFile abstractFile, Collecti
                    BlackboardAttribute.Type attributeType = tskCase.getAttributeType(attributeTypeName);
                    if (attributeType == null) {
                        // Add this attribute type to the case database.
-                       attributeType = tskCase.addArtifactAttributeType(attributeTypeName, 
+                       attributeType = tskCase.getBlackboard().getOrAddAttributeType(attributeTypeName, 
                                BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING, 
                                String.format("Email (%s)", StringUtils.capitalize(splitType.toLowerCase())));
                    }
                    ThunderbirdMboxFileIngestModule.addArtifactAttribute(email.getValue(), attributeType, attributes);
                } catch (TskCoreException ex) {
                    logger.log(Level.SEVERE, String.format("Unable to retrieve attribute type '%s' for file '%s' (id=%d).", attributeTypeName, abstractFile.getName(), abstractFile.getId()), ex);
-               } catch (TskDataException ex) {
+               } catch (BlackboardException ex) {
                    logger.log(Level.SEVERE, String.format("Unable to add custom attribute type '%s' for file '%s' (id=%d).", attributeTypeName, abstractFile.getName(), abstractFile.getId()), ex);
                }
            }