diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/AutopsyItemVisitor.java b/Core/src/org/sleuthkit/autopsy/datamodel/AutopsyItemVisitor.java index a99bbd48c7..d433d0d6df 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/AutopsyItemVisitor.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/AutopsyItemVisitor.java @@ -49,9 +49,9 @@ public interface AutopsyItemVisitor { T visit(DeletedContent.DeletedContentFilter dcf); - T visit(SuspectContent sc); + T visit(ScoreContent sc); - T visit(SuspectContent.SuspectContentFilter scf); + T visit(ScoreContent.ScoreContentFilter scf); T visit(FileSize fs); @@ -130,12 +130,12 @@ public interface AutopsyItemVisitor { } @Override - public T visit(SuspectContent dc) { + public T visit(ScoreContent dc) { return defaultVisit(dc); } @Override - public T visit(SuspectContent.SuspectContentFilter dcf) { + public T visit(ScoreContent.ScoreContentFilter dcf) { return defaultVisit(dcf); } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties-MERGED index d753a6e329..5dbdff8b89 100755 --- a/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties-MERGED @@ -412,6 +412,13 @@ ReportNode.reportNameProperty.name=Report Name ReportNode.reportNameProperty.displayName=Report Name ReportNode.reportNameProperty.desc=Name of the report ReportsListNode.displayName=Reports +ScoreContent_badFilter_text=Bad Items +ScoreContent_createSheet_filterType_desc=no description +ScoreContent_createSheet_filterType_displayName=Type +ScoreContent_createSheet_name_desc=no description +ScoreContent_createSheet_name_displayName=Name +ScoreContent_ScoreContentNode_name=Score +ScoreContent_susFilter_text=Suspicious Items SlackFileNode.getActions.viewInNewWin.text=View in New Window SlackFileNode.getActions.viewFileInDir.text=View File in Directory SpecialDirectoryNode.getActions.viewInNewWin.text=View in New Window diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNodeVisitor.java b/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNodeVisitor.java index 82c4b0ffb2..85e715382e 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNodeVisitor.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/DisplayableItemNodeVisitor.java @@ -32,8 +32,8 @@ import org.sleuthkit.autopsy.datamodel.FileSize.FileSizeRootNode; import org.sleuthkit.autopsy.datamodel.FileTypes.FileTypesNode; import org.sleuthkit.autopsy.datamodel.accounts.Accounts; import org.sleuthkit.autopsy.allcasessearch.CorrelationAttributeInstanceNode; -import org.sleuthkit.autopsy.datamodel.SuspectContent.SuspectContentsNode; -import org.sleuthkit.autopsy.datamodel.SuspectContent.SuspectContentsChildren.SuspectContentNode; +import org.sleuthkit.autopsy.datamodel.ScoreContent.ScoreContentsNode; +import org.sleuthkit.autopsy.datamodel.ScoreContent.ScoreContentsChildren.ScoreContentNode; /** * Visitor pattern that goes over all nodes in the directory tree. This includes @@ -80,9 +80,9 @@ public interface DisplayableItemNodeVisitor { T visit(DeletedContentsNode dcn); - T visit(SuspectContentNode scn); + T visit(ScoreContentNode scn); - T visit(SuspectContentsNode scn); + T visit(ScoreContentsNode scn); T visit(FileSizeRootNode fsrn); @@ -342,12 +342,12 @@ public interface DisplayableItemNodeVisitor { } @Override - public T visit(SuspectContentNode scn) { + public T visit(ScoreContentNode scn) { return defaultVisit(scn); } @Override - public T visit(SuspectContentsNode scn) { + public T visit(ScoreContentsNode scn) { return defaultVisit(scn); } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/RootContentChildren.java b/Core/src/org/sleuthkit/autopsy/datamodel/RootContentChildren.java index 1819d858e4..43c1253eb7 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/RootContentChildren.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/RootContentChildren.java @@ -24,8 +24,8 @@ import org.openide.nodes.AbstractNode; import org.openide.nodes.Children; import org.openide.nodes.Node; import org.openide.util.NbBundle; -import org.sleuthkit.autopsy.datamodel.SuspectContent.SuspectContentsChildren; -import org.sleuthkit.autopsy.datamodel.SuspectContent.SuspectContentsChildren.SuspectContentNode; +import org.sleuthkit.autopsy.datamodel.ScoreContent.ScoreContentsChildren; +import org.sleuthkit.autopsy.datamodel.ScoreContent.ScoreContentsChildren.ScoreContentNode; import org.sleuthkit.autopsy.datamodel.accounts.Accounts; import org.sleuthkit.datamodel.SleuthkitVisitableItem; @@ -101,8 +101,8 @@ public class RootContentChildren extends Children.Keys { } @Override - public AbstractNode visit(SuspectContent sc) { - return new SuspectContent.SuspectContentsNode(sc.getSleuthkitCase(), sc.filteringDataSourceObjId()); + public AbstractNode visit(ScoreContent sc) { + return new ScoreContent.ScoreContentsNode(sc.getSleuthkitCase(), sc.filteringDataSourceObjId()); } @Override diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/ScoreContent.java b/Core/src/org/sleuthkit/autopsy/datamodel/ScoreContent.java new file mode 100644 index 0000000000..c489efcaf2 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datamodel/ScoreContent.java @@ -0,0 +1,561 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2023 Basis Technology Corp. + * Contact: carrier sleuthkit 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.text.MessageFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import org.apache.commons.lang3.StringUtils; +import org.openide.nodes.AbstractNode; +import org.openide.nodes.ChildFactory; +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.guiutils.RefreshThrottler; +import org.sleuthkit.autopsy.ingest.IngestManager; +import org.sleuthkit.autopsy.ingest.IngestManager.IngestModuleEvent; +import org.sleuthkit.autopsy.ingest.ModuleDataEvent; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.BlackboardArtifact.Category; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.ContentVisitor; +import org.sleuthkit.datamodel.Directory; +import org.sleuthkit.datamodel.File; +import org.sleuthkit.datamodel.FsContent; +import org.sleuthkit.datamodel.LayoutFile; +import org.sleuthkit.datamodel.Score.Priority; +import org.sleuthkit.datamodel.Score.Significance; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.VirtualDirectory; + +/** + * Score content view nodes. + */ +public class ScoreContent implements AutopsyVisitableItem { + + private SleuthkitCase skCase; + private final long filteringDSObjId; // 0 if not filtering/grouping by data source + + @NbBundle.Messages({"ScoreContent_badFilter_text=Bad Items", + "ScoreContent_susFilter_text=Suspicious Items"}) + public enum ScoreContentFilter implements AutopsyVisitableItem { + + BAD_ITEM_FILTER(0, "BAD_ITEM_FILTER", //NON-NLS + Bundle.ScoreContent_badFilter_text()), + SUS_ITEM_FILTER(1, "SUS_ITEM_FILTER", //NON-NLS + Bundle.ScoreContent_susFilter_text()); + + private int id; + private String name; + private String displayName; + + private ScoreContentFilter(int id, String name, String displayName) { + this.id = id; + this.name = name; + this.displayName = displayName; + + } + + public String getName() { + return this.name; + } + + public int getId() { + return this.id; + } + + public String getDisplayName() { + return this.displayName; + } + + @Override + public T accept(AutopsyItemVisitor visitor) { + return visitor.visit(this); + } + } + + public ScoreContent(SleuthkitCase skCase) { + this(skCase, 0); + } + + public ScoreContent(SleuthkitCase skCase, long dsObjId) { + this.skCase = skCase; + this.filteringDSObjId = dsObjId; + } + + long filteringDataSourceObjId() { + return this.filteringDSObjId; + } + + @Override + public T accept(AutopsyItemVisitor visitor) { + return visitor.visit(this); + } + + public SleuthkitCase getSleuthkitCase() { + return this.skCase; + } + + private static final Set CASE_EVENTS_OF_INTEREST = EnumSet.of( + Case.Events.DATA_SOURCE_ADDED, + Case.Events.CURRENT_CASE, + Case.Events.CONTENT_TAG_ADDED, + Case.Events.CONTENT_TAG_DELETED, + Case.Events.BLACKBOARD_ARTIFACT_TAG_ADDED, + Case.Events.BLACKBOARD_ARTIFACT_TAG_DELETED + ); + private static final Set INGEST_JOB_EVENTS_OF_INTEREST = EnumSet.of(IngestManager.IngestJobEvent.COMPLETED, IngestManager.IngestJobEvent.CANCELLED); + private static final Set INGEST_MODULE_EVENTS_OF_INTEREST = EnumSet.of(IngestModuleEvent.CONTENT_CHANGED); + + private static PropertyChangeListener getPcl(final Runnable onRefresh, final Runnable onRemove) { + return (PropertyChangeEvent evt) -> { + String eventType = evt.getPropertyName(); + if (eventType.equals(IngestManager.IngestModuleEvent.CONTENT_CHANGED.toString())) { + /** + * + // @@@ COULD CHECK If the new file is deleted before + * notifying... Checking for a current case is a stop gap + * measure + update(); 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(); + // new file was added + // @@@ COULD CHECK If the new file is deleted before notifying... + if (onRefresh != null) { + onRefresh.run(); + } + } 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 && onRemove != null) { + onRemove.run(); + } + } else if (CASE_EVENTS_OF_INTEREST.contains(eventType)) { + /** + * 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(); + if (onRefresh != null) { + onRefresh.run(); + } + } catch (NoCurrentCaseException notUsed) { + /** + * Case is closed, do nothing. + */ + } + } + }; + } + + static private String getFileFilter(ScoreContent.ScoreContentFilter filter, long filteringDSObjId) throws IllegalArgumentException { + String aggregateScoreFilter = ""; + switch (filter) { + case SUS_ITEM_FILTER: + aggregateScoreFilter = " tsk_aggregate_score.significance = " + Significance.LIKELY_NOTABLE.getId() + " AND (tsk_aggregate_score.priority = " + Priority.NORMAL.getId() + " OR tsk_aggregate_score.priority = " + Priority.OVERRIDE.getId() + " )"; + + break; + case BAD_ITEM_FILTER: + aggregateScoreFilter = " tsk_aggregate_score.significance = " + Significance.NOTABLE.getId() + " AND (tsk_aggregate_score.priority = " + Priority.NORMAL.getId() + " OR tsk_aggregate_score.priority = " + Priority.OVERRIDE.getId() + " )"; + break; + + default: + throw new IllegalArgumentException(MessageFormat.format("Unsupported filter type to get suspect content: {0}", filter)); + + } + + String query = " obj_id IN (SELECT tsk_aggregate_score.obj_id FROM tsk_aggregate_score WHERE " + aggregateScoreFilter + ") "; + + if (filteringDSObjId > 0) { + query += " AND data_source_obj_id = " + filteringDSObjId; + } + return query; + } + + /** + * Checks for analysis results added to the case that could affect the + * aggregate score of the file. + * + * @param evt The event. + * @return True if has an analysis result. + */ + private static boolean isRefreshRequired(PropertyChangeEvent evt) { + String eventType = evt.getPropertyName(); + if (eventType.equals(IngestManager.IngestModuleEvent.DATA_ADDED.toString())) { + /** + * This 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. + */ + final ModuleDataEvent event = (ModuleDataEvent) evt.getOldValue(); + if (null != event && Category.ANALYSIS_RESULT.equals(event.getBlackboardArtifactType().getCategory())) { + return true; + } + } catch (NoCurrentCaseException notUsed) { + /** + * Case is closed, do nothing. + */ + } + } + return false; + } + + public static class ScoreContentsNode extends DisplayableItemNode { + + @NbBundle.Messages("ScoreContent_ScoreContentNode_name=Score") + private static final String NAME = Bundle.ScoreContent_ScoreContentNode_name(); + + ScoreContentsNode(SleuthkitCase skCase, long datasourceObjId) { + super(Children.create(new ScoreContentsChildren(skCase, datasourceObjId), true), Lookups.singleton(NAME)); + super.setName(NAME); + super.setDisplayName(NAME); + this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/red-circle-exclamation.png"); //NON-NLS + } + + @Override + public boolean isLeafTypeNode() { + return false; + } + + @Override + public T accept(DisplayableItemNodeVisitor visitor) { + return visitor.visit(this); + } + + @Override + @NbBundle.Messages({ + "ScoreContent_createSheet_name_displayName=Name", + "ScoreContent_createSheet_name_desc=no description"}) + 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<>("Name", //NON-NLS + Bundle.ScoreContent_createSheet_name_displayName(), + Bundle.ScoreContent_createSheet_name_desc(), + NAME)); + return sheet; + } + + @Override + public String getItemType() { + return getClass().getName(); + } + } + + public static class ScoreContentsChildren extends ChildFactory.Detachable implements RefreshThrottler.Refresher { + + private SleuthkitCase skCase; + private final long datasourceObjId; + + private final RefreshThrottler refreshThrottler = new RefreshThrottler(this); + + private final PropertyChangeListener pcl = getPcl( + () -> ScoreContentsChildren.this.refresh(false), + () -> ScoreContentsChildren.this.removeNotify()); + + private final PropertyChangeListener weakPcl = WeakListeners.propertyChange(pcl, null); + + private final Map typeNodeMap = new HashMap<>(); + + public ScoreContentsChildren(SleuthkitCase skCase, long dsObjId) { + this.skCase = skCase; + this.datasourceObjId = dsObjId; + } + + @Override + protected void addNotify() { + super.addNotify(); + refreshThrottler.registerForIngestModuleEvents(); + IngestManager.getInstance().addIngestJobEventListener(INGEST_JOB_EVENTS_OF_INTEREST, weakPcl); + IngestManager.getInstance().addIngestModuleEventListener(INGEST_MODULE_EVENTS_OF_INTEREST, weakPcl); + Case.addEventTypeSubscriber(CASE_EVENTS_OF_INTEREST, weakPcl); + } + + @Override + protected void removeNotify() { + refreshThrottler.unregisterEventListener(); + IngestManager.getInstance().removeIngestJobEventListener(INGEST_JOB_EVENTS_OF_INTEREST, weakPcl); + IngestManager.getInstance().removeIngestModuleEventListener(INGEST_MODULE_EVENTS_OF_INTEREST, weakPcl); + Case.removeEventTypeSubscriber(CASE_EVENTS_OF_INTEREST, weakPcl); + typeNodeMap.clear(); + } + + @Override + public void refresh() { + refresh(false); + } + + @Override + public boolean isRefreshRequired(PropertyChangeEvent evt) { + return ScoreContent.isRefreshRequired(evt); + } + + @Override + protected boolean createKeys(List list) { + list.addAll(Arrays.asList(ScoreContent.ScoreContentFilter.values())); + typeNodeMap.values().forEach(nd -> nd.updateDisplayName()); + return true; + } + + @Override + protected Node createNodeForKey(ScoreContent.ScoreContentFilter key) { + ScoreContentsChildren.ScoreContentNode nd = new ScoreContentsChildren.ScoreContentNode(skCase, key, datasourceObjId); + typeNodeMap.put(key, nd); + return nd; + } + + public class ScoreContentNode extends DisplayableItemNode { + + private static final Logger logger = Logger.getLogger(ScoreContentNode.class.getName()); + private final ScoreContent.ScoreContentFilter filter; + private final long datasourceObjId; + + ScoreContentNode(SleuthkitCase skCase, ScoreContent.ScoreContentFilter filter, long dsObjId) { + super(Children.create(new ScoreContentChildren(filter, skCase, dsObjId), true), Lookups.singleton(filter.getDisplayName())); + this.filter = filter; + this.datasourceObjId = dsObjId; + init(); + } + + private void init() { + super.setName(filter.getName()); + + String tooltip = filter.getDisplayName(); + this.setShortDescription(tooltip); + switch (this.filter) { + case SUS_ITEM_FILTER: + this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/yellow-circle-yield.png"); //NON-NLS + break; + default: + case BAD_ITEM_FILTER: + this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/red-circle-exclamation.png"); //NON-NLS + break; + } + + updateDisplayName(); + } + + void updateDisplayName() { + //get count of children without preloading all children nodes + long count = 0; + try { + count = calculateItems(skCase, filter, datasourceObjId); + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "An error occurred while fetching file counts", ex); + } + //final long count = getChildren().getNodesCount(true); + super.setDisplayName(filter.getDisplayName() + " (" + count + ")"); + } + + /** + * Get children count without actually loading all nodes + * + * @param sleuthkitCase + * @param filter + * + * @return + */ + private static long calculateItems(SleuthkitCase sleuthkitCase, ScoreContent.ScoreContentFilter filter, long datasourceObjId) throws TskCoreException { + return sleuthkitCase.countFilesWhere(getFileFilter(filter, datasourceObjId)); + } + + @Override + public T accept(DisplayableItemNodeVisitor visitor) { + return visitor.visit(this); + } + + @Override + @NbBundle.Messages({ + "ScoreContent_createSheet_filterType_displayName=Type", + "ScoreContent_createSheet_filterType_desc=no description"}) + 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<>("Type", //NON_NLS + Bundle.ScoreContent_createSheet_filterType_displayName(), + Bundle.ScoreContent_createSheet_filterType_desc(), + filter.getDisplayName())); + + return sheet; + } + + @Override + public boolean isLeafTypeNode() { + return true; + } + + @Override + public String getItemType() { + /** + * Return getClass().getName() + filter.getName() if custom + * settings are desired for different filters. + */ + return DisplayableItemNode.FILE_PARENT_NODE_KEY; + } + } + + static class ScoreContentChildren extends BaseChildFactory implements RefreshThrottler.Refresher { + + private final RefreshThrottler refreshThrottler = new RefreshThrottler(this); + + private final PropertyChangeListener pcl = getPcl( + () -> ScoreContentChildren.this.refresh(false), + () -> ScoreContentChildren.this.removeNotify()); + + private final PropertyChangeListener weakPcl = WeakListeners.propertyChange(pcl, null); + + private final SleuthkitCase skCase; + private final ScoreContent.ScoreContentFilter filter; + private static final Logger logger = Logger.getLogger(ScoreContentChildren.class.getName()); + + private final long datasourceObjId; + + ScoreContentChildren(ScoreContent.ScoreContentFilter filter, SleuthkitCase skCase, long datasourceObjId) { + super(filter.getName(), new ViewsKnownAndSlackFilter<>()); + this.skCase = skCase; + this.filter = filter; + this.datasourceObjId = datasourceObjId; + } + + @Override + protected void onAdd() { + refreshThrottler.registerForIngestModuleEvents(); + IngestManager.getInstance().addIngestJobEventListener(INGEST_JOB_EVENTS_OF_INTEREST, weakPcl); + IngestManager.getInstance().addIngestModuleEventListener(INGEST_MODULE_EVENTS_OF_INTEREST, weakPcl); + Case.addEventTypeSubscriber(CASE_EVENTS_OF_INTEREST, weakPcl); + } + + @Override + protected void onRemove() { + refreshThrottler.unregisterEventListener(); + IngestManager.getInstance().removeIngestJobEventListener(INGEST_JOB_EVENTS_OF_INTEREST, weakPcl); + IngestManager.getInstance().removeIngestModuleEventListener(INGEST_MODULE_EVENTS_OF_INTEREST, weakPcl); + Case.removeEventTypeSubscriber(CASE_EVENTS_OF_INTEREST, weakPcl); + } + + @Override + public void refresh() { + refresh(false); + } + + @Override + public boolean isRefreshRequired(PropertyChangeEvent evt) { + return ScoreContent.isRefreshRequired(evt); + } + + private List runFsQuery() { + List ret = new ArrayList<>(); + + String query = null; + try { + query = getFileFilter(filter, datasourceObjId); + ret = skCase.findAllFilesWhere(query); + } catch (TskCoreException | IllegalArgumentException e) { + logger.log(Level.SEVERE, "Error getting files for the deleted content view using: " + StringUtils.defaultString(query, ""), e); //NON-NLS + } + + return ret; + + } + + @Override + protected List makeKeys() { + return runFsQuery(); + } + + @Override + protected Node createNodeForKey(AbstractFile key) { + return key.accept(new ContentVisitor.Default() { + public FileNode visit(AbstractFile f) { + return new FileNode(f, false); + } + + public FileNode visit(FsContent f) { + return new FileNode(f, false); + } + + @Override + public FileNode visit(LayoutFile f) { + return new FileNode(f, false); + } + + @Override + public FileNode visit(File f) { + return new FileNode(f, false); + } + + @Override + public FileNode visit(Directory f) { + return new FileNode(f, false); + } + + @Override + public FileNode visit(VirtualDirectory f) { + return new FileNode(f, false); + } + + @Override + protected AbstractNode defaultVisit(Content di) { + throw new UnsupportedOperationException("Not supported for this type of Displayable Item: " + di.toString()); + } + }); + } + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/SuspectContent.java b/Core/src/org/sleuthkit/autopsy/datamodel/SuspectContent.java deleted file mode 100644 index cc48666f84..0000000000 --- a/Core/src/org/sleuthkit/autopsy/datamodel/SuspectContent.java +++ /dev/null @@ -1,506 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2023 Basis Technology Corp. - * Contact: carrier sleuthkit 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.util.ArrayList; -import java.util.Arrays; -import java.util.EnumSet; -import java.util.List; -import java.util.Observable; -import java.util.Observer; -import java.util.Set; -import java.util.logging.Level; -import org.openide.nodes.AbstractNode; -import org.openide.nodes.ChildFactory; -import org.openide.nodes.Children; -import org.openide.nodes.Node; -import org.openide.nodes.Sheet; -import org.openide.util.NbBundle; -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 static org.sleuthkit.autopsy.ingest.IngestManager.IngestModuleEvent.CONTENT_CHANGED; -import org.sleuthkit.datamodel.AbstractFile; -import org.sleuthkit.datamodel.Content; -import org.sleuthkit.datamodel.ContentVisitor; -import org.sleuthkit.datamodel.Directory; -import org.sleuthkit.datamodel.File; -import org.sleuthkit.datamodel.FsContent; -import org.sleuthkit.datamodel.LayoutFile; -import org.sleuthkit.datamodel.Score.Priority; -import org.sleuthkit.datamodel.Score.Significance; -import org.sleuthkit.datamodel.SleuthkitCase; -import org.sleuthkit.datamodel.TskCoreException; -import org.sleuthkit.datamodel.VirtualDirectory; - -/** - * Suspect content view nodes. - */ -public class SuspectContent implements AutopsyVisitableItem { - - private SleuthkitCase skCase; - private final long filteringDSObjId; // 0 if not filtering/grouping by data source - - @NbBundle.Messages({"SuspectContent_badFilter_text=Bad Items", - "SuspectContent_susFilter_text=Suspicious Items"}) - public enum SuspectContentFilter implements AutopsyVisitableItem { - - BAD_ITEM_FILTER(0, "BAD_ITEM_FILTER", //NON-NLS - Bundle.SuspectContent_badFilter_text()), - SUS_ITEM_FILTER(1, "SUS_ITEM_FILTER", //NON-NLS - Bundle.SuspectContent_susFilter_text()); - - private int id; - private String name; - private String displayName; - - private SuspectContentFilter(int id, String name, String displayName) { - this.id = id; - this.name = name; - this.displayName = displayName; - - } - - public String getName() { - return this.name; - } - - public int getId() { - return this.id; - } - - public String getDisplayName() { - return this.displayName; - } - - @Override - public T accept(AutopsyItemVisitor visitor) { - return visitor.visit(this); - } - } - - public SuspectContent(SleuthkitCase skCase) { - this(skCase, 0); - } - - public SuspectContent(SleuthkitCase skCase, long dsObjId) { - this.skCase = skCase; - this.filteringDSObjId = dsObjId; - } - - long filteringDataSourceObjId() { - return this.filteringDSObjId; - } - - @Override - public T accept(AutopsyItemVisitor visitor) { - return visitor.visit(this); - } - - public SleuthkitCase getSleuthkitCase() { - return this.skCase; - } - - public static class SuspectContentsNode extends DisplayableItemNode { - - @NbBundle.Messages("SuspectContent_SuspectContentNode_name=Score") - private static final String NAME = Bundle.SuspectContent_SuspectContentNode_name(); - - SuspectContentsNode(SleuthkitCase skCase, long datasourceObjId) { - super(Children.create(new SuspectContentsChildren(skCase, datasourceObjId), true), Lookups.singleton(NAME)); - super.setName(NAME); - super.setDisplayName(NAME); - this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/red-circle-exclamation.png"); //NON-NLS - } - - @Override - public boolean isLeafTypeNode() { - return false; - } - - @Override - public T accept(DisplayableItemNodeVisitor visitor) { - return visitor.visit(this); - } - - @Override - @NbBundle.Messages({ - "SuspectContent_createSheet_name_displayName=Name", - "SuspectContent_createSheet_name_desc=no description"}) - 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<>("Name", //NON-NLS - Bundle.SuspectContent_createSheet_name_displayName(), - Bundle.SuspectContent_createSheet_name_desc(), - NAME)); - return sheet; - } - - @Override - public String getItemType() { - return getClass().getName(); - } - } - - public static class SuspectContentsChildren extends ChildFactory { - - private SleuthkitCase skCase; - private Observable notifier; - private final long datasourceObjId; - // true if we have already told user that not all files will be shown - private static volatile boolean maxFilesDialogShown = false; - - public SuspectContentsChildren(SleuthkitCase skCase, long dsObjId) { - this.skCase = skCase; - this.datasourceObjId = dsObjId; - this.notifier = new SuspectContentsChildrenObservable(); - } - - /** - * Listens for case and ingest invest. Updates observers when events are - * fired. Other nodes are listening to this for changes. - */ - private static final class SuspectContentsChildrenObservable extends Observable { - - // GVDTODO are these all events of interest - private static final Set CASE_EVENTS_OF_INTEREST = EnumSet.of( - Case.Events.DATA_SOURCE_ADDED, - Case.Events.CURRENT_CASE - ); - private static final Set INGEST_JOB_EVENTS_OF_INTEREST = EnumSet.of(IngestManager.IngestJobEvent.COMPLETED, IngestManager.IngestJobEvent.CANCELLED); - private static final Set INGEST_MODULE_EVENTS_OF_INTEREST = EnumSet.of(CONTENT_CHANGED); - - SuspectContentsChildrenObservable() { - IngestManager.getInstance().addIngestJobEventListener(INGEST_JOB_EVENTS_OF_INTEREST, pcl); - IngestManager.getInstance().addIngestModuleEventListener(INGEST_MODULE_EVENTS_OF_INTEREST, pcl); - Case.addEventTypeSubscriber(CASE_EVENTS_OF_INTEREST, pcl); - } - - private void removeListeners() { - deleteObservers(); - IngestManager.getInstance().removeIngestJobEventListener(pcl); - IngestManager.getInstance().removeIngestModuleEventListener(pcl); - Case.removeEventTypeSubscriber(CASE_EVENTS_OF_INTEREST, pcl); - } - - private final PropertyChangeListener pcl = (PropertyChangeEvent evt) -> { - String eventType = evt.getPropertyName(); - if (eventType.equals(IngestManager.IngestModuleEvent.CONTENT_CHANGED.toString())) { - /** - * + // @@@ COULD CHECK If the new file is deleted before - * notifying... Checking for a current case is a stop gap - * measure + update(); 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(); - // new file was added - // @@@ COULD CHECK If the new file is deleted before notifying... - update(); - } catch (NoCurrentCaseException notUsed) { - /** - * Case is closed, do nothing. - */ - } - } else if (eventType.equals(IngestManager.IngestJobEvent.COMPLETED.toString()) - || eventType.equals(IngestManager.IngestJobEvent.CANCELLED.toString()) - || eventType.equals(Case.Events.DATA_SOURCE_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(); - 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) { - removeListeners(); - } - maxFilesDialogShown = false; - } - }; - - private void update() { - setChanged(); - notifyObservers(); - } - } - - @Override - - protected boolean createKeys(List list) { - list.addAll(Arrays.asList(SuspectContent.SuspectContentFilter.values())); - return true; - } - - @Override - protected Node createNodeForKey(SuspectContent.SuspectContentFilter key) { - return new SuspectContentsChildren.SuspectContentNode(skCase, key, notifier, datasourceObjId); - } - - // GVDTODO remove observable - public class SuspectContentNode extends DisplayableItemNode { - - private final SuspectContent.SuspectContentFilter filter; - private final long datasourceObjId; - - SuspectContentNode(SleuthkitCase skCase, SuspectContent.SuspectContentFilter filter, Observable o, long dsObjId) { - super(Children.create(new SuspectContentChildren(filter, skCase, o, dsObjId), true), Lookups.singleton(filter.getDisplayName())); - this.filter = filter; - this.datasourceObjId = dsObjId; - init(); - o.addObserver(new SuspectContentNodeObserver()); - } - - private void init() { - super.setName(filter.getName()); - - String tooltip = filter.getDisplayName(); - this.setShortDescription(tooltip); - switch (this.filter) { - case SUS_ITEM_FILTER: - this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/yellow-circle-yield.png"); //NON-NLS - break; - default: - case BAD_ITEM_FILTER: - this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/red-circle-exclamation.png"); //NON-NLS - break; - } - - updateDisplayName(); - } - - // update the display name when new events are fired - private class SuspectContentNodeObserver implements Observer { - - @Override - public void update(Observable o, Object arg) { - updateDisplayName(); - } - } - - private void updateDisplayName() { - //get count of children without preloading all children nodes - final long count = SuspectContentChildren.calculateItems(skCase, filter, datasourceObjId); - //final long count = getChildren().getNodesCount(true); - super.setDisplayName(filter.getDisplayName() + " (" + count + ")"); - } - - @Override - public T accept(DisplayableItemNodeVisitor visitor) { - return visitor.visit(this); - } - - @Override - @NbBundle.Messages({ - "SuspectContent_createSheet_filterType_displayName=Type", - "SuspectContent_createSheet_filterType_desc=no description"}) - 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<>("Type", //NON_NLS - Bundle.SuspectContent_createSheet_filterType_displayName(), - Bundle.SuspectContent_createSheet_filterType_desc(), - filter.getDisplayName())); - - return sheet; - } - - @Override - public boolean isLeafTypeNode() { - return true; - } - - @Override - public String getItemType() { - /** - * Return getClass().getName() + filter.getName() if custom - * settings are desired for different filters. - */ - return DisplayableItemNode.FILE_PARENT_NODE_KEY; - } - } - - static class SuspectContentChildren extends BaseChildFactory { - - private final SleuthkitCase skCase; - private final SuspectContent.SuspectContentFilter filter; - private static final Logger logger = Logger.getLogger(SuspectContentChildren.class.getName()); - - private final Observable notifier; - private final long datasourceObjId; - - // GVDTODO remove observable - SuspectContentChildren(SuspectContent.SuspectContentFilter filter, SleuthkitCase skCase, Observable o, long datasourceObjId) { - super(filter.getName(), new ViewsKnownAndSlackFilter<>()); - this.skCase = skCase; - this.filter = filter; - this.notifier = o; - this.datasourceObjId = datasourceObjId; - } - - private final Observer observer = new DeletedContentChildrenObserver(); - - @Override - protected List makeKeys() { - return runFsQuery(); - } - - // Cause refresh of children if there are changes - private class DeletedContentChildrenObserver implements Observer { - - @Override - public void update(Observable o, Object arg) { - refresh(true); - } - } - - @Override - protected void onAdd() { - if (notifier != null) { - notifier.addObserver(observer); - } - } - - @Override - protected void onRemove() { - if (notifier != null) { - notifier.deleteObserver(observer); - } - } - - static private String makeQuery(SuspectContent.SuspectContentFilter filter, long filteringDSObjId) { - String aggregateScoreFilter = ""; - switch (filter) { - case SUS_ITEM_FILTER: - aggregateScoreFilter = " tsk_aggregate_score.significance = " + Significance.LIKELY_NOTABLE.getId() + " AND (tsk_aggregate_score.priority = " + Priority.NORMAL.getId() + " OR tsk_aggregate_score.priority = " + Priority.OVERRIDE.getId() + " )"; - - break; - case BAD_ITEM_FILTER: - aggregateScoreFilter = " tsk_aggregate_score.significance = " + Significance.NOTABLE.getId() + " AND (tsk_aggregate_score.priority = " + Priority.NORMAL.getId() + " OR tsk_aggregate_score.priority = " + Priority.OVERRIDE.getId() + " )"; - break; - - default: - logger.log(Level.SEVERE, "Unsupported filter type to get suspect content: {0}", filter); //NON-NLS - - } - - String query = " obj_id IN (SELECT tsk_aggregate_score.obj_id FROM tsk_aggregate_score WHERE " + aggregateScoreFilter + ") "; - - if (filteringDSObjId > 0) { - query += " AND data_source_obj_id = " + filteringDSObjId; - } - return query; - } - - private List runFsQuery() { - List ret = new ArrayList<>(); - - String query = makeQuery(filter, datasourceObjId); - try { - ret = skCase.findAllFilesWhere(query); - } catch (TskCoreException e) { - logger.log(Level.SEVERE, "Error getting files for the deleted content view using: " + query, e); //NON-NLS - } - - return ret; - - } - - /** - * Get children count without actually loading all nodes - * - * @param sleuthkitCase - * @param filter - * - * @return - */ - static long calculateItems(SleuthkitCase sleuthkitCase, SuspectContent.SuspectContentFilter filter, long datasourceObjId) { - try { - return sleuthkitCase.countFilesWhere(makeQuery(filter, datasourceObjId)); - } catch (TskCoreException ex) { - logger.log(Level.SEVERE, "Error getting deleted files search view count", ex); //NON-NLS - return 0; - } - } - - @Override - protected Node createNodeForKey(AbstractFile key) { - return key.accept(new ContentVisitor.Default() { - public FileNode visit(AbstractFile f) { - return new FileNode(f, false); - } - - public FileNode visit(FsContent f) { - return new FileNode(f, false); - } - - @Override - public FileNode visit(LayoutFile f) { - return new FileNode(f, false); - } - - @Override - public FileNode visit(File f) { - return new FileNode(f, false); - } - - @Override - public FileNode visit(Directory f) { - return new FileNode(f, false); - } - - @Override - public FileNode visit(VirtualDirectory f) { - return new FileNode(f, false); - } - - @Override - protected AbstractNode defaultVisit(Content di) { - throw new UnsupportedOperationException("Not supported for this type of Displayable Item: " + di.toString()); - } - }); - } - } - } -} diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/ViewsNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/ViewsNode.java index 8044a9ca40..8307fe01c2 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/ViewsNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/ViewsNode.java @@ -47,7 +47,7 @@ public class ViewsNode extends DisplayableItemNode { // new RecentFiles(sleuthkitCase), new DeletedContent(sleuthkitCase, dsObjId), new FileSize(sleuthkitCase, dsObjId), - new SuspectContent(sleuthkitCase, dsObjId)) + new ScoreContent(sleuthkitCase, dsObjId)) ), Lookups.singleton(NAME) );