From 0c3394e88f6909ccb1f8176485798cf739a06cf6 Mon Sep 17 00:00:00 2001
From: Greg DiCristofaro <gregd@basistech.com>
Date: Thu, 20 Jul 2023 14:07:52 -0400
Subject: [PATCH] initial commit

---
 Core/ivy.xml                                  |  13 +
 Core/nbproject/project.properties             |   7 +
 Core/nbproject/project.xml                    |  44 +-
 .../autopsy/ctapi/CTCloudException.java       |  97 +++
 .../cybertriage/autopsy/ctapi/Constants.java  |  83 +++
 .../cybertriage/autopsy/ctapi/CtApiDAO.java   |  83 +++
 .../autopsy/ctapi/ProxySettings.java          | 443 ++++++++++++++
 .../autopsy/ctapi/json/AuthTokenRequest.java  |  59 ++
 .../autopsy/ctapi/json/AuthTokenResponse.java |  85 +++
 .../ctapi/json/BoostLicenseResponse.java      |  59 ++
 .../autopsy/ctapi/json/CTScore.java           |  77 +++
 .../ctapi/json/DecryptedLicenseResponse.java  |  87 +++
 .../ctapi/json/FileReputationResult.java      | 123 ++++
 .../autopsy/ctapi/json/LicenseInfo.java       |  45 ++
 .../autopsy/ctapi/json/LicenseRequest.java    |  59 ++
 .../autopsy/ctapi/json/LicenseResponse.java   |  59 ++
 .../autopsy/ctapi/json/MetadataLabel.java     |  43 ++
 .../ctapi/util/CTHostIDGenerationUtil.java    |  57 ++
 .../ctapi/util/LicenseDecryptorUtil.java      | 172 ++++++
 .../autopsy/ctapi/util/Md5HashUtil.java       |  40 ++
 .../autopsy/ctapi/util/ObjectMapperUtil.java  |  41 ++
 .../autopsy/ctoptions/Bundle.properties       |   5 +
 .../ctoptions/Bundle.properties-MERGED        |   5 +
 .../autopsy/ctoptions/CTOptionsPanel.form     |  39 ++
 .../autopsy/ctoptions/CTOptionsPanel.java     | 141 +++++
 .../ctoptions/CTOptionsPanelController.java   | 128 ++++
 .../ctoptions/ctcloud/Bundle.properties       |  23 +
 .../ctcloud/Bundle.properties-MERGED          |  55 ++
 .../ctoptions/ctcloud/CTLicenseDialog.form    | 138 +++++
 .../ctoptions/ctcloud/CTLicenseDialog.java    | 192 ++++++
 .../ctcloud/CTLicensePersistence.java         |  90 +++
 .../ctcloud/CTMalwareScannerOptionsPanel.form | 199 +++++++
 .../ctcloud/CTMalwareScannerOptionsPanel.java | 551 ++++++++++++++++++
 .../ctoptions/subpanel/CTOptionsSubPanel.java |  26 +
 .../df/cybertriage/autopsy/images/logo.png    | Bin 0 -> 10482 bytes
 .../autopsy/malwarescan/BatchProcessor.java   |  88 +++
 .../malwarescan/Bundle.properties-MERGED      |  23 +
 .../malwarescan/MalwareScanIngestModule.java  | 395 +++++++++++++
 .../MalwareScanIngestModuleFactory.java       |  72 +++
 CoreLibs/ivy.xml                              |   8 +-
 CoreLibs/manifest.mf                          |   2 +-
 CoreLibs/nbproject/project.properties         |   1 +
 CoreLibs/nbproject/project.xml                |  45 +-
 .../netbeans/core/startup/Bundle.properties   |   4 +-
 .../core/windows/view/ui/Bundle.properties    |   6 +-
 45 files changed, 3959 insertions(+), 53 deletions(-)
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CTCloudException.java
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/ctapi/Constants.java
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CtApiDAO.java
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/ctapi/ProxySettings.java
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/AuthTokenRequest.java
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/AuthTokenResponse.java
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/BoostLicenseResponse.java
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/CTScore.java
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/DecryptedLicenseResponse.java
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/FileReputationResult.java
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/LicenseInfo.java
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/LicenseRequest.java
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/LicenseResponse.java
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/MetadataLabel.java
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/ctapi/util/CTHostIDGenerationUtil.java
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/ctapi/util/LicenseDecryptorUtil.java
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/ctapi/util/Md5HashUtil.java
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/ctapi/util/ObjectMapperUtil.java
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/Bundle.properties
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/Bundle.properties-MERGED
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/CTOptionsPanel.form
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/CTOptionsPanel.java
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/CTOptionsPanelController.java
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties-MERGED
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTLicenseDialog.form
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTLicenseDialog.java
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTLicensePersistence.java
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTMalwareScannerOptionsPanel.form
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTMalwareScannerOptionsPanel.java
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/subpanel/CTOptionsSubPanel.java
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/images/logo.png
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/BatchProcessor.java
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/Bundle.properties-MERGED
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/MalwareScanIngestModule.java
 create mode 100644 Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/MalwareScanIngestModuleFactory.java

diff --git a/Core/ivy.xml b/Core/ivy.xml
index fba2d99acd..3d2352648d 100644
--- a/Core/ivy.xml
+++ b/Core/ivy.xml
@@ -1,3 +1,6 @@
+<!DOCTYPE ivy-module [
+    <!ENTITY httpcomponents.version "4.5.14">
+]>
 <ivy-module version="2.0">
     <info organisation="org.sleuthkit.autopsy" module="core"/>
     <configurations >
@@ -72,6 +75,15 @@
         <!-- annotations like guarded by -->
         <dependency conf="core->default" org="com.github.spotbugs" name="spotbugs-annotations" rev="4.6.0"/>
 
+        <dependency conf="core->default" org="com.license4j" name="license4j-runtime-library" rev="4.7.1"/>
+
+        <dependency conf="core->default" org="org.apache.httpcomponents" name="httpclient" rev="&httpcomponents.version;"/>
+        <dependency conf="core->default" org="org.apache.httpcomponents" name="httpmime" rev="&httpcomponents.version;"/>
+        <dependency conf="core->default" org="org.apache.httpcomponents" name="httpclient-win" rev="&httpcomponents.version;">
+            <exclude name="jna" />
+            <exclude name="jna-platform" />
+        </dependency>
+
         <override org="org.apache.zookeeper" module="zookeeper" rev="3.8.0"/>
         <override org="org.apache.zookeeper" module="zookeeper-jute" rev="3.8.0"/>
         
@@ -84,5 +96,6 @@
         <override org="org.bouncycastle" module="bcprov-ext-jdk15on" rev="1.70"/>
         <override org="org.bouncycastle" module="bcprov-jdk15on" rev="1.70"/>
         <override org="org.bouncycastle" module="bcpkix-jdk15on" rev="1.70"/>
+        <override org="junit" module="junit" rev="4.13.2"/>
     </dependencies>
 </ivy-module>
diff --git a/Core/nbproject/project.properties b/Core/nbproject/project.properties
index a24bcb423e..9adb1c7649 100644
--- a/Core/nbproject/project.properties
+++ b/Core/nbproject/project.properties
@@ -18,6 +18,7 @@ file.reference.bcprov-jdk15on-1.70.jar=release/modules/ext/bcprov-jdk15on-1.70.j
 file.reference.bcutil-jdk15on-1.70.jar=release/modules/ext/bcutil-jdk15on-1.70.jar
 file.reference.c3p0-0.9.5.5.jar=release/modules/ext/c3p0-0.9.5.5.jar
 file.reference.checker-qual-3.33.0.jar=release/modules/ext/checker-qual-3.33.0.jar
+file.reference.commons-codec-1.11.jar=release/modules/ext/commons-codec-1.11.jar
 file.reference.commons-dbcp2-2.9.0.jar=release/modules/ext/commons-dbcp2-2.9.0.jar
 file.reference.commons-io-2.11.0.jar=release/modules/ext/commons-io-2.11.0.jar
 file.reference.commons-lang3-3.10.jar=release/modules/ext/commons-lang3-3.10.jar
@@ -31,6 +32,10 @@ file.reference.decodetect-core-0.3.jar=release/modules/ext/decodetect-core-0.3.j
 file.reference.error_prone_annotations-2.18.0.jar=release/modules/ext/error_prone_annotations-2.18.0.jar
 file.reference.failureaccess-1.0.1.jar=release/modules/ext/failureaccess-1.0.1.jar
 file.reference.guava-32.0.1-jre.jar=release/modules/ext/guava-32.0.1-jre.jar
+file.reference.httpclient-4.5.14.jar=release/modules/ext/httpclient-4.5.14.jar
+file.reference.httpclient-win-4.5.14.jar=release/modules/ext/httpclient-win-4.5.14.jar
+file.reference.httpcore-4.4.16.jar=release/modules/ext/httpcore-4.4.16.jar
+file.reference.httpmime-4.5.14.jar=release/modules/ext/httpmime-4.5.14.jar
 file.reference.icepdf-core-6.2.2.jar=release/modules/ext/icepdf-core-6.2.2.jar
 file.reference.icepdf-viewer-6.2.2.jar=release/modules/ext/icepdf-viewer-6.2.2.jar
 file.reference.istack-commons-runtime-3.0.11.jar=release/modules/ext/istack-commons-runtime-3.0.11.jar
@@ -46,6 +51,7 @@ file.reference.javax.activation-api-1.2.0.jar=release/modules/ext/javax.activati
 file.reference.javax.ws.rs-api-2.1.1.jar=release/modules/ext/javax.ws.rs-api-2.1.1.jar
 file.reference.jaxb-api-2.3.1.jar=release/modules/ext/jaxb-api-2.3.1.jar
 file.reference.jaxb-runtime-2.3.3.jar=release/modules/ext/jaxb-runtime-2.3.3.jar
+file.reference.jdom-2.0.5-contrib.jar=release/modules/ext/jdom-2.0.5-contrib.jar
 file.reference.jdom-2.0.5.jar=release/modules/ext/jdom-2.0.5.jar
 file.reference.jfreechart-1.5.3.jar=release/modules/ext/jfreechart-1.5.3.jar
 file.reference.jgraphx-4.2.2.jar=release/modules/ext/jgraphx-4.2.2.jar
@@ -55,6 +61,7 @@ file.reference.jutf7-1.0.0.jar=release/modules/ext/jutf7-1.0.0.jar
 file.reference.jxmapviewer2-2.6.jar=release/modules/ext/jxmapviewer2-2.6.jar
 file.reference.jython-standalone-2.7.2.jar=release/modules/ext/jython-standalone-2.7.2.jar
 file.reference.libphonenumber-8.12.45.jar=release/modules/ext/libphonenumber-8.12.45.jar
+file.reference.license4j-runtime-library-4.7.1.jar=release/modules/ext/license4j-runtime-library-4.7.1.jar
 file.reference.listenablefuture-1.0.jar=release/modules/ext/listenablefuture-1.0.jar
 file.reference.logback-classic-1.2.10.jar=release/modules/ext/logback-classic-1.2.10.jar
 file.reference.logback-core-1.2.10.jar=release/modules/ext/logback-core-1.2.10.jar
diff --git a/Core/nbproject/project.xml b/Core/nbproject/project.xml
index 0553a915ca..2cab2a535f 100644
--- a/Core/nbproject/project.xml
+++ b/Core/nbproject/project.xml
@@ -66,6 +66,14 @@
                         <implementation-version/>
                     </run-dependency>
                 </dependency>
+                <dependency>
+                    <code-name-base>org.netbeans.modules.keyring</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>1.41</specification-version>
+                    </run-dependency>
+                </dependency>
                 <dependency>
                     <code-name-base>org.netbeans.modules.options.api</code-name-base>
                     <build-prerequisite/>
@@ -165,14 +173,6 @@
                         <specification-version>9.29</specification-version>
                     </run-dependency>
                 </dependency>
-                <!-- <dependency>
-                    <code-name-base>org.openide.filesystems.compat8</code-name-base>
-                    <build-prerequisite/>
-                    <compile-dependency/>
-                    <run-dependency>
-                        <specification-version>9.26</specification-version>
-                    </run-dependency>
-                </dependency> -->
                 <dependency>
                     <code-name-base>org.openide.filesystems.nb</code-name-base>
                     <build-prerequisite/>
@@ -448,6 +448,10 @@
                 <runtime-relative-path>ext/checker-qual-3.33.0.jar</runtime-relative-path>
                 <binary-origin>release/modules/ext/checker-qual-3.33.0.jar</binary-origin>
             </class-path-extension>
+            <class-path-extension>
+                <runtime-relative-path>ext/commons-codec-1.11.jar</runtime-relative-path>
+                <binary-origin>release/modules/ext/commons-codec-1.11.jar</binary-origin>
+            </class-path-extension>
             <class-path-extension>
                 <runtime-relative-path>ext/commons-dbcp2-2.9.0.jar</runtime-relative-path>
                 <binary-origin>release/modules/ext/commons-dbcp2-2.9.0.jar</binary-origin>
@@ -500,6 +504,22 @@
                 <runtime-relative-path>ext/guava-32.0.1-jre.jar</runtime-relative-path>
                 <binary-origin>release/modules/ext/guava-32.0.1-jre.jar</binary-origin>
             </class-path-extension>
+            <class-path-extension>
+                <runtime-relative-path>ext/httpclient-4.5.14.jar</runtime-relative-path>
+                <binary-origin>release/modules/ext/httpclient-4.5.14.jar</binary-origin>
+            </class-path-extension>
+            <class-path-extension>
+                <runtime-relative-path>ext/httpclient-win-4.5.14.jar</runtime-relative-path>
+                <binary-origin>release/modules/ext/httpclient-win-4.5.14.jar</binary-origin>
+            </class-path-extension>
+            <class-path-extension>
+                <runtime-relative-path>ext/httpcore-4.4.16.jar</runtime-relative-path>
+                <binary-origin>release/modules/ext/httpcore-4.4.16.jar</binary-origin>
+            </class-path-extension>
+            <class-path-extension>
+                <runtime-relative-path>ext/httpmime-4.5.14.jar</runtime-relative-path>
+                <binary-origin>release/modules/ext/httpmime-4.5.14.jar</binary-origin>
+            </class-path-extension>
             <class-path-extension>
                 <runtime-relative-path>ext/icepdf-core-6.2.2.jar</runtime-relative-path>
                 <binary-origin>release/modules/ext/icepdf-core-6.2.2.jar</binary-origin>
@@ -560,6 +580,10 @@
                 <runtime-relative-path>ext/jaxb-runtime-2.3.3.jar</runtime-relative-path>
                 <binary-origin>release/modules/ext/jaxb-runtime-2.3.3.jar</binary-origin>
             </class-path-extension>
+            <class-path-extension>
+                <runtime-relative-path>ext/jdom-2.0.5-contrib.jar</runtime-relative-path>
+                <binary-origin>release/modules/ext/jdom-2.0.5-contrib.jar</binary-origin>
+            </class-path-extension>
             <class-path-extension>
                 <runtime-relative-path>ext/jdom-2.0.5.jar</runtime-relative-path>
                 <binary-origin>release/modules/ext/jdom-2.0.5.jar</binary-origin>
@@ -596,6 +620,10 @@
                 <runtime-relative-path>ext/libphonenumber-8.12.45.jar</runtime-relative-path>
                 <binary-origin>release/modules/ext/libphonenumber-8.12.45.jar</binary-origin>
             </class-path-extension>
+            <class-path-extension>
+                <runtime-relative-path>ext/license4j-runtime-library-4.7.1.jar</runtime-relative-path>
+                <binary-origin>release/modules/ext/license4j-runtime-library-4.7.1.jar</binary-origin>
+            </class-path-extension>
             <class-path-extension>
                 <runtime-relative-path>ext/listenablefuture-1.0.jar</runtime-relative-path>
                 <binary-origin>release/modules/ext/listenablefuture-1.0.jar</binary-origin>
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CTCloudException.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CTCloudException.java
new file mode 100644
index 0000000000..8b0ff55ee5
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CTCloudException.java
@@ -0,0 +1,97 @@
+/** *************************************************************************
+ ** This data and information is proprietary to, and a valuable trade secret
+ ** of, Basis Technology Corp. It is given in confidence by Basis Technology
+ ** and may only be used as permitted under the license agreement under which
+ ** it has been distributed, and in no other way.
+ **
+ ** Copyright (c) 2020 Basis Technology Corp. All rights reserved.
+ **
+ ** The technical data and information provided herein are provided with
+ ** `limited rights', and the computer software provided herein is provided
+ ** with `restricted rights' as those terms are defined in DAR and ASPR
+ ** 7-104.9(a).
+ ************************************************************************** */
+package com.basistech.df.cybertriage.autopsy.ctapi;
+
+
+import java.util.Objects;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+
+/**
+ *
+ * @author rishwanth
+ */
+
+
+public class CTCloudException extends Exception{
+    private final ErrorCode errorCode;
+    
+    public enum ErrorCode {
+        BAD_REQUEST("CT-400", "Unknown or Bad request. Please contact Basis support at " + Constants.SUPPORT_AT_CYBERTRIAGE_DOT_COM + " for help diagnosing the problem."),
+        INVALID_KEY("CT-401", "An invalid license ID was used to access CyberTriage Cloud Service. Please contact Basis support " + Constants.SUPPORT_AT_CYBERTRIAGE_DOT_COM + " for help diagnosing the problem."),
+        GATEWAY_TIMEOUT("CT-504", "Request to CyberTriage Cloud Service timed out. Please retry after some time. If issue persists, please contact Basis support at " + Constants.SUPPORT_AT_CYBERTRIAGE_DOT_COM + " for assistance."),
+        UN_AUTHORIZED("CT-403", "An authorization error occurred. Please contact Basis support " + Constants.SUPPORT_AT_CYBERTRIAGE_DOT_COM + " for help diagnosing the problem."),
+        PROXY_UNAUTHORIZED("CT-407", "Proxy authentication failed. Please validate the connection settings from the Options panel Proxy Settings."),
+        TEMP_UNAVAILABLE("CT-500", "CyberTriage Cloud Service temporarily unavailable; please try again later. If this problem persists, contact Basis support at " + Constants.SUPPORT_AT_CYBERTRIAGE_DOT_COM),
+        UNKNOWN("CT-080", "Unknown error while communicating with CyberTriage Cloud Service. If this problem persists, contact Basis support at "+ Constants.SUPPORT_AT_CYBERTRIAGE_DOT_COM +" for assistance."),
+        UNKNOWN_HOST("CT-081", "Unknown host error. If this problem persists, contact Basis support at "+ Constants.SUPPORT_AT_CYBERTRIAGE_DOT_COM +" for assistance."),
+        NETWORK_ERROR("CT-015", "Error connecting to CyberTriage Cloud.\n"
+                + "Check your firewall or proxy settings.\n"
+                + "Contact Support (support@cybertriage.com) for further assistance");
+        private final String errorcode;
+        private final String description;
+        
+        private ErrorCode(String errorcode, String description) {
+            this.errorcode = errorcode;
+            this.description = description;
+        }
+
+        public String getCode() {
+            return errorcode;
+        }
+
+        public String getDescription() {
+            return description;
+        }
+
+    }
+
+    public CTCloudException(CTCloudException.ErrorCode errorCode) {
+        super(errorCode.name());
+        this.errorCode = errorCode;
+    }
+    
+    public CTCloudException(CTCloudException.ErrorCode errorCode, Throwable throwable) {
+        super(errorCode.name(), throwable);
+        this.errorCode = errorCode;
+    }
+
+    public ErrorCode getErrorCode() {
+        return errorCode;
+    }
+
+    public String getErrorDetails() {
+        if(getErrorCode() == CTCloudException.ErrorCode.UNKNOWN && Objects.nonNull(getCause())){
+            return String.format("Malware scan error %s occurred. Please try \"Re Scan\" from the dashboard to attempt Malware scaning again. "
+                    + "\nPlease contact Basis support at %s for help if the problem presists.",
+                    StringUtils.isNotBlank(getCause().getLocalizedMessage()) ? "("+getCause().getLocalizedMessage()+")": "(Unknown)",
+                    Constants.SUPPORT_AT_CYBERTRIAGE_DOT_COM );
+        }else {
+            return getErrorCode().getDescription();
+        }
+    }
+    
+    /*
+     * Attempts to find a more specific error code than "Unknown" for the given exception.
+     */
+    public static ErrorCode parseUnknownException(Throwable throwable) {
+        
+        String stackTrace = ExceptionUtils.getStackTrace(throwable);
+        if (stackTrace.contains("UnknownHostException")) {
+            return ErrorCode.UNKNOWN_HOST;
+        }
+        
+        return ErrorCode.UNKNOWN;
+    }
+}
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/Constants.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/Constants.java
new file mode 100644
index 0000000000..3cc077c06d
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/Constants.java
@@ -0,0 +1,83 @@
+/** *************************************************************************
+ ** This data and information is proprietary to, and a valuable trade secret
+ ** of, Basis Technology Corp. It is given in confidence by Basis Technology
+ ** and may only be used as permitted under the license agreement under which
+ ** it has been distributed, and in no other way.
+ **
+ ** Copyright (c) 2016 Basis Technology Corp. All rights reserved.
+ **
+ ** The technical data and information provided herein are provided with
+ ** `limited rights', and the computer software provided herein is provided
+ ** with `restricted rights' as those terms are defined in DAR and ASPR
+ ** 7-104.9(a).
+ ************************************************************************** */
+package com.basistech.df.cybertriage.autopsy.ctapi;
+
+import com.google.common.collect.ImmutableList;
+import java.net.URI;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+// TODO take out anything sensitive or not used
+final public class Constants {
+
+    public static final String CYBER_TRIAGE = "CyberTriage";
+
+    public static final String IS_MEMORY_IMAGE = "IS_MEMORY_IMAGE";
+
+
+    public static final String SSLTEST_URL = "https://www2.cybertriage.com/ssl_test.html";
+
+
+    public static final String CT_CLOUD_SERVER = "https://rep1.cybertriage.com";
+
+    public static final String CT_CLOUD_DEV_SERVER = "https://cyber-triage-dev.appspot.com";
+    
+    /**
+     * Link to watch demo video
+     * @since 3.1.0 
+     */
+    public static final String DEMO_VIDEO_URL = "https://www.cybertriage.com/video/cyber-triage-demo-video/?utm_source=Cyber+Triage+Tool&utm_campaign=Eval+Demo+Video";
+    
+    /**
+     * Link request quote
+     * @since 3.1.0 
+     */
+    public static final String REQUEST_QUOTE_URL = "https://www.cybertriage.com/request-quote/?utm_source=Cyber+Triage+Tool&utm_campaign=Eval+Quote";
+    
+    /**
+     * Latest help document URL
+     * @since 3.2.0
+    */
+    public static final URI  USER_GUIDE_LATEST_URL = URI.create("https://docs.cybertriage.com/en/latest/?utm_source=Cyber+Triage+Tool&utm_campaign=Help+Docs");
+    
+     /**
+     * Visit website URL
+     * @since 3.1.0 
+     */
+    public static final String VISIT_WEBSITE_URL ="https://www.cybertriage.com/eval_data_202109/?utm_source=Cyber+Triage+Tool&utm_campaign=Eval+Data+Button";
+    
+ 
+    /**
+     * URL for visiting the website after the data is ingested on the dashboard. 
+     */
+    public static final String EVAL_WEBSITE_AUTO_URL = "https://www.cybertriage.com/eval_data_202109_auto/?utm_source=Cyber+Triage+Tool&utm_campaign=Eval+Data+Auto/"; //CT-4045
+
+
+    public static final String SUPPORT_AT_CYBERTRIAGE_DOT_COM = "support@cybertriage.com";
+
+    public static final String SALES_AT_CYBERTRIAGE_DOT_COM = "sales@cybertriage.com";
+
+    public final static String AUTODETECT = "Auto Detect";
+
+    public final static int RESTAPI_PORT = 9443;
+
+    public static final String INVALID_HOSTNAME_REQUEST = "Request rejected. Invalid host name. Hostname contains characters that are not allowed. \n"
+                        + "Characters that are not allowed include `~!@#$&^*(){}[]\\\\|;'\",<>/? \n"
+                        + "You may input the host IP address if the name is not resolving.";
+    public static final String INVALID_HOSTNAME_UI = "Invalid host name. Hostname contains characters that are not allowed. \n"
+                        + "Characters that are not allowed include `~!@#$&^*(){}[]\\\\|;'\",<>/?";
+    
+}
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CtApiDAO.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CtApiDAO.java
new file mode 100644
index 0000000000..66fa03b8f1
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/CtApiDAO.java
@@ -0,0 +1,83 @@
+/** *************************************************************************
+ ** This data and information is proprietary to, and a valuable trade secret
+ ** of, Basis Technology Corp. It is given in confidence by Basis Technology
+ ** and may only be used as permitted under the license agreement under which
+ ** it has been distributed, and in no other way.
+ **
+ ** Copyright (c) 2023 Basis Technology Corp. All rights reserved.
+ **
+ ** The technical data and information provided herein are provided with
+ ** `limited rights', and the computer software provided herein is provided
+ ** with `restricted rights' as those terms are defined in DAR and ASPR
+ ** 7-104.9(a).
+ ************************************************************************** */
+package com.basistech.df.cybertriage.autopsy.ctapi;
+
+import com.basistech.df.cybertriage.autopsy.ctapi.json.AuthTokenRequest;
+import com.basistech.df.cybertriage.autopsy.ctapi.json.AuthTokenResponse;
+import com.basistech.df.cybertriage.autopsy.ctapi.json.FileReputationResult;
+import com.basistech.df.cybertriage.autopsy.ctapi.json.LicenseRequest;
+import com.basistech.df.cybertriage.autopsy.ctapi.json.LicenseResponse;
+import com.basistech.df.cybertriage.autopsy.ctapi.util.CTHostIDGenerationUtil;
+import com.basistech.df.cybertriage.autopsy.ctapi.util.ObjectMapperUtil;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.util.List;
+import org.sleuthkit.autopsy.coreutils.Version;
+
+/**
+ *
+ * Data access layer for handling the CT api.
+ */
+public class CtApiDAO {
+
+    private static final String LICENSE_REQUEST_PATH = "/_ah/api/license/v1/activate";
+    private static final String AUTH_TOKEN_REQUEST_PATH = "/_ah/api/auth/v2/generate_token";
+
+    private static final CtApiDAO instance = new CtApiDAO();
+    private final ObjectMapper mapper = ObjectMapperUtil.getInstance().getDefaultObjectMapper();
+
+    private CtApiDAO() {
+    }
+
+    public static CtApiDAO getInstance() {
+        return instance;
+    }
+    
+    private static String getAppVersion() {
+        return Version.getName() + " " + Version.getVersion();
+    }
+
+    private <T> T doPost(String urlPath, Object requestBody, Class<T> responseTypeRef) throws CTCloudException {
+        return null;
+        // TODO
+    }
+
+    public LicenseResponse getLicenseInfo(String licenseString) throws CTCloudException {
+        LicenseRequest licenseRequest = new LicenseRequest()
+                .setBoostLicenseCode(licenseString)
+                .setHostId(CTHostIDGenerationUtil.generateLicenseHostID())
+                .setProduct(getAppVersion());
+        
+        return doPost(LICENSE_REQUEST_PATH, licenseRequest, LicenseResponse.class);
+
+    }
+
+    public AuthTokenResponse getAuthToken(String boostLicenseId) throws CTCloudException {
+        AuthTokenRequest authTokenRequest = new AuthTokenRequest()
+                .setAutopsyVersion(getAppVersion())
+                .setRequestFileUpload(true)
+                .setBoostLicenseId(boostLicenseId);
+        
+        return doPost(AUTH_TOKEN_REQUEST_PATH, authTokenRequest, AuthTokenResponse.class);
+    }
+
+    public List<FileReputationResult> getReputationResults(String authToken, List<String> md5Hashes) throws CTCloudException {
+        // TODO
+//        return cloudServiceApi.lookupFileResults(md5Hashes, HashTypes.md5);
+        return null;
+    }
+
+    public enum ResultType {
+        OK, SERVER_ERROR, NOT_AUTHORIZED
+    }
+}
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/ProxySettings.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/ProxySettings.java
new file mode 100644
index 0000000000..d341b99e57
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/ProxySettings.java
@@ -0,0 +1,443 @@
+/** *************************************************************************
+ ** This data and information is proprietary to, and a valuable trade secret
+ ** of, Basis Technology Corp. It is given in confidence by Basis Technology
+ ** and may only be used as permitted under the license agreement under which
+ ** it has been distributed, and in no other way.
+ **
+ ** Copyright (c) 2023 Basis Technology Corp. All rights reserved.
+ **
+ ** The technical data and information provided herein are provided with
+ ** `limited rights', and the computer software provided herein is provided
+ ** with `restricted rights' as those terms are defined in DAR and ASPR
+ ** 7-104.9(a).
+ ************************************************************************** */
+package com.basistech.df.cybertriage.autopsy.ctapi;
+
+
+
+import java.net.*;
+import java.util.*;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.prefs.PreferenceChangeListener;
+import java.util.prefs.Preferences;
+import org.netbeans.api.keyring.Keyring;
+import org.openide.util.*;
+import org.openide.util.lookup.ServiceProvider;
+
+/**
+ * Taken from https://raw.githubusercontent.com/apache/netbeans/master/platform/o.n.core/src/org/netbeans/core/ProxySettings.java
+ * @author Jiri Rechtacek
+ */
+public class ProxySettings {
+    
+    public static final String PROXY_HTTP_HOST = "proxyHttpHost";   // NOI18N
+    public static final String PROXY_HTTP_PORT = "proxyHttpPort";   // NOI18N
+    public static final String PROXY_HTTPS_HOST = "proxyHttpsHost"; // NOI18N
+    public static final String PROXY_HTTPS_PORT = "proxyHttpsPort"; // NOI18N
+    public static final String PROXY_SOCKS_HOST = "proxySocksHost"; // NOI18N
+    public static final String PROXY_SOCKS_PORT = "proxySocksPort"; // NOI18N
+    public static final String NOT_PROXY_HOSTS = "proxyNonProxyHosts";  // NOI18N
+    public static final String PROXY_TYPE = "proxyType";                // NOI18N
+    public static final String USE_PROXY_AUTHENTICATION = "useProxyAuthentication"; // NOI18N
+    public static final String PROXY_AUTHENTICATION_USERNAME = "proxyAuthenticationUsername";   // NOI18N
+    public static final String PROXY_AUTHENTICATION_PASSWORD = "proxyAuthenticationPassword";   // NOI18N
+    public static final String USE_PROXY_ALL_PROTOCOLS = "useProxyAllProtocols";    // NOI18N
+    public static final String DIRECT = "DIRECT";   // NOI18N
+    public static final String PAC = "PAC";     // NOI18N
+    
+    public static final String SYSTEM_PROXY_HTTP_HOST = "systemProxyHttpHost";      // NOI18N
+    public static final String SYSTEM_PROXY_HTTP_PORT = "systemProxyHttpPort";      // NOI18N
+    public static final String SYSTEM_PROXY_HTTPS_HOST = "systemProxyHttpsHost";    // NOI18N
+    public static final String SYSTEM_PROXY_HTTPS_PORT = "systemProxyHttpsPort";    // NOI18N
+    public static final String SYSTEM_PROXY_SOCKS_HOST = "systemProxySocksHost";    // NOI18N
+    public static final String SYSTEM_PROXY_SOCKS_PORT = "systemProxySocksPort";    // NOI18N
+    public static final String SYSTEM_NON_PROXY_HOSTS = "systemProxyNonProxyHosts"; // NOI18N
+    public static final String SYSTEM_PAC = "systemPAC";                            // NOI18N
+    
+    // Only for testing purpose (Test connection in General options panel)
+    public static final String TEST_SYSTEM_PROXY_HTTP_HOST = "testSystemProxyHttpHost"; // NOI18N
+    public static final String TEST_SYSTEM_PROXY_HTTP_PORT = "testSystemProxyHttpPort"; // NOI18N
+    public static final String HTTP_CONNECTION_TEST_URL = "https://netbeans.apache.org";// NOI18N
+    
+    private static String presetNonProxyHosts;
+
+    /** No proxy is used to connect. */
+    public static final int DIRECT_CONNECTION = 0;
+    
+    /** Proxy setting is automatically detect in OS. */
+    public static final int AUTO_DETECT_PROXY = 1; // as default
+    
+    /** Manually set proxy host and port. */
+    public static final int MANUAL_SET_PROXY = 2;
+    
+    /** Proxy PAC file automatically detect in OS. */
+    public static final int AUTO_DETECT_PAC = 3;
+    
+    /** Proxy PAC file manually set. */
+    public static final int MANUAL_SET_PAC = 4;
+    
+    private static final Logger LOGGER = Logger.getLogger(ProxySettings.class.getName());
+    
+    private static Preferences getPreferences() {
+        return NbPreferences.forModule (ProxySettings.class);
+    }
+    
+    
+    public static String getHttpHost () {
+        return normalizeProxyHost (getPreferences ().get (PROXY_HTTP_HOST, ""));
+    }
+    
+    public static String getHttpPort () {
+        return getPreferences ().get (PROXY_HTTP_PORT, "");
+    }
+    
+    public static String getHttpsHost () {
+        if (useProxyAllProtocols ()) {
+            return getHttpHost ();
+        } else {
+            return getPreferences ().get (PROXY_HTTPS_HOST, "");
+        }
+    }
+    
+    public static String getHttpsPort () {
+        if (useProxyAllProtocols ()) {
+            return getHttpPort ();
+        } else {
+            return getPreferences ().get (PROXY_HTTPS_PORT, "");
+        }
+    }
+    
+    public static String getSocksHost () {
+        if (useProxyAllProtocols ()) {
+            return getHttpHost ();
+        } else {
+            return getPreferences ().get (PROXY_SOCKS_HOST, "");
+        }
+    }
+    
+    public static String getSocksPort () {
+        if (useProxyAllProtocols ()) {
+            return getHttpPort ();
+        } else {
+            return getPreferences ().get (PROXY_SOCKS_PORT, "");
+        }
+    }
+    
+    public static String getNonProxyHosts () {
+        String hosts = getPreferences ().get (NOT_PROXY_HOSTS, getDefaultUserNonProxyHosts ());
+        return compactNonProxyHosts(hosts);
+    }
+    
+    public static int getProxyType () {
+        int type = getPreferences ().getInt (PROXY_TYPE, AUTO_DETECT_PROXY);
+        if (AUTO_DETECT_PROXY == type) {
+            type = ProxySettings.getSystemPac() != null ? AUTO_DETECT_PAC : AUTO_DETECT_PROXY;
+        }
+        return type;
+    }
+    
+    
+    public static String getSystemHttpHost() {
+        return getPreferences().get(SYSTEM_PROXY_HTTP_HOST, "");
+    }
+    
+    public static String getSystemHttpPort() {
+        return getPreferences().get(SYSTEM_PROXY_HTTP_PORT, "");
+    }
+    
+    public static String getSystemHttpsHost() {
+        return getPreferences().get(SYSTEM_PROXY_HTTPS_HOST, "");
+    }
+    
+    public static String getSystemHttpsPort() {
+        return getPreferences().get(SYSTEM_PROXY_HTTPS_PORT, "");
+    }
+    
+    public static String getSystemSocksHost() {
+        return getPreferences().get(SYSTEM_PROXY_SOCKS_HOST, "");
+    }
+    
+    public static String getSystemSocksPort() {
+        return getPreferences().get(SYSTEM_PROXY_SOCKS_PORT, "");
+    }
+    
+    public static String getSystemNonProxyHosts() {
+        return getPreferences().get(SYSTEM_NON_PROXY_HOSTS, getModifiedNonProxyHosts(""));
+    }
+    
+    public static String getSystemPac() {
+        return getPreferences().get(SYSTEM_PAC, null);
+    }
+    
+    
+    public static String getTestSystemHttpHost() {
+        return getPreferences().get(TEST_SYSTEM_PROXY_HTTP_HOST, "");
+    }
+    
+    public static String getTestSystemHttpPort() {
+        return getPreferences().get(TEST_SYSTEM_PROXY_HTTP_PORT, "");
+    }
+    
+    
+    public static boolean useAuthentication () {
+        return getPreferences ().getBoolean (USE_PROXY_AUTHENTICATION, false);
+    }
+    
+    public static boolean useProxyAllProtocols () {
+        return getPreferences ().getBoolean (USE_PROXY_ALL_PROTOCOLS, false);
+    }
+    
+    public static String getAuthenticationUsername () {
+        return getPreferences ().get (PROXY_AUTHENTICATION_USERNAME, "");
+    }
+    
+    public static char[] getAuthenticationPassword () {
+        String old = getPreferences().get(PROXY_AUTHENTICATION_PASSWORD, null);
+        if (old != null) {
+            getPreferences().remove(PROXY_AUTHENTICATION_PASSWORD);
+            setAuthenticationPassword(old.toCharArray());
+        }
+        char[] pwd = Keyring.read(PROXY_AUTHENTICATION_PASSWORD);
+        return pwd != null ? pwd : new char[0];
+    }
+    
+    public static void setAuthenticationPassword(char[] password) {
+        Keyring.save(ProxySettings.PROXY_AUTHENTICATION_PASSWORD, password,
+                // XXX consider including getHttpHost and/or getHttpsHost
+                NbBundle.getMessage(ProxySettings.class, "ProxySettings.password.description"));    // NOI18N
+    }
+
+    public static void addPreferenceChangeListener (PreferenceChangeListener l) {
+        getPreferences ().addPreferenceChangeListener (l);
+    }
+    
+    public static void removePreferenceChangeListener (PreferenceChangeListener l) {
+        getPreferences ().removePreferenceChangeListener (l);
+    }
+    
+    private static String getPresetNonProxyHosts () {
+        if (presetNonProxyHosts == null) {
+            presetNonProxyHosts = System.getProperty ("http.nonProxyHosts", "");    // NOI18N
+        }
+        return presetNonProxyHosts;
+    }
+    
+    private static String getDefaultUserNonProxyHosts () {
+        return getModifiedNonProxyHosts (getSystemNonProxyHosts ());
+    }
+
+  
+    private static String concatProxies(String... proxies) {
+        StringBuilder sb = new StringBuilder();
+        for (String n : proxies) {
+            if (n == null) {
+                continue;
+            }
+            n = n.trim();
+            if (n.isEmpty()) {
+                continue;
+            }
+            if (sb.length() > 0 && sb.charAt(sb.length() - 1) != '|') { // NOI18N
+                if (!n.startsWith("|")) {   // NOI18N
+                    sb.append('|');         // NOI18N
+                }
+            }
+            sb.append(n);
+        }
+        return sb.toString();
+    }
+
+    private static String getModifiedNonProxyHosts (String systemPreset) {
+        String fromSystem = systemPreset.replace (";", "|").replace (",", "|"); //NOI18N
+        String fromUser = getPresetNonProxyHosts () == null ? "" : getPresetNonProxyHosts ().replace (";", "|").replace (",", "|"); //NOI18N
+        if (Utilities.isWindows ()) {
+            fromSystem = addReguralToNonProxyHosts (fromSystem);
+        }
+        final String staticNonProxyHosts = NbBundle.getMessage(ProxySettings.class, "StaticNonProxyHosts"); // NOI18N
+        String nonProxy = concatProxies(fromUser, fromSystem, staticNonProxyHosts); // NOI18N
+        String localhost;
+        try {
+            localhost = InetAddress.getLocalHost().getHostName();
+            if (!"localhost".equals(localhost)) { // NOI18N
+                nonProxy = nonProxy + "|" + localhost; // NOI18N
+            } else {
+                // Avoid this error when hostname == localhost:
+                // Error in http.nonProxyHosts system property:  sun.misc.REException: localhost is a duplicate
+            }
+        }
+        catch (UnknownHostException e) {
+            // OK. Sometimes a hostname is assigned by DNS, but a computer
+            // is later pulled off the network. It may then produce a bogus
+            // name for itself which can't actually be resolved. Normally
+            // "localhost" is aliased to 127.0.0.1 anyway.
+        }
+        /* per Milan's agreement it's removed. See issue #89868
+        try {
+            String localhost2 = InetAddress.getLocalHost().getCanonicalHostName();
+            if (!"localhost".equals(localhost2) && !localhost2.equals(localhost)) { // NOI18N
+                nonProxy = nonProxy + "|" + localhost2; // NOI18N
+            } else {
+                // Avoid this error when hostname == localhost:
+                // Error in http.nonProxyHosts system property:  sun.misc.REException: localhost is a duplicate
+            }
+        }
+        catch (UnknownHostException e) {
+            // OK. Sometimes a hostname is assigned by DNS, but a computer
+            // is later pulled off the network. It may then produce a bogus
+            // name for itself which can't actually be resolved. Normally
+            // "localhost" is aliased to 127.0.0.1 anyway.
+        }
+         */
+        return compactNonProxyHosts (nonProxy);
+    }
+
+
+    // avoid duplicate hosts
+    private static String compactNonProxyHosts (String hosts) {
+        StringTokenizer st = new StringTokenizer(hosts, ","); //NOI18N
+        StringBuilder nonProxyHosts = new StringBuilder();
+        while (st.hasMoreTokens()) {
+            String h = st.nextToken().trim();
+            if (h.length() == 0) {
+                continue;
+            }
+            if (nonProxyHosts.length() > 0) {
+                nonProxyHosts.append("|"); // NOI18N
+            }
+            nonProxyHosts.append(h);
+        }
+        st = new StringTokenizer (nonProxyHosts.toString(), "|"); //NOI18N
+        Set<String> set = new HashSet<String> (); 
+        StringBuilder compactedProxyHosts = new StringBuilder();
+        while (st.hasMoreTokens ()) {
+            String t = st.nextToken ();
+            if (set.add (t.toLowerCase (Locale.US))) {
+                if (compactedProxyHosts.length() > 0) {
+                    compactedProxyHosts.append('|');    // NOI18N
+                }
+                compactedProxyHosts.append(t);
+            }
+        }
+        return compactedProxyHosts.toString();
+    }
+    
+    private static String addReguralToNonProxyHosts (String nonProxyHost) {
+        StringTokenizer st = new StringTokenizer (nonProxyHost, "|");   // NOI18N
+        StringBuilder reguralProxyHosts = new StringBuilder();
+        while (st.hasMoreTokens ()) {
+            String t = st.nextToken ();
+            if (t.indexOf ('*') == -1) { //NOI18N
+                t = t + '*'; //NOI18N
+            }
+            if (reguralProxyHosts.length() > 0) 
+                reguralProxyHosts.append('|');  // NOI18N
+            reguralProxyHosts.append(t);
+        }
+
+        return reguralProxyHosts.toString();
+    }
+
+    public static String normalizeProxyHost (String proxyHost) {
+        if (proxyHost.toLowerCase (Locale.US).startsWith ("http://")) { // NOI18N
+            return proxyHost.substring (7, proxyHost.length ());
+        } else {
+            return proxyHost;
+        }
+    }
+    
+    private static InetSocketAddress analyzeProxy(URI uri) {
+        Parameters.notNull("uri", uri);     // NOI18N
+        List<Proxy> proxies = ProxySelector.getDefault().select(uri);
+        assert proxies != null : "ProxySelector cannot return null for " + uri;     // NOI18N
+        assert !proxies.isEmpty() : "ProxySelector cannot return empty list for " + uri;    // NOI18N
+        String protocol = uri.getScheme();
+        Proxy p = proxies.get(0);
+        if (Proxy.Type.DIRECT == p.type()) {
+            // return null for DIRECT proxy
+            return null;
+        }
+        if (protocol == null
+                || ((protocol.startsWith("http") || protocol.equals("ftp")) && Proxy.Type.HTTP == p.type()) // NOI18N
+                || !(protocol.startsWith("http") || protocol.equals("ftp"))) {  // NOI18N
+            if (p.address() instanceof InetSocketAddress) {
+                // check is
+                //assert ! ((InetSocketAddress) p.address()).isUnresolved() : p.address() + " must be resolved address.";
+                return (InetSocketAddress) p.address();
+            } else {
+                LOGGER.log(Level.INFO, p.address() + " is not instanceof InetSocketAddress but " + p.address().getClass()); // NOI18N
+                return null;
+            }
+        } else {
+            return null;
+        }
+    }
+    
+    public static void reload() {
+        Reloader reloader = Lookup.getDefault().lookup(Reloader.class);
+        reloader.reload();
+    }
+
+    @ServiceProvider(service = NetworkSettings.ProxyCredentialsProvider.class, position = 1000)
+    public static class NbProxyCredentialsProvider extends NetworkSettings.ProxyCredentialsProvider {
+
+        @Override
+        public String getProxyHost(URI u) {
+            if (getPreferences() == null) {
+                return null;
+            }
+            InetSocketAddress sa = analyzeProxy(u);
+            return sa == null ? null : sa.getHostName();
+        }
+
+        @Override
+        public String getProxyPort(URI u) {
+            if (getPreferences() == null) {
+                return null;
+            }
+            InetSocketAddress sa = analyzeProxy(u);
+            return sa == null ? null : Integer.toString(sa.getPort());
+        }
+
+        @Override
+        protected String getProxyUserName(URI u) {
+            if (getPreferences() == null) {
+                return null;
+            }
+            return ProxySettings.getAuthenticationUsername();
+        }
+
+        @Override
+        protected char[] getProxyPassword(URI u) {
+            if (getPreferences() == null) {
+                return null;
+            }
+            return ProxySettings.getAuthenticationPassword();
+        }
+
+        @Override
+        protected boolean isProxyAuthentication(URI u) {
+            if (getPreferences() == null) {
+                return false;
+            }
+            return getPreferences().getBoolean(USE_PROXY_AUTHENTICATION, false);
+        }
+
+    }
+    
+    /** A bridge between <code>o.n.core</code> and <code>core.network</code>.
+     * An implementation of this class brings a facility to reload Network Proxy Settings
+     * from underlying OS.
+     * The module <code>core.network</code> provides a implementation which may be accessible
+     * via <code>Lookup.getDefault()</code>. It's not guaranteed any implementation is found on all distribution. 
+     * 
+     * @since 3.40
+     */
+    public abstract static class Reloader {
+        
+        /** Reloads Network Proxy Settings from underlying system.
+         *
+         */
+        public abstract void reload();
+    }
+}
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/AuthTokenRequest.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/AuthTokenRequest.java
new file mode 100644
index 0000000000..6cda146c84
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/AuthTokenRequest.java
@@ -0,0 +1,59 @@
+/** *************************************************************************
+ ** This data and information is proprietary to, and a valuable trade secret
+ ** of, Basis Technology Corp. It is given in confidence by Basis Technology
+ ** and may only be used as permitted under the license agreement under which
+ ** it has been distributed, and in no other way.
+ **
+ ** Copyright (c) 2023 Basis Technology Corp. All rights reserved.
+ **
+ ** The technical data and information provided herein are provided with
+ ** `limited rights', and the computer software provided herein is provided
+ ** with `restricted rights' as those terms are defined in DAR and ASPR
+ ** 7-104.9(a).
+ ************************************************************************** */
+package com.basistech.df.cybertriage.autopsy.ctapi.json;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * POJO for an auth token request.
+ */
+public class AuthTokenRequest {
+
+    @JsonProperty("autopsy_version")
+    private String autopsyVersion;
+
+    @JsonProperty("boost_license_id")
+    private String boostLicenseId;
+
+    @JsonProperty("requestFileUpload")
+    private boolean requestFileUpload;
+
+    public String getAutopsyVersion() {
+        return autopsyVersion;
+    }
+
+    public AuthTokenRequest setAutopsyVersion(String autopsyVersion) {
+        this.autopsyVersion = autopsyVersion;
+        return this;
+    }
+
+    public String getBoostLicenseId() {
+        return boostLicenseId;
+    }
+
+    public AuthTokenRequest setBoostLicenseId(String boostLicenseId) {
+        this.boostLicenseId = boostLicenseId;
+        return this;
+    }
+
+    public boolean isRequestFileUpload() {
+        return requestFileUpload;
+    }
+
+    public AuthTokenRequest setRequestFileUpload(boolean requestFileUpload) {
+        this.requestFileUpload = requestFileUpload;
+        return this;
+    }
+
+}
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/AuthTokenResponse.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/AuthTokenResponse.java
new file mode 100644
index 0000000000..a010bbe13c
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/AuthTokenResponse.java
@@ -0,0 +1,85 @@
+/** *************************************************************************
+ ** This data and information is proprietary to, and a valuable trade secret
+ ** of, Basis Technology Corp. It is given in confidence by Basis Technology
+ ** and may only be used as permitted under the license agreement under which
+ ** it has been distributed, and in no other way.
+ **
+ ** Copyright (c) 2023 Basis Technology Corp. All rights reserved.
+ **
+ ** The technical data and information provided herein are provided with
+ ** `limited rights', and the computer software provided herein is provided
+ ** with `restricted rights' as those terms are defined in DAR and ASPR
+ ** 7-104.9(a).
+ ************************************************************************** */
+package com.basistech.df.cybertriage.autopsy.ctapi.json;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.time.ZonedDateTime;
+
+/**
+ * POJO for an auth token response.
+ */
+public class AuthTokenResponse {
+    private final String token;
+    private final String apiKey;
+    private final Long hashLookupCount;
+    private final Long hashLookupLimit;
+    private final Long fileUploadLimit;
+    private final Long fileUploadCount;
+    private final String fileUploadUrl;
+    private final ZonedDateTime expiration;
+
+    @JsonCreator
+    public AuthTokenResponse(
+            @JsonProperty("token") String token,
+            @JsonProperty("api_key") String apiKey,
+            @JsonProperty("hashLookupCount") Long hashLookupCount,
+            @JsonProperty("hashLookupLimit") Long hashLookupLimit,
+            @JsonProperty("fileUploadLimit") Long fileUploadLimit,
+            @JsonProperty("fileUploadCount") Long fileUploadCount,
+            @JsonProperty("fileUploadUrl") String fileUploadUrl,
+            @JsonProperty("expiration") ZonedDateTime expiration
+    ) {
+        this.token = token;
+        this.apiKey = apiKey;
+        this.hashLookupCount = hashLookupCount;
+        this.hashLookupLimit = hashLookupLimit;
+        this.fileUploadLimit = fileUploadLimit;
+        this.fileUploadCount = fileUploadCount;
+        this.fileUploadUrl = fileUploadUrl;
+        this.expiration = expiration;
+    }
+
+    public String getToken() {
+        return token;
+    }
+
+    public String getApiKey() {
+        return apiKey;
+    }
+
+    public Long getHashLookupCount() {
+        return hashLookupCount;
+    }
+
+    public Long getHashLookupLimit() {
+        return hashLookupLimit;
+    }
+
+    public Long getFileUploadLimit() {
+        return fileUploadLimit;
+    }
+
+    public Long getFileUploadCount() {
+        return fileUploadCount;
+    }
+
+    public String getFileUploadUrl() {
+        return fileUploadUrl;
+    }
+
+    public ZonedDateTime getExpiration() {
+        return expiration;
+    }
+}
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/BoostLicenseResponse.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/BoostLicenseResponse.java
new file mode 100644
index 0000000000..ce213aeba4
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/BoostLicenseResponse.java
@@ -0,0 +1,59 @@
+/** *************************************************************************
+ ** This data and information is proprietary to, and a valuable trade secret
+ ** of, Basis Technology Corp. It is given in confidence by Basis Technology
+ ** and may only be used as permitted under the license agreement under which
+ ** it has been distributed, and in no other way.
+ **
+ ** Copyright (c) 2023 Basis Technology Corp. All rights reserved.
+ **
+ ** The technical data and information provided herein are provided with
+ ** `limited rights', and the computer software provided herein is provided
+ ** with `restricted rights' as those terms are defined in DAR and ASPR
+ ** 7-104.9(a).
+ ************************************************************************** */
+package com.basistech.df.cybertriage.autopsy.ctapi.json;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * POJO for a boost license response object that is a part of the license
+ * response.
+ */
+public class BoostLicenseResponse {
+
+    private final String version;
+    private final String iv;
+    private final String encryptedKey;
+    private final String encryptedJson;
+
+    @JsonCreator
+    public BoostLicenseResponse(
+            @JsonProperty("version") String version,
+            @JsonProperty("iv") String iv,
+            @JsonProperty("encryptedKey") String encryptedKey,
+            @JsonProperty("encryptedJson") String encryptedJson) {
+
+        this.version = version;
+        this.iv = iv;
+        this.encryptedKey = encryptedKey;
+        this.encryptedJson = encryptedJson;
+    }
+
+    public String getVersion() {
+        return version;
+    }
+
+    public String getIv() {
+        return iv;
+    }
+
+    public String getEncryptedKey() {
+        return encryptedKey;
+    }
+
+    public String getEncryptedJson() {
+        return encryptedJson;
+    }
+
+}
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/CTScore.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/CTScore.java
new file mode 100644
index 0000000000..fbb5665fba
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/CTScore.java
@@ -0,0 +1,77 @@
+/** *************************************************************************
+ ** This data and information is proprietary to, and a valuable trade secret
+ ** of, Basis Technology Corp. It is given in confidence by Basis Technology
+ ** and may only be used as permitted under the license agreement under which
+ ** it has been distributed, and in no other way.
+ **
+ ** Copyright (c) 2014 - 2016 Basis Technology Corp. All rights reserved.
+ **
+ ** The technical data and information provided herein are provided with
+ ** `limited rights', and the computer software provided herein is provided
+ ** with `restricted rights' as those terms are defined in DAR and ASPR
+ ** 7-104.9(a).
+ ************************************************************************** */
+package com.basistech.df.cybertriage.autopsy.ctapi.json;
+
+import com.google.common.base.MoreObjects;
+import static com.google.common.base.Preconditions.checkArgument;
+import org.sleuthkit.datamodel.Score;
+import org.sleuthkit.datamodel.Score.Priority;
+import org.sleuthkit.datamodel.Score.Significance;
+
+/**
+ *
+ * Score class represents a conclusion and the relative confidence in the conclusion about 
+ * a subject. A subject may be an Item, a category/analysis result etc. 
+ * @since 1.7.0
+ * 
+ */
+public enum CTScore {
+    
+    /*
+    Enum names without method defaults to AUTO
+    NOTABLE -> NOTABLE
+    */
+    
+    // Unknown None
+    UNKNOWN(new Score(Significance.UNKNOWN, Priority.NORMAL)),
+    // GOOD_MEDIUM
+    LIKELY_NONE(new Score(Significance.LIKELY_NONE, Priority.NORMAL)),
+    // SUSPICIOUS_HIGH / BAD_MEDIUM
+    LIKELY_NOTABLE(new Score(Significance.LIKELY_NOTABLE, Priority.NORMAL)),
+    // GOOD_HIGH
+    NONE(new Score(Significance.NONE, Priority.NORMAL)),
+    // BAD_HIGH
+    NOTABLE(new Score(Significance.NOTABLE, Priority.NORMAL)),
+    // SUSPICIOUS (User flagged)
+    LIKELY_NOTABLE_MANUAL(new Score(Significance.LIKELY_NOTABLE, Priority.OVERRIDE)),
+    // Good (User flagged)
+    NONE_MANUAL(new Score(Significance.NONE, Priority.OVERRIDE)),
+    // Bad (User flagged)
+    NOTABLE_MANUAL(new Score(Significance.NOTABLE, Priority.OVERRIDE));
+    
+    
+    private final Score tskScore;
+
+    /**
+     * Create a CTScore instance based on score
+     * @param tskScore
+     */
+    private CTScore(Score tskScore) {
+        
+        checkArgument(tskScore.getSignificance() == Significance.UNKNOWN ? tskScore.getPriority() == Priority.NORMAL : true, "Unknown Conclusions expects no (NORMAL) priority");
+        this.tskScore = tskScore;
+    }
+    
+    public Score getTskCore() {
+        return tskScore;
+    }
+    
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("Method Category", tskScore.getPriority())
+                .add("Significance", tskScore.getSignificance()).toString();
+    }
+    
+}
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/DecryptedLicenseResponse.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/DecryptedLicenseResponse.java
new file mode 100644
index 0000000000..ec9e74a2ed
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/DecryptedLicenseResponse.java
@@ -0,0 +1,87 @@
+/** *************************************************************************
+ ** This data and information is proprietary to, and a valuable trade secret
+ ** of, Basis Technology Corp. It is given in confidence by Basis Technology
+ ** and may only be used as permitted under the license agreement under which
+ ** it has been distributed, and in no other way.
+ **
+ ** Copyright (c) 2023 Basis Technology Corp. All rights reserved.
+ **
+ ** The technical data and information provided herein are provided with
+ ** `limited rights', and the computer software provided herein is provided
+ ** with `restricted rights' as those terms are defined in DAR and ASPR
+ ** 7-104.9(a).
+ ************************************************************************** */
+package com.basistech.df.cybertriage.autopsy.ctapi.json;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.time.ZonedDateTime;
+
+/**
+ * POJO for after encrypted boost license has been decrypted.
+ */
+public class DecryptedLicenseResponse {
+
+    private final String boostLicenseId;
+    private final String licenseHostId;
+    private final ZonedDateTime expirationDate;
+    private final Long hashLookups;
+    private final Long fileUploads;
+    private final ZonedDateTime activationTime;
+    private final String product;
+    private final String limitType;
+
+    @JsonCreator
+    public DecryptedLicenseResponse(
+            @JsonProperty("boostLicenseId") String boostLicenseId,
+            @JsonProperty("licenseHostId") String licenseHostId,
+            @JsonProperty("expirationDate") ZonedDateTime expirationDate,
+            @JsonProperty("hashLookups") Long hashLookups,
+            @JsonProperty("fileUploads") Long fileUploads,
+            @JsonProperty("activationTime") ZonedDateTime activationTime,
+            @JsonProperty("product") String product,
+            @JsonProperty("limitType") String limitType
+    ) {
+        this.boostLicenseId = boostLicenseId;
+        this.licenseHostId = licenseHostId;
+        this.expirationDate = expirationDate;
+        this.hashLookups = hashLookups;
+        this.fileUploads = fileUploads;
+        this.activationTime = activationTime;
+        this.product = product;
+        this.limitType = limitType;
+    }
+
+    public String getBoostLicenseId() {
+        return boostLicenseId;
+    }
+
+    public String getLicenseHostId() {
+        return licenseHostId;
+    }
+
+    public Long getHashLookups() {
+        return hashLookups;
+    }
+
+    public Long getFileUploads() {
+        return fileUploads;
+    }
+
+    public ZonedDateTime getActivationTime() {
+        return activationTime;
+    }
+
+    public String getProduct() {
+        return product;
+    }
+
+    public String getLimitType() {
+        return limitType;
+    }
+
+    public ZonedDateTime getExpirationDate() {
+        return expirationDate;
+    }
+
+}
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/FileReputationResult.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/FileReputationResult.java
new file mode 100644
index 0000000000..53e604e381
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/FileReputationResult.java
@@ -0,0 +1,123 @@
+/** *************************************************************************
+ ** This data and information is proprietary to, and a valuable trade secret
+ ** of, Basis Technology Corp. It is given in confidence by Basis Technology
+ ** and may only be used as permitted under the license agreement under which
+ ** it has been distributed, and in no other way.
+ **
+ ** Copyright (c) 2023 Basis Technology Corp. All rights reserved.
+ **
+ ** The technical data and information provided herein are provided with
+ ** `limited rights', and the computer software provided herein is provided
+ ** with `restricted rights' as those terms are defined in DAR and ASPR
+ ** 7-104.9(a).
+ ************************************************************************** */
+package com.basistech.df.cybertriage.autopsy.ctapi.json;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.time.ZonedDateTime;
+import java.util.List;
+
+/**
+ * A file reputation result regarding malware status.
+ */
+public class FileReputationResult {
+
+    public static enum Status {
+        FOUND,
+        NOT_FOUND,
+        ERROR,
+        LIMITS_EXCEEDED,
+        BEING_SCANNED;
+    }
+
+    public enum CorrelationFrequency {
+        UNIQUE,
+        RARE,
+        COMMON;
+    }
+
+    private final String malwareDescription;
+    private final Status status;
+    private final CTScore score;
+    private final String md5Hash;
+    private final String sha1Hash;
+    private final ZonedDateTime firstScanDate;
+    private final ZonedDateTime lastScanDate;
+    private final List<MetadataLabel> metadata;
+    private final String statusDescription;
+    private final CorrelationFrequency frequency;
+    private final String frequencyDescription;
+
+    @JsonCreator
+    public FileReputationResult(
+            @JsonProperty("malwareDescription") String malwareDescription,
+            @JsonProperty("status") Status status,
+            @JsonProperty("score") CTScore score,
+            @JsonProperty("md5Hash") String md5Hash,
+            @JsonProperty("sha1Hash") String sha1Hash,
+            @JsonProperty("firstScanDate") ZonedDateTime firstScanDate,
+            @JsonProperty("lastScanDate") ZonedDateTime lastScanDate,
+            @JsonProperty("metadata") List<MetadataLabel> metadata,
+            @JsonProperty("statusDescription") String statusDescription,
+            @JsonProperty("frequency") CorrelationFrequency frequency,
+            @JsonProperty("frequencyDescription") String frequencyDescription
+    ) {
+        this.malwareDescription = malwareDescription;
+        this.status = status;
+        this.score = score;
+        this.md5Hash = md5Hash;
+        this.sha1Hash = sha1Hash;
+        this.firstScanDate = firstScanDate;
+        this.lastScanDate = lastScanDate;
+        this.metadata = metadata;
+        this.statusDescription = statusDescription;
+        this.frequency = frequency;
+        this.frequencyDescription = frequencyDescription;
+    }
+
+    public String getMalwareDescription() {
+        return malwareDescription;
+    }
+
+    public Status getStatus() {
+        return status;
+    }
+
+    public CTScore getScore() {
+        return score;
+    }
+
+    public String getMd5Hash() {
+        return md5Hash;
+    }
+
+    public String getSha1Hash() {
+        return sha1Hash;
+    }
+
+    public ZonedDateTime getFirstScanDate() {
+        return firstScanDate;
+    }
+
+    public ZonedDateTime getLastScanDate() {
+        return lastScanDate;
+    }
+
+    public List<MetadataLabel> getMetadata() {
+        return metadata;
+    }
+
+    public String getStatusDescription() {
+        return statusDescription;
+    }
+
+    public CorrelationFrequency getFrequency() {
+        return frequency;
+    }
+
+    public String getFrequencyDescription() {
+        return frequencyDescription;
+    }
+
+}
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/LicenseInfo.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/LicenseInfo.java
new file mode 100644
index 0000000000..8045686133
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/LicenseInfo.java
@@ -0,0 +1,45 @@
+/** *************************************************************************
+ ** This data and information is proprietary to, and a valuable trade secret
+ ** of, Basis Technology Corp. It is given in confidence by Basis Technology
+ ** and may only be used as permitted under the license agreement under which
+ ** it has been distributed, and in no other way.
+ **
+ ** Copyright (c) 2023 Basis Technology Corp. All rights reserved.
+ **
+ ** The technical data and information provided herein are provided with
+ ** `limited rights', and the computer software provided herein is provided
+ ** with `restricted rights' as those terms are defined in DAR and ASPR
+ ** 7-104.9(a).
+ ************************************************************************** */
+package com.basistech.df.cybertriage.autopsy.ctapi.json;
+
+/**
+ * Contains license info and decrypted boost license.
+ */
+public class LicenseInfo {
+    private final LicenseResponse licenseResponse;
+    private final DecryptedLicenseResponse decryptedLicense;
+
+    public LicenseInfo(LicenseResponse licenseResponse, DecryptedLicenseResponse decryptedLicense) {
+        this.licenseResponse = licenseResponse;
+        this.decryptedLicense = decryptedLicense;
+    }
+
+    public LicenseResponse getLicenseResponse() {
+        return licenseResponse;
+    }
+
+    public DecryptedLicenseResponse getDecryptedLicense() {
+        return decryptedLicense;
+    }
+    
+    // TODO
+    public String getUser() {
+        return "TBD";
+    }
+    
+    // TODO
+    public String getEmail() {
+        return "TBD";
+    }
+}
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/LicenseRequest.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/LicenseRequest.java
new file mode 100644
index 0000000000..c87878596f
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/LicenseRequest.java
@@ -0,0 +1,59 @@
+/** *************************************************************************
+ ** This data and information is proprietary to, and a valuable trade secret
+ ** of, Basis Technology Corp. It is given in confidence by Basis Technology
+ ** and may only be used as permitted under the license agreement under which
+ ** it has been distributed, and in no other way.
+ **
+ ** Copyright (c) 2023 Basis Technology Corp. All rights reserved.
+ **
+ ** The technical data and information provided herein are provided with
+ ** `limited rights', and the computer software provided herein is provided
+ ** with `restricted rights' as those terms are defined in DAR and ASPR
+ ** 7-104.9(a).
+ ************************************************************************** */
+package com.basistech.df.cybertriage.autopsy.ctapi.json;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * POJO for license request information.
+ */
+public class LicenseRequest {
+    @JsonProperty("host_id")
+    private String hostId;
+    
+    @JsonProperty("boost_license_code")
+    private String boostLicenseCode;
+    
+    @JsonProperty("product")
+    private String product;
+
+    public String getHostId() {
+        return hostId;
+    }
+
+    public LicenseRequest setHostId(String hostId) {
+        this.hostId = hostId;
+        return this;
+    }
+
+    public String getBoostLicenseCode() {
+        return boostLicenseCode;
+    }
+
+    public LicenseRequest setBoostLicenseCode(String boostLicenseCode) {
+        this.boostLicenseCode = boostLicenseCode;
+        return this;
+    }
+
+    public String getProduct() {
+        return product;
+    }
+
+    public LicenseRequest setProduct(String product) {
+        this.product = product;
+        return this;
+    }
+    
+    
+}
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/LicenseResponse.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/LicenseResponse.java
new file mode 100644
index 0000000000..39184322e2
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/LicenseResponse.java
@@ -0,0 +1,59 @@
+/** *************************************************************************
+ ** This data and information is proprietary to, and a valuable trade secret
+ ** of, Basis Technology Corp. It is given in confidence by Basis Technology
+ ** and may only be used as permitted under the license agreement under which
+ ** it has been distributed, and in no other way.
+ **
+ ** Copyright (c) 2023 Basis Technology Corp. All rights reserved.
+ **
+ ** The technical data and information provided herein are provided with
+ ** `limited rights', and the computer software provided herein is provided
+ ** with `restricted rights' as those terms are defined in DAR and ASPR
+ ** 7-104.9(a).
+ ************************************************************************** */
+package com.basistech.df.cybertriage.autopsy.ctapi.json;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Response POJO for request for license.
+ */
+public class LicenseResponse {
+    private final Boolean success;
+    private final Boolean hostChanged;
+    private final Long hostChangesRemaining;
+    private final BoostLicenseResponse boostLicense;
+
+    @JsonCreator
+    public LicenseResponse(
+            @JsonProperty("success") Boolean success, 
+            @JsonProperty("hostChanged") Boolean hostChanged, 
+            @JsonProperty("hostChangesRemaining") Long hostChangesRemaining, 
+            @JsonProperty("boostLicense") BoostLicenseResponse boostLicense
+    ) {
+        this.success = success;
+        this.hostChanged = hostChanged;
+        this.hostChangesRemaining = hostChangesRemaining;
+        this.boostLicense = boostLicense;
+    }
+
+    public Boolean isSuccess() {
+        return success;
+    }
+
+    public Boolean isHostChanged() {
+        return hostChanged;
+    }
+
+    public Long getHostChangesRemaining() {
+        return hostChangesRemaining;
+    }
+
+    public BoostLicenseResponse getBoostLicense() {
+        return boostLicense;
+    }
+
+    
+    
+}
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/MetadataLabel.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/MetadataLabel.java
new file mode 100644
index 0000000000..8077250c8b
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/json/MetadataLabel.java
@@ -0,0 +1,43 @@
+/*
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
+ * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template
+ */
+package com.basistech.df.cybertriage.autopsy.ctapi.json;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ *
+ * @author gregd
+ */
+public class MetadataLabel {
+
+    private final String key;
+    private final String value;
+    private final String extendedInfo;
+    
+    @JsonCreator
+    public MetadataLabel(
+            @JsonProperty("key") String key, 
+            @JsonProperty("value") String value, 
+            @JsonProperty("info") String extendedInfo
+    ) {
+        this.key = key;
+        this.value = value;
+        this.extendedInfo = extendedInfo;
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+    public String getValue() {
+        return value;
+    }
+    
+    public String getExtendedInfo() {
+        return extendedInfo;
+    }
+    
+}
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/util/CTHostIDGenerationUtil.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/util/CTHostIDGenerationUtil.java
new file mode 100644
index 0000000000..f7b68f6bea
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/util/CTHostIDGenerationUtil.java
@@ -0,0 +1,57 @@
+/** *************************************************************************
+ ** This data and information is proprietary to, and a valuable trade secret
+ ** of, Basis Technology Corp. It is given in confidence by Basis Technology
+ ** and may only be used as permitted under the license agreement under which
+ ** it has been distributed, and in no other way.
+ **
+ ** Copyright (c) 2021 Basis Technology Corp. All rights reserved.
+ **
+ ** The technical data and information provided herein are provided with
+ ** `limited rights', and the computer software provided herein is provided
+ ** with `restricted rights' as those terms are defined in DAR and ASPR
+ ** 7-104.9(a).
+ ************************************************************************** */
+package com.basistech.df.cybertriage.autopsy.ctapi.util;
+
+import com.license4j.HardwareID;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.logging.Level;
+import org.apache.commons.lang3.StringUtils;
+import org.sleuthkit.autopsy.coreutils.Logger;
+
+/**
+ * Utility class to generate license hostID and Target hostID for malware scan
+ *
+ * @author rishwanth
+ */
+public class CTHostIDGenerationUtil {
+
+    private static final Logger LOGGER = Logger.getLogger(CTHostIDGenerationUtil.class.getName());
+    private static final String USER_NAME = System.getProperty("user.name");
+    private static String HOST_NAME = "";
+
+    /**
+     * Host ID Algorithm: Get MAC address from License4J. Get MD5 hash of it and
+     * grab the first 16 characters of the hash. Get user name that Cyber Triage
+     * is running as. MD5 hash of user name. Grab first 16 characters.
+     * Concatenate them and separate with underscore. Example:
+     * c84f70d1baf96420_7d7519bf21602c24
+     *
+     * @return
+     */
+    public static String generateLicenseHostID() {
+        if (StringUtils.isBlank(HOST_NAME)) {
+
+            try {
+                HOST_NAME = StringUtils.defaultString(InetAddress.getLocalHost().getCanonicalHostName());
+            } catch (UnknownHostException ex) {
+                LOGGER.log(Level.WARNING, "UNable to determine host name.", ex);
+            }
+        }
+        String macAddressMd5 = StringUtils.isNotBlank(HardwareID.getHardwareIDFromEthernetAddress()) ? Md5HashUtil.getMD5MessageDigest(HardwareID.getHardwareIDFromEthernetAddress()).substring(0, 16) : Md5HashUtil.getMD5MessageDigest(HOST_NAME).substring(0, 16);
+        String usernameMd5 = StringUtils.isNotBlank(USER_NAME) ? Md5HashUtil.getMD5MessageDigest(USER_NAME).substring(0, 16) : Md5HashUtil.getMD5MessageDigest(HOST_NAME).substring(0, 16);
+        String md5 = macAddressMd5 + "_" + usernameMd5;
+        return md5;
+    }
+}
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/util/LicenseDecryptorUtil.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/util/LicenseDecryptorUtil.java
new file mode 100644
index 0000000000..efa91afd8d
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/util/LicenseDecryptorUtil.java
@@ -0,0 +1,172 @@
+/** *************************************************************************
+ ** This data and information is proprietary to, and a valuable trade secret
+ ** of, Basis Technology Corp. It is given in confidence by Basis Technology
+ ** and may only be used as permitted under the license agreement under which
+ ** it has been distributed, and in no other way.
+ **
+ ** Copyright (c) 2023 Basis Technology Corp. All rights reserved.
+ **
+ ** The technical data and information provided herein are provided with
+ ** `limited rights', and the computer software provided herein is provided
+ ** with `restricted rights' as those terms are defined in DAR and ASPR
+ ** 7-104.9(a).
+ ************************************************************************** */
+package com.basistech.df.cybertriage.autopsy.ctapi.util;
+
+import com.basistech.df.cybertriage.autopsy.ctapi.json.BoostLicenseResponse;
+import com.basistech.df.cybertriage.autopsy.ctapi.json.DecryptedLicenseResponse;
+import com.basistech.df.cybertriage.autopsy.ctapi.json.LicenseInfo;
+import com.basistech.df.cybertriage.autopsy.ctapi.json.LicenseResponse;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.KeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Base64;
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * Decrypts the payload of boost license.
+ */
+public class LicenseDecryptorUtil {
+
+    private static final LicenseDecryptorUtil instance = new LicenseDecryptorUtil();
+
+    public static LicenseDecryptorUtil getInstance() {
+        return instance;
+    }
+
+    private final ObjectMapper objectMapper = ObjectMapperUtil.getInstance().getDefaultObjectMapper();
+
+    private LicenseDecryptorUtil() {
+    }
+    
+    public LicenseInfo createLicenseInfo(LicenseResponse licenseResponse) throws JsonProcessingException, InvalidLicenseException {
+        if (licenseResponse == null || licenseResponse.getBoostLicense() == null) {
+            throw new InvalidLicenseException("License or boost license are null");
+        }
+        
+        DecryptedLicenseResponse decrypted = parseLicenseJSON(licenseResponse.getBoostLicense());
+        return new LicenseInfo(licenseResponse, decrypted);
+    }
+
+    /**
+     * Decrypts a boost license response.
+     *
+     * @param licenseResponse The boost license response.
+     * @return The decrypted license response.
+     * @throws JsonProcessingException
+     * @throws
+     * com.basistech.df.cybertriage.autopsy.ctapi.util.LicenseDecryptorUtil.InvalidLicenseException
+     */
+    public DecryptedLicenseResponse parseLicenseJSON(BoostLicenseResponse licenseResponse) throws JsonProcessingException, InvalidLicenseException {
+
+        String decryptedJsonResponse;
+        try {
+            decryptedJsonResponse = decryptLicenseString(
+                    licenseResponse.getEncryptedJson(),
+                    licenseResponse.getIv(),
+                    licenseResponse.getEncryptedKey(),
+                    licenseResponse.getVersion()
+            );
+        } catch (IOException | GeneralSecurityException ex) {
+            throw new InvalidLicenseException("An exception occurred while parsing the license string", ex);
+        }
+
+        DecryptedLicenseResponse decryptedLicense = objectMapper.readValue(decryptedJsonResponse, DecryptedLicenseResponse.class);
+        if (!"CYBERTRIAGE".equalsIgnoreCase(decryptedLicense.getProduct())) {
+            // license file is expected to contain product of "CYBERTRIAGE"
+            throw new InvalidLicenseException("Not a valid Cyber Triage license");
+        }
+
+        return decryptedLicense;
+    }
+
+    private String decryptLicenseString(String encryptedJson, String ivBase64, String encryptedKey, String version) throws IOException, GeneralSecurityException, InvalidLicenseException {
+        if (!"1.0".equals(version)) {
+            throw new InvalidLicenseException("Unexpected file version: " + version);
+        }
+
+        byte[] encryptedKeyBytes = Base64.getDecoder().decode(encryptedKey);
+        byte[] keyBytes = decryptKey(encryptedKeyBytes);
+        SecretKey key = new SecretKeySpec(keyBytes, 0, keyBytes.length, "AES");
+
+        byte[] ivBytes = Base64.getDecoder().decode(ivBase64);
+        IvParameterSpec iv = new IvParameterSpec(ivBytes);
+
+        byte[] encryptedLicenseJsonBytes = Base64.getDecoder().decode(encryptedJson);
+
+        String algorithm = "AES/CBC/PKCS5Padding";
+        Cipher cipher = Cipher.getInstance(algorithm);
+        cipher.init(Cipher.DECRYPT_MODE, key, iv);
+        byte[] licenseJsonBytes = cipher.doFinal(encryptedLicenseJsonBytes);
+
+        return new String(licenseJsonBytes, StandardCharsets.UTF_8);
+    }
+
+    private PublicKey getPublicKey() throws InvalidKeySpecException, NoSuchAlgorithmException {
+
+        String publicKeyString = """
+                                 -----BEGIN PUBLIC KEY-----
+                                 MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwIKulLyaLQ2WeO0gIW2G
+                                 3jQqny3Y/7VUevBKulAEywaUbvECvZ4zGsnaMyACjXxMNkA1xU2WeSMP/WqC03wz
+                                 4d71liUeAqOYKMdGHXFN2qswWz/ufK6An0pTEqYaoiUfcwSBVo2ZTUcMQexScKaS
+                                 ghmaWqBHBYx+lBkVMcLG2PtLDRZbqgJvJr2QCzMSVUpEGGQEWs7YolIq46KCgqsq
+                                 pTdfrdqd59x6oRhTLegswzxwLyouvrKbRqKR2ZRbVvlGtUnnnlLDuhEfd0flMxuv
+                                 W98Siw6dWe1K3x45nDu5py2G9Q9fZS8/2KHUC6QcLLstLIoPnZjCl9Lcur1U6s9N
+                                 f5aLI9mwMfmSJsoVOuwx2/MC98uHvPoPbG4ZjiT0aaGg4JccTGD6pssDA35zPhkk
+                                 1l6wktEYtyF2A7zjzuFxioQz8fHBzIbHPCxzu4S2gh3qOVFf7c9COmX9MsnB70o2
+                                 EZ1rxlFIJ7937IGJNwWOQuiMKTpEeT6BwTdQNZQPqCUGvZ5eEjhrm57yCF4zuyrt
+                                 AR8DG7ahK2YAarADHRyxTuxH1qY7E5/CTQKYk9tIYsV4O05CKj7B8rBMtjVNjb4b
+                                 d7JwPW43Z3J6jo/gLlVdGSPg8vQDNVLl6sdDM4Pm1eJEzgR2JlqXDCRDUGNNsXH2
+                                 qt9Ru8ykX7PAfF2Q3/qg1jkCAwEAAQ==
+                                 -----END PUBLIC KEY-----
+                                 """;
+
+        publicKeyString = publicKeyString.replaceAll("-----BEGIN PUBLIC KEY-----", "").replaceAll("-----END PUBLIC KEY-----", "").replaceAll("\\s", "");
+        byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyString);
+
+        KeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes);
+        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+        PublicKey publicKey = keyFactory.generatePublic(keySpec);
+
+        return publicKey;
+    }
+
+    private byte[] decryptKey(byte[] encryptedKeyBytes) throws InvalidKeySpecException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
+
+        PublicKey publicKey = getPublicKey();
+
+        Cipher decryptCipher = Cipher.getInstance("RSA");
+        decryptCipher.init(Cipher.DECRYPT_MODE, publicKey);
+
+        byte[] decryptedBytes = decryptCipher.doFinal(encryptedKeyBytes);
+
+        return decryptedBytes;
+    }
+
+    public class InvalidLicenseException extends Exception {
+
+        public InvalidLicenseException(String message) {
+            super(message);
+        }
+
+        public InvalidLicenseException(String message, Throwable cause) {
+            super(message, cause);
+        }
+
+    }
+}
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/util/Md5HashUtil.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/util/Md5HashUtil.java
new file mode 100644
index 0000000000..4ff7d262b7
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/util/Md5HashUtil.java
@@ -0,0 +1,40 @@
+/** *************************************************************************
+ ** This data and information is proprietary to, and a valuable trade secret
+ ** of, Basis Technology Corp. It is given in confidence by Basis Technology
+ ** and may only be used as permitted under the license agreement under which
+ ** it has been distributed, and in no other way.
+ **
+ ** Copyright (c) 2018 Basis Technology Corp. All rights reserved.
+ **
+ ** The technical data and information provided herein are provided with
+ ** `limited rights', and the computer software provided herein is provided
+ ** with `restricted rights' as those terms are defined in DAR and ASPR
+ ** 7-104.9(a).
+ ************************************************************************** */
+package com.basistech.df.cybertriage.autopsy.ctapi.util;
+import com.google.common.base.Charsets;
+import com.google.common.hash.HashCode;
+import com.google.common.hash.HashFunction;
+import com.google.common.hash.Hashing;
+import org.apache.commons.lang3.StringUtils;
+/**
+ *
+ * @author jayaram
+ */
+public class Md5HashUtil {
+     /**
+     * Returns MD5 hash value for the lower case value of the string provided.
+     * @param inp
+     * @return 
+     */
+    public static String getMD5MessageDigest(String inp) {
+        if (StringUtils.isNotBlank(inp)) {
+            HashFunction hf = Hashing.md5(); // Using despite its deprecation as md5 is good enough for our uses.  
+            HashCode hc = hf.newHasher()
+                    .putString(inp.toLowerCase(), Charsets.UTF_8)
+                    .hash();
+            return hc.toString();
+        }
+        return "";
+    }
+}
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/util/ObjectMapperUtil.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/util/ObjectMapperUtil.java
new file mode 100644
index 0000000000..79098ce650
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctapi/util/ObjectMapperUtil.java
@@ -0,0 +1,41 @@
+/** *************************************************************************
+ ** This data and information is proprietary to, and a valuable trade secret
+ ** of, Basis Technology Corp. It is given in confidence by Basis Technology
+ ** and may only be used as permitted under the license agreement under which
+ ** it has been distributed, and in no other way.
+ **
+ ** Copyright (c) 2023 Basis Technology Corp. All rights reserved.
+ **
+ ** The technical data and information provided herein are provided with
+ ** `limited rights', and the computer software provided herein is provided
+ ** with `restricted rights' as those terms are defined in DAR and ASPR
+ ** 7-104.9(a).
+ ************************************************************************** */
+package com.basistech.df.cybertriage.autopsy.ctapi.util;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+
+/**
+ * Creates default ObjectMapper
+ */
+public class ObjectMapperUtil {
+
+    private static final ObjectMapperUtil instance = new ObjectMapperUtil();
+
+    public static ObjectMapperUtil getInstance() {
+        return instance;
+    }
+
+    private ObjectMapperUtil() {
+
+    }
+
+    public ObjectMapper getDefaultObjectMapper() {
+        ObjectMapper defaultMapper = new ObjectMapper();
+        defaultMapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+        defaultMapper.registerModule(new JavaTimeModule());
+        return defaultMapper;
+    }
+
+}
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/Bundle.properties b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/Bundle.properties
new file mode 100644
index 0000000000..2d9daa4c7b
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/Bundle.properties
@@ -0,0 +1,5 @@
+
+# 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
+OptionsCategory_Name_CyberTriage=CyberTriage
+OptionsCategory_Keywords_CyberTriage=CyberTriage,Cyber,Triage
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/Bundle.properties-MERGED b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/Bundle.properties-MERGED
new file mode 100644
index 0000000000..2d9daa4c7b
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/Bundle.properties-MERGED
@@ -0,0 +1,5 @@
+
+# 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
+OptionsCategory_Name_CyberTriage=CyberTriage
+OptionsCategory_Keywords_CyberTriage=CyberTriage,Cyber,Triage
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/CTOptionsPanel.form b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/CTOptionsPanel.form
new file mode 100644
index 0000000000..1aee51eb33
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/CTOptionsPanel.form
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<Form version="1.5" maxVersion="1.9" type="org.netbeans.modules.form.forminfo.JPanelFormInfo">
+  <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,1,120,0,0,2,2"/>
+  </AuxValues>
+
+  <Layout class="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout"/>
+  <SubComponents>
+    <Container class="javax.swing.JScrollPane" name="scrollPane">
+      <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.DesignBorderLayout" value="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout$BorderConstraintsDescription">
+          <BorderConstraints direction="Center"/>
+        </Constraint>
+      </Constraints>
+
+      <Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/>
+      <SubComponents>
+        <Container class="javax.swing.JPanel" name="contentPane">
+
+          <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/>
+        </Container>
+      </SubComponents>
+    </Container>
+  </SubComponents>
+</Form>
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/CTOptionsPanel.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/CTOptionsPanel.java
new file mode 100644
index 0000000000..fd235b580e
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/CTOptionsPanel.java
@@ -0,0 +1,141 @@
+/** *************************************************************************
+ ** This data and information is proprietary to, and a valuable trade secret
+ ** of, Basis Technology Corp. It is given in confidence by Basis Technology
+ ** and may only be used as permitted under the license agreement under which
+ ** it has been distributed, and in no other way.
+ **
+ ** Copyright (c) 2023 Basis Technology Corp. All rights reserved.
+ **
+ ** The technical data and information provided herein are provided with
+ ** `limited rights', and the computer software provided herein is provided
+ ** with `restricted rights' as those terms are defined in DAR and ASPR
+ ** 7-104.9(a).
+ ************************************************************************** */
+package com.basistech.df.cybertriage.autopsy.ctoptions;
+
+import com.basistech.df.cybertriage.autopsy.ctoptions.subpanel.CTOptionsSubPanel;
+import java.awt.Dimension;
+import java.awt.GridBagConstraints;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.swing.JPanel;
+import org.netbeans.spi.options.OptionsPanelController;
+import org.openide.util.Lookup;
+import org.sleuthkit.autopsy.ingest.IngestModuleGlobalSettingsPanel;
+
+/**
+ * Options panel for CyberTriage.
+ */
+public class CTOptionsPanel extends IngestModuleGlobalSettingsPanel {
+    private static final int MAX_SUBPANEL_WIDTH = 500;
+
+    private static final Logger logger = Logger.getLogger(CTOptionsPanel.class.getName());
+
+    private final List<CTOptionsSubPanel> subPanels;
+
+    /**
+     * Creates new form CTOptions
+     */
+    public CTOptionsPanel() {
+        initComponents();
+        Collection<? extends CTOptionsSubPanel> coll = Lookup.getDefault().lookupAll(CTOptionsSubPanel.class);
+        Stream<? extends CTOptionsSubPanel> panelStream = coll != null ? coll.stream() : Stream.empty();
+        this.subPanels = panelStream
+                .sorted(Comparator.comparing(p -> p.getClass().getSimpleName().toUpperCase()))
+                .collect(Collectors.toList());
+        addSubOptionsPanels(this.subPanels);
+    }
+
+    private void addSubOptionsPanels(List<CTOptionsSubPanel> subPanels) {
+        for (int i = 0; i < subPanels.size(); i++) {
+            CTOptionsSubPanel subPanel = subPanels.get(i);
+
+            subPanel.addPropertyChangeListener(new PropertyChangeListener() {
+                @Override
+                public void propertyChange(PropertyChangeEvent evt) {
+                    if (evt.getPropertyName().equals(OptionsPanelController.PROP_CHANGED)) {
+                        CTOptionsPanel.this.firePropertyChange(OptionsPanelController.PROP_CHANGED, null, null);
+                    }
+                }
+            });
+            
+            GridBagConstraints gridBagConstraints = new GridBagConstraints();
+            gridBagConstraints.gridx = 0;
+            gridBagConstraints.gridy = i;
+            gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
+            gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
+            gridBagConstraints.insets = new java.awt.Insets(i == 0 ? 5 : 0, 5, 5, 5);
+            gridBagConstraints.weighty = 0;
+            gridBagConstraints.weightx = 0;
+
+            contentPane.add(subPanel, gridBagConstraints);
+        }
+        
+            GridBagConstraints verticalConstraints = new GridBagConstraints();
+            verticalConstraints.gridx = 0;
+            verticalConstraints.gridy = subPanels.size();
+            verticalConstraints.weighty = 1;
+            verticalConstraints.weightx = 0;
+            
+            JPanel verticalSpacer = new JPanel();
+            
+            verticalSpacer.setMinimumSize(new Dimension(MAX_SUBPANEL_WIDTH, 0));
+            verticalSpacer.setPreferredSize(new Dimension(MAX_SUBPANEL_WIDTH, 0));
+            verticalSpacer.setMaximumSize(new Dimension(MAX_SUBPANEL_WIDTH, Short.MAX_VALUE));
+            contentPane.add(verticalSpacer, verticalConstraints);
+            
+            
+            GridBagConstraints horizontalConstraints = new GridBagConstraints();
+            horizontalConstraints.gridx = 1;
+            horizontalConstraints.gridy = 0;
+            horizontalConstraints.weighty = 0;
+            horizontalConstraints.weightx = 1;
+            
+            JPanel horizontalSpacer = new JPanel();
+            contentPane.add(horizontalSpacer, horizontalConstraints);
+    }
+
+    /**
+     * 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() {
+
+        javax.swing.JScrollPane scrollPane = new javax.swing.JScrollPane();
+        contentPane = new javax.swing.JPanel();
+
+        setLayout(new java.awt.BorderLayout());
+
+        contentPane.setLayout(new java.awt.GridBagLayout());
+        scrollPane.setViewportView(contentPane);
+
+        add(scrollPane, java.awt.BorderLayout.CENTER);
+    }// </editor-fold>//GEN-END:initComponents
+
+    @Override
+    public void saveSettings() {
+        subPanels.forEach(panel -> panel.saveSettings());
+    }
+
+    public void loadSavedSettings() {
+        subPanels.forEach(panel -> panel.loadSettings());
+    }
+
+    public boolean valid() {
+        return subPanels.stream().allMatch(panel -> panel.valid());
+    }
+
+
+    // Variables declaration - do not modify//GEN-BEGIN:variables
+    private javax.swing.JPanel contentPane;
+    // End of variables declaration//GEN-END:variables
+}
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/CTOptionsPanelController.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/CTOptionsPanelController.java
new file mode 100644
index 0000000000..93dfe4960c
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/CTOptionsPanelController.java
@@ -0,0 +1,128 @@
+/** *************************************************************************
+ ** This data and information is proprietary to, and a valuable trade secret
+ ** of, Basis Technology Corp. It is given in confidence by Basis Technology
+ ** and may only be used as permitted under the license agreement under which
+ ** it has been distributed, and in no other way.
+ **
+ ** Copyright (c) 2023 Basis Technology Corp. All rights reserved.
+ **
+ ** The technical data and information provided herein are provided with
+ ** `limited rights', and the computer software provided herein is provided
+ ** with `restricted rights' as those terms are defined in DAR and ASPR
+ ** 7-104.9(a).
+ ************************************************************************** */
+package com.basistech.df.cybertriage.autopsy.ctoptions;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+import javax.swing.JComponent;
+import org.netbeans.spi.options.OptionsPanelController;
+import org.openide.util.HelpCtx;
+import org.openide.util.Lookup;
+
+/**
+ * Options panel controller for CyberTriage.
+ */
+@OptionsPanelController.TopLevelRegistration(categoryName = "#OptionsCategory_Name_CyberTriage",
+        iconBase = "com/basistech/df/cybertriage/autopsy/images/logo.png",
+        position = 999999,
+        keywords = "#OptionsCategory_Keywords_CyberTriage",
+        keywordsCategory = "CyberTriage")
+public final class CTOptionsPanelController extends OptionsPanelController {
+
+    private CTOptionsPanel panel;
+    private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
+    private boolean changed;
+
+    /**
+     * Component should load its data here.
+     */
+    @Override
+    public void update() {
+        getPanel().loadSavedSettings();
+        changed = false;
+    }
+
+    /**
+     * This method is called when both the Ok and Apply buttons are pressed. It
+     * applies to any of the panels that have been opened in the process of
+     * using the options pane.
+     */
+    @Override
+    public void applyChanges() {
+        if (changed) {
+            getPanel().saveSettings();
+            changed = false;
+        }
+    }
+
+    /**
+     * This method is called when the Cancel button is pressed. It applies to
+     * any of the panels that have been opened in the process of using the
+     * options pane.
+     */
+    @Override
+    public void cancel() {
+    }
+
+    @Override
+    public boolean isValid() {
+        return getPanel().valid();
+    }
+
+    /**
+     * Used to determine whether any changes have been made to this controller's
+     * panel.
+     *
+     * @return Whether or not a change has been made.
+     */
+    @Override
+    public boolean isChanged() {
+        return changed;
+    }
+
+    @Override
+    public HelpCtx getHelpCtx() {
+        return null;
+    }
+
+    @Override
+    public JComponent getComponent(Lookup masterLookup) {
+        return getPanel();
+    }
+
+    @Override
+    public void addPropertyChangeListener(PropertyChangeListener l) {
+        pcs.addPropertyChangeListener(l);
+    }
+
+    @Override
+    public void removePropertyChangeListener(PropertyChangeListener l) {
+        pcs.removePropertyChangeListener(l);
+    }
+
+    private CTOptionsPanel getPanel() {
+        if (panel == null) {
+            panel = new CTOptionsPanel();
+            panel.addPropertyChangeListener(new PropertyChangeListener() {
+                @Override
+                public void propertyChange(PropertyChangeEvent evt) {
+                    if (evt.getPropertyName().equals(OptionsPanelController.PROP_CHANGED)) {
+                        changed();
+                    }
+                }
+            });
+        }
+        return panel;
+    }
+
+    void changed() {
+        if (!changed) {
+            changed = true;
+            pcs.firePropertyChange(OptionsPanelController.PROP_CHANGED, false, true);
+        }
+        pcs.firePropertyChange(OptionsPanelController.PROP_VALID, null, null);
+
+    }
+}
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties
new file mode 100644
index 0000000000..5df431e10c
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties
@@ -0,0 +1,23 @@
+
+# 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
+
+
+CTLicenseDialog.licenseNumberLabel.text=License Number:
+CTLicenseDialog.licenseNumberTextField.text=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
+CTLicenseDialog.cancelButton.text=Cancel
+CTLicenseDialog.okButton.text=Ok
+CTLicenseDialog.warningLabel.text=
+CTMalwareScannerOptionsPanel.hashLookupsRemainingLabel.text=
+CTMalwareScannerOptionsPanel.licenseInfoMessageLabel.text=
+CTMalwareScannerOptionsPanel.countersResetLabel.text=
+CTMalwareScannerOptionsPanel.licenseInfoPanel.border.title=License Info
+CTMalwareScannerOptionsPanel.maxFileUploadsLabel.text=
+CTMalwareScannerOptionsPanel.maxHashLookupsLabel.text=
+CTMalwareScannerOptionsPanel.malwareScansMessageLabel.text=
+CTMalwareScannerOptionsPanel.malwareScansPanel.border.title=Malware Scans
+CTMalwareScannerOptionsPanel.licenseInfoAddButton.text=Add License
+CTMalwareScannerOptionsPanel.licenseInfoIdLabel.text=
+CTMalwareScannerOptionsPanel.licenseInfoExpiresLabel.text=
+CTMalwareScannerOptionsPanel.fileUploadsRemainingLabel.text=
+CTMalwareScannerOptionsPanel.licenseInfoUserLabel.text=
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties-MERGED b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties-MERGED
new file mode 100644
index 0000000000..6eaa730072
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties-MERGED
@@ -0,0 +1,55 @@
+
+# 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
+
+
+CTLicenseDialog.licenseNumberLabel.text=License Number:
+CTLicenseDialog.licenseNumberTextField.text=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
+CTLicenseDialog.cancelButton.text=Cancel
+CTLicenseDialog.okButton.text=Ok
+CTLicenseDialog.warningLabel.text=
+CTLicenseDialog_verifyInput_licenseNumberError=<html>Please verify license number format of 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'</html>
+CTMalwareScannerOptionsPanel.hashLookupsRemainingLabel.text=
+CTMalwareScannerOptionsPanel.licenseInfoMessageLabel.text=
+CTMalwareScannerOptionsPanel.countersResetLabel.text=
+CTMalwareScannerOptionsPanel.licenseInfoPanel.border.title=License Info
+CTMalwareScannerOptionsPanel.maxFileUploadsLabel.text=
+CTMalwareScannerOptionsPanel.maxHashLookupsLabel.text=
+CTMalwareScannerOptionsPanel.malwareScansMessageLabel.text=
+CTMalwareScannerOptionsPanel.malwareScansPanel.border.title=Malware Scans
+CTMalwareScannerOptionsPanel.licenseInfoAddButton.text=Add License
+CTMalwareScannerOptionsPanel.licenseInfoIdLabel.text=
+CTMalwareScannerOptionsPanel.licenseInfoExpiresLabel.text=
+CTMalwareScannerOptionsPanel.fileUploadsRemainingLabel.text=
+CTMalwareScannerOptionsPanel.licenseInfoUserLabel.text=
+CTMalwareScannerOptionsPanel_licenseAddDialog_desc=License Number:
+CTMalwareScannerOptionsPanel_licenseAddDialog_title=Add a License...
+CTMalwareScannerOptionsPanel_licenseAddDialogEnteredErr_desc=The license number has already been entered
+CTMalwareScannerOptionsPanel_licenseAddDialogEnteredErr_title=License Number Already Entered
+CTMalwareScannerOptionsPanel_licenseAddDialogPatternErr_desc=Please verify that license number is of format 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'
+CTMalwareScannerOptionsPanel_licenseAddDialogPatternErr_title=Invalid License Number
+CTMalwareScannerOptionsPanel_LicenseFetcher_apiErr_title=Server Error
+CTMalwareScannerOptionsPanel_LicenseFetcher_localErr_desc=A general error occurred while fetching license information.  Please try again later.
+CTMalwareScannerOptionsPanel_LicenseFetcher_localErr_title=General Error
+# {0} - expiresDate
+CTMalwareScannerOptionsPanel_licenseInfo_expires=Expires: {0}
+# {0} - idNumber
+CTMalwareScannerOptionsPanel_licenseInfo_id=ID: {0}
+# {0} - userName
+# {1} - email
+CTMalwareScannerOptionsPanel_licenseInfo_userInfo=<html>User: {0}<br/>Email: {1}</html>
+# {0} - countersResetDate
+CTMalwareScannerOptionsPanel_malwareScans_countersReset=Counters reset: {0}
+# {0} - fileUploadsRemaining
+CTMalwareScannerOptionsPanel_malwareScans_fileUploadsRemaining=File uploads remaining: {0}
+# {0} - hashLookupsRemaining
+CTMalwareScannerOptionsPanel_malwareScans_hashLookupsRemaining=Hash lookups remaining: {0}
+# {0} - maxDailyFileLookups
+CTMalwareScannerOptionsPanel_malwareScans_maxDailyFileLookups=Max file uploads: {0}/day
+# {0} - maxDailyLookups
+CTMalwareScannerOptionsPanel_malwareScans_maxDailyHashLookups=Max Hash lookups: {0}/day
+CTMalwareScannerOptionsPanel_MalwareScansFetcher_apiErr_title=Server Error
+CTMalwareScannerOptionsPanel_MalwareScansFetcher_localErr_desc=A general error occurred while fetching malware scans information.  Please try again later.
+CTMalwareScannerOptionsPanel_MalwareScansFetcher_localErr_title=General Error
+CTOPtionsPanel_loadLicenseInfo_loading=Loading...
+CTOPtionsPanel_loadMalwareScansInfo_loading=Loading...
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTLicenseDialog.form b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTLicenseDialog.form
new file mode 100644
index 0000000000..e7cd2743a0
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTLicenseDialog.form
@@ -0,0 +1,138 @@
+<?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/ctoptions/Bundle.properties" key="CTLicenseDialog.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,122,0,0,1,-19"/>
+  </AuxValues>
+
+  <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/>
+  <SubComponents>
+    <Component class="javax.swing.JLabel" name="licenseNumberLabel">
+      <Properties>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties" key="CTLicenseDialog.licenseNumberLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </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="3" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="5" insetsLeft="5" insetsBottom="5" insetsRight="5" anchor="18" weightX="0.0" weightY="0.0"/>
+        </Constraint>
+      </Constraints>
+    </Component>
+    <Component class="javax.swing.JLabel" name="warningLabel">
+      <Properties>
+        <Property name="foreground" type="java.awt.Color" editor="org.netbeans.modules.form.RADConnectionPropertyEditor">
+          <Connection code="java.awt.Color.RED" type="code"/>
+        </Property>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties" key="CTLicenseDialog.warningLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+        <Property name="maximumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
+          <Dimension value="[419, 36]"/>
+        </Property>
+        <Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
+          <Dimension value="[419, 36]"/>
+        </Property>
+        <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor">
+          <Dimension value="[419, 36]"/>
+        </Property>
+      </Properties>
+      <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="3" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="5" insetsBottom="5" insetsRight="5" anchor="18" weightX="0.0" weightY="0.0"/>
+        </Constraint>
+      </Constraints>
+    </Component>
+    <Container class="javax.swing.JPanel" name="buttonPadding">
+      <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="3" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="10" weightX="1.0" weightY="0.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/ctoptions/ctcloud/Bundle.properties" key="CTLicenseDialog.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>
+      <Constraints>
+        <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
+          <GridBagConstraints gridX="2" gridY="3" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="5" insetsBottom="5" insetsRight="5" anchor="18" weightX="0.0" weightY="0.0"/>
+        </Constraint>
+      </Constraints>
+    </Component>
+    <Component class="javax.swing.JButton" name="cancelButton">
+      <Properties>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties" key="CTLicenseDialog.cancelButton.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="cancelButtonActionPerformed"/>
+      </Events>
+      <Constraints>
+        <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
+          <GridBagConstraints gridX="1" gridY="3" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="5" insetsBottom="5" insetsRight="5" anchor="18" weightX="0.0" weightY="0.0"/>
+        </Constraint>
+      </Constraints>
+    </Component>
+    <Component class="javax.swing.JTextField" name="licenseNumberTextField">
+      <Properties>
+        <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+          <ResourceString bundle="com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties" key="CTLicenseDialog.licenseNumberTextField.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+        </Property>
+      </Properties>
+      <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="3" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="5" insetsBottom="5" insetsRight="5" anchor="10" weightX="0.0" weightY="0.0"/>
+        </Constraint>
+      </Constraints>
+    </Component>
+  </SubComponents>
+</Form>
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTLicenseDialog.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTLicenseDialog.java
new file mode 100644
index 0000000000..8af5795517
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTLicenseDialog.java
@@ -0,0 +1,192 @@
+/** *************************************************************************
+ ** This data and information is proprietary to, and a valuable trade secret
+ ** of, Basis Technology Corp. It is given in confidence by Basis Technology
+ ** and may only be used as permitted under the license agreement under which
+ ** it has been distributed, and in no other way.
+ **
+ ** Copyright (c) 2023 Basis Technology Corp. All rights reserved.
+ **
+ ** The technical data and information provided herein are provided with
+ ** `limited rights', and the computer software provided herein is provided
+ ** with `restricted rights' as those terms are defined in DAR and ASPR
+ ** 7-104.9(a).
+ ************************************************************************** */
+package com.basistech.df.cybertriage.autopsy.ctoptions.ctcloud;
+
+import java.util.regex.Pattern;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import org.apache.commons.lang3.StringUtils;
+import org.openide.util.NbBundle.Messages;
+
+/**
+ * License dialog
+ */
+public class CTLicenseDialog extends javax.swing.JDialog {
+
+    private static final Pattern LICENSE_PATTERN = Pattern.compile("^\\s*[0-9a-zA-Z]{8}\\-[0-9a-zA-Z]{4}\\-[0-9a-zA-Z]{4}\\-[0-9a-zA-Z]{4}\\-[0-9a-zA-Z]{12}\\s*$");
+
+    private String licenseString = null;
+
+    /**
+     * Creates new form CTLicenseDialog
+     */
+    public CTLicenseDialog(java.awt.Frame parent, boolean modal) {
+        super(parent, modal);
+        initComponents();
+        this.licenseNumberTextField.getDocument().putProperty("filterNewlines", Boolean.TRUE);
+        this.licenseNumberTextField.getDocument().addDocumentListener(new DocumentListener() {
+            @Override
+            public void changedUpdate(DocumentEvent e) {
+                verifyInput();
+            }
+
+            @Override
+            public void insertUpdate(DocumentEvent e) {
+                verifyInput();
+            }
+
+            @Override
+            public void removeUpdate(DocumentEvent e) {
+                verifyInput();
+            }
+        });
+    }
+
+    String getValue() {
+        return licenseString;
+    }
+
+    @Messages({
+        "CTLicenseDialog_verifyInput_licenseNumberError=<html>Please verify license number format of 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'</html>"
+    })
+    private void verifyInput() {
+        String licenseInput = StringUtils.defaultString(this.licenseNumberTextField.getText());
+        if (LICENSE_PATTERN.matcher(licenseInput).matches()) {
+            this.warningLabel.setText("");
+            this.okButton.setEnabled(true);
+        } else {
+            this.warningLabel.setText(Bundle.CTLicenseDialog_verifyInput_licenseNumberError());
+            this.okButton.setEnabled(false);
+        }
+    }
+
+    /**
+     * 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 licenseNumberLabel = new javax.swing.JLabel();
+        warningLabel = new javax.swing.JLabel();
+        javax.swing.JPanel buttonPadding = new javax.swing.JPanel();
+        okButton = new javax.swing.JButton();
+        cancelButton = new javax.swing.JButton();
+        licenseNumberTextField = new javax.swing.JTextField();
+
+        setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE);
+        setTitle(org.openide.util.NbBundle.getMessage(CTLicenseDialog.class, "CTLicenseDialog.title")); // NOI18N
+        setAlwaysOnTop(true);
+        setResizable(false);
+        getContentPane().setLayout(new java.awt.GridBagLayout());
+
+        org.openide.awt.Mnemonics.setLocalizedText(licenseNumberLabel, org.openide.util.NbBundle.getMessage(CTLicenseDialog.class, "CTLicenseDialog.licenseNumberLabel.text")); // NOI18N
+        gridBagConstraints = new java.awt.GridBagConstraints();
+        gridBagConstraints.gridx = 0;
+        gridBagConstraints.gridy = 0;
+        gridBagConstraints.gridwidth = 3;
+        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
+        gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
+        gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5);
+        getContentPane().add(licenseNumberLabel, gridBagConstraints);
+
+        warningLabel.setForeground(java.awt.Color.RED);
+        org.openide.awt.Mnemonics.setLocalizedText(warningLabel, org.openide.util.NbBundle.getMessage(CTLicenseDialog.class, "CTLicenseDialog.warningLabel.text")); // NOI18N
+        warningLabel.setMaximumSize(new java.awt.Dimension(419, 36));
+        warningLabel.setMinimumSize(new java.awt.Dimension(419, 36));
+        warningLabel.setPreferredSize(new java.awt.Dimension(419, 36));
+        gridBagConstraints = new java.awt.GridBagConstraints();
+        gridBagConstraints.gridx = 0;
+        gridBagConstraints.gridy = 2;
+        gridBagConstraints.gridwidth = 3;
+        gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
+        gridBagConstraints.insets = new java.awt.Insets(0, 5, 5, 5);
+        getContentPane().add(warningLabel, gridBagConstraints);
+
+        javax.swing.GroupLayout buttonPaddingLayout = new javax.swing.GroupLayout(buttonPadding);
+        buttonPadding.setLayout(buttonPaddingLayout);
+        buttonPaddingLayout.setHorizontalGroup(
+            buttonPaddingLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addGap(0, 0, Short.MAX_VALUE)
+        );
+        buttonPaddingLayout.setVerticalGroup(
+            buttonPaddingLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+            .addGap(0, 0, Short.MAX_VALUE)
+        );
+
+        gridBagConstraints = new java.awt.GridBagConstraints();
+        gridBagConstraints.gridx = 0;
+        gridBagConstraints.gridy = 3;
+        gridBagConstraints.weightx = 1.0;
+        getContentPane().add(buttonPadding, gridBagConstraints);
+
+        org.openide.awt.Mnemonics.setLocalizedText(okButton, org.openide.util.NbBundle.getMessage(CTLicenseDialog.class, "CTLicenseDialog.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 = 2;
+        gridBagConstraints.gridy = 3;
+        gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
+        gridBagConstraints.insets = new java.awt.Insets(0, 5, 5, 5);
+        getContentPane().add(okButton, gridBagConstraints);
+
+        org.openide.awt.Mnemonics.setLocalizedText(cancelButton, org.openide.util.NbBundle.getMessage(CTLicenseDialog.class, "CTLicenseDialog.cancelButton.text")); // NOI18N
+        cancelButton.addActionListener(new java.awt.event.ActionListener() {
+            public void actionPerformed(java.awt.event.ActionEvent evt) {
+                cancelButtonActionPerformed(evt);
+            }
+        });
+        gridBagConstraints = new java.awt.GridBagConstraints();
+        gridBagConstraints.gridx = 1;
+        gridBagConstraints.gridy = 3;
+        gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
+        gridBagConstraints.insets = new java.awt.Insets(0, 5, 5, 5);
+        getContentPane().add(cancelButton, gridBagConstraints);
+
+        licenseNumberTextField.setText(org.openide.util.NbBundle.getMessage(CTLicenseDialog.class, "CTLicenseDialog.licenseNumberTextField.text")); // NOI18N
+        gridBagConstraints = new java.awt.GridBagConstraints();
+        gridBagConstraints.gridx = 0;
+        gridBagConstraints.gridy = 1;
+        gridBagConstraints.gridwidth = 3;
+        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
+        gridBagConstraints.insets = new java.awt.Insets(0, 5, 5, 5);
+        getContentPane().add(licenseNumberTextField, gridBagConstraints);
+
+        pack();
+    }// </editor-fold>//GEN-END:initComponents
+
+    private void okButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_okButtonActionPerformed
+        this.licenseString = this.licenseNumberTextField.getText();
+        this.dispose();
+    }//GEN-LAST:event_okButtonActionPerformed
+
+    private void cancelButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cancelButtonActionPerformed
+        this.licenseString = null;
+        this.dispose();
+    }//GEN-LAST:event_cancelButtonActionPerformed
+
+
+    // Variables declaration - do not modify//GEN-BEGIN:variables
+    private javax.swing.JButton cancelButton;
+    private javax.swing.JTextField licenseNumberTextField;
+    private javax.swing.JButton okButton;
+    private javax.swing.JLabel warningLabel;
+    // End of variables declaration//GEN-END:variables
+}
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTLicensePersistence.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTLicensePersistence.java
new file mode 100644
index 0000000000..8f9d61a79e
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTLicensePersistence.java
@@ -0,0 +1,90 @@
+/** *************************************************************************
+ ** This data and information is proprietary to, and a valuable trade secret
+ ** of, Basis Technology Corp. It is given in confidence by Basis Technology
+ ** and may only be used as permitted under the license agreement under which
+ ** it has been distributed, and in no other way.
+ **
+ ** Copyright (c) 2023 Basis Technology Corp. All rights reserved.
+ **
+ ** The technical data and information provided herein are provided with
+ ** `limited rights', and the computer software provided herein is provided
+ ** with `restricted rights' as those terms are defined in DAR and ASPR
+ ** 7-104.9(a).
+ ************************************************************************** */
+package com.basistech.df.cybertriage.autopsy.ctoptions.ctcloud;
+
+import com.basistech.df.cybertriage.autopsy.ctapi.json.LicenseInfo;
+import com.basistech.df.cybertriage.autopsy.ctapi.json.LicenseResponse;
+import com.basistech.df.cybertriage.autopsy.ctapi.util.LicenseDecryptorUtil;
+import com.basistech.df.cybertriage.autopsy.ctapi.util.ObjectMapperUtil;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Paths;
+import java.util.Optional;
+import java.util.logging.Level;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.coreutils.PlatformUtil;
+
+/**
+ * Handles persisting CT Settings.
+ */
+public class CTLicensePersistence {
+
+    private static final String CT_SETTINGS_DIR = "CyberTriage";
+    private static final String CT_LICENSE_FILENAME = "CyberTriageLicense.json";
+
+    private static final Logger logger = Logger.getLogger(CTLicensePersistence.class.getName());
+
+    private static final CTLicensePersistence instance = new CTLicensePersistence();
+
+    private final ObjectMapper objectMapper = ObjectMapperUtil.getInstance().getDefaultObjectMapper();
+
+    public static CTLicensePersistence getInstance() {
+        return instance;
+    }
+
+    public synchronized boolean saveLicenseResponse(LicenseResponse licenseResponse) {
+        if (licenseResponse != null) {
+            File licenseFile = getCTLicenseFile();
+            try {
+                objectMapper.writeValue(licenseFile, licenseResponse);
+                return true;
+            } catch (IOException ex) {
+                logger.log(Level.WARNING, "There was an error writing CyberTriage license to file: " + licenseFile.getAbsolutePath(), ex);
+            }
+        }
+
+        return false;
+    }
+
+    public synchronized Optional<LicenseResponse> loadLicenseResponse() {
+        Optional<LicenseResponse> toRet = Optional.empty();
+        File licenseFile = getCTLicenseFile();
+        if (licenseFile.isFile()) {
+            try {
+                toRet = Optional.ofNullable(objectMapper.readValue(licenseFile, LicenseResponse.class));
+            } catch (IOException ex) {
+                logger.log(Level.WARNING, "There was an error reading CyberTriage license to file: " + licenseFile.getAbsolutePath(), ex);
+            }
+        }
+
+        return toRet;
+    }
+
+    public synchronized Optional<LicenseInfo> loadLicenseInfo() {
+        return loadLicenseResponse().flatMap((license) -> {
+            try {
+                return Optional.ofNullable(LicenseDecryptorUtil.getInstance().createLicenseInfo(license));
+            } catch (JsonProcessingException | LicenseDecryptorUtil.InvalidLicenseException ex) {
+                logger.log(Level.WARNING, "There was an error decrypting license data from license file", ex);
+                return Optional.empty();
+            }
+        });
+    }
+
+    private File getCTLicenseFile() {
+        return Paths.get(PlatformUtil.getModuleConfigDirectory(), CT_SETTINGS_DIR, CT_LICENSE_FILENAME).toFile();
+    }
+}
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTMalwareScannerOptionsPanel.form b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTMalwareScannerOptionsPanel.form
new file mode 100644
index 0000000000..77361419b6
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTMalwareScannerOptionsPanel.form
@@ -0,0 +1,199 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<Form version="1.8" maxVersion="1.9" type="org.netbeans.modules.form.forminfo.JPanelFormInfo">
+  <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,-109,0,0,1,-29"/>
+  </AuxValues>
+
+  <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/>
+  <SubComponents>
+    <Container class="javax.swing.JPanel" name="licenseInfoPanel">
+      <Properties>
+        <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor">
+          <Border info="org.netbeans.modules.form.compat2.border.TitledBorderInfo">
+            <TitledBorder title="License Info">
+              <ResourceString PropertyName="titleX" bundle="com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties" key="CTMalwareScannerOptionsPanel.licenseInfoPanel.border.title" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+            </TitledBorder>
+          </Border>
+        </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="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="18" weightX="1.0" weightY="0.0"/>
+        </Constraint>
+      </Constraints>
+
+      <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/>
+      <SubComponents>
+        <Component class="javax.swing.JLabel" name="licenseInfoMessageLabel">
+          <Properties>
+            <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+              <ResourceString bundle="com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties" key="CTMalwareScannerOptionsPanel.licenseInfoMessageLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+            </Property>
+          </Properties>
+          <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="licenseInfoUserLabel">
+          <Properties>
+            <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+              <ResourceString bundle="com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties" key="CTMalwareScannerOptionsPanel.licenseInfoUserLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+            </Property>
+          </Properties>
+          <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="5" insetsLeft="5" insetsBottom="5" insetsRight="5" anchor="18" weightX="1.0" weightY="0.0"/>
+            </Constraint>
+          </Constraints>
+        </Component>
+        <Component class="javax.swing.JLabel" name="licenseInfoExpiresLabel">
+          <Properties>
+            <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+              <ResourceString bundle="com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties" key="CTMalwareScannerOptionsPanel.licenseInfoExpiresLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+            </Property>
+          </Properties>
+          <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="0" 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="licenseInfoIdLabel">
+          <Properties>
+            <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+              <ResourceString bundle="com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties" key="CTMalwareScannerOptionsPanel.licenseInfoIdLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+            </Property>
+          </Properties>
+          <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="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="5" insetsBottom="5" insetsRight="5" anchor="18" weightX="1.0" weightY="0.0"/>
+            </Constraint>
+          </Constraints>
+        </Component>
+        <Component class="javax.swing.JButton" name="licenseInfoAddButton">
+          <Properties>
+            <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+              <ResourceString bundle="com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties" key="CTMalwareScannerOptionsPanel.licenseInfoAddButton.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="licenseInfoAddButtonActionPerformed"/>
+          </Events>
+          <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="0" insetsLeft="5" insetsBottom="5" insetsRight="5" anchor="12" weightX="1.0" weightY="0.0"/>
+            </Constraint>
+          </Constraints>
+        </Component>
+      </SubComponents>
+    </Container>
+    <Container class="javax.swing.JPanel" name="malwareScansPanel">
+      <Properties>
+        <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor">
+          <Border info="org.netbeans.modules.form.compat2.border.TitledBorderInfo">
+            <TitledBorder title="Malware Scans">
+              <ResourceString PropertyName="titleX" bundle="com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties" key="CTMalwareScannerOptionsPanel.malwareScansPanel.border.title" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+            </TitledBorder>
+          </Border>
+        </Property>
+      </Properties>
+      <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="1" gridHeight="1" fill="2" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="0" insetsBottom="0" insetsRight="0" anchor="18" weightX="1.0" weightY="0.0"/>
+        </Constraint>
+      </Constraints>
+
+      <Layout class="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout"/>
+      <SubComponents>
+        <Component class="javax.swing.JLabel" name="malwareScansMessageLabel">
+          <Properties>
+            <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+              <ResourceString bundle="com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties" key="CTMalwareScannerOptionsPanel.malwareScansMessageLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+            </Property>
+          </Properties>
+          <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="maxHashLookupsLabel">
+          <Properties>
+            <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+              <ResourceString bundle="com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties" key="CTMalwareScannerOptionsPanel.maxHashLookupsLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+            </Property>
+          </Properties>
+          <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="5" insetsLeft="5" insetsBottom="5" insetsRight="5" anchor="18" weightX="1.0" weightY="0.0"/>
+            </Constraint>
+          </Constraints>
+        </Component>
+        <Component class="javax.swing.JLabel" name="maxFileUploadsLabel">
+          <Properties>
+            <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+              <ResourceString bundle="com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties" key="CTMalwareScannerOptionsPanel.maxFileUploadsLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+            </Property>
+          </Properties>
+          <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="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="5" insetsBottom="5" insetsRight="5" anchor="18" weightX="1.0" weightY="0.0"/>
+            </Constraint>
+          </Constraints>
+        </Component>
+        <Component class="javax.swing.JLabel" name="countersResetLabel">
+          <Properties>
+            <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+              <ResourceString bundle="com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties" key="CTMalwareScannerOptionsPanel.countersResetLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+            </Property>
+          </Properties>
+          <Constraints>
+            <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout" value="org.netbeans.modules.form.compat2.layouts.DesignGridBagLayout$GridBagConstraintsDescription">
+              <GridBagConstraints gridX="0" gridY="3" gridWidth="1" gridHeight="1" fill="0" ipadX="0" ipadY="0" insetsTop="0" insetsLeft="5" insetsBottom="5" insetsRight="5" anchor="18" weightX="1.0" weightY="0.0"/>
+            </Constraint>
+          </Constraints>
+        </Component>
+        <Component class="javax.swing.JLabel" name="hashLookupsRemainingLabel">
+          <Properties>
+            <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+              <ResourceString bundle="com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties" key="CTMalwareScannerOptionsPanel.hashLookupsRemainingLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+            </Property>
+          </Properties>
+          <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="0" 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="fileUploadsRemainingLabel">
+          <Properties>
+            <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor">
+              <ResourceString bundle="com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/Bundle.properties" key="CTMalwareScannerOptionsPanel.fileUploadsRemainingLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, &quot;{key}&quot;)"/>
+            </Property>
+          </Properties>
+          <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="0" insetsLeft="5" insetsBottom="5" insetsRight="5" anchor="18" weightX="1.0" weightY="0.0"/>
+            </Constraint>
+          </Constraints>
+        </Component>
+      </SubComponents>
+    </Container>
+  </SubComponents>
+</Form>
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTMalwareScannerOptionsPanel.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTMalwareScannerOptionsPanel.java
new file mode 100644
index 0000000000..069d9fb653
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/ctcloud/CTMalwareScannerOptionsPanel.java
@@ -0,0 +1,551 @@
+/** *************************************************************************
+ ** This data and information is proprietary to, and a valuable trade secret
+ ** of, Basis Technology Corp. It is given in confidence by Basis Technology
+ ** and may only be used as permitted under the license agreement under which
+ ** it has been distributed, and in no other way.
+ **
+ ** Copyright (c) 2023 Basis Technology Corp. All rights reserved.
+ **
+ ** The technical data and information provided herein are provided with
+ ** `limited rights', and the computer software provided herein is provided
+ ** with `restricted rights' as those terms are defined in DAR and ASPR
+ ** 7-104.9(a).
+ ************************************************************************** */
+package com.basistech.df.cybertriage.autopsy.ctoptions.ctcloud;
+
+import com.basistech.df.cybertriage.autopsy.ctoptions.subpanel.CTOptionsSubPanel;
+import com.basistech.df.cybertriage.autopsy.ctapi.CTCloudException;
+import com.basistech.df.cybertriage.autopsy.ctapi.CtApiDAO;
+import com.basistech.df.cybertriage.autopsy.ctapi.json.AuthTokenResponse;
+import com.basistech.df.cybertriage.autopsy.ctapi.json.LicenseInfo;
+import com.basistech.df.cybertriage.autopsy.ctapi.json.LicenseResponse;
+import com.basistech.df.cybertriage.autopsy.ctapi.util.LicenseDecryptorUtil;
+import java.awt.event.ComponentAdapter;
+import java.awt.event.ComponentEvent;
+import java.text.SimpleDateFormat;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.swing.JOptionPane;
+import javax.swing.SwingWorker;
+import org.apache.commons.lang3.StringUtils;
+import org.openide.util.NbBundle;
+import org.openide.util.NbBundle.Messages;
+import org.openide.util.lookup.ServiceProvider;
+import org.openide.windows.WindowManager;
+
+/**
+ * Options panel for CyberTriage options for importing a CyberTriage incident
+ */
+@ServiceProvider(service = CTOptionsSubPanel.class)
+public class CTMalwareScannerOptionsPanel extends CTOptionsSubPanel {
+
+    private static final Logger logger = Logger.getLogger(CTMalwareScannerOptionsPanel.class.getName());
+
+    private static final SimpleDateFormat LICENSE_EXPIRES_FORMAT = new SimpleDateFormat("MMMM d, YYYY");
+    private static final SimpleDateFormat MALWARE_SCANS_RESET_FORMAT = new SimpleDateFormat("MMM d, YYYY' at 'h:mma");
+
+    private final CtApiDAO ctApiDAO = CtApiDAO.getInstance();
+    private final CTLicensePersistence ctPersistence = CTLicensePersistence.getInstance();
+
+    private volatile LicenseInfo licenseInfo = null;
+    private volatile String licenseInfoMessage = null;
+    private volatile LicenseFetcher licenseFetcher = null;
+
+    private volatile AuthTokenResponse authTokenResponse = null;
+    private volatile String authTokenMessage = null;
+    private volatile AuthTokenFetcher authTokenFetcher = null;
+
+    /**
+     * Creates new form CTIncidentImportOptionsPanel
+     */
+    public CTMalwareScannerOptionsPanel() {
+        initComponents();
+
+        this.addComponentListener(new ComponentAdapter() {
+            @Override
+            public void componentHidden(ComponentEvent e) {
+                synchronized (CTMalwareScannerOptionsPanel.this) {
+                    if (CTMalwareScannerOptionsPanel.this.isLicenseAddRunning()) {
+                        CTMalwareScannerOptionsPanel.this.licenseFetcher.cancel(true);
+                        CTMalwareScannerOptionsPanel.this.licenseFetcher = null;
+                    }
+
+                    if (CTMalwareScannerOptionsPanel.this.isMalwareScansRunning()) {
+                        CTMalwareScannerOptionsPanel.this.authTokenFetcher.cancel(true);
+                        CTMalwareScannerOptionsPanel.this.authTokenFetcher = null;
+                    }
+                }
+            }
+
+            @Override
+            public void componentShown(ComponentEvent e) {
+                synchronized (CTMalwareScannerOptionsPanel.this) {
+                    if (CTMalwareScannerOptionsPanel.this.licenseInfo != null) {
+                        loadMalwareScansInfo(CTMalwareScannerOptionsPanel.this.licenseInfo);
+                    }
+                }
+            }
+        }
+        );
+    }
+
+    @Override
+    public synchronized void saveSettings() {
+        ctPersistence.saveLicenseResponse(getLicenseInfo());
+    }
+
+    @Override
+    public boolean valid() {
+        return true;
+    }
+
+    @Override
+    public synchronized void loadSettings() {
+        Optional<LicenseInfo> licenseInfoOpt = ctPersistence.loadLicenseInfo();
+        LicenseInfo licenseInfo = licenseInfoOpt.orElse(null);
+        setLicenseDisplay(licenseInfo, null);
+        setMalwareScansDisplay(null, null);
+        if (licenseInfo != null) {
+            loadMalwareScansInfo(licenseInfo);
+        }
+    }
+
+    private synchronized LicenseResponse getLicenseInfo() {
+        return this.licenseInfo == null ? null : this.licenseInfo.getLicenseResponse();
+    }
+
+    private synchronized void setLicenseDisplay(LicenseInfo licenseInfo, String licenseMessage) {
+        this.licenseInfo = licenseInfo;
+        this.licenseInfoMessage = licenseMessage;
+        renderLicenseState();
+    }
+
+    private synchronized void setMalwareScansDisplay(AuthTokenResponse authTokenResponse, String authTokenMessage) {
+        this.authTokenResponse = authTokenResponse;
+        this.authTokenMessage = authTokenMessage;
+        renderLicenseState();
+    }
+
+    /**
+     * @return True if there is an operation to fetch the license.
+     */
+    private synchronized boolean isLicenseAddRunning() {
+        return this.licenseFetcher != null && !this.licenseFetcher.isCancelled() && !this.licenseFetcher.isDone();
+    }
+
+    /**
+     * @return True if there is an operation to fetch malware scans information.
+     */
+    private synchronized boolean isMalwareScansRunning() {
+        return this.authTokenFetcher != null && !this.authTokenFetcher.isCancelled() && !this.authTokenFetcher.isDone();
+    }
+
+    @Messages({
+        "CTOPtionsPanel_loadLicenseInfo_loading=Loading..."
+    })
+    private synchronized void loadLicenseInfo(String licenseNumber) {
+        if (isLicenseAddRunning()) {
+            this.licenseFetcher.cancel(true);
+        }
+        setLicenseDisplay(null, Bundle.CTOPtionsPanel_loadLicenseInfo_loading());
+        this.licenseFetcher = new LicenseFetcher(licenseNumber);
+        this.licenseFetcher.execute();
+    }
+
+    @Messages({
+        "CTOPtionsPanel_loadMalwareScansInfo_loading=Loading..."
+    })
+    private synchronized void loadMalwareScansInfo(LicenseInfo licenseInfo) {
+        if (isMalwareScansRunning()) {
+            this.authTokenFetcher.cancel(true);
+        }
+
+        setMalwareScansDisplay(null, Bundle.CTOPtionsPanel_loadMalwareScansInfo_loading());
+
+        if (licenseInfo == null || licenseInfo.getDecryptedLicense() == null) {
+            return;
+        }
+
+        this.authTokenFetcher = new AuthTokenFetcher(licenseInfo.getDecryptedLicense().getBoostLicenseId());
+        this.authTokenFetcher.execute();
+    }
+
+    /**
+     * 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.JPanel licenseInfoPanel = new javax.swing.JPanel();
+        licenseInfoMessageLabel = new javax.swing.JLabel();
+        licenseInfoUserLabel = new javax.swing.JLabel();
+        licenseInfoExpiresLabel = new javax.swing.JLabel();
+        licenseInfoIdLabel = new javax.swing.JLabel();
+        licenseInfoAddButton = new javax.swing.JButton();
+        malwareScansPanel = new javax.swing.JPanel();
+        malwareScansMessageLabel = new javax.swing.JLabel();
+        maxHashLookupsLabel = new javax.swing.JLabel();
+        maxFileUploadsLabel = new javax.swing.JLabel();
+        countersResetLabel = new javax.swing.JLabel();
+        hashLookupsRemainingLabel = new javax.swing.JLabel();
+        fileUploadsRemainingLabel = new javax.swing.JLabel();
+
+        setLayout(new java.awt.GridBagLayout());
+
+        licenseInfoPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(org.openide.util.NbBundle.getMessage(CTMalwareScannerOptionsPanel.class, "CTMalwareScannerOptionsPanel.licenseInfoPanel.border.title"))); // NOI18N
+        licenseInfoPanel.setLayout(new java.awt.GridBagLayout());
+
+        org.openide.awt.Mnemonics.setLocalizedText(licenseInfoMessageLabel, org.openide.util.NbBundle.getMessage(CTMalwareScannerOptionsPanel.class, "CTMalwareScannerOptionsPanel.licenseInfoMessageLabel.text")); // NOI18N
+        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);
+        licenseInfoPanel.add(licenseInfoMessageLabel, gridBagConstraints);
+
+        org.openide.awt.Mnemonics.setLocalizedText(licenseInfoUserLabel, org.openide.util.NbBundle.getMessage(CTMalwareScannerOptionsPanel.class, "CTMalwareScannerOptionsPanel.licenseInfoUserLabel.text")); // NOI18N
+        gridBagConstraints = new java.awt.GridBagConstraints();
+        gridBagConstraints.gridx = 0;
+        gridBagConstraints.gridy = 1;
+        gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
+        gridBagConstraints.weightx = 1.0;
+        gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5);
+        licenseInfoPanel.add(licenseInfoUserLabel, gridBagConstraints);
+
+        org.openide.awt.Mnemonics.setLocalizedText(licenseInfoExpiresLabel, org.openide.util.NbBundle.getMessage(CTMalwareScannerOptionsPanel.class, "CTMalwareScannerOptionsPanel.licenseInfoExpiresLabel.text")); // NOI18N
+        gridBagConstraints = new java.awt.GridBagConstraints();
+        gridBagConstraints.gridx = 1;
+        gridBagConstraints.gridy = 1;
+        gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
+        gridBagConstraints.weightx = 1.0;
+        gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5);
+        licenseInfoPanel.add(licenseInfoExpiresLabel, gridBagConstraints);
+
+        org.openide.awt.Mnemonics.setLocalizedText(licenseInfoIdLabel, org.openide.util.NbBundle.getMessage(CTMalwareScannerOptionsPanel.class, "CTMalwareScannerOptionsPanel.licenseInfoIdLabel.text")); // NOI18N
+        gridBagConstraints = new java.awt.GridBagConstraints();
+        gridBagConstraints.gridx = 0;
+        gridBagConstraints.gridy = 2;
+        gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
+        gridBagConstraints.weightx = 1.0;
+        gridBagConstraints.insets = new java.awt.Insets(0, 5, 5, 5);
+        licenseInfoPanel.add(licenseInfoIdLabel, gridBagConstraints);
+
+        org.openide.awt.Mnemonics.setLocalizedText(licenseInfoAddButton, org.openide.util.NbBundle.getMessage(CTMalwareScannerOptionsPanel.class, "CTMalwareScannerOptionsPanel.licenseInfoAddButton.text")); // NOI18N
+        licenseInfoAddButton.addActionListener(new java.awt.event.ActionListener() {
+            public void actionPerformed(java.awt.event.ActionEvent evt) {
+                licenseInfoAddButtonActionPerformed(evt);
+            }
+        });
+        gridBagConstraints = new java.awt.GridBagConstraints();
+        gridBagConstraints.gridx = 1;
+        gridBagConstraints.gridy = 2;
+        gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHEAST;
+        gridBagConstraints.weightx = 1.0;
+        gridBagConstraints.insets = new java.awt.Insets(0, 5, 5, 5);
+        licenseInfoPanel.add(licenseInfoAddButton, gridBagConstraints);
+
+        gridBagConstraints = new java.awt.GridBagConstraints();
+        gridBagConstraints.gridx = 0;
+        gridBagConstraints.gridy = 1;
+        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
+        gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
+        gridBagConstraints.weightx = 1.0;
+        add(licenseInfoPanel, gridBagConstraints);
+
+        malwareScansPanel.setBorder(javax.swing.BorderFactory.createTitledBorder(org.openide.util.NbBundle.getMessage(CTMalwareScannerOptionsPanel.class, "CTMalwareScannerOptionsPanel.malwareScansPanel.border.title"))); // NOI18N
+        malwareScansPanel.setLayout(new java.awt.GridBagLayout());
+
+        org.openide.awt.Mnemonics.setLocalizedText(malwareScansMessageLabel, org.openide.util.NbBundle.getMessage(CTMalwareScannerOptionsPanel.class, "CTMalwareScannerOptionsPanel.malwareScansMessageLabel.text")); // NOI18N
+        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);
+        malwareScansPanel.add(malwareScansMessageLabel, gridBagConstraints);
+
+        org.openide.awt.Mnemonics.setLocalizedText(maxHashLookupsLabel, org.openide.util.NbBundle.getMessage(CTMalwareScannerOptionsPanel.class, "CTMalwareScannerOptionsPanel.maxHashLookupsLabel.text")); // NOI18N
+        gridBagConstraints = new java.awt.GridBagConstraints();
+        gridBagConstraints.gridx = 0;
+        gridBagConstraints.gridy = 1;
+        gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
+        gridBagConstraints.weightx = 1.0;
+        gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5);
+        malwareScansPanel.add(maxHashLookupsLabel, gridBagConstraints);
+
+        org.openide.awt.Mnemonics.setLocalizedText(maxFileUploadsLabel, org.openide.util.NbBundle.getMessage(CTMalwareScannerOptionsPanel.class, "CTMalwareScannerOptionsPanel.maxFileUploadsLabel.text")); // NOI18N
+        gridBagConstraints = new java.awt.GridBagConstraints();
+        gridBagConstraints.gridx = 0;
+        gridBagConstraints.gridy = 2;
+        gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
+        gridBagConstraints.weightx = 1.0;
+        gridBagConstraints.insets = new java.awt.Insets(0, 5, 5, 5);
+        malwareScansPanel.add(maxFileUploadsLabel, gridBagConstraints);
+
+        org.openide.awt.Mnemonics.setLocalizedText(countersResetLabel, org.openide.util.NbBundle.getMessage(CTMalwareScannerOptionsPanel.class, "CTMalwareScannerOptionsPanel.countersResetLabel.text")); // NOI18N
+        gridBagConstraints = new java.awt.GridBagConstraints();
+        gridBagConstraints.gridx = 0;
+        gridBagConstraints.gridy = 3;
+        gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
+        gridBagConstraints.weightx = 1.0;
+        gridBagConstraints.insets = new java.awt.Insets(0, 5, 5, 5);
+        malwareScansPanel.add(countersResetLabel, gridBagConstraints);
+
+        org.openide.awt.Mnemonics.setLocalizedText(hashLookupsRemainingLabel, org.openide.util.NbBundle.getMessage(CTMalwareScannerOptionsPanel.class, "CTMalwareScannerOptionsPanel.hashLookupsRemainingLabel.text")); // NOI18N
+        gridBagConstraints = new java.awt.GridBagConstraints();
+        gridBagConstraints.gridx = 1;
+        gridBagConstraints.gridy = 1;
+        gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
+        gridBagConstraints.weightx = 1.0;
+        gridBagConstraints.insets = new java.awt.Insets(5, 5, 5, 5);
+        malwareScansPanel.add(hashLookupsRemainingLabel, gridBagConstraints);
+
+        org.openide.awt.Mnemonics.setLocalizedText(fileUploadsRemainingLabel, org.openide.util.NbBundle.getMessage(CTMalwareScannerOptionsPanel.class, "CTMalwareScannerOptionsPanel.fileUploadsRemainingLabel.text")); // NOI18N
+        gridBagConstraints = new java.awt.GridBagConstraints();
+        gridBagConstraints.gridx = 1;
+        gridBagConstraints.gridy = 2;
+        gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
+        gridBagConstraints.weightx = 1.0;
+        gridBagConstraints.insets = new java.awt.Insets(0, 5, 5, 5);
+        malwareScansPanel.add(fileUploadsRemainingLabel, gridBagConstraints);
+
+        gridBagConstraints = new java.awt.GridBagConstraints();
+        gridBagConstraints.gridx = 0;
+        gridBagConstraints.gridy = 2;
+        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
+        gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
+        gridBagConstraints.weightx = 1.0;
+        add(malwareScansPanel, gridBagConstraints);
+    }// </editor-fold>//GEN-END:initComponents
+
+    @Messages({
+        "CTMalwareScannerOptionsPanel_licenseAddDialog_title=Add a License...",
+        "CTMalwareScannerOptionsPanel_licenseAddDialog_desc=License Number:",
+        "CTMalwareScannerOptionsPanel_licenseAddDialogEnteredErr_title=License Number Already Entered",
+        "CTMalwareScannerOptionsPanel_licenseAddDialogEnteredErr_desc=The license number has already been entered",
+        "CTMalwareScannerOptionsPanel_licenseAddDialogPatternErr_title=Invalid License Number",
+        "CTMalwareScannerOptionsPanel_licenseAddDialogPatternErr_desc=Please verify that license number is of format 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'"})
+    private void licenseInfoAddButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_licenseInfoAddButtonActionPerformed
+        CTLicenseDialog licenseDialog = new CTLicenseDialog(WindowManager.getDefault().getMainWindow(), true);
+        licenseDialog.setLocationRelativeTo(this);
+        licenseDialog.setVisible(true);
+        String licenseNumber = licenseDialog.getValue();
+        if (licenseNumber != null) {
+            synchronized (this) {
+                if (this.licenseInfo == null || !licenseNumber.trim().equalsIgnoreCase(this.licenseInfo.getDecryptedLicense().getBoostLicenseId())) {
+                    loadLicenseInfo(licenseNumber);
+                    return;
+                }
+            }
+
+            JOptionPane.showMessageDialog(
+                    this,
+                    Bundle.CTMalwareScannerOptionsPanel_licenseAddDialogEnteredErr_desc(),
+                    Bundle.CTMalwareScannerOptionsPanel_licenseAddDialogEnteredErr_title(),
+                    JOptionPane.INFORMATION_MESSAGE);
+
+        }
+    }//GEN-LAST:event_licenseInfoAddButtonActionPerformed
+
+    @NbBundle.Messages({
+        "# {0} - userName",
+        "# {1} - email",
+        "CTMalwareScannerOptionsPanel_licenseInfo_userInfo=<html>User: {0}<br/>Email: {1}</html>",
+        "# {0} - expiresDate",
+        "CTMalwareScannerOptionsPanel_licenseInfo_expires=Expires: {0}",
+        "# {0} - idNumber",
+        "CTMalwareScannerOptionsPanel_licenseInfo_id=ID: {0}",
+        "# {0} - maxDailyLookups",
+        "CTMalwareScannerOptionsPanel_malwareScans_maxDailyHashLookups=Max Hash lookups: {0}/day",
+        "# {0} - maxDailyFileLookups",
+        "CTMalwareScannerOptionsPanel_malwareScans_maxDailyFileLookups=Max file uploads: {0}/day",
+        "# {0} - countersResetDate",
+        "CTMalwareScannerOptionsPanel_malwareScans_countersReset=Counters reset: {0}",
+        "# {0} - hashLookupsRemaining",
+        "CTMalwareScannerOptionsPanel_malwareScans_hashLookupsRemaining=Hash lookups remaining: {0}",
+        "# {0} - fileUploadsRemaining",
+        "CTMalwareScannerOptionsPanel_malwareScans_fileUploadsRemaining=File uploads remaining: {0}"})
+    private synchronized void renderLicenseState() {
+        this.licenseInfoAddButton.setEnabled(!isLicenseAddRunning());
+
+        this.licenseInfoMessageLabel.setVisible(StringUtils.isNotBlank(this.licenseInfoMessage));
+        this.licenseInfoMessageLabel.setText(this.licenseInfoMessage);
+
+        if (licenseInfo == null) {
+            this.licenseInfoExpiresLabel.setVisible(false);
+            this.licenseInfoIdLabel.setVisible(false);
+            this.licenseInfoUserLabel.setVisible(false);
+        } else {
+            this.licenseInfoExpiresLabel.setVisible(true);
+            this.licenseInfoExpiresLabel.setText(Bundle.CTMalwareScannerOptionsPanel_licenseInfo_expires(LICENSE_EXPIRES_FORMAT.format(this.licenseInfo.getDecryptedLicense().getExpirationDate())));
+            this.licenseInfoIdLabel.setVisible(true);
+            this.licenseInfoIdLabel.setText(Bundle.CTMalwareScannerOptionsPanel_licenseInfo_id(this.licenseInfo.getDecryptedLicense().getBoostLicenseId()));
+            this.licenseInfoUserLabel.setVisible(true);
+            this.licenseInfoUserLabel.setText(Bundle.CTMalwareScannerOptionsPanel_licenseInfo_userInfo(this.licenseInfo.getUser(), this.licenseInfo.getEmail()));
+        }
+
+        this.malwareScansPanel.setVisible(StringUtils.isNotBlank(this.authTokenMessage) || authTokenResponse != null);
+
+        this.malwareScansMessageLabel.setVisible(StringUtils.isNotBlank(this.authTokenMessage));
+        this.malwareScansMessageLabel.setText(this.authTokenMessage);
+
+        if (authTokenResponse == null) {
+            this.maxHashLookupsLabel.setVisible(false);
+            this.maxFileUploadsLabel.setVisible(false);
+            this.countersResetLabel.setVisible(false);
+            this.hashLookupsRemainingLabel.setVisible(false);
+            this.fileUploadsRemainingLabel.setVisible(false);
+        } else {
+            this.maxHashLookupsLabel.setVisible(true);
+            this.maxHashLookupsLabel.setText(Bundle.CTMalwareScannerOptionsPanel_malwareScans_maxDailyHashLookups(this.authTokenResponse.getHashLookupLimit()));
+            this.maxFileUploadsLabel.setVisible(true);
+            this.maxFileUploadsLabel.setText(Bundle.CTMalwareScannerOptionsPanel_malwareScans_maxDailyFileLookups(this.authTokenResponse.getFileUploadLimit()));
+            this.countersResetLabel.setVisible(true);
+            this.countersResetLabel.setText(Bundle.CTMalwareScannerOptionsPanel_malwareScans_countersReset(MALWARE_SCANS_RESET_FORMAT.format(this.authTokenResponse.getExpiration())));
+            this.hashLookupsRemainingLabel.setVisible(true);
+            this.hashLookupsRemainingLabel.setText(Bundle.CTMalwareScannerOptionsPanel_malwareScans_hashLookupsRemaining(remaining(this.authTokenResponse.getHashLookupLimit(), this.authTokenResponse.getHashLookupCount())));
+            this.fileUploadsRemainingLabel.setVisible(true);
+            this.fileUploadsRemainingLabel.setText(Bundle.CTMalwareScannerOptionsPanel_malwareScans_fileUploadsRemaining(remaining(this.authTokenResponse.getFileUploadLimit(), this.authTokenResponse.getFileUploadCount())));
+        }
+    }
+
+    private long remaining(Long total, Long used) {
+        total = total == null ? 0 : total;
+        used = used == null ? 0 : used;
+        return total - used;
+    }
+
+    @NbBundle.Messages({
+        "CTMalwareScannerOptionsPanel_LicenseFetcher_apiErr_title=Server Error",
+        "CTMalwareScannerOptionsPanel_LicenseFetcher_localErr_title=General Error",
+        "CTMalwareScannerOptionsPanel_LicenseFetcher_localErr_desc=A general error occurred while fetching license information.  Please try again later.",})
+    private class LicenseFetcher extends SwingWorker<LicenseInfo, Void> {
+
+        private final String licenseText;
+
+        public LicenseFetcher(String licenseText) {
+            this.licenseText = licenseText;
+        }
+
+        @Override
+        protected LicenseInfo doInBackground() throws Exception {
+            LicenseResponse licenseResponse = ctApiDAO.getLicenseInfo(licenseText);
+            ctPersistence.saveLicenseResponse(licenseResponse);
+            return LicenseDecryptorUtil.getInstance().createLicenseInfo(licenseResponse);
+        }
+
+        @Override
+        protected void done() {
+            LicenseInfo licenseInfo = null;
+            try {
+                licenseInfo = get();
+            } catch (InterruptedException ex) {
+                // ignore cancellation
+            } catch (ExecutionException ex) {
+                if (ex.getCause() != null && ex.getCause() instanceof CTCloudException cloudEx) {
+                    logger.log(Level.WARNING, "An API error occurred while fetching license information", cloudEx);
+                    JOptionPane.showMessageDialog(
+                            CTMalwareScannerOptionsPanel.this,
+                            cloudEx.getErrorCode().getDescription(),
+                            Bundle.CTMalwareScannerOptionsPanel_LicenseFetcher_apiErr_title(),
+                            JOptionPane.ERROR_MESSAGE);
+                } else {
+                    logger.log(Level.WARNING, "An error occurred while fetching data", ex);
+                    JOptionPane.showMessageDialog(
+                            CTMalwareScannerOptionsPanel.this,
+                            Bundle.CTMalwareScannerOptionsPanel_LicenseFetcher_localErr_desc(),
+                            Bundle.CTMalwareScannerOptionsPanel_LicenseFetcher_localErr_title(),
+                            JOptionPane.ERROR_MESSAGE);
+                }
+
+            } finally {
+
+                synchronized (CTMalwareScannerOptionsPanel.this) {
+                    CTMalwareScannerOptionsPanel.this.licenseFetcher = null;
+                    if (!this.isCancelled()) {
+                        setLicenseDisplay(licenseInfo, null);
+                        loadMalwareScansInfo(licenseInfo);
+                    }
+                }
+            }
+        }
+    }
+
+    @NbBundle.Messages({
+        "CTMalwareScannerOptionsPanel_MalwareScansFetcher_apiErr_title=Server Error",
+        "CTMalwareScannerOptionsPanel_MalwareScansFetcher_localErr_title=General Error",
+        "CTMalwareScannerOptionsPanel_MalwareScansFetcher_localErr_desc=A general error occurred while fetching malware scans information.  Please try again later.",})
+    private class AuthTokenFetcher extends SwingWorker<AuthTokenResponse, Void> {
+
+        private final String boostLicenseId;
+
+        public AuthTokenFetcher(String boostLicenseId) {
+            this.boostLicenseId = boostLicenseId;
+        }
+
+        @Override
+        protected AuthTokenResponse doInBackground() throws Exception {
+            return ctApiDAO.getAuthToken(boostLicenseId);
+        }
+
+        @Override
+        protected void done() {
+            AuthTokenResponse authTokenResponse = null;
+            try {
+                authTokenResponse = get();
+            } catch (InterruptedException ex) {
+                // ignore cancellation
+            } catch (ExecutionException ex) {
+                if (ex.getCause() != null && ex.getCause() instanceof CTCloudException cloudEx) {
+                    logger.log(Level.WARNING, "An API error occurred while fetching malware scans information for license", cloudEx);
+                    JOptionPane.showMessageDialog(
+                            CTMalwareScannerOptionsPanel.this,
+                            cloudEx.getErrorDetails(),
+                            Bundle.CTMalwareScannerOptionsPanel_MalwareScansFetcher_apiErr_title(),
+                            JOptionPane.ERROR_MESSAGE);
+                } else {
+                    logger.log(Level.WARNING, "An error occurred while fetching data", ex);
+                    JOptionPane.showMessageDialog(
+                            CTMalwareScannerOptionsPanel.this,
+                            Bundle.CTMalwareScannerOptionsPanel_MalwareScansFetcher_localErr_desc(),
+                            Bundle.CTMalwareScannerOptionsPanel_MalwareScansFetcher_localErr_title(),
+                            JOptionPane.ERROR_MESSAGE);
+                }
+            } finally {
+                synchronized (CTMalwareScannerOptionsPanel.this) {
+                    CTMalwareScannerOptionsPanel.this.authTokenFetcher = null;
+                    if (!this.isCancelled()) {
+                        setMalwareScansDisplay(authTokenResponse, null);
+                    }
+                }
+            }
+        }
+    }
+
+
+    // Variables declaration - do not modify//GEN-BEGIN:variables
+    private javax.swing.JLabel countersResetLabel;
+    private javax.swing.JLabel fileUploadsRemainingLabel;
+    private javax.swing.JLabel hashLookupsRemainingLabel;
+    private javax.swing.JButton licenseInfoAddButton;
+    private javax.swing.JLabel licenseInfoExpiresLabel;
+    private javax.swing.JLabel licenseInfoIdLabel;
+    private javax.swing.JLabel licenseInfoMessageLabel;
+    private javax.swing.JLabel licenseInfoUserLabel;
+    private javax.swing.JLabel malwareScansMessageLabel;
+    private javax.swing.JPanel malwareScansPanel;
+    private javax.swing.JLabel maxFileUploadsLabel;
+    private javax.swing.JLabel maxHashLookupsLabel;
+    // End of variables declaration//GEN-END:variables
+}
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/subpanel/CTOptionsSubPanel.java b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/subpanel/CTOptionsSubPanel.java
new file mode 100644
index 0000000000..67f727bb13
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/ctoptions/subpanel/CTOptionsSubPanel.java
@@ -0,0 +1,26 @@
+/** *************************************************************************
+ ** This data and information is proprietary to, and a valuable trade secret
+ ** of, Basis Technology Corp. It is given in confidence by Basis Technology
+ ** and may only be used as permitted under the license agreement under which
+ ** it has been distributed, and in no other way.
+ **
+ ** Copyright (c) 2023 Basis Technology Corp. All rights reserved.
+ **
+ ** The technical data and information provided herein are provided with
+ ** `limited rights', and the computer software provided herein is provided
+ ** with `restricted rights' as those terms are defined in DAR and ASPR
+ ** 7-104.9(a).
+ ************************************************************************** */
+package com.basistech.df.cybertriage.autopsy.ctoptions.subpanel;
+
+import javax.swing.JPanel;
+
+/**
+ * A panel to be put in the CyberTriage options.
+ */
+
+public abstract class CTOptionsSubPanel extends JPanel {
+    public abstract void loadSettings();
+    public abstract void saveSettings();
+    public abstract boolean valid();
+}
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/images/logo.png b/Core/src/com/basistech/df/cybertriage/autopsy/images/logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..7f5ab5ba4c2cd031b422d0ed333d665261a39af8
GIT binary patch
literal 10482
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
O7~0d-&t;ucLK6VGd8;V^

literal 0
HcmV?d00001

diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/BatchProcessor.java b/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/BatchProcessor.java
new file mode 100644
index 0000000000..9adb7410ce
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/BatchProcessor.java
@@ -0,0 +1,88 @@
+/** *************************************************************************
+ ** This data and information is proprietary to, and a valuable trade secret
+ ** of, Basis Technology Corp. It is given in confidence by Basis Technology
+ ** and may only be used as permitted under the license agreement under which
+ ** it has been distributed, and in no other way.
+ **
+ ** Copyright (c) 2023 Basis Technology Corp. All rights reserved.
+ **
+ ** The technical data and information provided herein are provided with
+ ** `limited rights', and the computer software provided herein is provided
+ ** with `restricted rights' as those terms are defined in DAR and ASPR
+ ** 7-104.9(a).
+ ************************************************************************** */
+package com.basistech.df.cybertriage.autopsy.malwarescan;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.function.Consumer;
+
+/**
+ * Processes a batch when number of items reaches batchSize or flush. Processing
+ * blocks (and subsequently add and flush operations) until previous batch
+ * finishes.
+ */
+public class BatchProcessor<T> {
+
+    private final ExecutorService processingExecutorService = Executors.newSingleThreadExecutor();
+
+    private final BlockingQueue<T> batchingQueue;
+    private final List<T> processingQueue;
+    private final int batchSize;
+    private final Consumer<List<T>> itemsConsumer;
+    private final long millisTimeout;
+
+    private Future<?> lastProcessingFuture = CompletableFuture.runAsync(() -> {
+    });
+
+    public BatchProcessor(int batchSize, long millisTimeout, Consumer<List<T>> itemsConsumer) {
+        this.batchingQueue = new LinkedBlockingQueue<>(batchSize);
+        this.processingQueue = new ArrayList<>(batchSize);
+        this.batchSize = batchSize;
+        this.itemsConsumer = itemsConsumer;
+        this.millisTimeout = millisTimeout;
+    }
+
+    public synchronized void clearCurrentBatch() {
+        batchingQueue.clear();
+    }
+    
+    public synchronized void flush(boolean blockUntilFinished) throws InterruptedException {
+        asyncProcessBatch();
+        if (blockUntilFinished) {
+            lastProcessingFuture.wait(millisTimeout);
+        }
+    }
+
+    public synchronized void add(T item) throws InterruptedException {
+        batchingQueue.add(item);
+        if (batchingQueue.size() >= batchSize) {
+            asyncProcessBatch();
+        }
+    }
+
+    private synchronized void asyncProcessBatch() throws InterruptedException {
+        if (!batchingQueue.isEmpty()) {
+            // wait for previous processing to finish
+            lastProcessingFuture.wait(millisTimeout);
+
+            // if 'andThen' doesn't run, clear the processing queue
+            processingQueue.clear();
+
+            // transfer batching queue to processing queue
+            batchingQueue.drainTo(processingQueue);
+
+            // submit to processor and then clear processing queue
+            lastProcessingFuture = processingExecutorService.submit(
+                    () -> itemsConsumer.andThen(processingQueue -> processingQueue.clear()).accept(processingQueue)
+            );
+        }
+    }
+
+}
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/Bundle.properties-MERGED b/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/Bundle.properties-MERGED
new file mode 100644
index 0000000000..9b715cf1fb
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/Bundle.properties-MERGED
@@ -0,0 +1,23 @@
+MalwareScanIngestModule_malwareTypeDisplayName=Malware
+# {0} - errorResponse
+MalwareScanIngestModule_SharedProcessing_authTokenResponseError_desc=Received error: ''{0}'' when fetching the API authentication token for the license
+MalwareScanIngestModule_SharedProcessing_authTokenResponseError_title=Authentication API error
+MalwareScanIngestModule_SharedProcessing_createAnalysisResult_No=NO
+MalwareScanIngestModule_SharedProcessing_createAnalysisResult_Yes=YES
+MalwareScanIngestModule_SharedProcessing_exhaustedHashLookups_desc=The remaining hash lookups for this license have been exhausted
+MalwareScanIngestModule_SharedProcessing_exhaustedHashLookups_title=Hash Lookups Exhausted
+MalwareScanIngestModule_SharedProcessing_flushTimeout_desc=A timeout occurred while finishing processing
+MalwareScanIngestModule_SharedProcessing_flushTimeout_title=Processing Timeout
+MalwareScanIngestModule_SharedProcessing_generalProcessingError_desc=An error occurred while processing hash lookup results
+MalwareScanIngestModule_SharedProcessing_generalProcessingError_title=Hash Lookup Error
+# {0} - errorResponse
+MalwareScanIngestModule_SharedProcessing_repServicenResponseError_desc=Received error: ''{0}'' when fetching hash lookup results
+MalwareScanIngestModule_SharedProcessing_repServicenResponseError_title=Lookup API error
+MalwareScanIngestModule_ShareProcessing_batchTimeout_desc=Batch processing timed out
+MalwareScanIngestModule_ShareProcessing_batchTimeout_title=Batch Processing Timeout
+# {0} - remainingLookups
+MalwareScanIngestModule_ShareProcessing_lowLimitWarning_desc=This license only has {0} lookups remaining
+MalwareScanIngestModule_ShareProcessing_lowLimitWarning_title=Hash Lookups Low
+MalwareScanIngestModuleFactory_description=The malware scan ingest module queries the CyberTriage cloud API for any possible malicious executables.
+MalwareScanIngestModuleFactory_displayName=Malware Scan
+MalwareScanIngestModuleFactory_version=1.0.0
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/MalwareScanIngestModule.java b/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/MalwareScanIngestModule.java
new file mode 100644
index 0000000000..2b049124bf
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/MalwareScanIngestModule.java
@@ -0,0 +1,395 @@
+/** *************************************************************************
+ ** This data and information is proprietary to, and a valuable trade secret
+ ** of, Basis Technology Corp. It is given in confidence by Basis Technology
+ ** and may only be used as permitted under the license agreement under which
+ ** it has been distributed, and in no other way.
+ **
+ ** Copyright (c) 2023 Basis Technology Corp. All rights reserved.
+ **
+ ** The technical data and information provided herein are provided with
+ ** `limited rights', and the computer software provided herein is provided
+ ** with `restricted rights' as those terms are defined in DAR and ASPR
+ ** 7-104.9(a).
+ ************************************************************************** */
+package com.basistech.df.cybertriage.autopsy.malwarescan;
+
+import com.basistech.df.cybertriage.autopsy.ctapi.CtApiDAO;
+import com.basistech.df.cybertriage.autopsy.ctapi.json.AuthTokenResponse;
+import com.basistech.df.cybertriage.autopsy.ctapi.json.FileReputationResult;
+import com.basistech.df.cybertriage.autopsy.ctapi.json.LicenseInfo;
+import com.basistech.df.cybertriage.autopsy.ctoptions.ctcloud.CTLicensePersistence;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.apache.commons.lang3.StringUtils;
+import org.openide.util.NbBundle.Messages;
+import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
+import org.sleuthkit.autopsy.ingest.FileIngestModule;
+import org.sleuthkit.autopsy.ingest.IngestJobContext;
+import org.sleuthkit.autopsy.ingest.IngestModule;
+import org.sleuthkit.autopsy.modules.filetypeid.FileTypeDetector;
+import org.sleuthkit.datamodel.AbstractFile;
+import org.sleuthkit.datamodel.Blackboard;
+import org.sleuthkit.datamodel.BlackboardArtifact;
+import org.sleuthkit.datamodel.Score;
+import org.sleuthkit.datamodel.SleuthkitCase;
+import org.sleuthkit.datamodel.TskData;
+
+/**
+ * Uses CT cloud API to determine if file is malware
+ */
+public class MalwareScanIngestModule implements FileIngestModule {
+
+    private static final SharedProcessing sharedProcessing = new SharedProcessing();
+
+    @Override
+    public void startUp(IngestJobContext context) throws IngestModuleException {
+        sharedProcessing.startUp(context);
+    }
+
+    @Override
+    public ProcessResult process(AbstractFile af) {
+        return sharedProcessing.process(af);
+    }
+
+    @Override
+    public void shutDown() {
+        sharedProcessing.shutDown();
+    }
+
+    /**
+     * Does the bulk of processing for the ingest module and handles concurrent
+     * ingest modules adding files simultaneously.
+     */
+    private static class SharedProcessing {
+
+        // batch size of 200 files max
+        private static final int BATCH_SIZE = 200;
+        // 3 minute timeout for an API request
+        private static final long BATCH_MILLIS_TIMEOUT = 3 * 60 * 1000;
+
+        //minimum lookups left before issuing warning
+        private static final long LOW_LOOKUPS_REMAINING = 250;
+
+        private static final Set<String> EXECUTABLE_MIME_TYPES = Stream.of(
+                "application/x-bat",//NON-NLS
+                "application/x-dosexec",//NON-NLS
+                "application/vnd.microsoft.portable-executable",//NON-NLS
+                "application/x-msdownload",//NON-NLS
+                "application/exe",//NON-NLS
+                "application/x-exe",//NON-NLS
+                "application/dos-exe",//NON-NLS
+                "vms/exe",//NON-NLS
+                "application/x-winexe",//NON-NLS
+                "application/msdos-windows",//NON-NLS
+                "application/x-msdos-program"//NON-NLS
+        ).collect(Collectors.toSet());
+
+        private static final String MALWARE_TYPE_NAME = "TSK_MALWARE";
+        private static final String MALWARE_CONFIG = "Cyber Triage Cloud";
+
+        private static final Logger logger = Logger.getLogger(MalwareScanIngestModule.class.getName());
+        private final BatchProcessor<FileRecord> batchProcessor = new BatchProcessor<FileRecord>(BATCH_SIZE, BATCH_MILLIS_TIMEOUT, this::handleBatch);
+
+        private final CTLicensePersistence ctSettingsPersistence = CTLicensePersistence.getInstance();
+        private final CtApiDAO ctApiDAO = CtApiDAO.getInstance();
+
+        private FileTypeDetector fileTypeDetector;
+        private RunState runState = null;
+        private SleuthkitCase tskCase = null;
+        private LicenseInfo licenseInfo = null;
+        private BlackboardArtifact.Type malwareType = null;
+        private boolean noMoreHashLookups = false;
+        private IngestModuleException startupException;
+        private long dsId = 0;
+
+        @Messages({
+            "MalwareScanIngestModule_ShareProcessing_lowLimitWarning_title=Hash Lookups Low",
+            "# {0} - remainingLookups",
+            "MalwareScanIngestModule_ShareProcessing_lowLimitWarning_desc=This license only has {0} lookups remaining",
+            "MalwareScanIngestModule_malwareTypeDisplayName=Malware"
+        })
+        synchronized void startUp(IngestJobContext context) throws IngestModuleException {
+            // only run this code once per startup
+            if (runState == RunState.STARTED_UP) {
+                if (startupException != null) {
+                    throw startupException;
+                } else {
+                    return;
+                }
+            }
+
+            try {
+                // get saved license
+                Optional<LicenseInfo> licenseInfoOpt = ctSettingsPersistence.loadLicenseInfo();
+                if (licenseInfoOpt.isEmpty() || licenseInfoOpt.get().getDecryptedLicense() == null) {
+                    throw new IngestModuleException("No saved license was found");
+                }
+
+                String licenseStr = licenseInfoOpt.get().getDecryptedLicense().getBoostLicenseId();
+                AuthTokenResponse authTokenResponse = ctApiDAO.getAuthToken(licenseStr);
+                // syncronously fetch malware scans info
+
+                // determine lookups remaining
+                long lookupsRemaining = remaining(authTokenResponse.getHashLookupLimit(), authTokenResponse.getHashLookupCount());
+                if (lookupsRemaining <= 0) {
+                    throw new IngestModuleException("There are no more file hash lookups for this license");
+                } else if (lookupsRemaining < LOW_LOOKUPS_REMAINING) {
+                    notifyWarning(
+                            Bundle.MalwareScanIngestModule_ShareProcessing_lowLimitWarning_title(),
+                            Bundle.MalwareScanIngestModule_ShareProcessing_lowLimitWarning_desc(lookupsRemaining),
+                            null);
+                }
+
+                // setup necessary variables for processing
+                tskCase = Case.getCurrentCaseThrows().getSleuthkitCase();
+                malwareType = tskCase.getBlackboard().getOrAddArtifactType(
+                        MALWARE_TYPE_NAME,
+                        Bundle.MalwareScanIngestModule_malwareTypeDisplayName(),
+                        BlackboardArtifact.Category.ANALYSIS_RESULT);
+                fileTypeDetector = new FileTypeDetector();
+                dsId = context.getDataSource().getId();
+                licenseInfo = licenseInfoOpt.get();
+                startupException = null;
+                noMoreHashLookups = false;
+            } catch (IngestModuleException ex) {
+                startupException = ex;
+                throw startupException;
+            } catch (Exception ex) {
+                startupException = new IngestModuleException("An exception occurred on MalwareScanIngestModule startup", ex);
+                throw startupException;
+            }
+        }
+
+        private static long remaining(Long limit, Long used) {
+            limit = limit == null ? 0 : limit;
+            used = used == null ? 0 : used;
+            return limit - used;
+        }
+
+        @Messages({
+            "MalwareScanIngestModule_ShareProcessing_batchTimeout_title=Batch Processing Timeout",
+            "MalwareScanIngestModule_ShareProcessing_batchTimeout_desc=Batch processing timed out"
+        })
+        IngestModule.ProcessResult process(AbstractFile af) {
+            try {
+                if (af.getKnown() != TskData.FileKnown.KNOWN
+                        && EXECUTABLE_MIME_TYPES.contains(StringUtils.defaultString(fileTypeDetector.getMIMEType(af)).trim().toLowerCase())) {
+                    batchProcessor.add(new FileRecord(af.getId(), af.getMd5Hash()));
+
+                }
+                return ProcessResult.OK;
+            } catch (InterruptedException ex) {
+                notifyWarning(
+                        Bundle.MalwareScanIngestModule_ShareProcessing_batchTimeout_title(),
+                        Bundle.MalwareScanIngestModule_ShareProcessing_batchTimeout_desc(),
+                        ex);
+                return IngestModule.ProcessResult.ERROR;
+            }
+        }
+
+        @Messages({
+            "MalwareScanIngestModule_SharedProcessing_authTokenResponseError_title=Authentication API error",
+            "# {0} - errorResponse",
+            "MalwareScanIngestModule_SharedProcessing_authTokenResponseError_desc=Received error: ''{0}'' when fetching the API authentication token for the license",
+            "MalwareScanIngestModule_SharedProcessing_repServicenResponseError_title=Lookup API error",
+            "# {0} - errorResponse",
+            "MalwareScanIngestModule_SharedProcessing_repServicenResponseError_desc=Received error: ''{0}'' when fetching hash lookup results",
+            "MalwareScanIngestModule_SharedProcessing_exhaustedHashLookups_title=Hash Lookups Exhausted",
+            "MalwareScanIngestModule_SharedProcessing_exhaustedHashLookups_desc=The remaining hash lookups for this license have been exhausted",
+            "MalwareScanIngestModule_SharedProcessing_generalProcessingError_title=Hash Lookup Error",
+            "MalwareScanIngestModule_SharedProcessing_generalProcessingError_desc=An error occurred while processing hash lookup results",})
+        private void handleBatch(List<FileRecord> fileRecords) {
+            if (fileRecords == null || fileRecords.isEmpty() || noMoreHashLookups) {
+                return;
+            }
+
+            // create mapping of md5 to corresponding object ids as well as just the list of md5's
+            Map<String, List<Long>> md5ToObjId = new HashMap<>();
+            List<String> md5Hashes = new ArrayList<>();
+            for (FileRecord fr : fileRecords) {
+                if (fr == null || StringUtils.isBlank(fr.getMd5hash()) || fr.getObjId() <= 0) {
+                    continue;
+                }
+
+                String sanitizedMd5 = sanitizedMd5(fr.getMd5hash());
+                md5ToObjId
+                        .computeIfAbsent(sanitizedMd5, (k) -> new ArrayList<>())
+                        .add(fr.getObjId());
+
+                md5Hashes.add(sanitizedMd5);
+            }
+
+            if (md5Hashes.isEmpty()) {
+                return;
+            }
+
+            try {
+                // get an auth token with the license
+                AuthTokenResponse authTokenResponse = ctApiDAO.getAuthToken(licenseInfo.getDecryptedLicense().getBoostLicenseId());
+
+                // make sure we are in bounds for the remaining scans
+                long remainingScans = remaining(authTokenResponse.getHashLookupLimit(), authTokenResponse.getHashLookupCount());
+                if (remainingScans <= 0) {
+                    noMoreHashLookups = true;
+                    notifyWarning(
+                            Bundle.MalwareScanIngestModule_SharedProcessing_exhaustedHashLookups_title(),
+                            Bundle.MalwareScanIngestModule_SharedProcessing_exhaustedHashLookups_desc(),
+                            null);
+                    return;
+                }
+
+                // if the size of this batch will exceed limit, shrink list to limit and fail after processing
+                boolean exceededScanLimit = false;
+                if (remainingScans < md5Hashes.size()) {
+                    md5Hashes = md5Hashes.subList(0, (int) remainingScans);
+                    exceededScanLimit = true;
+                }
+
+                // using auth token, get results
+                List<FileReputationResult> repResult = ctApiDAO.getReputationResults(authTokenResponse.getToken(), md5Hashes);
+
+                if (repResult != null && !repResult.isEmpty()) {
+                    SleuthkitCase.CaseDbTransaction trans = null;
+                    try {
+                        trans = tskCase.beginTransaction();
+                        for (FileReputationResult result : repResult) {
+                            String sanitizedMd5 = sanitizedMd5(result.getMd5Hash());
+                            List<Long> objIds = md5ToObjId.remove(sanitizedMd5);
+                            if (objIds == null || objIds.isEmpty()) {
+                                continue;
+                            }
+
+                            for (Long objId : objIds) {
+                                createAnalysisResult(objId, result, trans);
+                            }
+                        }
+
+                        trans.commit();
+                        trans = null;
+                    } finally {
+                        if (trans != null) {
+                            trans.rollback();
+                            trans = null;
+                        }
+                    }
+
+                    // if we only processed part of the batch, after processing, notify that we are out of scans.
+                    if (exceededScanLimit) {
+                        noMoreHashLookups = true;
+                        notifyWarning(
+                                Bundle.MalwareScanIngestModule_SharedProcessing_exhaustedHashLookups_title(),
+                                Bundle.MalwareScanIngestModule_SharedProcessing_exhaustedHashLookups_desc(),
+                                null);
+                        return;
+                    }
+                }
+            } catch (Exception ex) {
+                notifyWarning(
+                        Bundle.MalwareScanIngestModule_SharedProcessing_generalProcessingError_title(),
+                        Bundle.MalwareScanIngestModule_SharedProcessing_generalProcessingError_desc(),
+                        ex);
+            }
+        }
+
+        private String sanitizedMd5(String orig) {
+            return StringUtils.defaultString(orig).trim().toLowerCase();
+        }
+
+        @Messages({
+            "MalwareScanIngestModule_SharedProcessing_createAnalysisResult_Yes=YES",
+            "MalwareScanIngestModule_SharedProcessing_createAnalysisResult_No=NO"
+        })
+        private void createAnalysisResult(Long objId, FileReputationResult fileReputationResult, SleuthkitCase.CaseDbTransaction trans) throws Blackboard.BlackboardException {
+            if (objId == null || fileReputationResult == null) {
+                return;
+            }
+
+            Score score = fileReputationResult.getScore() == null ? Score.SCORE_UNKNOWN : fileReputationResult.getScore().getTskCore();
+            
+            String conclusion = score.getSignificance() == Score.Significance.NOTABLE || score.getSignificance() == Score.Significance.LIKELY_NOTABLE
+                    ? Bundle.MalwareScanIngestModule_SharedProcessing_createAnalysisResult_Yes()
+                    : Bundle.MalwareScanIngestModule_SharedProcessing_createAnalysisResult_No();
+            
+            String justification = fileReputationResult.getStatusDescription();
+
+            tskCase.getBlackboard().newAnalysisResult(
+                    malwareType,
+                    objId,
+                    dsId,
+                    score,
+                    conclusion,
+                    MALWARE_CONFIG,
+                    justification,
+                    Collections.emptyList(),
+                    trans);
+        }
+
+        @Messages({
+            "MalwareScanIngestModule_SharedProcessing_flushTimeout_title=Processing Timeout",
+            "MalwareScanIngestModule_SharedProcessing_flushTimeout_desc=A timeout occurred while finishing processing"
+        })
+        synchronized void shutDown() {
+            // if already shut down, return
+            if (runState == RunState.SHUT_DOWN) {
+                return;
+            }
+
+            // flush any remaining items
+            try {
+                batchProcessor.flush(true);
+            } catch (InterruptedException ex) {
+                notifyWarning(
+                        Bundle.MalwareScanIngestModule_SharedProcessing_flushTimeout_title(),
+                        Bundle.MalwareScanIngestModule_SharedProcessing_flushTimeout_desc(),
+                        ex);
+            } finally {
+                // set state to shut down and clear any remaining
+                malwareType = null;
+                fileTypeDetector = null;
+                noMoreHashLookups = false;
+                runState = RunState.SHUT_DOWN;
+                startupException = null;
+                batchProcessor.clearCurrentBatch();
+            }
+        }
+
+        private void notifyWarning(String title, String message, Exception ex) {
+            MessageNotifyUtil.Notify.warn(title, message);
+            logger.log(Level.WARNING, message, ex);
+        }
+
+        private enum RunState {
+            STARTED_UP, SHUT_DOWN
+        }
+
+        class FileRecord {
+
+            private final long objId;
+            private final String md5hash;
+
+            FileRecord(long objId, String md5hash) {
+                this.objId = objId;
+                this.md5hash = md5hash;
+            }
+
+            long getObjId() {
+                return objId;
+            }
+
+            String getMd5hash() {
+                return md5hash;
+            }
+
+        }
+    }
+}
diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/MalwareScanIngestModuleFactory.java b/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/MalwareScanIngestModuleFactory.java
new file mode 100644
index 0000000000..0a7fffb416
--- /dev/null
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/MalwareScanIngestModuleFactory.java
@@ -0,0 +1,72 @@
+/** *************************************************************************
+ ** This data and information is proprietary to, and a valuable trade secret
+ ** of, Basis Technology Corp. It is given in confidence by Basis Technology
+ ** and may only be used as permitted under the license agreement under which
+ ** it has been distributed, and in no other way.
+ **
+ ** Copyright (c) 2023 Basis Technology Corp. All rights reserved.
+ **
+ ** The technical data and information provided herein are provided with
+ ** `limited rights', and the computer software provided herein is provided
+ ** with `restricted rights' as those terms are defined in DAR and ASPR
+ ** 7-104.9(a).
+ ************************************************************************** */
+package com.basistech.df.cybertriage.autopsy.malwarescan;
+
+import com.basistech.df.cybertriage.autopsy.ctoptions.CTOptionsPanel;
+import org.openide.util.NbBundle.Messages;
+import org.openide.util.lookup.ServiceProvider;
+import org.sleuthkit.autopsy.ingest.FileIngestModule;
+import org.sleuthkit.autopsy.ingest.IngestModuleFactoryAdapter;
+import org.sleuthkit.autopsy.ingest.IngestModuleGlobalSettingsPanel;
+import org.sleuthkit.autopsy.ingest.IngestModuleIngestJobSettings;
+
+/**
+ * Factory for malware scan ingest modules.
+ */
+@ServiceProvider(service = org.sleuthkit.autopsy.ingest.IngestModuleFactory.class)
+@Messages({
+    "MalwareScanIngestModuleFactory_displayName=Malware Scan",
+    "MalwareScanIngestModuleFactory_description=The malware scan ingest module queries the CyberTriage cloud API for any possible malicious executables.",
+    "MalwareScanIngestModuleFactory_version=1.0.0"
+})
+public class MalwareScanIngestModuleFactory extends IngestModuleFactoryAdapter {
+
+    @Override
+    public String getModuleDisplayName() {
+        return Bundle.MalwareScanIngestModuleFactory_displayName();
+    }
+
+    @Override
+    public String getModuleDescription() {
+        return Bundle.MalwareScanIngestModuleFactory_description();
+    }
+
+    @Override
+    public String getModuleVersionNumber() {
+        return Bundle.MalwareScanIngestModuleFactory_version();
+    }
+
+    @Override
+    public boolean isFileIngestModuleFactory() {
+        return true;
+    }
+
+    @Override
+    public FileIngestModule createFileIngestModule(IngestModuleIngestJobSettings ingestOptions) {
+        return new MalwareScanIngestModule();
+    }
+
+    @Override
+    public boolean hasGlobalSettingsPanel() {
+        return true;
+    }
+
+    @Override
+    public IngestModuleGlobalSettingsPanel getGlobalSettingsPanel() {
+        CTOptionsPanel optionsPanel = new CTOptionsPanel();
+        optionsPanel.loadSavedSettings();
+        return optionsPanel;
+    }
+
+}
diff --git a/CoreLibs/ivy.xml b/CoreLibs/ivy.xml
index 51bbeb8220..d60ce17600 100644
--- a/CoreLibs/ivy.xml
+++ b/CoreLibs/ivy.xml
@@ -1,5 +1,6 @@
 <!DOCTYPE ivy-module [
     <!ENTITY javafx.version "17.0.7">
+    <!ENTITY jackson.version "2.15.2">
 ]>
 <ivy-module version="2.0" xmlns:e="http://ant.apache.org/ivy/extra">
     <info organisation="org.sleuthkit.autopsy" module="corelibs"/>
@@ -87,7 +88,8 @@
 
         <dependency conf="autopsy_core->default" org="net.htmlparser.jericho" name="jericho-html" rev="3.4"/>
 
-        <dependency conf="autopsy_core->default" org="com.fasterxml.jackson.dataformat" name="jackson-dataformat-csv" rev="2.15.2"/>
+        <dependency conf="autopsy_core->default" org="com.fasterxml.jackson.dataformat" name="jackson-dataformat-csv" rev="&jackson.version;"/>
+        <dependency conf="autopsy_core->default" org="com.fasterxml.jackson.datatype" name="jackson-datatype-jsr310" rev="&jackson.version;"/>
         
         <!-- better image resizing -->
         <dependency conf="autopsy_core->default" org="org.imgscalr" name="imgscalr-lib" rev="4.2" />
@@ -142,8 +144,8 @@
         
         <override org="com.google.code.gson" module="gson" rev="2.9.0"/>
         <override org="com.google.guava" module="guava" rev="32.0.1-jre"/>
-        <override org="com.fasterxml.jackson.core" module="jackson-databind" rev="2.15.2"/>
-        <override org="com.fasterxml.jackson.core" module="jackson-core" rev="2.15.2"/>
+        <override org="com.fasterxml.jackson.core" module="jackson-databind" rev="&jackson.version;"/>
+        <override org="com.fasterxml.jackson.core" module="jackson-core" rev="&jackson.version;"/>
         
         <!-- changes to bouncy castle version may also be reflected in thirdparty/IcePDF 6.2.2 -->
         <override org="org.bouncycastle" module="bcprov-jdk15on" rev="1.70"/>
diff --git a/CoreLibs/manifest.mf b/CoreLibs/manifest.mf
index b8faef2527..1d3168bf2c 100644
--- a/CoreLibs/manifest.mf
+++ b/CoreLibs/manifest.mf
@@ -11,4 +11,4 @@ Specification-Version: 1.0
 Specification-Vendor: CoreLibs ImageIO Fields
 Implementation-Title: org.sleuthkit.autopsy.corelibs.ImageIO
 Implementation-Version: 1.0
-Implementation-Vendor: CoreLibs ImageIO Fields
\ No newline at end of file
+Implementation-Vendor: CoreLibs ImageIO Fields
diff --git a/CoreLibs/nbproject/project.properties b/CoreLibs/nbproject/project.properties
index fe5e78acff..320a1e36e5 100644
--- a/CoreLibs/nbproject/project.properties
+++ b/CoreLibs/nbproject/project.properties
@@ -84,6 +84,7 @@ file.reference.jackson-annotations-2.15.2.jar=release/modules/ext/jackson-annota
 file.reference.jackson-core-2.15.2.jar=release/modules/ext/jackson-core-2.15.2.jar
 file.reference.jackson-databind-2.15.2.jar=release/modules/ext/jackson-databind-2.15.2.jar
 file.reference.jackson-dataformat-csv-2.15.2.jar=release/modules/ext/jackson-dataformat-csv-2.15.2.jar
+file.reference.jackson-datatype-jsr310-2.15.2.jar=release/modules/ext/jackson-datatype-jsr310-2.15.2.jar
 file.reference.javafx-base-17.0.7-linux.jar=release/modules/ext/javafx-base-17.0.7-linux.jar
 file.reference.javafx-base-17.0.7-mac.jar=release/modules/ext/javafx-base-17.0.7-mac.jar
 file.reference.javafx-base-17.0.7-win.jar=release/modules/ext/javafx-base-17.0.7-win.jar
diff --git a/CoreLibs/nbproject/project.xml b/CoreLibs/nbproject/project.xml
index f7b139f502..b2f08dc42b 100644
--- a/CoreLibs/nbproject/project.xml
+++ b/CoreLibs/nbproject/project.xml
@@ -63,6 +63,8 @@
                 <package>com.fasterxml.jackson.databind.type</package>
                 <package>com.fasterxml.jackson.databind.util</package>
                 <package>com.fasterxml.jackson.dataformat.csv</package>
+                <package>com.fasterxml.jackson.datatype.jsr310</package>
+                <package>com.fasterxml.jackson.datatype.jsr310.util</package>
                 <package>com.github.lgooddatepicker.components</package>
                 <package>com.github.lgooddatepicker.optionalusertools</package>
                 <package>com.github.lgooddatepicker.zinternaltools</package>
@@ -197,8 +199,6 @@
                 <package>com.google.rpc.context</package>
                 <package>com.google.thirdparty.publicsuffix</package>
                 <package>com.google.type</package>
-                <package>com.microsoft.schemas.vml</package>
-                <package>com.microsoft.schemas.vml.impl</package>
                 <package>com.sun.javafx</package>
                 <package>com.sun.javafx.animation</package>
                 <package>com.sun.javafx.application</package>
@@ -321,9 +321,6 @@
                 <package>com.twelvemonkeys.util.regex</package>
                 <package>com.twelvemonkeys.util.service</package>
                 <package>com.twelvemonkeys.xml</package>
-                <package>javax.annotation</package>
-                <package>javax.annotation.concurrent</package>
-                <package>javax.annotation.meta</package>
                 <package>javafx.animation</package>
                 <package>javafx.application</package>
                 <package>javafx.beans</package>
@@ -340,7 +337,6 @@
                 <package>javafx.event</package>
                 <package>javafx.fxml</package>
                 <package>javafx.geometry</package>
-                <package>javafx.graphics</package>
                 <package>javafx.print</package>
                 <package>javafx.scene</package>
                 <package>javafx.scene.canvas</package>
@@ -362,19 +358,9 @@
                 <package>javafx.stage</package>
                 <package>javafx.util</package>
                 <package>javafx.util.converter</package>
-                <package>javax.jms</package>
-                <package>javax.mail</package>
-                <package>javax.mail.event</package>
-                <package>javax.mail.internet</package>
-                <package>javax.mail.search</package>
-                <package>javax.mail.util</package>
-                <package>javax.servlet</package>
-                <package>javax.servlet.http</package>
-                <package>javax.xml.parsers</package>
-                <package>javax.xml.transform</package>
-                <package>javax.xml.transform.dom</package>
-                <package>javax.xml.transform.sax</package>
-                <package>javax.xml.transform.stream</package>
+                <package>javax.annotation</package>
+                <package>javax.annotation.concurrent</package>
+                <package>javax.annotation.meta</package>
                 <package>jfxtras.animation</package>
                 <package>jfxtras.css</package>
                 <package>jfxtras.css.converters</package>
@@ -442,16 +428,6 @@
                 <package>org.apache.commons.lang3.tuple</package>
                 <package>org.apache.commons.logging</package>
                 <package>org.apache.commons.logging.impl</package>
-                <package>org.apache.log</package>
-                <package>org.apache.log.filter</package>
-                <package>org.apache.log.format</package>
-                <package>org.apache.log.output</package>
-                <package>org.apache.log.output.db</package>
-                <package>org.apache.log.output.io</package>
-                <package>org.apache.log.output.io.rotate</package>
-                <package>org.apache.log.output.jms</package>
-                <package>org.apache.log.output.net</package>
-                <package>org.apache.log.util</package>
                 <package>org.apache.commons.text</package>
                 <package>org.apache.commons.validator.routines</package>
                 <package>org.apache.commons.validator.routines.checkdigit</package>
@@ -460,14 +436,7 @@
                 <package>org.apache.log4j.config</package>
                 <package>org.apache.log4j.helpers</package>
                 <package>org.apache.log4j.jdbc</package>
-                <package>org.apache.log4j.jmx</package>
-                <package>org.apache.log4j.lf5</package>
-                <package>org.apache.log4j.lf5.util</package>
-                <package>org.apache.log4j.lf5.viewer</package>
-                <package>org.apache.log4j.lf5.viewer.categoryexplorer</package>
-                <package>org.apache.log4j.lf5.viewer.configure</package>
                 <package>org.apache.log4j.net</package>
-                <package>org.apache.log4j.nt</package>
                 <package>org.apache.log4j.or</package>
                 <package>org.apache.log4j.or.jms</package>
                 <package>org.apache.log4j.or.sax</package>
@@ -931,6 +900,10 @@
                 <runtime-relative-path>ext/jackson-dataformat-csv-2.15.2.jar</runtime-relative-path>
                 <binary-origin>release/modules/ext/jackson-dataformat-csv-2.15.2.jar</binary-origin>
             </class-path-extension>
+            <class-path-extension>
+                <runtime-relative-path>ext/jackson-datatype-jsr310-2.15.2.jar</runtime-relative-path>
+                <binary-origin>release/modules/ext/jackson-datatype-jsr310-2.15.2.jar</binary-origin>
+            </class-path-extension>
             <class-path-extension>
                 <runtime-relative-path>ext/javafx-base-17.0.7-linux.jar</runtime-relative-path>
                 <binary-origin>release/modules/ext/javafx-base-17.0.7-linux.jar</binary-origin>
diff --git a/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties b/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties
index 6d7443a4a6..f8cea13909 100644
--- a/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties
+++ b/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties
@@ -1,5 +1,5 @@
 #Updated by build script
-#Wed, 28 Sep 2022 13:57:05 -0400
+#Thu, 20 Jul 2023 14:02:30 -0400
 LBL_splash_window_title=Starting Autopsy
 SPLASH_HEIGHT=314
 SPLASH_WIDTH=538
@@ -8,4 +8,4 @@ SplashRunningTextBounds=0,289,538,18
 SplashRunningTextColor=0x0
 SplashRunningTextFontSize=19
 
-currentVersion=Autopsy 4.19.3
+currentVersion=Autopsy 4.20.0
diff --git a/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties b/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties
index f28a6b96b3..2387b67597 100644
--- a/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties
+++ b/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties
@@ -1,4 +1,4 @@
 #Updated by build script
-#Wed, 28 Sep 2022 13:57:05 -0400
-CTL_MainWindow_Title=Autopsy 4.19.3
-CTL_MainWindow_Title_No_Project=Autopsy 4.19.3
+#Thu, 20 Jul 2023 14:02:30 -0400
+CTL_MainWindow_Title=Autopsy 4.20.0
+CTL_MainWindow_Title_No_Project=Autopsy 4.20.0
-- 
GitLab