diff --git a/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/MalwareScanIngestModule.java b/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/MalwareScanIngestModule.java
index c52e72a350db8f8c6267460ec4531e6d19082912..4698392478914b2c7858f6e840d41e96996e6b0d 100644
--- a/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/MalwareScanIngestModule.java
+++ b/Core/src/com/basistech/df/cybertriage/autopsy/malwarescan/MalwareScanIngestModule.java
@@ -775,7 +775,11 @@ private void createAnalysisResults(IngestJobState ingestJobState, List<CTCloudBe
                     for (Long objId : objIds) {
                         AnalysisResult res = createAnalysisResult(ingestJobState, trans, result, objId);
                         if (res != null) {
-                            createdArtifacts.add(res);
+                            // only post results that have score NOTABLE or LIKELY_NOTABLE
+                            Score score = res.getScore();
+                            if (score.getSignificance() == Score.Significance.NOTABLE || score.getSignificance() == Score.Significance.LIKELY_NOTABLE) {
+                                createdArtifacts.add(res);
+                            }
                         }
                     }
                 }
@@ -837,7 +841,7 @@ private AnalysisResult createAnalysisResult(IngestJobState ingestJobState, Sleut
                     : Bundle.MalwareScanIngestModule_SharedProcessing_createAnalysisResult_No();
 
             String justification = cloudBean.getMalwareResult().getStatusDescription();
-
+            
             return ingestJobState.getTskCase().getBlackboard().newAnalysisResult(
                     ingestJobState.getMalwareType(),
                     objId,
diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/Artifacts.java b/Core/src/org/sleuthkit/autopsy/datamodel/Artifacts.java
index 3dee2839996272ad689bd02beef13d9af647b64d..a38383c1830b281bb00e0694854055071303616f 100644
--- a/Core/src/org/sleuthkit/autopsy/datamodel/Artifacts.java
+++ b/Core/src/org/sleuthkit/autopsy/datamodel/Artifacts.java
@@ -1,7 +1,7 @@
 /*
  * Autopsy Forensic Browser
  *
- * Copyright 2011-2021 Basis Technology Corp.
+ * Copyright 2011-2023 Basis Technology Corp.
  * Contact: carrier <at> sleuthkit <dot> org
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -72,6 +72,10 @@ public class Artifacts {
 
     private static final Set<IngestManager.IngestJobEvent> INGEST_JOB_EVENTS_OF_INTEREST
             = EnumSet.of(IngestManager.IngestJobEvent.COMPLETED, IngestManager.IngestJobEvent.CANCELLED);
+    
+    // this is currently a custom TSK artifact type, created in MalwareScanIngestModule
+    private static BlackboardArtifact.Type MALWARE_ARTIFACT_TYPE = null;
+    private static final String MALWARE_HITS = "TSK_MALWARE";
 
     /**
      * Base class for a parent node of artifacts.
@@ -242,6 +246,16 @@ static class TypeFactory extends ChildFactory.Detachable<TypeNodeKey> implements
          */
         @SuppressWarnings("deprecation")
         private static TypeNodeKey getTypeKey(BlackboardArtifact.Type type, SleuthkitCase skCase, long dsObjId) {
+
+            // Get the custom TSK_MALWARE artifact type from case database
+            if (MALWARE_ARTIFACT_TYPE == null) {
+                try {
+                    MALWARE_ARTIFACT_TYPE = skCase.getArtifactType(MALWARE_HITS);
+                } catch (TskCoreException ex) {
+                    logger.log(Level.WARNING, "Unable to get TSK_MALWARE artifact type from database : ", ex); //NON-NLS
+                }
+            }
+
             int typeId = type.getTypeID();
             if (TSK_EMAIL_MSG.getTypeID() == typeId) {
                 EmailExtracted.RootNode emailNode = new EmailExtracted(skCase, dsObjId).new RootNode();
@@ -267,7 +281,9 @@ private static TypeNodeKey getTypeKey(BlackboardArtifact.Type type, SleuthkitCas
             } else if (TSK_HASHSET_HIT.getTypeID() == typeId) {
                 HashsetHits.RootNode hashsetHits = new HashsetHits(skCase, dsObjId).new RootNode();
                 return new TypeNodeKey(hashsetHits, TSK_HASHSET_HIT);
-
+            } else if (MALWARE_ARTIFACT_TYPE != null && MALWARE_ARTIFACT_TYPE.getTypeID() == typeId) {
+                MalwareHits.RootNode malwareHits = new MalwareHits(skCase, dsObjId).new RootNode();
+                return new TypeNodeKey(malwareHits, MALWARE_ARTIFACT_TYPE);
             } else {
                 return new TypeNodeKey(type, dsObjId);
             }
diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/AutopsyItemVisitor.java b/Core/src/org/sleuthkit/autopsy/datamodel/AutopsyItemVisitor.java
index d433d0d6df40cd81369cdec5e01914fc3e4b8f71..5ebd7c421603c041c72192f2bee24774244ed478 100644
--- a/Core/src/org/sleuthkit/autopsy/datamodel/AutopsyItemVisitor.java
+++ b/Core/src/org/sleuthkit/autopsy/datamodel/AutopsyItemVisitor.java
@@ -60,6 +60,8 @@ public interface AutopsyItemVisitor<T> {
     T visit(KeywordHits kh);
 
     T visit(HashsetHits hh);
+    
+    T visit(MalwareHits mh);
 
     T visit(EmailExtracted ee);
 
@@ -169,6 +171,11 @@ public T visit(HashsetHits hh) {
             return defaultVisit(hh);
         }
 
+        @Override
+        public T visit(MalwareHits mh) {
+            return defaultVisit(mh);
+        }
+        
         @Override
         public T visit(InterestingHits ih) {
             return defaultVisit(ih);
diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNodeVisitor.java b/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNodeVisitor.java
index 85e715382eac73ae9d081fa03e5153ef55c25d77..b80fa67d9404915dff1bfb2dc506ecc0b25a06a5 100644
--- a/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNodeVisitor.java
+++ b/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNodeVisitor.java
@@ -111,6 +111,8 @@ public interface DisplayableItemNodeVisitor<T> {
     T visit(HashsetHits.RootNode hhrn);
 
     T visit(HashsetHits.HashsetNameNode hhsn);
+    
+    T visit(MalwareHits.RootNode mhrn);  
 
     T visit(EmailExtracted.RootNode eern);
 
@@ -431,6 +433,11 @@ public T visit(HashsetHits.HashsetNameNode hhsn) {
             return defaultVisit(hhsn);
         }
 
+        @Override
+        public T visit(MalwareHits.RootNode mhrn) {
+            return defaultVisit(mhrn);
+        }     
+
         @Override
         public T visit(InterestingHits.RootNode ihrn) {
             return defaultVisit(ihrn);
diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/MalwareHits.java b/Core/src/org/sleuthkit/autopsy/datamodel/MalwareHits.java
new file mode 100755
index 0000000000000000000000000000000000000000..c1761a7ad57840dbe30a96d7ba8c6c7f80a2b0f5
--- /dev/null
+++ b/Core/src/org/sleuthkit/autopsy/datamodel/MalwareHits.java
@@ -0,0 +1,357 @@
+/*
+ * Autopsy Forensic Browser
+ *
+ * Copyright 2023 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.autopsy.datamodel;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Observable;
+import java.util.Observer;
+import java.util.Set;
+import java.util.logging.Level;
+import org.openide.nodes.Children;
+import org.openide.nodes.Node;
+import org.openide.nodes.Sheet;
+import org.openide.util.NbBundle;
+import org.openide.util.WeakListeners;
+import org.openide.util.lookup.Lookups;
+import org.sleuthkit.autopsy.casemodule.Case;
+import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
+import org.sleuthkit.autopsy.coreutils.Logger;
+import org.sleuthkit.autopsy.ingest.IngestManager;
+import org.sleuthkit.autopsy.ingest.ModuleDataEvent;
+import org.sleuthkit.datamodel.BlackboardArtifact;
+import org.sleuthkit.datamodel.SleuthkitCase;
+import org.sleuthkit.datamodel.SleuthkitCase.CaseDbQuery;
+import org.sleuthkit.datamodel.TskCoreException;
+import org.sleuthkit.autopsy.datamodel.Artifacts.UpdatableCountTypeNode;
+import org.sleuthkit.datamodel.AnalysisResult;
+import org.sleuthkit.datamodel.Score;
+
+/**
+ * Malware hits node support. Inner classes have all of the nodes in the tree.
+ */
+public class MalwareHits implements AutopsyVisitableItem {
+
+    private static final String MALWARE_HITS = "TSK_MALWARE"; // this is currently a custom TSK artifact type, created in MalwareScanIngestModule
+    private static BlackboardArtifact.Type MALWARE_ARTIFACT_TYPE = null;
+    private static String DISPLAY_NAME;
+    private static final Logger logger = Logger.getLogger(MalwareHits.class.getName());
+    private static final Set<IngestManager.IngestJobEvent> INGEST_JOB_EVENTS_OF_INTEREST = EnumSet.of(IngestManager.IngestJobEvent.COMPLETED, IngestManager.IngestJobEvent.CANCELLED);
+    private static final Set<IngestManager.IngestModuleEvent> INGEST_MODULE_EVENTS_OF_INTEREST = EnumSet.of(IngestManager.IngestModuleEvent.DATA_ADDED);
+    private SleuthkitCase skCase;
+    private final MalwareResults malwareResults;
+    private final long filteringDSObjId; // 0 if not filtering/grouping by data source
+
+    /**
+     * Constructor
+     *
+     * @param skCase Case DB
+     *
+     */
+    public MalwareHits(SleuthkitCase skCase) {
+        this(skCase, 0);
+    }
+
+    /**
+     * Constructor
+     *
+     * @param skCase Case DB
+     * @param objId  Object id of the data source
+     *
+     */
+    public MalwareHits(SleuthkitCase skCase, long objId) {
+        this.skCase = skCase;
+        this.filteringDSObjId = objId;
+        malwareResults = new MalwareResults();
+    }
+
+    @Override
+    public <T> T accept(AutopsyItemVisitor<T> visitor) {
+        return visitor.visit(this);
+    }
+
+    /**
+     * Stores all of the malware results in a single class that is observable
+     * for the child nodes
+     */
+    private class MalwareResults extends Observable implements Observer {
+
+        // list of artifacts
+        // NOTE: the list can be accessed by multiple worker threads and needs to be synchronized
+        private final Set<Long> malwareHits = new HashSet<>();
+
+        MalwareResults() {
+            addNotify();
+            update();
+        }
+
+        Set<Long> getArtifactIds() {
+            synchronized (malwareHits) {
+                return Collections.unmodifiableSet(malwareHits);
+            }
+        }
+
+        @SuppressWarnings("deprecation")
+        final void update() {
+            synchronized (malwareHits) {
+                malwareHits.clear();
+            }
+
+            if (skCase == null) {
+                return;
+            }
+
+            // Get the custom TSK_MALWARE artifact type from case database
+            if (MALWARE_ARTIFACT_TYPE == null) {
+                try {
+                    MALWARE_ARTIFACT_TYPE = skCase.getArtifactType(MALWARE_HITS);
+                    DISPLAY_NAME = MALWARE_ARTIFACT_TYPE.getDisplayName();
+                } catch (TskCoreException ex) {
+                    logger.log(Level.WARNING, "Unable to get TSK_MALWARE artifact type from database : ", ex); //NON-NLS
+                    return;
+                }
+            }
+
+            String query = "SELECT blackboard_artifacts.artifact_obj_id " //NON-NLS
+                    + "FROM blackboard_artifacts,tsk_analysis_results WHERE " //NON-NLS
+                    + "blackboard_artifacts.artifact_type_id=" + MALWARE_ARTIFACT_TYPE.getTypeID() //NON-NLS
+                    + " AND tsk_analysis_results.artifact_obj_id=blackboard_artifacts.artifact_obj_id" //NON-NLS
+                    + " AND (tsk_analysis_results.significance=" + Score.Significance.NOTABLE.getId() //NON-NLS
+                    + " OR tsk_analysis_results.significance=" + Score.Significance.LIKELY_NOTABLE.getId() + " )"; //NON-NLS
+            if (filteringDSObjId > 0) {
+                query += "  AND blackboard_artifacts.data_source_obj_id = " + filteringDSObjId; //NON-NLS
+            }
+
+            try (CaseDbQuery dbQuery = skCase.executeQuery(query)) {
+                ResultSet resultSet = dbQuery.getResultSet();
+                synchronized (malwareHits) {
+                    while (resultSet.next()) {
+                        long artifactObjId = resultSet.getLong("artifact_obj_id"); //NON-NLS
+                        malwareHits.add(artifactObjId);
+                    }
+                }
+            } catch (TskCoreException | SQLException ex) {
+                logger.log(Level.WARNING, "SQL Exception occurred: ", ex); //NON-NLS
+            }
+
+            setChanged();
+            notifyObservers();
+        }
+
+        private final PropertyChangeListener pcl = new PropertyChangeListener() {
+            @Override
+            public void propertyChange(PropertyChangeEvent evt) {
+                String eventType = evt.getPropertyName();
+                if (eventType.equals(IngestManager.IngestModuleEvent.DATA_ADDED.toString())) {
+                    /**
+                     * Checking for a current case is a stop gap measure until a
+                     * different way of handling the closing of cases is worked
+                     * out. Currently, remote events may be received for a case
+                     * that is already closed.
+                     */
+                    try {
+                        Case.getCurrentCaseThrows();
+                        /**
+                         * Due to some unresolved issues with how cases are
+                         * closed, it is possible for the event to have a null
+                         * oldValue if the event is a remote event.
+                         */
+                        ModuleDataEvent eventData = (ModuleDataEvent) evt.getOldValue();
+                        if (null != eventData && eventData.getBlackboardArtifactType().getTypeID() == MALWARE_ARTIFACT_TYPE.getTypeID()) {
+                            malwareResults.update();
+                        }
+                    } catch (NoCurrentCaseException notUsed) {
+                        /**
+                         * Case is closed, do nothing.
+                         */
+                    }
+                } else if (eventType.equals(IngestManager.IngestJobEvent.COMPLETED.toString())
+                        || eventType.equals(IngestManager.IngestJobEvent.CANCELLED.toString())) {
+                    /**
+                     * Checking for a current case is a stop gap measure until a
+                     * different way of handling the closing of cases is worked
+                     * out. Currently, remote events may be received for a case
+                     * that is already closed.
+                     */
+                    try {
+                        Case.getCurrentCaseThrows();
+                        malwareResults.update();
+                    } catch (NoCurrentCaseException notUsed) {
+                        /**
+                         * Case is closed, do nothing.
+                         */
+                    }
+                } else if (eventType.equals(Case.Events.CURRENT_CASE.toString())) {
+                    // case was closed. Remove listeners so that we don't get called with a stale case handle
+                    if (evt.getNewValue() == null) {
+                        removeNotify();
+                        skCase = null;
+                    }
+                }
+            }
+        };
+
+        private final PropertyChangeListener weakPcl = WeakListeners.propertyChange(pcl, null);
+
+        protected void addNotify() {
+            IngestManager.getInstance().addIngestJobEventListener(INGEST_JOB_EVENTS_OF_INTEREST, weakPcl);
+            IngestManager.getInstance().addIngestModuleEventListener(INGEST_MODULE_EVENTS_OF_INTEREST, weakPcl);
+            Case.addEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), weakPcl);
+        }
+        
+        protected void removeNotify() {
+            IngestManager.getInstance().removeIngestJobEventListener(weakPcl);
+            IngestManager.getInstance().removeIngestModuleEventListener(weakPcl);
+            Case.removeEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), weakPcl);            
+        }
+
+        @Override
+        protected void finalize() throws Throwable {
+            super.finalize();
+            removeNotify();
+        }
+
+        @Override
+        public void update(Observable o, Object arg) {
+            update();
+        }
+    }
+
+    /**
+     * Top-level node for all malware hits
+     */
+    public class RootNode extends UpdatableCountTypeNode {
+
+        public RootNode() {
+            super(Children.create(new HitFactory(DISPLAY_NAME), true),
+                    Lookups.singleton(DISPLAY_NAME),
+                    DISPLAY_NAME,
+                    filteringDSObjId,
+                    MALWARE_ARTIFACT_TYPE);
+
+            super.setName(MALWARE_HITS);
+            // TODO make an icon
+            this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/artifact-icon.png");
+        }
+
+        @Override
+        public boolean isLeafTypeNode() {
+            return true;
+        }
+
+        @Override
+        public <T> T accept(DisplayableItemNodeVisitor<T> visitor) {
+            return visitor.visit(this);
+        }
+
+        @Override
+        protected Sheet createSheet() {
+            Sheet sheet = super.createSheet();
+            Sheet.Set sheetSet = sheet.get(Sheet.PROPERTIES);
+            if (sheetSet == null) {
+                sheetSet = Sheet.createPropertiesSet();
+                sheet.put(sheetSet);
+            }
+
+            sheetSet.put(new NodeProperty<>(NbBundle.getMessage(this.getClass(), "MalwareHits.createSheet.name.name"),
+                    NbBundle.getMessage(this.getClass(), "MalwareHits.createSheet.name.displayName"),
+                    NbBundle.getMessage(this.getClass(), "MalwareHits.createSheet.name.desc"),
+                    getName()));
+
+            return sheet;
+        }
+
+        @Override
+        public String getItemType() {
+            return getClass().getName();
+        }
+
+        /**
+         * When this method is called, the count to be displayed will be
+         * updated.
+         */
+        @Override
+        void updateDisplayName() {
+            super.setDisplayName(DISPLAY_NAME + " (" + malwareResults.getArtifactIds().size() + ")");
+        }
+    }
+
+    /**
+     * Creates the nodes for the malware hits.
+     */
+    private class HitFactory extends BaseChildFactory<AnalysisResult> implements Observer {
+
+        private final Map<Long, AnalysisResult> artifactHits = new HashMap<>();
+
+        private HitFactory(String nodeName) {
+            super(nodeName);
+        }
+
+        @Override
+        protected void onAdd() {
+            malwareResults.addObserver(this);
+        }
+
+        @Override
+        protected void onRemove() {
+            malwareResults.deleteObserver(this);
+        }
+
+        @Override
+        protected Node createNodeForKey(AnalysisResult key) {
+            return new BlackboardArtifactNode(key);
+        }
+
+        @Override
+        public void update(Observable o, Object arg) {
+            refresh(true);
+        }
+
+        @Override
+        protected List<AnalysisResult> makeKeys() {
+            if (skCase != null) {
+
+                malwareResults.getArtifactIds().forEach((id) -> {
+                    try {
+                        if (!artifactHits.containsKey(id)) {
+                            AnalysisResult art = skCase.getBlackboard().getAnalysisResultById(id);
+                            //Cache attributes while we are off the EDT.
+                            //See JIRA-5969
+                            art.getAttributes();
+                            artifactHits.put(id, art);
+                        }
+                    } catch (TskCoreException ex) {
+                        logger.log(Level.SEVERE, "TSK Exception occurred", ex); //NON-NLS
+                    }
+                });
+                return new ArrayList<>(artifactHits.values());
+            }
+            return Collections.emptyList();
+        }
+    }
+}