diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/Bundle.properties b/Core/src/com/basistech/df/cybertriage/autopsy/Bundle.properties
new file mode 100644
index 0000000000000000000000000000000000000000..caab36116de9c8116301e01c385b5dbd5c654edd
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/Bundle.properties
@@ -0,0 +1,9 @@
+
+# Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
+# Click nbfs://nbhost/SystemFileSystem/Templates/Other/properties.properties to edit this template
+
+
+CTIntegrationMissingDialog.title=Cyber Triage Importer Module Required
+CTIntegrationMissingDialog.descriptionLabel.text=<html><body><p>The Cyber Triage Importer Module is required to open this case. </p><p>To open this case:</p><ul><li>Extract the module from the Integrations tab in the Cyber Triage options panel.</li><li>Select the 'Plugins' option from the 'Tools' menu, and go to the 'Downloaded' tab.</li><li>Click 'Add Plugins...' and select the path of the plugin.</li><li>Press 'Install' to finish the installation.</li></ul></body></html>
+CTIntegrationMissingDialog.docsLabel.text=<html>For more information, refer to the Cyber Triage Users Guide</html>
+CTIntegrationMissingDialog.okButton.text=OK
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/Bundle.properties-MERGED b/Core/src/com/basistech/df/cybertriage/autopsy/Bundle.properties-MERGED
new file mode 100644
index 0000000000000000000000000000000000000000..caab36116de9c8116301e01c385b5dbd5c654edd
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/Bundle.properties-MERGED
@@ -0,0 +1,9 @@
+
+# Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
+# Click nbfs://nbhost/SystemFileSystem/Templates/Other/properties.properties to edit this template
+
+
+CTIntegrationMissingDialog.title=Cyber Triage Importer Module Required
+CTIntegrationMissingDialog.descriptionLabel.text=<html><body><p>The Cyber Triage Importer Module is required to open this case. </p><p>To open this case:</p><ul><li>Extract the module from the Integrations tab in the Cyber Triage options panel.</li><li>Select the 'Plugins' option from the 'Tools' menu, and go to the 'Downloaded' tab.</li><li>Click 'Add Plugins...' and select the path of the plugin.</li><li>Press 'Install' to finish the installation.</li></ul></body></html>
+CTIntegrationMissingDialog.docsLabel.text=<html>For more information, refer to the Cyber Triage Users Guide</html>
+CTIntegrationMissingDialog.okButton.text=OK
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/CTIntegrationMissingDialog.form b/Core/src/com/basistech/df/cybertriage/autopsy/CTIntegrationMissingDialog.form
new file mode 100644
index 0000000000000000000000000000000000000000..e61f2f6cbdf1ba9d4e1c665bae3a9e278efab40f
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/CTIntegrationMissingDialog.form
@@ -0,0 +1,131 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<Form version="1.5" maxVersion="1.9" type="org.netbeans.modules.form.forminfo.JDialogFormInfo">
+  <Properties>
+    <Property name="defaultCloseOperation" type="int" value="2"/>
+    <Property name="title" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+      <ResourceString bundle="com/basistech/df/cybertriage/autopsy/Bundle.properties" key="CTIntegrationMissingDialog.title" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+    </Property>
+    <Property name="alwaysOnTop" type="boolean" value="true"/>
+    <Property name="resizable" type="boolean" value="false"/>
+  </Properties>
+  <SyntheticProperties>
+    <SyntheticProperty name="formSizePolicy" type="int" value="1"/>
+    <SyntheticProperty name="generateCenter" type="boolean" value="false"/>
+  </SyntheticProperties>
+  <AuxValues>
+    <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="1"/>
+    <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/>
+    <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="true"/>
+    <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/>
+    <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/>
+    <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/>
+    <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/>
+    <AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,0,-65,0,0,1,-9"/>
+  </AuxValues>
+
+  <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/>
+  <SubComponents>
+    <Component class="javax.swing.JLabel" name="descriptionLabel">
+      <Properties>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="com/basistech/df/cybertriage/autopsy/Bundle.properties" key="CTIntegrationMissingDialog.descriptionLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+        <Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
+          <Dimension value="[483, 116]"/>
+        </Property>
+      </Properties>
+      <AuxValues>
+        <AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/>
+        <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/>
+      </AuxValues>
+      <Constraints>
+        <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
+          <GridBagConstraints gridX="0" gridY="0" gridWidth="2" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="5" insetsLeft="5" insetsBottom="5" insetsRight="5" anchor="18" weightX="1.0" weightY="0.0"/>
+        </Constraint>
+      </Constraints>
+    </Component>
+    <Component class="javax.swing.JLabel" name="docsLabel">
+      <Properties>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="com/basistech/df/cybertriage/autopsy/Bundle.properties" key="CTIntegrationMissingDialog.docsLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+        <Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
+          <Dimension value="[312, 16]"/>
+        </Property>
+      </Properties>
+      <AuxValues>
+        <AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/>
+        <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/>
+      </AuxValues>
+      <Constraints>
+        <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
+          <GridBagConstraints gridX="0" gridY="1" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="5" insetsBottom="5" insetsRight="0" anchor="18" weightX="0.0" weightY="0.0"/>
+        </Constraint>
+      </Constraints>
+    </Component>
+    <Component class="javax.swing.JLabel" name="link">
+      <Properties>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.form.RADConnectionPropertyEditor">
+          <Connection code="&quot;&lt;html&gt;&lt;span style=\&quot;color: blue; text-decoration: underline\&quot;&gt;&quot; + DOCS_PAGE_URL + &quot;&lt;/span&gt;&lt;/html&gt;&quot;" type="code"/>
+        </Property>
+        <Property name="cursor" type="java.awt.Cursor" editor="org.netbeans.modules.form.editors2.CursorEditor">
+          <Color id="Hand Cursor"/>
+        </Property>
+      </Properties>
+      <Events>
+        <EventHandler event="mouseClicked" listener="java.awt.event.MouseListener" parameters="java.awt.event.MouseEvent" handler="linkMouseClicked"/>
+      </Events>
+      <Constraints>
+        <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
+          <GridBagConstraints gridX="1" gridY="1" gridWidth="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="5" insetsBottom="5" insetsRight="5" anchor="18" weightX="1.0" weightY="0.0"/>
+        </Constraint>
+      </Constraints>
+    </Component>
+    <Container class="javax.swing.JPanel" name="paddingPanel">
+      <AuxValues>
+        <AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/>
+        <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/>
+      </AuxValues>
+      <Constraints>
+        <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
+          <GridBagConstraints gridX="0" gridY="2" gridWidth="2" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="0.0" weightY="1.0"/>
+        </Constraint>
+      </Constraints>
+
+      <Layout>
+        <DimensionLayout dim="0">
+          <Group type="103" groupAlignment="0" attributes="0">
+              <EmptySpace min="0" pref="0" max="32767" attributes="0"/>
+          </Group>
+        </DimensionLayout>
+        <DimensionLayout dim="1">
+          <Group type="103" groupAlignment="0" attributes="0">
+              <EmptySpace min="0" pref="0" max="32767" attributes="0"/>
+          </Group>
+        </DimensionLayout>
+      </Layout>
+    </Container>
+    <Component class="javax.swing.JButton" name="okButton">
+      <Properties>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="com/basistech/df/cybertriage/autopsy/Bundle.properties" key="CTIntegrationMissingDialog.okButton.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+      <Events>
+        <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="okButtonActionPerformed"/>
+      </Events>
+      <AuxValues>
+        <AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/>
+        <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/>
+      </AuxValues>
+      <Constraints>
+        <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
+          <GridBagConstraints gridX="1" gridY="2" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="10" insetsLeft="5" insetsBottom="5" insetsRight="5" anchor="12" weightX="0.0" weightY="0.0"/>
+        </Constraint>
+      </Constraints>
+    </Component>
+  </SubComponents>
+</Form>
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/CTIntegrationMissingDialog.java b/Core/src/com/basistech/df/cybertriage/autopsy/CTIntegrationMissingDialog.java
new file mode 100644
index 0000000000000000000000000000000000000000..9e534c98332ac4eb0c5771dfc445515b5abd93bf
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/CTIntegrationMissingDialog.java
@@ -0,0 +1,165 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2023 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 com.basistech.df.cybertriage.autopsy;
+
+import java.awt.Desktop;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.logging.Level;
+import javax.swing.JComponent;
+import org.sleuthkit.autopsy.coreutils.Logger;
+
+/**
+ * Provides directions with how to enable CT integration with Autopsy when
+ * trying to open a CT exported case.
+ */
+public class CTIntegrationMissingDialog extends javax.swing.JDialog {
+
+    private static final String DOCS_PAGE_URL = "https://docs.cybertriage.com/en/latest/chapters/integrations/autopsy.html";
+
+    private static final Logger LOGGER = Logger.getLogger(CTIntegrationMissingDialog.class.getName());
+
+    /**
+     * Creates new form CTIntegrationMissingDialog
+     */
+    public CTIntegrationMissingDialog(java.awt.Frame parent, boolean modal) {
+        super(parent, modal);
+        initComponents();
+    }
+
+    /**
+     * This method is called from within the constructor to initialize the form.
+     * WARNING: Do NOT modify this code. The content of this method is always
+     * regenerated by the Form Editor.
+     */
+    @SuppressWarnings("unchecked")
+    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
+    private void initComponents() {
+        java.awt.GridBagConstraints gridBagConstraints;
+
+        javax.swing.JLabel descriptionLabel = new javax.swing.JLabel();
+        javax.swing.JLabel docsLabel = new javax.swing.JLabel();
+        link = new javax.swing.JLabel();
+        javax.swing.JPanel paddingPanel = new javax.swing.JPanel();
+        javax.swing.JButton okButton = new javax.swing.JButton();
+
+        setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE);
+        setTitle(org.openide.util.NbBundle.getMessage(CTIntegrationMissingDialog.class, "CTIntegrationMissingDialog.title")); // NOI18N
+        setAlwaysOnTop(true);
+        setResizable(false);
+        getContentPane().setLayout(new java.awt.GridBagLayout());
+
+        org.openide.awt.Mnemonics.setLocalizedText(descriptionLabel, org.openide.util.NbBundle.getMessage(CTIntegrationMissingDialog.class, "CTIntegrationMissingDialog.descriptionLabel.text")); // NOI18N
+        descriptionLabel.setMinimumSize(new java.awt.Dimension(483, 116));
+        gridBagConstraints = new java.awt.GridBagConstraints();
+        gridBagConstraints.gridx = 0;
+        gridBagConstraints.gridy = 0;
+        gridBagConstraints.gridwidth = 2;
+        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
+        gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
+        gridBagConstraints.weightx = 1.0;
+        gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5);
+        getContentPane().add(descriptionLabel, gridBagConstraints);
+
+        org.openide.awt.Mnemonics.setLocalizedText(docsLabel, org.openide.util.NbBundle.getMessage(CTIntegrationMissingDialog.class, "CTIntegrationMissingDialog.docsLabel.text")); // NOI18N
+        docsLabel.setMinimumSize(new java.awt.Dimension(312, 16));
+        gridBagConstraints = new java.awt.GridBagConstraints();
+        gridBagConstraints.gridx = 0;
+        gridBagConstraints.gridy = 1;
+        gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
+        gridBagConstraints.insets = new java.awt.Insets(0, 5, 5, 0);
+        getContentPane().add(docsLabel, gridBagConstraints);
+
+        org.openide.awt.Mnemonics.setLocalizedText(link, "<html><span style=\"color: blue; text-decoration: underline\">" + DOCS_PAGE_URL + "</span></html>");
+        link.setCursor(new java.awt.Cursor(java.awt.Cursor.HAND_CURSOR));
+        link.addMouseListener(new java.awt.event.MouseAdapter() {
+            public void mouseClicked(java.awt.event.MouseEvent evt) {
+                linkMouseClicked(evt);
+            }
+        });
+        gridBagConstraints = new java.awt.GridBagConstraints();
+        gridBagConstraints.gridx = 1;
+        gridBagConstraints.gridy = 1;
+        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
+        gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
+        gridBagConstraints.weightx = 1.0;
+        gridBagConstraints.insets = new java.awt.Insets(0, 5, 5, 5);
+        getContentPane().add(link, gridBagConstraints);
+
+        javax.swing.GroupLayout paddingPanelLayout = new javax.swing.GroupLayout(paddingPanel);
+        paddingPanel.setLayout(paddingPanelLayout);
+        paddingPanelLayout.setHorizontalGroup(
+            paddingPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addGap(0, 0, Short.MAX_VALUE)
+        );
+        paddingPanelLayout.setVerticalGroup(
+            paddingPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addGap(0, 0, Short.MAX_VALUE)
+        );
+
+        gridBagConstraints = new java.awt.GridBagConstraints();
+        gridBagConstraints.gridx = 0;
+        gridBagConstraints.gridy = 2;
+        gridBagConstraints.gridwidth = 2;
+        gridBagConstraints.weighty = 1.0;
+        getContentPane().add(paddingPanel, gridBagConstraints);
+
+        org.openide.awt.Mnemonics.setLocalizedText(okButton, org.openide.util.NbBundle.getMessage(CTIntegrationMissingDialog.class, "CTIntegrationMissingDialog.okButton.text")); // NOI18N
+        okButton.addActionListener(new java.awt.event.ActionListener() {
+            public void actionPerformed(java.awt.event.ActionEvent evt) {
+                okButtonActionPerformed(evt);
+            }
+        });
+        gridBagConstraints = new java.awt.GridBagConstraints();
+        gridBagConstraints.gridx = 1;
+        gridBagConstraints.gridy = 2;
+        gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHEAST;
+        gridBagConstraints.insets = new java.awt.Insets(10, 5, 5, 5);
+        getContentPane().add(okButton, gridBagConstraints);
+
+        pack();
+    }// </editor-fold>//GEN-END:initComponents
+
+    private void linkMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_linkMouseClicked
+        if (Desktop.isDesktopSupported()) {
+            try {
+                Desktop.getDesktop().browse(new URI(DOCS_PAGE_URL));
+            } catch (IOException | URISyntaxException e) {
+                LOGGER.log(Level.SEVERE, "Error opening link to: " + DOCS_PAGE_URL, e);
+            }
+        } else {
+            LOGGER.log(Level.WARNING, "Desktop API is not supported.  Link cannot be opened.");
+        }
+    }//GEN-LAST:event_linkMouseClicked
+
+    private void okButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_okButtonActionPerformed
+        dispose();
+    }//GEN-LAST:event_okButtonActionPerformed
+
+    public void showDialog(JComponent parentComp) {
+        setLocationRelativeTo(parentComp == null ? getParent() : parentComp);
+        pack();
+        setVisible(true);
+    }
+
+    // Variables declaration - do not modify//GEN-BEGIN:variables
+    private javax.swing.JLabel link;
+    // End of variables declaration//GEN-END:variables
+}
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED
index b6719401180be72a2b873a423204978798a529b3..dc96292d9a3ed409495ff76ebe59b4f28de631e3 100755
--- a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED
+++ b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED
@@ -16,6 +16,7 @@ Case.exceptionMessage.cannotDeleteCurrentCase=Cannot delete current case, it mus
 Case.exceptionMessage.cannotGetLockToDeleteCase=Cannot delete case because it is open for another user or host.
 Case.exceptionMessage.cannotLocateMainWindow=Cannot locate main application window
 Case.exceptionMessage.cannotOpenMultiUserCaseNoSettings=Multi-user settings are missing (see Tools, Options, Multi-user tab), cannot open a multi-user case.
+Case.exceptionMessage.contentProviderCouldNotBeFound=Content provider was specified for the case but could not be loaded.
 # {0} - exception message
 Case.exceptionMessage.couldNotCreatCollaborationMonitor=Failed to create collaboration monitor:\n{0}.
 # {0} - exception message
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java
index 6ece795e35547c8b59b54e1d2b086b4a75efff29..b1d2f28c3137d176830956aa7bd2a13b983760b3 100644
--- a/Core/src/org/sleuthkit/autopsy/casemodule/Case.java
+++ b/Core/src/org/sleuthkit/autopsy/casemodule/Case.java
@@ -18,6 +18,7 @@
  */
 package org.sleuthkit.autopsy.casemodule;
 
+import com.basistech.df.cybertriage.autopsy.CTIntegrationMissingDialog;
 import org.sleuthkit.autopsy.featureaccess.FeatureAccessUtils;
 import com.google.common.annotations.Beta;
 import com.google.common.eventbus.Subscribe;
@@ -177,6 +178,7 @@ public class Case {
     private static final String CASE_ACTION_THREAD_NAME = "%s-case-action";
     private static final String CASE_RESOURCES_THREAD_NAME = "%s-manage-case-resources";
     private static final String NO_NODE_ERROR_MSG_FRAGMENT = "KeeperErrorCode = NoNode";
+    private static final String CT_PROVIDER_PREFIX = "CTStandardContentProvider_";
     private static final Logger logger = Logger.getLogger(Case.class.getName());
     private static final AutopsyEventPublisher eventPublisher = new AutopsyEventPublisher();
     private static final Object caseActionSerializationLock = new Object();
@@ -2729,6 +2731,7 @@ private void createCaseDatabase(ProgressIndicator progressIndicator) throws Case
         "Case.progressMessage.openingCaseDatabase=Opening case database...",
         "# {0} - exception message", "Case.exceptionMessage.couldNotOpenCaseDatabase=Failed to open case database:\n{0}.",
         "# {0} - exception message", "Case.exceptionMessage.unsupportedSchemaVersionMessage=Unsupported case database schema version:\n{0}.",
+        "Case.exceptionMessage.contentProviderCouldNotBeFound=Content provider was specified for the case but could not be loaded.",
         "Case.open.exception.multiUserCaseNotEnabled=Cannot open a multi-user case if multi-user cases are not enabled. See Tools, Options, Multi-User."
     })
     private void openCaseDataBase(ProgressIndicator progressIndicator) throws CaseActionException {
@@ -2737,14 +2740,15 @@ private void openCaseDataBase(ProgressIndicator progressIndicator) throws CaseAc
             String databaseName = metadata.getCaseDatabaseName();
             
             ContentStreamProvider contentProvider = loadContentProvider(metadata.getContentProviderName());
+            if (StringUtils.isNotBlank(metadata.getContentProviderName()) && contentProvider == null) {
+                if (metadata.getContentProviderName().trim().toUpperCase().startsWith(CT_PROVIDER_PREFIX.toUpperCase())) {
+                    new CTIntegrationMissingDialog(WindowManager.getDefault().getMainWindow(), true).showDialog(null);
+                }
+                throw new CaseActionException(Bundle.Case_exceptionMessage_contentProviderCouldNotBeFound());
+            }
             
             if (CaseType.SINGLE_USER_CASE == metadata.getCaseType()) {
-                // only prefix with metadata directory if databaseName is a relative path
-                String fullDatabasePath = (new File(databaseName).isAbsolute())
-                        ? databaseName
-                        : Paths.get(metadata.getCaseDirectory(), databaseName).toString();
-                        
-                caseDb = SleuthkitCase.openCase(fullDatabasePath, contentProvider);
+                caseDb = SleuthkitCase.openCase(metadata.getCaseDatabasePath(), contentProvider);
             } else if (UserPreferences.getIsMultiUserModeEnabled()) {
                 caseDb = SleuthkitCase.openCase(databaseName, UserPreferences.getDatabaseConnectionInfo(), metadata.getCaseDirectory(), contentProvider);
             } else {
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java b/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java
index c9170b1e68d5a7920d265d9cebfd81a9a9c4cf32..91566a01bb0afc6f03e87db01eaf864932193ebe 100644
--- a/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java
+++ b/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java
@@ -29,16 +29,8 @@
 import java.nio.file.Paths;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
 import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
 import java.util.Locale;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.stream.Collectors;
 import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.parsers.ParserConfigurationException;
@@ -51,13 +43,10 @@
 import javax.xml.transform.dom.DOMSource;
 import javax.xml.transform.stream.StreamResult;
 import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.tuple.Pair;
-import org.openide.util.Lookup;
 import org.sleuthkit.autopsy.coreutils.Version;
 import org.sleuthkit.autopsy.coreutils.XMLUtil;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
-import org.w3c.dom.Node;
 import org.w3c.dom.NodeList;
 import org.xml.sax.SAXException;
 
@@ -121,7 +110,6 @@ public final class CaseMetadata {
     private static final String SCHEMA_VERSION_SIX = "6.0";
     private final static String CONTENT_PROVIDER_ELEMENT_NAME = "ContentProvider";
     private final static String CONTENT_PROVIDER_NAME_ELEMENT_NAME = "Name";
-    private final static String CONTENT_PROVIDER_ARG_DEFAULT_KEY = "DEFAULT";
     
     /*
      * Unread fields, regenerated on save.
@@ -136,7 +124,7 @@ public final class CaseMetadata {
     private String caseName;
     private CaseDetails caseDetails;
     private String caseDatabaseName;
-    private String caseDatabasePath; // Legacy
+    private String caseDatabasePath;
     private String textIndexName; // Legacy
     private String createdDate;
     private String createdByVersion;
@@ -258,7 +246,9 @@ public Path getFilePath() {
      * @return The case directory.
      */
     public String getCaseDirectory() {
-        return metadataFilePath.getParent().toString();
+        return StringUtils.isBlank(this.caseDatabasePath)
+                ? metadataFilePath.getParent().toString()
+                : Paths.get(this.caseDatabasePath).getParent().toString();
     }
 
     /**
@@ -637,6 +627,7 @@ private void readFromFile() throws CaseMetadataException {
                     this.textIndexName = getElementTextContent(caseElement, TEXT_INDEX_ELEMENT, false);
                     break;
                 default:
+                    this.caseDatabasePath = getElementTextContent(caseElement, CASE_DB_ABSOLUTE_PATH_ELEMENT_NAME, false);
                     this.caseDatabaseName = getElementTextContent(caseElement, CASE_DB_NAME_RELATIVE_ELEMENT_NAME, true);
                     this.textIndexName = getElementTextContent(caseElement, TEXT_INDEX_ELEMENT, false);
                     break;
@@ -650,11 +641,9 @@ private void readFromFile() throws CaseMetadataException {
              */
             Path possibleAbsoluteCaseDbPath = Paths.get(this.caseDatabaseName);
             Path caseDirectoryPath = Paths.get(getCaseDirectory());
-            if (possibleAbsoluteCaseDbPath.getNameCount() > 1) {
+            if (possibleAbsoluteCaseDbPath.toFile().isAbsolute()) {
                 this.caseDatabasePath = this.caseDatabaseName;
                 this.caseDatabaseName = caseDirectoryPath.relativize(possibleAbsoluteCaseDbPath).toString();
-            } else {
-                this.caseDatabasePath = caseDirectoryPath.resolve(caseDatabaseName).toAbsolutePath().toString();
             }
 
         } catch (ParserConfigurationException | SAXException | IOException ex) {
@@ -719,12 +708,12 @@ private CaseMetadataException(String message, Throwable cause) {
      * @return The full path to the case database file for a single-user case.
      *
      * @throws UnsupportedOperationException If called for a multi-user case.
-     * @deprecated Do not use.
      */
-    @Deprecated
     public String getCaseDatabasePath() throws UnsupportedOperationException {
         if (Case.CaseType.SINGLE_USER_CASE == caseType) {
-            return Paths.get(getCaseDirectory(), caseDatabaseName).toString();
+            return StringUtils.isBlank(this.caseDatabasePath)
+                    ? this.metadataFilePath.getParent().resolve(this.caseDatabaseName).toString()
+                    : this.caseDatabasePath;
         } else {
             throw new UnsupportedOperationException();
         }
diff --git a/NEWS.txt b/NEWS.txt
index 11c56f01c82252906e3658d45245cc85b2bde5f4..b8db91ca62c032d10cf7ae0cc6e4df3b281ac191 100644
--- a/NEWS.txt
+++ b/NEWS.txt
@@ -1,3 +1,32 @@
+---------------- VERSION 4.21.0 ---------------
+Library Updates
+- Update Java to version 17
+- Update aLeapp/iLeapp executables.
+- Update JNA Version
+- Update SQLite library version
+- Updated 3rd party libraries that have known CVE's
+
+Ingest Module Updates:
+- Recent Activity checks for malicious Chrome extensions from list provided by https://github.com/randomaccess3/detections
+- Keyword Search module now can search without needing to index text into Solr. 
+- New Cyber Triage Malware Scanner module that uses Reversing Labs (requires license)
+
+Add Data Source Updates:
+- Timestamps for logical files can be added. Issue https://github.com/sleuthkit/autopsy/issues/5852, https://github.com/sleuthkit/autopsy/issues/1788
+- List of logical files/folders can be edited before they are added.  Issue https://github.com/sleuthkit/autopsy/issues/7347 
+
+GUI Updates:
+- Add "has attachments" flag for emails. Issue https://github.com/sleuthkit/autopsy/issues/7358
+- Add Score to tree view
+
+Bugs:
+- Fix path for lnk files
+- Fix exporting of CSV files. Issue https://github.com/sleuthkit/autopsy/issues/6717
+
+Misc:
+- Added File Repository concept for data source files that are in a central location
+- Added Spanish language support, contributor https://github.com/AburtoArielPM
+
 ---------------- VERSION 4.20.0  --------------
 Recent Activity Updates:
 - Added Favicons, Profiles and Extensions to Chromium Browsers
diff --git a/docs/doxygen-user/ct_malware_scanner.dox b/docs/doxygen-user/ct_malware_scanner.dox
index 4d4939415656bc33ec8472349fccc1fd73d1646f..196a4d5b00ffbf4772cfb05a5b127bab65642b11 100644
--- a/docs/doxygen-user/ct_malware_scanner.dox
+++ b/docs/doxygen-user/ct_malware_scanner.dox
@@ -1,28 +1,57 @@
-/*! \page ct_malware_scanner Cyber Triage Malware Scanner
+/*! \page ct_malware_scanner_page Cyber Triage Malware Scanner Module
 
 [TOC]
 
+What Does It Do
+========
 
-\section ct_malware_scanner_overview Overview
+The Cyber Triage Malware Scanner module will use the malware scanning infrastructure from Cyber Triage to identify if any Windows executables are malware. It will query an online service using the file's hash value to see if the file was already analyzed and allows you to upload files for analysis if they are new.
 
-The Malware Scanner Ingest Module uses Cyber Triage Cloud to identify if any executables in a data source are malware based on the executable's md5 hash.
+This module requires a commercial license from Cyber Triage.
 
-\section ct_malware_scanner_config Configuration
+For more information on obtaining a license, refer to [CyberTriage.com](https://cybertriage.com/autopsy-malware-module). The remainder of this page is about the use of the module once it is licensed. 
 
-Before using the Malware Scanner Ingest Module, you must register a Cyber Triage Cloud License.  A license number can be added by selecting the 'Options' menu item from the 'Tools' menu, going to the 'Cyber Triage' tab, and then clicking 'Add License'.
 
-\image html ct_malware_scanner_options_panel.png
+Configuration
+=======
 
-The user will then be presented with a dialog to enter your license number.  Enter your license number and then press 'OK'.  If your license number is validated, you will be presented with the Cyber Triage End User License Agreement.  The window may take a moment to load.
+Once you have a license, you must add it on the Options panel.  Choose the 'Cyber Triage' tab and choose 'Add License'.
 
-\image html ct_malware_license_agreement.png
 
-Read through the license agreement, and press 'Accept'.  At that point, your options panel should load with information pertaining to remaining lookups.
+IMAGE
 
-\image html ct_upload_file.png
+After you enter the license number that you should have received from your email, you will then need to review and agree to the license terms. 
 
-\section ct_upload_executable Uploading Executable
 
-In the screenshot above, there is the option “Upload executable if executable is unknown.”  In the event that an executable has not previously been seen by Cyber Triage Cloud, this option provides the ability to upload the executable for scanning.  This option may cause increased processing time in order to upload the file and wait for scanning to complete.
+The options panel should now display information about the lookup limits. You can always refer back to here about what your limits are and when they reset. 
+
+IMAGE
+
+
+
+Using the Module
+======
+
+Ingest Settings
+------
+
+For each data source, you select if you want files to be uploaded if they have not already been analyzed. By default, they are uploaded. You can choose to not upload them though. 
+
+IMAGE
+
+
+
+Out of Scans
+-------
+
+If you go beyond your limits, you will get a dialog that not all files were analyzed.  You can wait until your limits reset and then start ingest again with only the malware scanning module enabled. It will ignore the files that are already analyzed.
+
+
+Seeing Results
+------
+
+Once ingest has completed, the files with malware will be listed in the Malware node in the tree. 
+
+IMAGE
 
 */
diff --git a/docs/doxygen-user/main.dox b/docs/doxygen-user/main.dox
index 61f42084a7996fb664cf85554f0611d4c6290c72..193a5abcba9f03498b46ec1cb63bcfca415c74d5 100644
--- a/docs/doxygen-user/main.dox
+++ b/docs/doxygen-user/main.dox
@@ -58,7 +58,7 @@ The following topics are available here:
  - \subpage ileapp_page
  - \subpage aleapp_page
  - \subpage yara_page
- - \subpage ct_malware_scanner
+ - \subpage ct_malware_scanner_page
 
 - Reviewing the Results
  - \subpage uilayout_page