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(); + } + } +}