diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/IngestJobInfoPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/IngestJobInfoPanel.java index b1e6d83482..6381acd32d 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/IngestJobInfoPanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/IngestJobInfoPanel.java @@ -31,7 +31,9 @@ import javax.swing.JOptionPane; import javax.swing.event.ListSelectionEvent; import javax.swing.table.AbstractTableModel; import org.openide.util.NbBundle.Messages; +import static org.sleuthkit.autopsy.casemodule.Case.Events.CURRENT_CASE; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.events.AutopsyEvent; import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.datamodel.IngestJobInfo; import org.sleuthkit.datamodel.IngestModuleInfo; @@ -47,6 +49,8 @@ public final class IngestJobInfoPanel extends javax.swing.JPanel { private static final Logger logger = Logger.getLogger(IngestJobInfoPanel.class.getName()); private static final Set INGEST_JOB_EVENTS_OF_INTEREST = EnumSet.of(IngestManager.IngestJobEvent.STARTED, IngestManager.IngestJobEvent.CANCELLED, IngestManager.IngestJobEvent.COMPLETED); + private static final Set CASE_EVENTS_OF_INTEREST = EnumSet.of(Case.Events.CURRENT_CASE); + private List ingestJobs; private final List ingestJobsForSelectedDataSource = new ArrayList<>(); private IngestJobTableModel ingestJobTableModel = new IngestJobTableModel(); @@ -79,6 +83,16 @@ public final class IngestJobInfoPanel extends javax.swing.JPanel { refresh(); } }); + + Case.addEventTypeSubscriber(CASE_EVENTS_OF_INTEREST, (PropertyChangeEvent evt) -> { + if (!(evt instanceof AutopsyEvent) || (((AutopsyEvent) evt).getSourceType() != AutopsyEvent.SourceType.LOCAL)) { + return; + } + + if (CURRENT_CASE == Case.Events.valueOf(evt.getPropertyName())) { + refresh(); + } + }); } /** @@ -110,9 +124,15 @@ public final class IngestJobInfoPanel extends javax.swing.JPanel { */ private void refresh() { try { - SleuthkitCase skCase = Case.getCurrentCaseThrows().getSleuthkitCase(); - this.ingestJobs = skCase.getIngestJobs(); - setDataSource(selectedDataSource); + if (Case.isCaseOpen()) { + SleuthkitCase skCase = Case.getCurrentCaseThrows().getSleuthkitCase(); + this.ingestJobs = skCase.getIngestJobs(); + setDataSource(selectedDataSource); + } else { + this.ingestJobs = new ArrayList<>(); + setDataSource(null); + } + } catch (TskCoreException | NoCurrentCaseException ex) { logger.log(Level.SEVERE, "Failed to load ingest jobs.", ex); JOptionPane.showMessageDialog(this, Bundle.IngestJobInfoPanel_loadIngestJob_error_text(), Bundle.IngestJobInfoPanel_loadIngestJob_error_title(), JOptionPane.ERROR_MESSAGE); diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/Bundle.properties b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/Bundle.properties index 2cb4756460..24a312f733 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/Bundle.properties @@ -36,3 +36,4 @@ DataSourceSummaryDetailsPanel.unallocatedSizeValue.text= DataSourceSummaryCountsPanel.byMimeTypeLabel.text=Files by MIME Type DataSourceSummaryCountsPanel.byCategoryLabel.text=Files by Category DataSourceSummaryCountsPanel.jLabel1.text=Results by Type +DataSourceSummaryUserActivityPanel.programsRunLabel.text=Top Programs Run diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/Bundle.properties-MERGED index fe998b0e57..ba0d3510c7 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/Bundle.properties-MERGED @@ -1,4 +1,6 @@ CTL_DataSourceSummaryAction=Data Source Summary +DataSourceSummaryCountsPanel.ArtifactCountsTableModel.count.header=Count +DataSourceSummaryCountsPanel.ArtifactCountsTableModel.type.header=Result Type DataSourceSummaryCountsPanel.FilesByCategoryTableModel.all.row=All DataSourceSummaryCountsPanel.FilesByCategoryTableModel.allocated.row=Allocated DataSourceSummaryCountsPanel.FilesByCategoryTableModel.count.header=Count @@ -59,9 +61,6 @@ DataSourceSummaryDetailsPanel.unallocatedSizeValue.text= DataSourceSummaryCountsPanel.byMimeTypeLabel.text=Files by MIME Type DataSourceSummaryCountsPanel.byCategoryLabel.text=Files by Category DataSourceSummaryCountsPanel.jLabel1.text=Results by Type -DataSourceSummaryDialog.countsTab.title=Counts -DataSourceSummaryDialog.detailsTab.title=Details -DataSourceSummaryDialog.ingestHistoryTab.title=Ingest History DataSourceSummaryDialog.window.title=Data Sources Summary DataSourceSummaryNode.column.dataSourceName.header=Data Source Name DataSourceSummaryNode.column.files.header=Files @@ -70,4 +69,14 @@ DataSourceSummaryNode.column.status.header=Ingest Status DataSourceSummaryNode.column.tags.header=Tags DataSourceSummaryNode.column.type.header=Type DataSourceSummaryNode.viewDataSourceAction.text=Go to Data Source +DataSourceSummaryTabbedPane_countsTab_title=Counts +DataSourceSummaryTabbedPane_detailsTab_title=Details +DataSourceSummaryTabbedPane_ingestHistoryTab_title=Ingest History +DataSourceSummaryTabbedPane_userActivityTab_title=User Activity +DataSourceSummaryUserActivityPanel.programsRunLabel.text=Top Programs Run +DataSourceSummaryUserActivityPanel_tab_title=User Activity +DataSourceSummaryUserActivityPanel_TopProgramsTableModel_count_header=Run Times +DataSourceSummaryUserActivityPanel_TopProgramsTableModel_folder_header=Folder +DataSourceSummaryUserActivityPanel_TopProgramsTableModel_lastrun_header=Last Run +DataSourceSummaryUserActivityPanel_TopProgramsTableModel_name_header=Program ViewSummaryInformationAction.name.text=View Summary Information diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceInfoUtilities.java b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceInfoUtilities.java index c2d8263056..7e66e56682 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceInfoUtilities.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceInfoUtilities.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2019 Basis Technology Corp. + * Copyright 2019 - 2020 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +18,11 @@ */ package org.sleuthkit.autopsy.casemodule.datasourcesummary; +import java.io.File; +import java.sql.ResultSet; +import java.sql.SQLException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.logging.Level; @@ -26,13 +30,20 @@ import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.datamodel.SleuthkitCase; import org.sleuthkit.datamodel.TskCoreException; import java.util.Collections; +import java.util.Date; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.apache.commons.lang.StringUtils; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE; import org.sleuthkit.datamodel.TskData; import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE; import org.sleuthkit.datamodel.DataSource; /** @@ -43,6 +54,609 @@ final class DataSourceInfoUtilities { private static final Logger logger = Logger.getLogger(DataSourceInfoUtilities.class.getName()); + /** + * Gets a count of files for a particular datasource where it is not a + * virtual directory and has a name. + * + * @param currentDataSource The datasource. + * @param additionalWhere Additional sql where clauses. + * @param onError The message to log on error. + * + * @return The count of files or null on error. + */ + private static Long getCountOfFiles(DataSource currentDataSource, String additionalWhere, String onError) { + if (currentDataSource != null) { + try { + SleuthkitCase skCase = Case.getCurrentCaseThrows().getSleuthkitCase(); + return skCase.countFilesWhere( + "dir_type<>" + TskData.TSK_FS_NAME_TYPE_ENUM.VIRT_DIR.getValue() + + " AND name<>''" + + " AND data_source_obj_id=" + currentDataSource.getId() + + " AND " + additionalWhere); + } catch (TskCoreException | NoCurrentCaseException ex) { + logger.log(Level.WARNING, onError, ex); + //unable to get count of files for the specified types cell will be displayed as empty + } + } + return null; + } + + /** + * Get count of files in a data source. + * + * @param currentDataSource The data source. + * + * @return The count. + */ + static Long getCountOfFiles(DataSource currentDataSource) { + return getCountOfFiles(currentDataSource, + "type<>" + TskData.TSK_DB_FILES_TYPE_ENUM.VIRTUAL_DIR.getFileType(), + "Unable to get count of files, providing empty results"); + } + + /** + * Get count of unallocated files in a data source. + * + * @param currentDataSource The data source. + * + * @return The count. + */ + static Long getCountOfUnallocatedFiles(DataSource currentDataSource) { + return getCountOfFiles(currentDataSource, + "type<>" + TskData.TSK_DB_FILES_TYPE_ENUM.VIRTUAL_DIR.getFileType() + + " AND dir_flags=" + TskData.TSK_FS_NAME_FLAG_ENUM.UNALLOC.getValue(), + "Unable to get counts of unallocated files for datasource, providing empty results"); + } + + /** + * Get count of directories in a data source. + * + * @param currentDataSource The data source. + * + * @return The count. + */ + static Long getCountOfDirectories(DataSource currentDataSource) { + return getCountOfFiles(currentDataSource, + "type<>" + TskData.TSK_DB_FILES_TYPE_ENUM.VIRTUAL_DIR.getFileType() + + " AND meta_type=" + TskData.TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_DIR.getValue(), + "Unable to get count of directories for datasource, providing empty results"); + } + + /** + * Get count of slack files in a data source. + * + * @param currentDataSource The data source. + * + * @return The count. + */ + static Long getCountOfSlackFiles(DataSource currentDataSource) { + return getCountOfFiles(currentDataSource, + "type=" + TskData.TSK_DB_FILES_TYPE_ENUM.SLACK.getFileType(), + "Unable to get count of slack files for datasources, providing empty results"); + } + + /** + * An interface for handling a result set and returning a value. + */ + private interface ResultSetHandler { + + T process(ResultSet resultset) throws SQLException; + } + + /** + * Retrieves a result based on the provided query. + * + * @param query The query. + * @param processor The result set handler. + * @param errorMessage The error message to display if there is an error + * retrieving the resultset. + * + * @return The ResultSetHandler value or null if no ResultSet could be + * obtained. + */ + private static T getBaseQueryResult(String query, ResultSetHandler processor, String errorMessage) { + try (SleuthkitCase.CaseDbQuery dbQuery = Case.getCurrentCaseThrows().getSleuthkitCase().executeQuery(query)) { + ResultSet resultSet = dbQuery.getResultSet(); + try { + return processor.process(resultSet); + } catch (SQLException ex) { + logger.log(Level.WARNING, errorMessage, ex); + } + } catch (TskCoreException | NoCurrentCaseException ex) { + logger.log(Level.WARNING, errorMessage, ex); + } + return null; + } + + /** + * Gets the size of unallocated files in a particular datasource. + * + * @param currentDataSource The data source. + * + * @return The size or null if the query could not be executed. + */ + static Long getSizeOfUnallocatedFiles(DataSource currentDataSource) { + if (currentDataSource == null) { + return null; + } + + final String valueParam = "value"; + final String countParam = "count"; + String query = "SELECT SUM(size) AS " + valueParam + ", COUNT(*) AS " + countParam + + " FROM tsk_files WHERE type<>" + TskData.TSK_DB_FILES_TYPE_ENUM.VIRTUAL_DIR.getFileType() + + " AND dir_type<>" + TskData.TSK_FS_NAME_TYPE_ENUM.VIRT_DIR.getValue() + + " AND dir_flags=" + TskData.TSK_FS_NAME_FLAG_ENUM.UNALLOC.getValue() + + " AND name<>''" + + " AND data_source_obj_id=" + currentDataSource.getId(); + + ResultSetHandler handler = (resultSet) -> { + if (resultSet.next()) { + // ensure that there is an unallocated count result that is attached to this data source + long resultCount = resultSet.getLong(valueParam); + return (resultCount > 0) ? resultSet.getLong(valueParam) : null; + } else { + return null; + } + }; + String errorMessage = "Unable to get size of unallocated files; returning null."; + + return getBaseQueryResult(query, handler, errorMessage); + } + + /** + * Generates a result set handler that will return a map of string to long. + * + * @param keyParam The named parameter in the result set representing the + * key. + * @param valueParam The named parameter in the result set representing the + * value. + * + * @return The result set handler to generate the map of string to long. + */ + private static ResultSetHandler> getStringLongResultSetHandler(String keyParam, String valueParam) { + return (resultSet) -> { + LinkedHashMap toRet = new LinkedHashMap<>(); + while (resultSet.next()) { + try { + toRet.put(resultSet.getString(keyParam), resultSet.getLong(valueParam)); + } catch (SQLException ex) { + logger.log(Level.WARNING, "Failed to get a result pair from the result set.", ex); + } + } + + return toRet; + }; + } + + /** + * Retrieves counts for each artifact type in a data source. + * + * @param selectedDataSource The data source. + * + * @return A mapping of artifact type name to the counts or null if there + * was an error executing the query. + */ + static Map getCountsOfArtifactsByType(DataSource selectedDataSource) { + if (selectedDataSource == null) { + return Collections.emptyMap(); + } + + final String nameParam = "name"; + final String valueParam = "value"; + String query + = "SELECT bbt.display_name AS " + nameParam + ", COUNT(*) AS " + valueParam + + " FROM blackboard_artifacts bba " + + " INNER JOIN blackboard_artifact_types bbt ON bba.artifact_type_id = bbt.artifact_type_id" + + " WHERE bba.data_source_obj_id =" + selectedDataSource.getId() + + " GROUP BY bbt.display_name"; + + String errorMessage = "Unable to get artifact type counts; returning null."; + return getBaseQueryResult(query, getStringLongResultSetHandler(nameParam, valueParam), errorMessage); + } + + /** + * Describes a result of a program run on a datasource. + */ + static class TopProgramsResult { + + private final String programName; + private final String programPath; + private final Long runTimes; + private final Date lastRun; + + /** + * Main constructor. + * + * @param programName The name of the program. + * @param programPath The path of the program. + * @param runTimes The number of runs. + */ + TopProgramsResult(String programName, String programPath, Long runTimes, Date lastRun) { + this.programName = programName; + this.programPath = programPath; + this.runTimes = runTimes; + this.lastRun = lastRun; + } + + /** + * @return The name of the program + */ + String getProgramName() { + return programName; + } + + /** + * @return The path of the program. + */ + String getProgramPath() { + return programPath; + } + + /** + * @return The number of run times or null if not present. + */ + Long getRunTimes() { + return runTimes; + } + + /** + * @return The last time the program was run or null if not present. + */ + public Date getLastRun() { + return lastRun; + } + } + + /** + * A SQL join type. + */ + private enum JoinType { + LEFT, + RIGHT, + INNER, + OUTER + } + + /** + * A blackboard attribute value column. + */ + private enum AttributeColumn { + value_text, + value_int32, + value_int64 + } + + /** + * The suffix joined to a key name for use as an identifier of a query. + */ + private static final String QUERY_SUFFIX = "_query"; + + /** + * Creates a sql statement querying the blackboard attributes table for a + * particular attribute type and returning a specified value. That query + * also joins with the blackboard artifact table. + * + * @param joinType The type of join statement to create. + * @param attributeColumn The blackboard attribute column that should be + * returned. + * @param attrType The attribute type to query for. + * @param keyName The aliased name of the attribute to return. This + * is also used to calculate the alias of the query + * same as getFullKey. + * @param bbaName The blackboard artifact table alias. + * + * @return The generated sql statement. + */ + private static String getAttributeJoin(JoinType joinType, AttributeColumn attributeColumn, ATTRIBUTE_TYPE attrType, String keyName, String bbaName) { + String queryName = keyName + QUERY_SUFFIX; + String innerQueryName = "inner_attribute_" + queryName; + + return "\n" + joinType + " JOIN (\n" + + " SELECT \n" + + " " + innerQueryName + ".artifact_id,\n" + + " " + innerQueryName + "." + attributeColumn + " AS " + keyName + "\n" + + " FROM blackboard_attributes " + innerQueryName + "\n" + + " WHERE " + innerQueryName + ".attribute_type_id = " + attrType.getTypeID() + " -- " + attrType.name() + "\n" + + ") " + queryName + " ON " + queryName + ".artifact_id = " + bbaName + ".artifact_id\n"; + } + + /** + * Given a column key, creates the full name for the column key. + * + * @param key The column key. + * + * @return The full identifier for the column key. + */ + private static String getFullKey(String key) { + return key + QUERY_SUFFIX + "." + key; + } + + /** + * Constructs a SQL 'where' statement from a list of clauses and puts + * parenthesis around each clause. + * + * @param clauses The clauses + * + * @return The generated 'where' statement. + */ + private static String getWhereString(List clauses) { + if (clauses.isEmpty()) { + return ""; + } + + List parenthesized = clauses.stream() + .map(c -> "(" + c + ")") + .collect(Collectors.toList()); + + return "\nWHERE " + String.join("\n AND ", parenthesized) + "\n"; + } + + /** + * Generates a [column] LIKE sql clause. + * + * @param column The column identifier. + * @param likeString The string that will be used as column comparison. + * @param isLike if false, the statement becomes NOT LIKE. + * + * @return The generated statement. + */ + private static String getLikeClause(String column, String likeString, boolean isLike) { + return column + (isLike ? "" : " NOT") + " LIKE '" + likeString + "'"; + } + + /** + * Retrieves a list of the top programs used on the data source. Currently + * determines this based off of which prefetch results return the highest + * count. + * + * @param dataSource The data source. + * @param count The number of programs to return. + * + * @return + */ + static List getTopPrograms(DataSource dataSource, int count) { + if (dataSource == null || count <= 0) { + return Collections.emptyList(); + } + + // ntosboot should be ignored + final String ntosBootIdentifier = "NTOSBOOT"; + // programs in windows directory to be ignored + final String windowsDir = "/WINDOWS%"; + + final String nameParam = "name"; + final String pathParam = "path"; + final String runCountParam = "run_count"; + final String lastRunParam = "last_run"; + + String bbaQuery = "bba"; + + final String query = "SELECT\n" + + " " + getFullKey(nameParam) + " AS " + nameParam + ",\n" + + " " + getFullKey(pathParam) + " AS " + pathParam + ",\n" + + " MAX(" + getFullKey(runCountParam) + ") AS " + runCountParam + ",\n" + + " MAX(" + getFullKey(lastRunParam) + ") AS " + lastRunParam + "\n" + + "FROM blackboard_artifacts " + bbaQuery + "\n" + + getAttributeJoin(JoinType.INNER, AttributeColumn.value_text, ATTRIBUTE_TYPE.TSK_PROG_NAME, nameParam, bbaQuery) + + getAttributeJoin(JoinType.LEFT, AttributeColumn.value_text, ATTRIBUTE_TYPE.TSK_PATH, pathParam, bbaQuery) + + getAttributeJoin(JoinType.LEFT, AttributeColumn.value_int32, ATTRIBUTE_TYPE.TSK_COUNT, runCountParam, bbaQuery) + + getAttributeJoin(JoinType.LEFT, AttributeColumn.value_int64, ATTRIBUTE_TYPE.TSK_DATETIME, lastRunParam, bbaQuery) + + getWhereString(Arrays.asList( + bbaQuery + ".artifact_type_id = " + ARTIFACT_TYPE.TSK_PROG_RUN.getTypeID(), + bbaQuery + ".data_source_obj_id = " + dataSource.getId(), + // exclude ntosBootIdentifier from results + getLikeClause(getFullKey(nameParam), ntosBootIdentifier, false), + // exclude windows directory items from results + getFullKey(pathParam) + " IS NULL OR " + getLikeClause(getFullKey(pathParam), windowsDir, false) + )) + + "GROUP BY " + getFullKey(nameParam) + ", " + getFullKey(pathParam) + "\n" + + "ORDER BY \n" + + " MAX(" + getFullKey(runCountParam) + ") DESC,\n" + + " MAX(" + getFullKey(lastRunParam) + ") DESC,\n" + + " " + getFullKey(nameParam) + " ASC"; + + final String errorMessage = "Unable to get top program results; returning null."; + + ResultSetHandler> handler = (resultSet) -> { + List progResults = new ArrayList<>(); + + boolean quitAtCount = false; + + while (resultSet.next() && (!quitAtCount || progResults.size() < count)) { + try { + long lastRunEpoch = resultSet.getLong(lastRunParam); + Date lastRun = (resultSet.wasNull()) ? null : new Date(lastRunEpoch * 1000); + + Long runCount = resultSet.getLong(runCountParam); + if (resultSet.wasNull()) { + runCount = null; + } + + if (lastRun != null || runCount != null) { + quitAtCount = true; + } + + progResults.add(new TopProgramsResult( + resultSet.getString(nameParam), + resultSet.getString(pathParam), + runCount, + lastRun)); + + } catch (SQLException ex) { + logger.log(Level.WARNING, "Failed to get a top program result from the result set.", ex); + } + } + + return progResults; + }; + + return getBaseQueryResult(query, handler, errorMessage); + } + + /** + * Functions that determine the folder name of a list of path elements. If + * not matched, function returns null. + */ + private static final List, String>> SHORT_FOLDER_MATCHERS = Arrays.asList( + // handle Program Files and Program Files (x86) - if true, return the next folder + (pathList) -> { + if (pathList.size() < 2) { + return null; + } + + String rootParent = pathList.get(0).toUpperCase(); + if ("PROGRAM FILES".equals(rootParent) || "PROGRAM FILES (X86)".equals(rootParent)) { + return pathList.get(1); + } else { + return null; + } + }, + // if there is a folder named "APPLICATION DATA" or "APPDATA" + (pathList) -> { + for (String pathEl : pathList) { + String uppered = pathEl.toUpperCase(); + if ("APPLICATION DATA".equals(uppered) || "APPDATA".equals(uppered)) { + return "AppData"; + } + } + return null; + } + ); + + /** + * Determines a short folder name if any. Otherwise, returns empty string. + * + * @param strPath The string path. + * + * @return The short folder name or empty string if not found. + */ + static String getShortFolderName(String strPath, String applicationName) { + if (strPath == null) { + return ""; + } + + List pathEls = new ArrayList<>(Arrays.asList(applicationName)); + + File file = new File(strPath); + while (file != null && StringUtils.isNotBlank(file.getName())) { + pathEls.add(file.getName()); + file = file.getParentFile(); + } + + Collections.reverse(pathEls); + + for (Function, String> matchEntry : SHORT_FOLDER_MATCHERS) { + String result = matchEntry.apply(pathEls); + if (StringUtils.isNotBlank(result)) { + return result; + } + } + + return ""; + } + + /** + * Generates a string which is a concatenation of the value received from + * the result set. + * + * @param query The query. + * @param valueParam The parameter for the value in the result set. + * @param separator The string separator used in concatenation. + * @param errorMessage The error message if the result set could not + * be received. + * @param singleErrorMessage The error message if a single result could not + * be obtained. + * + * @return The concatenated string or null if the query could not be + * executed. + */ + private static String getConcattedStringsResult(String query, String valueParam, String separator, String errorMessage, String singleErrorMessage) { + ResultSetHandler handler = (resultSet) -> { + String toRet = ""; + boolean first = true; + while (resultSet.next()) { + try { + if (first) { + first = false; + } else { + toRet += separator; + } + toRet += resultSet.getString(valueParam); + } catch (SQLException ex) { + logger.log(Level.WARNING, singleErrorMessage, ex); + } + } + + return toRet; + }; + + return getBaseQueryResult(query, handler, errorMessage); + } + + /** + * Generates a concatenated string value based on the text value of a + * particular attribute in a particular artifact for a specific data source. + * + * @param dataSourceId The data source id. + * @param artifactTypeId The artifact type id. + * @param attributeTypeId The attribute type id. + * + * @return The concatenated value or null if the query could not be + * executed. + */ + private static String getConcattedAttrValue(long dataSourceId, int artifactTypeId, int attributeTypeId) { + final String valueParam = "concatted_attribute_value"; + String query = "SELECT attr.value_text AS " + valueParam + + " FROM blackboard_artifacts bba " + + " INNER JOIN blackboard_attributes attr ON bba.artifact_id = attr.artifact_id " + + " WHERE bba.data_source_obj_id = " + dataSourceId + + " AND bba.artifact_type_id = " + artifactTypeId + + " AND attr.attribute_type_id = " + attributeTypeId; + + String errorMessage = "Unable to execute query to retrieve concatted attribute values."; + String singleErrorMessage = "There was an error retrieving one of the results. That result will be omitted from concatted value."; + String separator = ", "; + return getConcattedStringsResult(query, valueParam, separator, errorMessage, singleErrorMessage); + } + + /** + * Retrieves the concatenation of operating system attributes for a + * particular data source. + * + * @param dataSource The data source. + * + * @return The concatenated value or null if the query could not be + * executed. + */ + static String getOperatingSystems(DataSource dataSource) { + if (dataSource == null) { + return null; + } + + return getConcattedAttrValue(dataSource.getId(), + BlackboardArtifact.ARTIFACT_TYPE.TSK_OS_INFO.getTypeID(), + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME.getTypeID()); + } + + /** + * Retrieves the concatenation of data source usage for a particular data + * source. + * + * @param dataSource The data source. + * + * @return The concatenated value or null if the query could not be + * executed. + */ + static String getDataSourceType(DataSource dataSource) { + if (dataSource == null) { + return null; + } + + return getConcattedAttrValue(dataSource.getId(), + BlackboardArtifact.ARTIFACT_TYPE.TSK_DATA_SOURCE_USAGE.getTypeID(), + BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DESCRIPTION.getTypeID()); + } + /** * Get a map containing the TSK_DATA_SOURCE_USAGE description attributes * associated with each data source in the current case. @@ -147,39 +761,6 @@ final class DataSourceInfoUtilities { } } - /** - * Get a map containing the names of operating systems joined in a comma - * seperated list to the Data Source they exist on in the current case. No - * item will exist in the map for data sources which do not contain - * TS_OS_INFO artifacts which have a program name. - * - * @return Collection which maps datasource id to a String which is a comma - * seperated list of Operating system names found on the data - * source. - */ - static Map getOperatingSystems() { - Map osDetailMap = new HashMap<>(); - try { - SleuthkitCase skCase = Case.getCurrentCaseThrows().getSleuthkitCase(); - ArrayList osInfoArtifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_OS_INFO); - for (BlackboardArtifact osInfo : osInfoArtifacts) { - BlackboardAttribute programName = osInfo.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME)); - if (programName != null) { - String currentOsString = osDetailMap.get(osInfo.getDataSource().getId()); - if (currentOsString == null || currentOsString.isEmpty()) { - currentOsString = programName.getValueString(); - } else { - currentOsString = currentOsString + ", " + programName.getValueString(); - } - osDetailMap.put(osInfo.getDataSource().getId(), currentOsString); - } - } - } catch (TskCoreException | NoCurrentCaseException ex) { - logger.log(Level.SEVERE, "Failed to load OS info artifacts.", ex); - } - return osDetailMap; - } - /** * Get the number of files in the case database for the current data source * which have the specified mimetypes. @@ -212,134 +793,6 @@ final class DataSourceInfoUtilities { return null; } - /** - * Get a map containing the number of unallocated files in each data source - * in the current case. - * - * @return Collection which maps datasource id to a count for the number of - * unallocated files in the datasource, will only contain entries - * for datasources which have at least 1 unallocated file - */ - static Map getCountsOfUnallocatedFiles() { - try { - final String countUnallocatedFilesQuery = "data_source_obj_id, COUNT(*) AS value" - + " FROM tsk_files WHERE type<>" + TskData.TSK_DB_FILES_TYPE_ENUM.VIRTUAL_DIR.getFileType() - + " AND dir_type<>" + TskData.TSK_FS_NAME_TYPE_ENUM.VIRT_DIR.getValue() - + " AND dir_flags=" + TskData.TSK_FS_NAME_FLAG_ENUM.UNALLOC.getValue() - + " AND name<>'' GROUP BY data_source_obj_id"; //NON-NLS - return getValuesMap(countUnallocatedFilesQuery); - } catch (TskCoreException | NoCurrentCaseException ex) { - logger.log(Level.WARNING, "Unable to get counts of unallocated files for all datasources, providing empty results", ex); - return Collections.emptyMap(); - } - } - - /** - * Get a map containing the total size of unallocated files in each data - * source in the current case. - * - * @return Collection which maps datasource id to a total size in bytes of - * unallocated files in the datasource, will only contain entries - * for datasources which have at least 1 unallocated file - */ - static Map getSizeOfUnallocatedFiles() { - try { - final String countUnallocatedFilesQuery = "data_source_obj_id, sum(size) AS value" - + " FROM tsk_files WHERE type<>" + TskData.TSK_DB_FILES_TYPE_ENUM.VIRTUAL_DIR.getFileType() - + " AND dir_type<>" + TskData.TSK_FS_NAME_TYPE_ENUM.VIRT_DIR.getValue() - + " AND dir_flags=" + TskData.TSK_FS_NAME_FLAG_ENUM.UNALLOC.getValue() - + " AND name<>'' GROUP BY data_source_obj_id"; //NON-NLS - return getValuesMap(countUnallocatedFilesQuery); - } catch (TskCoreException | NoCurrentCaseException ex) { - logger.log(Level.WARNING, "Unable to get size of unallocated files for all datasources, providing empty results", ex); - return Collections.emptyMap(); - } - } - - /** - * Get a map containing the number of directories in each data source in the - * current case. - * - * @return Collection which maps datasource id to a count for the number of - * directories in the datasource, will only contain entries for - * datasources which have at least 1 directory - */ - static Map getCountsOfDirectories() { - try { - final String countDirectoriesQuery = "data_source_obj_id, COUNT(*) AS value" - + " FROM tsk_files WHERE type<>" + TskData.TSK_DB_FILES_TYPE_ENUM.VIRTUAL_DIR.getFileType() - + " AND dir_type<>" + TskData.TSK_FS_NAME_TYPE_ENUM.VIRT_DIR.getValue() - + " AND meta_type=" + TskData.TSK_FS_META_TYPE_ENUM.TSK_FS_META_TYPE_DIR.getValue() - + " AND name<>'' GROUP BY data_source_obj_id"; //NON-NLS - return getValuesMap(countDirectoriesQuery); - } catch (TskCoreException | NoCurrentCaseException ex) { - logger.log(Level.WARNING, "Unable to get counts of directories for all datasources, providing empty results", ex); - return Collections.emptyMap(); - } - } - - /** - * Get a map containing the number of slack files in each data source in the - * current case. - * - * @return Collection which maps datasource id to a count for the number of - * slack files in the datasource, will only contain entries for - * datasources which have at least 1 slack file - */ - static Map getCountsOfSlackFiles() { - try { - final String countSlackFilesQuery = "data_source_obj_id, COUNT(*) AS value" - + " FROM tsk_files WHERE type=" + TskData.TSK_DB_FILES_TYPE_ENUM.SLACK.getFileType() - + " AND dir_type<>" + TskData.TSK_FS_NAME_TYPE_ENUM.VIRT_DIR.getValue() - + " AND name<>'' GROUP BY data_source_obj_id"; //NON-NLS - return getValuesMap(countSlackFilesQuery); - } catch (TskCoreException | NoCurrentCaseException ex) { - logger.log(Level.WARNING, "Unable to get counts of slack files for all datasources, providing empty results", ex); - return Collections.emptyMap(); - } - } - - /** - * Get a map containing maps which map artifact type to the number of times - * it exosts in each data source in the current case. - * - * @return Collection which maps datasource id to maps of artifact display - * name to number of occurences in the datasource, will only contain - * entries for artifacts which have at least one occurence in the - * data source. - */ - static Map> getCountsOfArtifactsByType() { - try { - final String countArtifactsQuery = "blackboard_artifacts.data_source_obj_id, blackboard_artifact_types.display_name AS label, COUNT(*) AS value" - + " FROM blackboard_artifacts, blackboard_artifact_types" - + " WHERE blackboard_artifacts.artifact_type_id = blackboard_artifact_types.artifact_type_id" - + " GROUP BY blackboard_artifacts.data_source_obj_id, blackboard_artifact_types.display_name"; - return getLabeledValuesMap(countArtifactsQuery); - } catch (TskCoreException | NoCurrentCaseException ex) { - logger.log(Level.WARNING, "Unable to get counts of all artifact types for all datasources, providing empty results", ex); - return Collections.emptyMap(); - } - } - - /** - * Helper method to execute a select query with a - * DataSourceLabeledValueCallback. - * - * @param query the portion of the query which should follow the SELECT - * - * @return a map of datasource object IDs to maps of String labels to the - * values associated with them. - * - * @throws TskCoreException - * @throws NoCurrentCaseException - */ - private static Map> getLabeledValuesMap(String query) throws TskCoreException, NoCurrentCaseException { - SleuthkitCase skCase = Case.getCurrentCaseThrows().getSleuthkitCase(); - DataSourceLabeledValueCallback callback = new DataSourceLabeledValueCallback(); - skCase.getCaseDbAccessManager().select(query, callback); - return callback.getMapOfLabeledValues(); - } - /** * Helper method to execute a select query with a * DataSourceSingleValueCallback. diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryCountsPanel.form b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryCountsPanel.form index d3a17e0205..9a82746da6 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryCountsPanel.form +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryCountsPanel.form @@ -70,18 +70,13 @@ - - - - - - + @@ -93,25 +88,20 @@ - - - - - - + - + @@ -123,15 +113,6 @@ - - - - - - -
-
-
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryCountsPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryCountsPanel.java index 1d99f1c445..6bb6603c11 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryCountsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryCountsPanel.java @@ -18,15 +18,12 @@ */ package org.sleuthkit.autopsy.casemodule.datasourcesummary; -import java.util.ArrayList; -import java.util.List; import java.util.Map; import org.sleuthkit.autopsy.coreutils.Logger; import javax.swing.JLabel; -import javax.swing.table.AbstractTableModel; import javax.swing.table.DefaultTableCellRenderer; -import javax.swing.table.DefaultTableModel; import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.coreutils.FileTypeUtils; import org.sleuthkit.datamodel.DataSource; @@ -34,69 +31,224 @@ import org.sleuthkit.datamodel.DataSource; * Panel for displaying summary information on the known files present in the * specified DataSource */ +@Messages({"DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.type.header=File Type", + "DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.count.header=Count", + "DataSourceSummaryCountsPanel.ArtifactCountsTableModel.type.header=Result Type", + "DataSourceSummaryCountsPanel.ArtifactCountsTableModel.count.header=Count", + "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.type.header=File Type", + "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.count.header=Count" +}) class DataSourceSummaryCountsPanel extends javax.swing.JPanel { + // Result returned for a data model if no data found. + private static final Object[][] EMPTY_PAIRS = new Object[][]{}; + + // column headers for mime type table + private static final Object[] MIME_TYPE_COLUMN_HEADERS = new Object[]{ + Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_type_header(), + Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_count_header() + }; + + // column headers for file by category table + private static final Object[] FILE_BY_CATEGORY_COLUMN_HEADERS = new Object[]{ + Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_type_header(), + Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_count_header() + }; + + // column headers for artifact counts table + private static final Object[] ARTIFACT_COUNTS_COLUMN_HEADERS = new Object[]{ + Bundle.DataSourceSummaryCountsPanel_ArtifactCountsTableModel_type_header(), + Bundle.DataSourceSummaryCountsPanel_ArtifactCountsTableModel_count_header() + }; + private static final long serialVersionUID = 1L; - private FilesByMimeTypeTableModel filesByMimeTypeTableModel = new FilesByMimeTypeTableModel(null); - private FilesByCategoryTableModel filesByCategoryTableModel = new FilesByCategoryTableModel(null); private static final Logger logger = Logger.getLogger(DataSourceSummaryCountsPanel.class.getName()); - private final Map allFilesCountsMap; - private final Map slackFilesCountsMap; - private final Map directoriesCountsMap; - private final Map unallocatedFilesCountsMap; - private final Map> artifactsByTypeCountsMap; private final DefaultTableCellRenderer rightAlignedRenderer = new DefaultTableCellRenderer(); + private DataSource dataSource; + /** * Creates new form DataSourceSummaryCountsPanel */ - DataSourceSummaryCountsPanel(Map fileCountsMap) { - this.allFilesCountsMap = fileCountsMap; - this.slackFilesCountsMap = DataSourceInfoUtilities.getCountsOfSlackFiles(); - this.directoriesCountsMap = DataSourceInfoUtilities.getCountsOfDirectories(); - this.unallocatedFilesCountsMap = DataSourceInfoUtilities.getCountsOfUnallocatedFiles(); - this.artifactsByTypeCountsMap = DataSourceInfoUtilities.getCountsOfArtifactsByType(); + DataSourceSummaryCountsPanel() { rightAlignedRenderer.setHorizontalAlignment(JLabel.RIGHT); initComponents(); fileCountsByMimeTypeTable.getTableHeader().setReorderingAllowed(false); fileCountsByCategoryTable.getTableHeader().setReorderingAllowed(false); + artifactCountsTable.getTableHeader().setReorderingAllowed(false); + setDataSource(null); } /** - * Specify the DataSource to display file information for + * The datasource currently used as the model in this panel. * - * @param selectedDataSource the DataSource to display file information for + * @return The datasource currently being used as the model in this panel. */ - void updateCountsTableData(DataSource selectedDataSource) { - filesByMimeTypeTableModel = new FilesByMimeTypeTableModel(selectedDataSource); - fileCountsByMimeTypeTable.setModel(filesByMimeTypeTableModel); + public DataSource getDataSource() { + return dataSource; + } + + /** + * Sets datasource to visualize in the panel. + * + * @param dataSource The datasource to use in this panel. + */ + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + if (dataSource == null || !Case.isCaseOpen()) { + updateCountsTableData(EMPTY_PAIRS, + EMPTY_PAIRS, + EMPTY_PAIRS); + } else { + updateCountsTableData(getMimeTypeModel(dataSource), + getFileCategoryModel(dataSource), + getArtifactCountsModel(dataSource)); + } + + } + + /** + * Specify the DataSource to display file information for. + * + * @param mimeTypeDataModel The mime type data model. + * @param fileCategoryDataModel The file category data model. + * @param artifactDataModel The artifact type data model. + */ + private void updateCountsTableData(Object[][] mimeTypeDataModel, Object[][] fileCategoryDataModel, Object[][] artifactDataModel) { + fileCountsByMimeTypeTable.setModel(new NonEditableTableModel(mimeTypeDataModel, MIME_TYPE_COLUMN_HEADERS)); fileCountsByMimeTypeTable.getColumnModel().getColumn(1).setCellRenderer(rightAlignedRenderer); fileCountsByMimeTypeTable.getColumnModel().getColumn(0).setPreferredWidth(130); - filesByCategoryTableModel = new FilesByCategoryTableModel(selectedDataSource); - fileCountsByCategoryTable.setModel(filesByCategoryTableModel); + + fileCountsByCategoryTable.setModel(new NonEditableTableModel(fileCategoryDataModel, FILE_BY_CATEGORY_COLUMN_HEADERS)); fileCountsByCategoryTable.getColumnModel().getColumn(1).setCellRenderer(rightAlignedRenderer); fileCountsByCategoryTable.getColumnModel().getColumn(0).setPreferredWidth(130); - updateArtifactCounts(selectedDataSource); + + artifactCountsTable.setModel(new NonEditableTableModel(artifactDataModel, ARTIFACT_COUNTS_COLUMN_HEADERS)); + artifactCountsTable.getColumnModel().getColumn(0).setPreferredWidth(230); + artifactCountsTable.getColumnModel().getColumn(1).setCellRenderer(rightAlignedRenderer); + this.repaint(); } /** - * Helper method to update the artifact specific counts by clearing the - * table and adding counts for the artifacts which exist in the selected - * data source. + * Determines the JTable data model for datasource mime types. * - * @param selectedDataSource the data source to display artifact counts for + * @param dataSource The DataSource. + * + * @return The model to be used with a JTable. */ - private void updateArtifactCounts(DataSource selectedDataSource) { - ((DefaultTableModel) artifactCountsTable.getModel()).setRowCount(0); - if (selectedDataSource != null && artifactsByTypeCountsMap.get(selectedDataSource.getId()) != null) { - Map artifactCounts = artifactsByTypeCountsMap.get(selectedDataSource.getId()); - for (String key : artifactCounts.keySet()) { - ((DefaultTableModel) artifactCountsTable.getModel()).addRow(new Object[]{key, artifactCounts.get(key)}); - } + @Messages({ + "DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.images.row=Images", + "DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.videos.row=Videos", + "DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.audio.row=Audio", + "DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.documents.row=Documents", + "DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.executables.row=Executables" + }) + private static Object[][] getMimeTypeModel(DataSource dataSource) { + return new Object[][]{ + new Object[]{Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_images_row(), + getCount(dataSource, FileTypeUtils.FileTypeCategory.IMAGE)}, + new Object[]{Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_videos_row(), + getCount(dataSource, FileTypeUtils.FileTypeCategory.VIDEO)}, + new Object[]{Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_audio_row(), + getCount(dataSource, FileTypeUtils.FileTypeCategory.AUDIO)}, + new Object[]{Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_documents_row(), + getCount(dataSource, FileTypeUtils.FileTypeCategory.DOCUMENTS)}, + new Object[]{Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_executables_row(), + getCount(dataSource, FileTypeUtils.FileTypeCategory.EXECUTABLE)} + }; + } + + /** + * Retrieves the counts of files of a particular mime type for a particular + * DataSource. + * + * @param dataSource The DataSource. + * @param category The mime type category. + * + * @return The count. + */ + private static Long getCount(DataSource dataSource, FileTypeUtils.FileTypeCategory category) { + return DataSourceInfoUtilities.getCountOfFilesForMimeTypes(dataSource, category.getMediaTypes()); + } + + /** + * Determines the JTable data model for datasource file categories. + * + * @param dataSource The DataSource. + * + * @return The model to be used with a JTable. + */ + @Messages({ + "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.all.row=All", + "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.allocated.row=Allocated", + "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.unallocated.row=Unallocated", + "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.slack.row=Slack", + "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.directory.row=Directory" + }) + private static Object[][] getFileCategoryModel(DataSource selectedDataSource) { + Long fileCount = zeroIfNull(DataSourceInfoUtilities.getCountOfFiles(selectedDataSource)); + Long unallocatedFiles = zeroIfNull(DataSourceInfoUtilities.getCountOfUnallocatedFiles(selectedDataSource)); + Long allocatedFiles = zeroIfNull(getAllocatedCount(fileCount, unallocatedFiles)); + Long slackFiles = zeroIfNull(DataSourceInfoUtilities.getCountOfSlackFiles(selectedDataSource)); + Long directories = zeroIfNull(DataSourceInfoUtilities.getCountOfDirectories(selectedDataSource)); + + return new Object[][]{ + new Object[]{Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_all_row(), fileCount}, + new Object[]{Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_allocated_row(), allocatedFiles}, + new Object[]{Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_unallocated_row(), unallocatedFiles}, + new Object[]{Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_slack_row(), slackFiles}, + new Object[]{Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_directory_row(), directories} + }; + } + + /** + * Returns 0 if value is null. + * + * @param origValue The original value. + * + * @return The value or 0 if null. + */ + private static Long zeroIfNull(Long origValue) { + return origValue == null ? 0 : origValue; + } + + /** + * Safely gets the allocated files count. + * + * @param allFilesCount The count of all files. + * @param unallocatedFilesCount The count of unallocated files. + * + * @return The count of allocated files. + */ + private static long getAllocatedCount(Long allFilesCount, Long unallocatedFilesCount) { + if (allFilesCount == null) { + return 0; + } else if (unallocatedFilesCount == null) { + return allFilesCount; + } else { + return allFilesCount - unallocatedFilesCount; } - artifactCountsTable.getColumnModel().getColumn(0).setPreferredWidth(230); - artifactCountsTable.getColumnModel().getColumn(1).setCellRenderer(rightAlignedRenderer); + } + + /** + * The counts of different artifact types found in a DataSource. + * + * @param selectedDataSource The DataSource. + * + * @return The JTable data model of counts of artifact types. + */ + private static Object[][] getArtifactCountsModel(DataSource selectedDataSource) { + Map artifactMapping = DataSourceInfoUtilities.getCountsOfArtifactsByType(selectedDataSource); + if (artifactMapping == null) { + return EMPTY_PAIRS; + } + + return artifactMapping.entrySet().stream() + .filter((entrySet) -> entrySet != null && entrySet.getKey() != null) + .sorted((a, b) -> a.getKey().compareTo(b.getKey())) + .map((entrySet) -> new Object[]{entrySet.getKey(), entrySet.getValue()}) + .toArray(Object[][]::new); } /** @@ -118,35 +270,16 @@ class DataSourceSummaryCountsPanel extends javax.swing.JPanel { artifactCountsScrollPane = new javax.swing.JScrollPane(); artifactCountsTable = new javax.swing.JTable(); - fileCountsByMimeTypeTable.setModel(filesByMimeTypeTableModel); fileCountsByMimeTypeScrollPane.setViewportView(fileCountsByMimeTypeTable); org.openide.awt.Mnemonics.setLocalizedText(byMimeTypeLabel, org.openide.util.NbBundle.getMessage(DataSourceSummaryCountsPanel.class, "DataSourceSummaryCountsPanel.byMimeTypeLabel.text")); // NOI18N - fileCountsByCategoryTable.setModel(filesByCategoryTableModel); fileCountsByCategoryScrollPane.setViewportView(fileCountsByCategoryTable); org.openide.awt.Mnemonics.setLocalizedText(byCategoryLabel, org.openide.util.NbBundle.getMessage(DataSourceSummaryCountsPanel.class, "DataSourceSummaryCountsPanel.byCategoryLabel.text")); // NOI18N org.openide.awt.Mnemonics.setLocalizedText(jLabel1, org.openide.util.NbBundle.getMessage(DataSourceSummaryCountsPanel.class, "DataSourceSummaryCountsPanel.jLabel1.text")); // NOI18N - artifactCountsTable.setAutoCreateRowSorter(true); - artifactCountsTable.setModel(new javax.swing.table.DefaultTableModel( - new Object [][] { - - }, - new String [] { - "Result Type", "Count" - } - ) { - boolean[] canEdit = new boolean [] { - false, false - }; - - public boolean isCellEditable(int rowIndex, int columnIndex) { - return canEdit [columnIndex]; - } - }); artifactCountsScrollPane.setViewportView(artifactCountsTable); javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); @@ -206,190 +339,4 @@ class DataSourceSummaryCountsPanel extends javax.swing.JPanel { private javax.swing.JTable fileCountsByMimeTypeTable; private javax.swing.JLabel jLabel1; // End of variables declaration//GEN-END:variables - - /** - * Table model for the files table model to display counts of specific file - * types by mime type found in the currently selected data source. - */ - @Messages({"DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.type.header=File Type", - "DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.count.header=Count"}) - private class FilesByMimeTypeTableModel extends AbstractTableModel { - - private static final long serialVersionUID = 1L; - private final DataSource currentDataSource; - private final List columnHeaders = new ArrayList<>(); - private static final int IMAGES_ROW_INDEX = 0; - private static final int VIDEOS_ROW_INDEX = 1; - private static final int AUDIO_ROW_INDEX = 2; - private static final int DOCUMENTS_ROW_INDEX = 3; - private static final int EXECUTABLES_ROW_INDEX = 4; - - /** - * Create a FilesByMimeTypeTableModel for the speicified datasource. - * - * @param selectedDataSource the datasource which this - * FilesByMimeTypeTablemodel will represent - */ - FilesByMimeTypeTableModel(DataSource selectedDataSource) { - columnHeaders.add(Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_type_header()); - columnHeaders.add(Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_count_header()); - currentDataSource = selectedDataSource; - } - - @Override - public int getRowCount() { - //should be kept equal to the number of types we are displaying in the tables - return 5; - } - - @Override - public int getColumnCount() { - return columnHeaders.size(); - } - - @Messages({ - "DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.images.row=Images", - "DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.videos.row=Videos", - "DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.audio.row=Audio", - "DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.documents.row=Documents", - "DataSourceSummaryCountsPanel.FilesByMimeTypeTableModel.executables.row=Executables" - }) - @Override - public Object getValueAt(int rowIndex, int columnIndex) { - if (columnIndex == 0) { - switch (rowIndex) { - case IMAGES_ROW_INDEX: - return Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_images_row(); - case VIDEOS_ROW_INDEX: - return Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_videos_row(); - case AUDIO_ROW_INDEX: - return Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_audio_row(); - case DOCUMENTS_ROW_INDEX: - return Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_documents_row(); - case EXECUTABLES_ROW_INDEX: - return Bundle.DataSourceSummaryCountsPanel_FilesByMimeTypeTableModel_executables_row(); - default: - break; - } - } else if (columnIndex == 1) { - switch (rowIndex) { - case 0: - return DataSourceInfoUtilities.getCountOfFilesForMimeTypes(currentDataSource, FileTypeUtils.FileTypeCategory.IMAGE.getMediaTypes()); - case 1: - return DataSourceInfoUtilities.getCountOfFilesForMimeTypes(currentDataSource, FileTypeUtils.FileTypeCategory.VIDEO.getMediaTypes()); - case 2: - return DataSourceInfoUtilities.getCountOfFilesForMimeTypes(currentDataSource, FileTypeUtils.FileTypeCategory.AUDIO.getMediaTypes()); - case 3: - return DataSourceInfoUtilities.getCountOfFilesForMimeTypes(currentDataSource, FileTypeUtils.FileTypeCategory.DOCUMENTS.getMediaTypes()); - case 4: - return DataSourceInfoUtilities.getCountOfFilesForMimeTypes(currentDataSource, FileTypeUtils.FileTypeCategory.EXECUTABLE.getMediaTypes()); - default: - break; - } - } - return null; - } - - @Override - public String getColumnName(int column) { - return columnHeaders.get(column); - } - } - - /** - * Table model for the files table model to display counts of specific file - * types by category found in the currently selected data source. - */ - @Messages({"DataSourceSummaryCountsPanel.FilesByCategoryTableModel.type.header=File Type", - "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.count.header=Count"}) - private class FilesByCategoryTableModel extends AbstractTableModel { - - private static final long serialVersionUID = 1L; - private final DataSource currentDataSource; - private final List columnHeaders = new ArrayList<>(); - private static final int ALL_FILES_ROW_INDEX = 0; - private static final int ALLOCATED_FILES_ROW_INDEX = 1; - private static final int UNALLOCATED_FILES_ROW_INDEX = 2; - private static final int SLACK_FILES_ROW_INDEX = 3; - private static final int DIRECTORIES_ROW_INDEX = 4; - /** - * Create a FilesByCategoryTableModel for the speicified datasource. - * - * @param selectedDataSource the datasource which this - * FilesByCategoryTablemodel will represent - */ - FilesByCategoryTableModel(DataSource selectedDataSource) { - columnHeaders.add(Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_type_header()); - columnHeaders.add(Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_count_header()); - currentDataSource = selectedDataSource; - } - - @Override - public int getRowCount() { - //should be kept equal to the number of types we are displaying in the tables - return 5; - } - - @Override - public int getColumnCount() { - return columnHeaders.size(); - } - - @Messages({ - "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.all.row=All", - "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.allocated.row=Allocated", - "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.unallocated.row=Unallocated", - "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.slack.row=Slack", - "DataSourceSummaryCountsPanel.FilesByCategoryTableModel.directory.row=Directory" - }) - @Override - public Object getValueAt(int rowIndex, int columnIndex) { - if (columnIndex == 0) { - switch (rowIndex) { - case ALL_FILES_ROW_INDEX: - return Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_all_row(); - case ALLOCATED_FILES_ROW_INDEX: - return Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_allocated_row(); - case UNALLOCATED_FILES_ROW_INDEX: - return Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_unallocated_row(); - case SLACK_FILES_ROW_INDEX: - return Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_slack_row(); - case DIRECTORIES_ROW_INDEX: - return Bundle.DataSourceSummaryCountsPanel_FilesByCategoryTableModel_directory_row(); - default: - break; - } - } else if (columnIndex == 1 && currentDataSource != null) { - switch (rowIndex) { - case 0: - return allFilesCountsMap.get(currentDataSource.getId()) == null ? 0 : allFilesCountsMap.get(currentDataSource.getId()); - case 1: - //All files should be either allocated or unallocated as dir_flags only has two values so any file that isn't unallocated is allocated - Long unallocatedFilesCount = unallocatedFilesCountsMap.get(currentDataSource.getId()); - Long allFilesCount = allFilesCountsMap.get(currentDataSource.getId()); - if (allFilesCount == null) { - return 0; - } else if (unallocatedFilesCount == null) { - return allFilesCount; - } else { - return allFilesCount - unallocatedFilesCount; - } - case 2: - return unallocatedFilesCountsMap.get(currentDataSource.getId()) == null ? 0 : unallocatedFilesCountsMap.get(currentDataSource.getId()); - case 3: - return slackFilesCountsMap.get(currentDataSource.getId()) == null ? 0 : slackFilesCountsMap.get(currentDataSource.getId()); - case 4: - return directoriesCountsMap.get(currentDataSource.getId()) == null ? 0 : directoriesCountsMap.get(currentDataSource.getId()); - default: - break; - } - } - return null; - } - - @Override - public String getColumnName(int column) { - return columnHeaders.get(column); - } - } } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDetailsPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDetailsPanel.java index 1b898f4995..21caa2c4b8 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDetailsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDetailsPanel.java @@ -19,12 +19,12 @@ package org.sleuthkit.autopsy.casemodule.datasourcesummary; import java.text.DecimalFormat; -import java.util.Map; -import java.util.HashMap; import java.util.logging.Level; import org.sleuthkit.autopsy.coreutils.Logger; import javax.swing.table.DefaultTableModel; +import org.apache.commons.lang3.StringUtils; import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.datamodel.DataSource; import org.sleuthkit.datamodel.Image; import org.sleuthkit.datamodel.TskCoreException; @@ -36,23 +36,47 @@ class DataSourceSummaryDetailsPanel extends javax.swing.JPanel { //Because this panel was made using the gridbaglayout and netbean's Customize Layout tool it will be best to continue to modify it through that private static final long serialVersionUID = 1L; - private Map osDetailMap = new HashMap<>(); private static final Integer SIZE_COVERSION_CONSTANT = 1000; private static final DecimalFormat APPROXIMATE_SIZE_FORMAT = new DecimalFormat("#.##"); - private final Map unallocatedFilesSizeMap; - private final Map usageMap; private static final Logger logger = Logger.getLogger(DataSourceSummaryDetailsPanel.class.getName()); + private DataSource dataSource; + /** * Creates new form DataSourceSummaryDetailsPanel */ @Messages({"DataSourceSummaryDetailsPanel.getDataSources.error.text=Failed to get the list of datasources for the current case.", "DataSourceSummaryDetailsPanel.getDataSources.error.title=Load Failure"}) - DataSourceSummaryDetailsPanel(Map usageMap) { + DataSourceSummaryDetailsPanel() { initComponents(); - this.usageMap = usageMap; - this.unallocatedFilesSizeMap = DataSourceInfoUtilities.getSizeOfUnallocatedFiles(); - osDetailMap = DataSourceInfoUtilities.getOperatingSystems(); + setDataSource(null); + } + + /** + * The datasource currently used as the model in this panel. + * + * @return The datasource currently being used as the model in this panel. + */ + public DataSource getDataSource() { + return dataSource; + } + + /** + * Sets datasource to visualize in the panel. + * + * @param dataSource The datasource to use in this panel. + */ + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + + if (dataSource == null || !Case.isCaseOpen()) { + updateDetailsPanelData(null, null, null, null); + } else { + updateDetailsPanelData(dataSource, + DataSourceInfoUtilities.getSizeOfUnallocatedFiles(dataSource), + DataSourceInfoUtilities.getOperatingSystems(dataSource), + DataSourceInfoUtilities.getDataSourceType(dataSource)); + } } /** @@ -60,77 +84,80 @@ class DataSourceSummaryDetailsPanel extends javax.swing.JPanel { * * @param selectedDataSource the DataSource to display details about. */ - void updateDetailsPanelData(DataSource selectedDataSource) { + private void updateDetailsPanelData(DataSource selectedDataSource, Long unallocatedFilesSize, String osDetails, String usage) { clearTableValues(); if (selectedDataSource != null) { - String sizeString = ""; - String sectorSizeString = ""; - String md5String = ""; - String sha1String = ""; - String sha256String = ""; - String acquisitionDetailsString = ""; - String imageTypeString = ""; - String[] filePaths = new String[0]; - String osDetailString = osDetailMap.get(selectedDataSource.getId()) == null ? "" : osDetailMap.get(selectedDataSource.getId()); - String dataSourceTypeString = usageMap.get(selectedDataSource.getId()) == null ? "" : usageMap.get(selectedDataSource.getId()); - try { - acquisitionDetailsString = selectedDataSource.getAcquisitionDetails(); - } catch (TskCoreException ex) { - logger.log(Level.WARNING, "Unable to get aquisition details for selected data source", ex); - } - if (selectedDataSource instanceof Image) { - imageTypeString = ((Image) selectedDataSource).getType().getName(); - filePaths = ((Image) selectedDataSource).getPaths(); - sizeString = getSizeString(selectedDataSource.getSize()); - sectorSizeString = getSizeString(((Image) selectedDataSource).getSsize()); - try { - //older databases may have null as the hash values - md5String = ((Image) selectedDataSource).getMd5(); - if (md5String == null) { - md5String = ""; - } - } catch (TskCoreException ex) { - logger.log(Level.WARNING, "Unable to get MD5 for selected data source", ex); - } - try { - sha1String = ((Image) selectedDataSource).getSha1(); - if (sha1String == null) { - sha1String = ""; - } - } catch (TskCoreException ex) { - logger.log(Level.WARNING, "Unable to get SHA1 for selected data source", ex); - } - try { - sha256String = ((Image) selectedDataSource).getSha256(); - if (sha256String == null) { - sha256String = ""; - } - } catch (TskCoreException ex) { - logger.log(Level.WARNING, "Unable to get SHA256 for selected data source", ex); - } - } + unallocatedSizeValue.setText(getSizeString(unallocatedFilesSize)); + operatingSystemValue.setText(StringUtils.isBlank(osDetails) ? "" : osDetails); + dataSourceUsageValue.setText(StringUtils.isBlank(usage) ? "" : usage); + + timeZoneValue.setText(selectedDataSource.getTimeZone()); displayNameValue.setText(selectedDataSource.getName()); originalNameValue.setText(selectedDataSource.getName()); deviceIdValue.setText(selectedDataSource.getDeviceId()); - dataSourceUsageValue.setText(dataSourceTypeString); - operatingSystemValue.setText(osDetailString); - timeZoneValue.setText(selectedDataSource.getTimeZone()); - acquisitionDetailsTextArea.setText(acquisitionDetailsString); - imageTypeValue.setText(imageTypeString); - sizeValue.setText(sizeString); - unallocatedSizeValue.setText(getSizeString(unallocatedFilesSizeMap.get(selectedDataSource.getId()))); - sectorSizeValue.setText(sectorSizeString); - md5HashValue.setText(md5String); - sha1HashValue.setText(sha1String); - sha256HashValue.setText(sha256String); - for (String path : filePaths) { - ((DefaultTableModel) filePathsTable.getModel()).addRow(new Object[]{path}); + + try { + acquisitionDetailsTextArea.setText(selectedDataSource.getAcquisitionDetails()); + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "Unable to get aquisition details for selected data source", ex); + } + + if (selectedDataSource instanceof Image) { + setFieldsForImage((Image) selectedDataSource); } } updateFieldVisibility(); this.repaint(); } + /** + * Sets text fields for an image. This should be called after + * clearTableValues and before updateFieldVisibility to ensure the proper + * rendering. + * + * @param selectedImage The selected image. + */ + private void setFieldsForImage(Image selectedImage) { + imageTypeValue.setText(selectedImage.getType().getName()); + sizeValue.setText(getSizeString(selectedImage.getSize())); + sectorSizeValue.setText(getSizeString(selectedImage.getSsize())); + + for (String path : selectedImage.getPaths()) { + ((DefaultTableModel) filePathsTable.getModel()).addRow(new Object[]{path}); + } + + try { + //older databases may have null as the hash values + String md5String = selectedImage.getMd5(); + if (md5String == null) { + md5String = ""; + } + md5HashValue.setText(md5String); + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "Unable to get MD5 for selected data source", ex); + } + + try { + String sha1String = selectedImage.getSha1(); + if (sha1String == null) { + sha1String = ""; + } + sha1HashValue.setText(sha1String); + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "Unable to get SHA1 for selected data source", ex); + } + + try { + String sha256String = selectedImage.getSha256(); + if (sha256String == null) { + sha256String = ""; + } + sha256HashValue.setText(sha256String); + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "Unable to get SHA256 for selected data source", ex); + } + } + /** * Get a long size in bytes as a string formated to be read by users. * @@ -147,7 +174,7 @@ class DataSourceSummaryDetailsPanel extends javax.swing.JPanel { "DataSourceSummaryDetailsPanel.units.terabytes= TB", "DataSourceSummaryDetailsPanel.units.petabytes= PB" }) - private String getSizeString(Long size) { + private static String getSizeString(Long size) { if (size == null) { return ""; } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDialog.form b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDialog.form index 72c45cb5a7..272fc9b041 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDialog.form +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDialog.form @@ -68,6 +68,11 @@ + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDialog.java b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDialog.java index 3c68b7b3ed..9cd7549c06 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDialog.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDialog.java @@ -27,7 +27,6 @@ import java.util.Observer; import java.util.Set; import javax.swing.event.ListSelectionEvent; import org.openide.util.NbBundle.Messages; -import org.sleuthkit.autopsy.casemodule.IngestJobInfoPanel; import org.sleuthkit.autopsy.ingest.IngestManager; import org.sleuthkit.autopsy.ingest.events.DataSourceAnalysisCompletedEvent; import org.sleuthkit.autopsy.ingest.events.DataSourceAnalysisCompletedEvent.Reason; @@ -41,10 +40,8 @@ final class DataSourceSummaryDialog extends javax.swing.JDialog implements Obser private static final long serialVersionUID = 1L; private static final Set INGEST_JOB_EVENTS_OF_INTEREST = EnumSet.of(IngestManager.IngestJobEvent.DATA_SOURCE_ANALYSIS_COMPLETED); - private final DataSourceSummaryCountsPanel countsPanel; - private final DataSourceSummaryDetailsPanel detailsPanel; private final DataSourceBrowser dataSourcesPanel; - private final IngestJobInfoPanel ingestHistoryPanel; + private final DataSourceSummaryTabbedPane dataSourceSummaryTabbedPane; /** * Creates new form DataSourceSummaryDialog for displaying a summary of the @@ -52,30 +49,20 @@ final class DataSourceSummaryDialog extends javax.swing.JDialog implements Obser * datasource. */ @Messages({ - "DataSourceSummaryDialog.window.title=Data Sources Summary", - "DataSourceSummaryDialog.countsTab.title=Counts", - "DataSourceSummaryDialog.detailsTab.title=Details", - "DataSourceSummaryDialog.ingestHistoryTab.title=Ingest History" + "DataSourceSummaryDialog.window.title=Data Sources Summary" }) DataSourceSummaryDialog(Frame owner) { super(owner, Bundle.DataSourceSummaryDialog_window_title(), true); Map usageMap = DataSourceInfoUtilities.getDataSourceTypes(); Map fileCountsMap = DataSourceInfoUtilities.getCountsOfFiles(); - countsPanel = new DataSourceSummaryCountsPanel(fileCountsMap); - detailsPanel = new DataSourceSummaryDetailsPanel(usageMap); dataSourcesPanel = new DataSourceBrowser(usageMap, fileCountsMap); - ingestHistoryPanel = new IngestJobInfoPanel(); + dataSourceSummaryTabbedPane = new DataSourceSummaryTabbedPane(); initComponents(); dataSourceSummarySplitPane.setLeftComponent(dataSourcesPanel); - dataSourceTabbedPane.addTab(Bundle.DataSourceSummaryDialog_detailsTab_title(), detailsPanel); - dataSourceTabbedPane.addTab(Bundle.DataSourceSummaryDialog_countsTab_title(), countsPanel); - dataSourceTabbedPane.addTab(Bundle.DataSourceSummaryDialog_ingestHistoryTab_title(), ingestHistoryPanel); dataSourcesPanel.addListSelectionListener((ListSelectionEvent e) -> { if (!e.getValueIsAdjusting()) { DataSource selectedDataSource = dataSourcesPanel.getSelectedDataSource(); - countsPanel.updateCountsTableData(selectedDataSource); - detailsPanel.updateDetailsPanelData(selectedDataSource); - ingestHistoryPanel.setDataSource(selectedDataSource); + dataSourceSummaryTabbedPane.setDataSource(selectedDataSource); this.repaint(); } }); @@ -116,7 +103,7 @@ final class DataSourceSummaryDialog extends javax.swing.JDialog implements Obser closeButton = new javax.swing.JButton(); dataSourceSummarySplitPane = new javax.swing.JSplitPane(); - dataSourceTabbedPane = new javax.swing.JTabbedPane(); + javax.swing.JTabbedPane dataSourceTabbedPane = dataSourceSummaryTabbedPane; org.openide.awt.Mnemonics.setLocalizedText(closeButton, org.openide.util.NbBundle.getMessage(DataSourceSummaryDialog.class, "DataSourceSummaryDialog.closeButton.text")); // NOI18N closeButton.addActionListener(new java.awt.event.ActionListener() { @@ -173,6 +160,5 @@ final class DataSourceSummaryDialog extends javax.swing.JDialog implements Obser // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JButton closeButton; private javax.swing.JSplitPane dataSourceSummarySplitPane; - private javax.swing.JTabbedPane dataSourceTabbedPane; // End of variables declaration//GEN-END:variables } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryTabbedPane.java b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryTabbedPane.java new file mode 100644 index 0000000000..dddc4ee123 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryTabbedPane.java @@ -0,0 +1,81 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.casemodule.datasourcesummary; + +import javax.swing.JTabbedPane; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.casemodule.IngestJobInfoPanel; +import org.sleuthkit.datamodel.DataSource; + +/** + * A tabbed pane showing the summary of a data source including tabs of: + * DataSourceSummaryCountsPanel, DataSourceSummaryDetailsPanel, and + * IngestJobInfoPanel. + */ +@Messages({ + "DataSourceSummaryTabbedPane_countsTab_title=Counts", + "DataSourceSummaryTabbedPane_detailsTab_title=Details", + "DataSourceSummaryTabbedPane_userActivityTab_title=User Activity", + "DataSourceSummaryTabbedPane_ingestHistoryTab_title=Ingest History" +}) +public class DataSourceSummaryTabbedPane extends JTabbedPane { + + private static final long serialVersionUID = 1L; + + private final DataSourceSummaryCountsPanel countsPanel = new DataSourceSummaryCountsPanel(); + private final DataSourceSummaryDetailsPanel detailsPanel = new DataSourceSummaryDetailsPanel(); + private final DataSourceSummaryUserActivityPanel userActivityPanel = new DataSourceSummaryUserActivityPanel(); + private final IngestJobInfoPanel ingestHistoryPanel = new IngestJobInfoPanel(); + + private DataSource dataSource = null; + + /** + * Constructs a tabbed pane showing the summary of a data source. + */ + public DataSourceSummaryTabbedPane() { + + addTab(Bundle.DataSourceSummaryTabbedPane_detailsTab_title(), detailsPanel); + addTab(Bundle.DataSourceSummaryTabbedPane_countsTab_title(), countsPanel); + addTab(Bundle.DataSourceSummaryTabbedPane_userActivityTab_title(), userActivityPanel); + addTab(Bundle.DataSourceSummaryTabbedPane_ingestHistoryTab_title(), ingestHistoryPanel); + } + + /** + * The datasource currently used as the model in this panel. + * + * @return The datasource currently being used as the model in this panel. + */ + public DataSource getDataSource() { + return dataSource; + } + + /** + * Sets datasource to visualize in the tabbed panel. + * + * @param dataSource The datasource to use in this panel. + */ + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + + detailsPanel.setDataSource(dataSource); + countsPanel.setDataSource(dataSource); + userActivityPanel.setDataSource(dataSource); + ingestHistoryPanel.setDataSource(dataSource); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryUserActivityPanel.form b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryUserActivityPanel.form new file mode 100644 index 0000000000..b938286e96 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryUserActivityPanel.form @@ -0,0 +1,70 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryUserActivityPanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryUserActivityPanel.java new file mode 100644 index 0000000000..c97e2ec18c --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryUserActivityPanel.java @@ -0,0 +1,291 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.casemodule.datasourcesummary; + +import java.awt.Component; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import javax.swing.JLabel; +import javax.swing.JTable; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.TableCellRenderer; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.datamodel.DataSource; + +/** + * A panel to display user activity. + */ +@Messages({ + "DataSourceSummaryUserActivityPanel_tab_title=User Activity", + "DataSourceSummaryUserActivityPanel_TopProgramsTableModel_name_header=Program", + "DataSourceSummaryUserActivityPanel_TopProgramsTableModel_folder_header=Folder", + "DataSourceSummaryUserActivityPanel_TopProgramsTableModel_count_header=Run Times", + "DataSourceSummaryUserActivityPanel_TopProgramsTableModel_lastrun_header=Last Run" +}) +public class DataSourceSummaryUserActivityPanel extends javax.swing.JPanel { + + private static final long serialVersionUID = 1L; + private static final DateFormat DATETIME_FORMAT = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.getDefault()); + private static final int TOP_PROGS_COUNT = 10; + private static final DefaultTableCellRenderer RIGHT_ALIGNED_RENDERER = new DefaultTableCellRenderer(); + + static { + RIGHT_ALIGNED_RENDERER.setHorizontalAlignment(JLabel.RIGHT); + } + + private DataSource dataSource; + + /** + * Creates new form DataSourceUserActivityPanel + */ + public DataSourceSummaryUserActivityPanel() { + initComponents(); + topProgramsTable.getTableHeader().setReorderingAllowed(false); + } + + /** + * The datasource currently used as the model in this panel. + * + * @return The datasource currently being used as the model in this panel. + */ + public DataSource getDataSource() { + return dataSource; + } + + /** + * Sets datasource to visualize in the panel. + * + * @param dataSource The datasource to use in this panel. + */ + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + if (dataSource == null || !Case.isCaseOpen()) { + updateTopPrograms(new TopProgramsModel(null)); + } else { + updateTopPrograms(getTopProgramsModel(dataSource)); + } + } + + /** + * Updates the Top Programs Table in the gui. + * + * @param data The data in Object[][] form to be used by the + * DefaultTableModel. + */ + private void updateTopPrograms(TopProgramsModel model) { + topProgramsTable.setModel(model); + topProgramsTable.getColumnModel().getColumn(0).setPreferredWidth(250); + topProgramsTable.getColumnModel().getColumn(0).setCellRenderer(PATH_CELL_RENDERER); + topProgramsTable.getColumnModel().getColumn(1).setPreferredWidth(150); + topProgramsTable.getColumnModel().getColumn(2).setCellRenderer(RIGHT_ALIGNED_RENDERER); + topProgramsTable.getColumnModel().getColumn(2).setPreferredWidth(80); + topProgramsTable.getColumnModel().getColumn(3).setPreferredWidth(150); + topProgramsScrollPane.getVerticalScrollBar().setValue(0); + this.repaint(); + } + + /** + * The counts of top programs run. + * + * @param selectedDataSource The DataSource. + * + * @return The JTable data model of counts of program runs. + */ + private static TopProgramsModel getTopProgramsModel(DataSource selectedDataSource) { + List topProgramList + = DataSourceInfoUtilities.getTopPrograms(selectedDataSource, TOP_PROGS_COUNT); + + if (topProgramList == null) { + return new TopProgramsModel(null); + } else { + return new TopProgramsModel(topProgramList); + } + } + + /** + * A POJO defining the values present in the name cell. Defines the name as + * well as the path for the tooltip. + */ + private static class ProgramNameCellValue { + + private final String programName; + private final String programPath; + + ProgramNameCellValue(String programName, String programPath) { + this.programName = programName; + this.programPath = programPath; + } + + @Override + public String toString() { + // override so that the value in the cell reads as programName + return programName; + } + + /** + * @return The program name. + */ + String getProgramName() { + return programName; + } + + /** + * @return The path of the program. + */ + String getProgramPath() { + return programPath; + } + } + + /** + * Defines a cell renderer for the first cell rendering the name as the text + * and path as the tooltip. + */ + private static TableCellRenderer PATH_CELL_RENDERER = new DefaultTableCellRenderer() { + + public Component getTableCellRendererComponent( + JTable table, Object value, + boolean isSelected, boolean hasFocus, + int row, int column) { + JLabel c = (JLabel) super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + if (value instanceof ProgramNameCellValue) { + ProgramNameCellValue cellValue = (ProgramNameCellValue) value; + c.setToolTipText(cellValue.getProgramPath()); + } + return c; + } + }; + + /** + * Defines the table model for a JTable of the programs. Accepts a list of + * TopProgramsResult objects as rows data source. + */ + private static class TopProgramsModel extends AbstractTableModel { + + private static final long serialVersionUID = 1L; + + // column headers for artifact counts table + private static final String[] TOP_PROGS_COLUMN_HEADERS = new String[]{ + Bundle.DataSourceSummaryUserActivityPanel_TopProgramsTableModel_name_header(), + Bundle.DataSourceSummaryUserActivityPanel_TopProgramsTableModel_folder_header(), + Bundle.DataSourceSummaryUserActivityPanel_TopProgramsTableModel_count_header(), + Bundle.DataSourceSummaryUserActivityPanel_TopProgramsTableModel_lastrun_header() + }; + + private final List programResults; + + /** + * Main constructor. + * + * @param programResults The results to display. + */ + TopProgramsModel(List programResults) { + this.programResults = programResults == null ? new ArrayList<>() : Collections.unmodifiableList(programResults); + } + + @Override + public String getColumnName(int column) { + return column < 0 || column >= TOP_PROGS_COLUMN_HEADERS.length ? null : TOP_PROGS_COLUMN_HEADERS[column]; + } + + @Override + public int getRowCount() { + return programResults.size(); + } + + @Override + public int getColumnCount() { + return TOP_PROGS_COLUMN_HEADERS.length; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + if (rowIndex < 0 || rowIndex >= programResults.size()) { + return null; + } + + DataSourceInfoUtilities.TopProgramsResult result = programResults.get(rowIndex); + switch (columnIndex) { + case 0: + return new ProgramNameCellValue(result.getProgramName(), result.getProgramPath()); + case 1: + return DataSourceInfoUtilities.getShortFolderName(result.getProgramPath(), result.getProgramName()); + case 2: + return result.getRunTimes(); + case 3: + return result.getLastRun() == null ? null : DATETIME_FORMAT.format(result.getLastRun()); + default: + return null; + } + } + + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + javax.swing.JLabel programsRunLabel = new javax.swing.JLabel(); + topProgramsScrollPane = new javax.swing.JScrollPane(); + topProgramsTable = new javax.swing.JTable(); + + org.openide.awt.Mnemonics.setLocalizedText(programsRunLabel, org.openide.util.NbBundle.getMessage(DataSourceSummaryUserActivityPanel.class, "DataSourceSummaryUserActivityPanel.programsRunLabel.text")); // NOI18N + + topProgramsScrollPane.setPreferredSize(new java.awt.Dimension(750, 187)); + topProgramsScrollPane.setViewportView(topProgramsTable); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(programsRunLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 155, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(topProgramsScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 460, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addContainerGap(128, Short.MAX_VALUE)) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(programsRunLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(topProgramsScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + ); + }// //GEN-END:initComponents + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JScrollPane topProgramsScrollPane; + private javax.swing.JTable topProgramsTable; + // End of variables declaration//GEN-END:variables +} diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/NonEditableTableModel.java b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/NonEditableTableModel.java new file mode 100644 index 0000000000..8ed965e2ab --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/NonEditableTableModel.java @@ -0,0 +1,37 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.casemodule.datasourcesummary; + +import javax.swing.table.DefaultTableModel; + +/** + * A Table model where cells are not editable. + */ +class NonEditableTableModel extends DefaultTableModel { + private static final long serialVersionUID = 1L; + + NonEditableTableModel(Object[][] data, Object[] columnNames) { + super(data, columnNames); + } + + @Override + public boolean isCellEditable(int row, int column) { + return false; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/communications/AccountDeviceInstanceNode.java b/Core/src/org/sleuthkit/autopsy/communications/AccountDeviceInstanceNode.java index 4a50969ef3..24d190bf2d 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/AccountDeviceInstanceNode.java +++ b/Core/src/org/sleuthkit/autopsy/communications/AccountDeviceInstanceNode.java @@ -35,8 +35,12 @@ import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.datamodel.NodeProperty; import org.sleuthkit.datamodel.Account; import org.sleuthkit.datamodel.AccountDeviceInstance; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME; import org.sleuthkit.datamodel.CommunicationsFilter; import org.sleuthkit.datamodel.CommunicationsManager; +import org.sleuthkit.datamodel.TskCoreException; /** * Node to represent an Account Device Instance in the CVT @@ -49,6 +53,8 @@ final class AccountDeviceInstanceNode extends AbstractNode { private final CommunicationsManager commsManager; private final Account account; + private static final BlackboardAttribute.Type NAME_ATTRIBUTE = new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.fromID(TSK_NAME.getTypeID())); + AccountDeviceInstanceNode(AccountDeviceInstanceKey accountDeviceInstanceKey, CommunicationsManager commsManager) { super(Children.LEAF, Lookups.fixed(accountDeviceInstanceKey, commsManager)); this.accountDeviceInstanceKey = accountDeviceInstanceKey; @@ -122,15 +128,17 @@ final class AccountDeviceInstanceNode extends AbstractNode { @Override public String getShortDescription() { List personaList; + List contactArtifactList; try { personaList = CVTPersonaCache.getPersonaAccounts(account); + contactArtifactList = ContactCache.getContacts(account); } catch (ExecutionException ex) { logger.log(Level.WARNING, "Failed to retrieve Persona details for node.", ex); return getDisplayName(); } String personaName; - if (!personaList.isEmpty()) { + if (personaList != null && !personaList.isEmpty()) { personaName = personaList.get(0).getPersona().getName(); if (personaList.size() > 1) { personaName += Bundle.AccountInstanceNode_Tooltip_suffix(Integer.toString(personaList.size())); @@ -139,6 +147,19 @@ final class AccountDeviceInstanceNode extends AbstractNode { personaName = "None"; } - return Bundle.AccountInstanceNode_Tooltip_Template(getDisplayName(), personaName); + String contactName = getDisplayName(); + if (contactArtifactList != null && !contactArtifactList.isEmpty()) { + try { + BlackboardArtifact contactArtifact = contactArtifactList.get(0); + BlackboardAttribute attribute = contactArtifact.getAttribute(NAME_ATTRIBUTE); + if (attribute != null) { + contactName = attribute.getValueString(); + } + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "Failed to retrive name attribute from contact artifact.", ex); + } + } + + return Bundle.AccountInstanceNode_Tooltip_Template(contactName, personaName); } } diff --git a/Core/src/org/sleuthkit/autopsy/communications/ContactCache.java b/Core/src/org/sleuthkit/autopsy/communications/ContactCache.java new file mode 100755 index 0000000000..2b0cc8f678 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/communications/ContactCache.java @@ -0,0 +1,166 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 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.communications; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.ingest.IngestManager; +import static org.sleuthkit.autopsy.ingest.IngestManager.IngestModuleEvent.DATA_ADDED; +import org.sleuthkit.autopsy.ingest.ModuleDataEvent; +import org.sleuthkit.datamodel.Account; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * A singleton cache of the Contact artifacts for accounts. The map of account + * unique ids to list of contact artifacts is stored in a LoadingCache which + * expires after 10 of non-use. + * + */ +final class ContactCache { + + private static final Logger logger = Logger.getLogger(ContactCache.class.getName()); + + private static ContactCache instance; + + private final LoadingCache>> accountMap; + + /** + * Returns the list of Contacts for the given Account. + * + * @param account Account instance. + * + * @return List of TSK_CONTACT artifacts that references the given Account. + * An empty list is returned if no contacts are found. + * + * @throws ExecutionException + */ + static synchronized List getContacts(Account account) throws ExecutionException { + return getInstance().accountMap.get("realMap").get(account.getTypeSpecificID()); + } + + /** + * Force the cache to invalidate all entries. + */ + static synchronized void invalidateCache() { + getInstance().accountMap.invalidateAll(); + } + + /** + * Construct a new instance. + */ + private ContactCache() { + + accountMap = CacheBuilder.newBuilder().expireAfterAccess(10, TimeUnit.MINUTES).build( + new CacheLoader>>() { + @Override + public Map> load(String key) { + try { + return buildMap(); + } catch (SQLException | TskCoreException ex) { + logger.log(Level.WARNING, "Failed to build account to contact map", ex); + } + return new HashMap<>(); // Return an empty map if there is an exception to avoid NPE and continual trying. + } + }); + + PropertyChangeListener ingestListener = pce -> { + String eventType = pce.getPropertyName(); + if (eventType.equals(DATA_ADDED.toString())) { + ModuleDataEvent eventData = (ModuleDataEvent) pce.getOldValue(); + if (eventData.getBlackboardArtifactType().getTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_CONTACT.getTypeID()) { + invalidateCache(); + } + } + }; + + Case.addEventTypeSubscriber(EnumSet.of(Case.Events.CURRENT_CASE), (PropertyChangeEvent event) -> { + if (event.getNewValue() == null) { + invalidateCache(); + } + }); + + IngestManager.getInstance().addIngestModuleEventListener(EnumSet.of(DATA_ADDED), ingestListener); + } + + /** + * Returns the singleton instance of the cache object. + * + * @return AccountCache instance. + */ + private static synchronized ContactCache getInstance() { + if (instance == null) { + instance = new ContactCache(); + } + + return instance; + } + + /** + * Builds the map of account IDs to contacts that reference them. + * + * @return A map of account IDs to contact artifacts. + * + * @throws TskCoreException + * @throws SQLException + */ + private Map> buildMap() throws TskCoreException, SQLException { + Map> acctMap = new HashMap<>(); + List contactList = Case.getCurrentCase().getSleuthkitCase().getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_CONTACT); + + for (BlackboardArtifact contactArtifact : contactList) { + List contactAttributes = contactArtifact.getAttributes(); + for (BlackboardAttribute attribute : contactAttributes) { + String typeName = attribute.getAttributeType().getTypeName(); + + if (typeName.startsWith("TSK_EMAIL") + || typeName.startsWith("TSK_PHONE") + || typeName.startsWith("TSK_NAME") + || typeName.startsWith("TSK_ID")) { + String accountID = attribute.getValueString(); + List artifactList = acctMap.getOrDefault(accountID, new ArrayList<>()); + + acctMap.put(accountID, artifactList); + + if (!artifactList.contains(contactArtifact)) { + artifactList.add(contactArtifact); + } + } + } + + } + + return acctMap; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/communications/Utils.java b/Core/src/org/sleuthkit/autopsy/communications/Utils.java index 4a3e03e1f2..3309bab92e 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/Utils.java +++ b/Core/src/org/sleuthkit/autopsy/communications/Utils.java @@ -18,9 +18,12 @@ */ package org.sleuthkit.autopsy.communications; +import java.awt.Component; import java.time.ZoneId; import java.time.ZoneOffset; import java.util.TimeZone; +import javax.swing.table.TableCellRenderer; +import org.netbeans.swing.outline.Outline; import org.sleuthkit.autopsy.core.UserPreferences; import org.sleuthkit.autopsy.datamodel.accounts.Accounts; import org.sleuthkit.datamodel.Account; @@ -47,5 +50,29 @@ public final class Utils { static public final String getIconFilePath(Account.Type type) { return Accounts.getIconFilePath(type); } + + static public void setColumnWidths(Outline outline) { + int margin = 4; + int padding = 8; + + final int rows = Math.min(100, outline.getRowCount()); + + for (int column = 0; column < outline.getColumnCount(); column++) { + int columnWidthLimit = 500; + int columnWidth = 0; + + // find the maximum width needed to fit the values for the first 100 rows, at most + for (int row = 0; row < rows; row++) { + TableCellRenderer renderer = outline.getCellRenderer(row, column); + Component comp = outline.prepareRenderer(renderer, row, column); + columnWidth = Math.max(comp.getPreferredSize().width, columnWidth); + } + + columnWidth += 2 * margin + padding; // add margin and regular padding + columnWidth = Math.min(columnWidth, columnWidthLimit); + + outline.getColumnModel().getColumn(column).setPreferredWidth(columnWidth); + } + } } diff --git a/Core/src/org/sleuthkit/autopsy/communications/relationships/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/communications/relationships/Bundle.properties-MERGED index 6778d8dc53..616408978d 100755 --- a/Core/src/org/sleuthkit/autopsy/communications/relationships/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/communications/relationships/Bundle.properties-MERGED @@ -73,5 +73,5 @@ SummaryViewer.referencesLabel.text=Communication References: SummaryViewer.referencesDataLabel.text= SummaryViewer.contactsLabel.text=Book Entries: SummaryViewer.accountCountry.text= -SummaryViewer.fileRefPane.border.title=File Referernce(s) in Current Case +SummaryViewer.fileRefPane.border.title=File References in Current Case SummaryViewer.selectAccountFileRefLabel.text=