diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/AutopsyTreeChildFactory.java b/Core/src/org/sleuthkit/autopsy/datamodel/AutopsyTreeChildFactory.java index d4becd5d63..f76bfe5469 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/AutopsyTreeChildFactory.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/AutopsyTreeChildFactory.java @@ -143,14 +143,16 @@ public final class AutopsyTreeChildFactory extends ChildFactory.Detachable 0. */ @@ -153,11 +168,17 @@ public class ScoreContent implements AutopsyVisitableItem { Case.Events.BLACKBOARD_ARTIFACT_TAG_ADDED, Case.Events.BLACKBOARD_ARTIFACT_TAG_DELETED ); + private static final Set CASE_EVENTS_OF_INTEREST_STRS = CASE_EVENTS_OF_INTEREST.stream() + .map(evt -> evt.name()) + .collect(Collectors.toSet()); + 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); /** - * Returns a property change listener listening for possible updates to aggregate score updates for files. + * Returns a property change listener listening for possible updates to + * aggregate score updates for files. + * * @param onRefresh Action on refresh. * @param onRemove Action to remove listener (i.e. case close). * @return The property change listener. @@ -182,7 +203,7 @@ public class ScoreContent implements AutopsyVisitableItem { if (evt.getNewValue() == null && onRemove != null) { onRemove.run(); } - } else if (CASE_EVENTS_OF_INTEREST.contains(eventType)) { + } else if (CASE_EVENTS_OF_INTEREST_STRS.contains(eventType)) { // only refresh if there is a current case. try { Case.getCurrentCaseThrows(); @@ -199,34 +220,61 @@ public class ScoreContent implements AutopsyVisitableItem { } /** - * The sql where statement for the files. + * The sql where statement for the content. + * + * @param filter The filter type. + * @param objIdAlias The alias for the object id of the content. Must be sql + * safe. + * @param dsIdAlias The alias for the data source id. Must be sql safe. + * @param filteringDSObjId The data source object id to filter on if > 0. + * @return The sql where statement. + * @throws IllegalArgumentException + */ + private static String getFilter(ScoreContent.ScoreContentFilter filter, String objIdAlias, String dsIdAlias, long filteringDSObjId) throws IllegalArgumentException { + String aggregateScoreFilter = getScoreFilter(filter); + String query = " " + objIdAlias + " IN (SELECT tsk_aggregate_score.obj_id FROM tsk_aggregate_score WHERE " + aggregateScoreFilter + ") "; + + if (filteringDSObjId > 0) { + query += " AND " + dsIdAlias + " = " + filteringDSObjId; + } + return query; + } + + private static String getScoreFilter(ScoreContentFilter filter) throws IllegalArgumentException { + switch (filter) { + case SUS_ITEM_FILTER: + return " tsk_aggregate_score.significance = " + Significance.LIKELY_NOTABLE.getId() + + " AND (tsk_aggregate_score.priority = " + Priority.NORMAL.getId() + " OR tsk_aggregate_score.priority = " + Priority.OVERRIDE.getId() + " )"; + case BAD_ITEM_FILTER: + return " tsk_aggregate_score.significance = " + Significance.NOTABLE.getId() + + " AND (tsk_aggregate_score.priority = " + Priority.NORMAL.getId() + " OR tsk_aggregate_score.priority = " + Priority.OVERRIDE.getId() + " )"; + default: + throw new IllegalArgumentException(MessageFormat.format("Unsupported filter type to get suspect content: {0}", filter)); + } + } + + /** + * Returns a sql where statement for files. + * * @param filter The filter type. * @param filteringDSObjId The data source object id to filter on if > 0. * @return The sql where statement. - * @throws IllegalArgumentException + * @throws IllegalArgumentException */ - 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() + " )"; + private static String getFileFilter(ScoreContent.ScoreContentFilter filter, long filteringDsObjId) throws IllegalArgumentException { + return getFilter(filter, "obj_id", "data_source_obj_id", filteringDsObjId); + } - 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; + /** + * Returns a sql where statement for files. + * + * @param filter The filter type. + * @param filteringDSObjId The data source object id to filter on if > 0. + * @return The sql where statement. + * @throws IllegalArgumentException + */ + private static String getDataArtifactFilter(ScoreContent.ScoreContentFilter filter, long filteringDsObjId) throws IllegalArgumentException { + return getFilter(filter, "artifacts.artifact_obj_id", "artifacts.data_source_obj_id", filteringDsObjId); } /** @@ -424,7 +472,34 @@ public class ScoreContent implements AutopsyVisitableItem { * @return */ private static long calculateItems(SleuthkitCase sleuthkitCase, ScoreContent.ScoreContentFilter filter, long datasourceObjId) throws TskCoreException { - return sleuthkitCase.countFilesWhere(getFileFilter(filter, datasourceObjId)); + AtomicLong retVal = new AtomicLong(0L); + AtomicReference exRef = new AtomicReference(null); + + String query = " COUNT(tsk_aggregate_score.obj_id) AS count FROM tsk_aggregate_score WHERE\n" + + getScoreFilter(filter) + "\n" + + ((datasourceObjId > 0) ? "AND tsk_aggregate_score.data_source_obj_id = \n" + datasourceObjId : "") + + " AND tsk_aggregate_score.obj_id IN\n" + + " (SELECT tsk_files.obj_id AS obj_id FROM tsk_files UNION\n" + + " SELECT blackboard_artifacts.artifact_obj_id AS obj_id FROM blackboard_artifacts WHERE blackboard_artifacts.artifact_type_id IN\n" + + " (SELECT artifact_type_id FROM blackboard_artifact_types WHERE category_type = " + Category.DATA_ARTIFACT.getID() + ")) "; + sleuthkitCase.getCaseDbAccessManager().select(query, (rs) -> { + try { + if (rs.next()) { + retVal.set(rs.getLong("count")); + } + } catch (SQLException ex) { + exRef.set(ex); + } + }); + + SQLException sqlEx = exRef.get(); + if (sqlEx != null) { + throw new TskCoreException( + MessageFormat.format("A sql exception occurred fetching results with query: SELECT {0}", query), + sqlEx); + } else { + return retVal.get(); + } } @Override @@ -466,7 +541,7 @@ public class ScoreContent implements AutopsyVisitableItem { /** * Children showing files for a score filter. */ - static class ScoreContentChildren extends BaseChildFactory implements RefreshThrottler.Refresher { + static class ScoreContentChildren extends BaseChildFactory implements RefreshThrottler.Refresher { private final RefreshThrottler refreshThrottler = new RefreshThrottler(this); @@ -515,15 +590,21 @@ public class ScoreContent implements AutopsyVisitableItem { return ScoreContent.isRefreshRequired(evt); } - private List runFsQuery() { - List ret = new ArrayList<>(); + private List runFsQuery() { + List ret = new ArrayList<>(); - String query = null; + String fileFilter = null; + String dataArtifactFilter = null; try { - query = getFileFilter(filter, datasourceObjId); - ret = skCase.findAllFilesWhere(query); + fileFilter = getFileFilter(filter, datasourceObjId); + dataArtifactFilter = getDataArtifactFilter(filter, datasourceObjId); + ret.addAll(skCase.findAllFilesWhere(fileFilter)); + ret.addAll(skCase.getBlackboard().getDataArtifactsWhere(dataArtifactFilter)); } catch (TskCoreException | IllegalArgumentException e) { - logger.log(Level.SEVERE, "Error getting files for the deleted content view using: " + StringUtils.defaultString(query, ""), e); //NON-NLS + logger.log(Level.SEVERE, MessageFormat.format( + "Error getting files for the deleted content view using file filter: {0} data artifact filter: {1}", + StringUtils.defaultString(fileFilter, ""), + StringUtils.defaultString(dataArtifactFilter, "")), e); //NON-NLS } return ret; @@ -531,66 +612,201 @@ public class ScoreContent implements AutopsyVisitableItem { } @Override - protected List makeKeys() { + protected List makeKeys() { return runFsQuery(); } @Override - protected Node createNodeForKey(AbstractFile key) { + protected Node createNodeForKey(Content key) { return key.accept(new ContentVisitor.Default() { public FileNode visit(AbstractFile f) { - return new FileNode(f, false); + return new ScoreFileNode(f, false); } public FileNode visit(FsContent f) { - return new FileNode(f, false); + return new ScoreFileNode(f, false); } @Override public FileNode visit(LayoutFile f) { - return new FileNode(f, false); + return new ScoreFileNode(f, false); } @Override public FileNode visit(File f) { - return new FileNode(f, false); + return new ScoreFileNode(f, false); } @Override public FileNode visit(Directory f) { - return new FileNode(f, false); + return new ScoreFileNode(f, false); } @Override public FileNode visit(VirtualDirectory f) { - return new FileNode(f, false); + return new ScoreFileNode(f, false); } @Override public AbstractNode visit(SlackFile sf) { - return new FileNode(sf, false); + return new ScoreFileNode(sf, false); } @Override public AbstractNode visit(LocalFile lf) { - return new FileNode(lf, false); + return new ScoreFileNode(lf, false); } @Override public AbstractNode visit(DerivedFile df) { - return new FileNode(df, false); + return new ScoreFileNode(df, false); } - + + @Override + public AbstractNode visit(BlackboardArtifact ba) { + return new ScoreArtifactNode(ba); + } + @Override protected AbstractNode defaultVisit(Content di) { if (di instanceof AbstractFile) { return visit((AbstractFile) di); } else { - throw new UnsupportedOperationException("Not supported for this type of Displayable Item: " + di.toString()); + throw new UnsupportedOperationException("Not supported for this type of Displayable Item: " + di.toString()); } } }); } } } + + private static final String SOURCE_PROP = "Source"; + private static final String TYPE_PROP = "Type"; + private static final String PATH_PROP = "Path"; + private static final String DATE_PROP = "Created Date"; + + private static Sheet createScoreSheet(String type, String path, Long time) { + Sheet sheet = new Sheet(); + Sheet.Set sheetSet = Sheet.createPropertiesSet(); + sheet.put(sheetSet); + + List> properties = new ArrayList<>(); + properties.add(new NodeProperty<>( + SOURCE_PROP, + SOURCE_PROP, + NO_DESCR, + StringUtils.defaultString(path))); + + properties.add(new NodeProperty<>( + TYPE_PROP, + TYPE_PROP, + NO_DESCR, + type)); + + if (StringUtils.isNotBlank(path)) { + properties.add(new NodeProperty<>( + PATH_PROP, + PATH_PROP, + NO_DESCR, + path)); + } + + if (time != null && time > 0) { + properties.add(new NodeProperty<>( + DATE_PROP, + DATE_PROP, + NO_DESCR, + TimeZoneUtils.getFormattedTime(time))); + } + + properties.forEach((property) -> { + sheetSet.put(property); + }); + + return sheet; + } + + public static class ScoreArtifactNode extends BlackboardArtifactNode { + + private static final Logger logger = Logger.getLogger(ScoreArtifactNode.class.getName()); + + private static final List TIME_ATTRS = Arrays.asList( + BlackboardAttribute.Type.TSK_DATETIME, + BlackboardAttribute.Type.TSK_DATETIME_ACCESSED, + BlackboardAttribute.Type.TSK_DATETIME_RCVD, + BlackboardAttribute.Type.TSK_DATETIME_SENT, + BlackboardAttribute.Type.TSK_DATETIME_CREATED, + BlackboardAttribute.Type.TSK_DATETIME_MODIFIED, + BlackboardAttribute.Type.TSK_DATETIME_START, + BlackboardAttribute.Type.TSK_DATETIME_END, + BlackboardAttribute.Type.TSK_DATETIME_DELETED, + BlackboardAttribute.Type.TSK_DATETIME_PASSWORD_RESET, + BlackboardAttribute.Type.TSK_DATETIME_PASSWORD_FAIL + ); + + private static final Map TIME_ATTR_IMPORTANCE = IntStream.range(0, TIME_ATTRS.size()) + .mapToObj(idx -> Pair.of(TIME_ATTRS.get(idx).getTypeID(), idx)) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue, (v1, v2) -> v1)); + + public ScoreArtifactNode(BlackboardArtifact artifact) { + super(artifact); + } + + private Long getTime(BlackboardArtifact artifact) { + try { + BlackboardAttribute timeAttr = artifact.getAttributes().stream() + .filter((attr) -> TIME_ATTR_IMPORTANCE.keySet().contains(attr.getAttributeType().getTypeID())) + .sorted(Comparator.comparing(attr -> TIME_ATTR_IMPORTANCE.get(attr.getAttributeType().getTypeID()))) + .findFirst() + .orElse(null); + + if (timeAttr != null) { + return timeAttr.getValueLong(); + } else { + return (artifact.getParent() instanceof AbstractFile) ? ((AbstractFile) artifact.getParent()).getCtime() : null; + } + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "An exception occurred while fetching time for artifact", ex); + return null; + } + } + + @Override + protected synchronized Sheet createSheet() { + try { + return createScoreSheet( + this.content.getType().getDisplayName(), + this.content.getUniquePath(), + getTime(this.content) + ); + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "An error occurred while fetching sheet data for score artifact.", ex); + return new Sheet(); + } + } + } + + @Messages("ScoreContent_ScoreFileNode_type=File") + public static class ScoreFileNode extends FileNode { + + private static final Logger logger = Logger.getLogger(ScoreFileNode.class.getName()); + + public ScoreFileNode(AbstractFile af, boolean directoryBrowseMode) { + super(af, directoryBrowseMode); + } + + @Override + protected synchronized Sheet createSheet() { + try { + return createScoreSheet( + Bundle.ScoreContent_ScoreFileNode_type(), + this.content.getUniquePath(), + this.content.getCtime() + ); + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "An error occurred while fetching sheet data for score file.", ex); + return new Sheet(); + } + } + } } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/ViewsNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/ViewsNode.java index 8307fe01c2..423e58e1ed 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/ViewsNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/ViewsNode.java @@ -46,8 +46,7 @@ public class ViewsNode extends DisplayableItemNode { // add it back in if we can filter the results to a more managable size. // new RecentFiles(sleuthkitCase), new DeletedContent(sleuthkitCase, dsObjId), - new FileSize(sleuthkitCase, dsObjId), - new ScoreContent(sleuthkitCase, dsObjId)) + new FileSize(sleuthkitCase, dsObjId)) ), Lookups.singleton(NAME) );