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 eb7affe9ee..ba0d3510c7 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/Bundle.properties-MERGED @@ -61,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 @@ -72,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 dcbf9bb82d..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,8 +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; @@ -27,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; /** @@ -193,6 +203,31 @@ final class DataSourceInfoUtilities { 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. * @@ -215,24 +250,311 @@ final class DataSourceInfoUtilities { + " WHERE bba.data_source_obj_id =" + selectedDataSource.getId() + " GROUP BY bbt.display_name"; - ResultSetHandler> handler = (resultSet) -> { - Map toRet = new HashMap<>(); - while (resultSet.next()) { + 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 { - toRet.put(resultSet.getString(nameParam), resultSet.getLong(valueParam)); + 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 result pair from the result set.", ex); + logger.log(Level.WARNING, "Failed to get a top program result from the result set.", ex); } } - return toRet; + return progResults; }; - String errorMessage = "Unable to get artifact type counts; returning null."; - 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. diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDialog.java b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDialog.java index e1197fa83a..9cd7549c06 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDialog.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryDialog.java @@ -49,10 +49,7 @@ 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); diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryTabbedPane.java b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryTabbedPane.java index e1d15a6b0d..dddc4ee123 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryTabbedPane.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/datasourcesummary/DataSourceSummaryTabbedPane.java @@ -19,6 +19,7 @@ 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; @@ -27,13 +28,20 @@ import org.sleuthkit.datamodel.DataSource; * 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; - private final DataSourceSummaryDetailsPanel detailsPanel; - private final IngestJobInfoPanel ingestHistoryPanel; + + 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; @@ -41,13 +49,11 @@ public class DataSourceSummaryTabbedPane extends JTabbedPane { * Constructs a tabbed pane showing the summary of a data source. */ public DataSourceSummaryTabbedPane() { - countsPanel = new DataSourceSummaryCountsPanel(); - detailsPanel = new DataSourceSummaryDetailsPanel(); - ingestHistoryPanel = new IngestJobInfoPanel(); - - addTab(Bundle.DataSourceSummaryDialog_detailsTab_title(), detailsPanel); - addTab(Bundle.DataSourceSummaryDialog_countsTab_title(), countsPanel); - addTab(Bundle.DataSourceSummaryDialog_ingestHistoryTab_title(), ingestHistoryPanel); + + addTab(Bundle.DataSourceSummaryTabbedPane_detailsTab_title(), detailsPanel); + addTab(Bundle.DataSourceSummaryTabbedPane_countsTab_title(), countsPanel); + addTab(Bundle.DataSourceSummaryTabbedPane_userActivityTab_title(), userActivityPanel); + addTab(Bundle.DataSourceSummaryTabbedPane_ingestHistoryTab_title(), ingestHistoryPanel); } /** @@ -69,6 +75,7 @@ public class DataSourceSummaryTabbedPane extends JTabbedPane { 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/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/contentviewers/artifactviewers/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/Bundle.properties-MERGED index 317d7050de..a6e2ce47fe 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/Bundle.properties-MERGED @@ -26,6 +26,9 @@ CallLogArtifactViewer_label_from=From CallLogArtifactViewer_label_to=To CallLogArtifactViewer_suffix_local=(Local) CallLogArtifactViewer_value_unknown=Unknown +#{0} - contact name +CommunicationArtifactViewerHelper_contact_label=Contact: {0} +CommunicationArtifactViewerHelper_contact_label_unknown=Unknown CommunicationArtifactViewerHelper_menuitem_copy=Copy CommunicationArtifactViewerHelper_persona_button_create=Create CommunicationArtifactViewerHelper_persona_button_view=View @@ -61,6 +64,7 @@ DefaultArtifactContentViewer.copyMenuItem.text=Copy DefaultArtifactContentViewer.selectAllMenuItem.text=Select All MessageAccountPanel_button_create_label=Create MessageAccountPanel_button_view_label=View +MessageAccountPanel_contact_label=Contact: MessageAccountPanel_no_matches=No matches found. MessageAccountPanel_persona_label=Persona: MessageAccountPanel_unknown_label=Unknown diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/CallLogArtifactViewer.java b/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/CallLogArtifactViewer.java index cdac130944..16171d7f32 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/CallLogArtifactViewer.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/CallLogArtifactViewer.java @@ -36,6 +36,7 @@ import org.openide.util.NbBundle; import org.openide.util.lookup.ServiceProvider; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.guiutils.ContactCache; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardAttribute; import org.sleuthkit.datamodel.Content; @@ -66,7 +67,7 @@ public class CallLogArtifactViewer extends javax.swing.JPanel implements Artifac private GridBagLayout m_gridBagLayout = new GridBagLayout(); private GridBagConstraints m_constraints = new GridBagConstraints(); - + private PersonaAccountFetcher currentAccountFetcher = null; /** @@ -106,9 +107,9 @@ public class CallLogArtifactViewer extends javax.swing.JPanel implements Artifac // update the view with the call log data if (callLogViewData != null) { List personaSearchDataList = updateView(callLogViewData); - if(!personaSearchDataList.isEmpty()) { + if (!personaSearchDataList.isEmpty()) { currentAccountFetcher = new PersonaAccountFetcher(artifact, personaSearchDataList, this); - currentAccountFetcher.execute(); + currentAccountFetcher.execute(); } else { currentAccountFetcher = null; } @@ -143,6 +144,8 @@ public class CallLogArtifactViewer extends javax.swing.JPanel implements Artifac String fromAccountIdentifier = null; String toAccountIdentifier = null; List otherParties = null; + List toContactNames = null; + List fromContactNames = null; Content dataSource = artifact.getDataSource(); String deviceId = ((DataSource) dataSource).getDeviceId(); @@ -186,6 +189,7 @@ public class CallLogArtifactViewer extends javax.swing.JPanel implements Artifac String fromAccountAttrValue = fromAccountAttr.getValueString(); if (fromAccountAttrValue.equalsIgnoreCase(deviceId) == false) { fromAccountIdentifier = fromAccountAttrValue; + fromContactNames = ContactCache.getContactNameList(fromAccountIdentifier); } } @@ -195,6 +199,7 @@ public class CallLogArtifactViewer extends javax.swing.JPanel implements Artifac String toAccountAttrValue = StringUtils.trim(numbers[0]); if (toAccountAttrValue.equalsIgnoreCase(deviceId) == false) { toAccountIdentifier = toAccountAttrValue; + toContactNames = ContactCache.getContactNameList(toAccountIdentifier); } // if more than one To address, then stick the rest of them in the @@ -228,6 +233,9 @@ public class CallLogArtifactViewer extends javax.swing.JPanel implements Artifac } callLogViewData.setOtherAttributes(extractOtherAttributes(artifact)); + + callLogViewData.setFromContactNameList(fromContactNames); + callLogViewData.setToContactNameList(toContactNames); } return callLogViewData; @@ -237,9 +245,9 @@ public class CallLogArtifactViewer extends javax.swing.JPanel implements Artifac * Extract the call time and duration from the artifact and saves in the * CallLogViewData. * - * @param artifact Call log artifact. + * @param artifact Call log artifact. * @param callLogViewData CallLogViewData object to save the time & duration - * in. + * in. * * @throws TskCoreException */ @@ -290,7 +298,7 @@ public class CallLogArtifactViewer extends javax.swing.JPanel implements Artifac * Update the viewer with the call log data. * * @param callLogViewData Call log data to update the view with. - * + * * @return List of AccountPersonaSearcherData objects. */ @NbBundle.Messages({ @@ -312,8 +320,13 @@ public class CallLogArtifactViewer extends javax.swing.JPanel implements Artifac String accountDisplayString = getAccountDisplayString(callLogViewData.getFromAccount(), callLogViewData); CommunicationArtifactViewerHelper.addValue(this, m_gridBagLayout, this.m_constraints, accountDisplayString); + List contactNames = callLogViewData.getFromContactNameList(); + for (String name : contactNames) { + CommunicationArtifactViewerHelper.addContactRow(this, m_gridBagLayout, m_constraints, name); + } + // show persona - dataList.addAll( CommunicationArtifactViewerHelper.addPersonaRow(this, m_gridBagLayout, this.m_constraints, callLogViewData.getFromAccount())); + dataList.addAll(CommunicationArtifactViewerHelper.addPersonaRow(this, m_gridBagLayout, this.m_constraints, callLogViewData.getFromAccount())); } else { CommunicationArtifactViewerHelper.addValue(this, m_gridBagLayout, this.m_constraints, Bundle.CallLogArtifactViewer_value_unknown()); } @@ -324,8 +337,13 @@ public class CallLogArtifactViewer extends javax.swing.JPanel implements Artifac String accountDisplayString = getAccountDisplayString(callLogViewData.getToAccount(), callLogViewData); CommunicationArtifactViewerHelper.addValue(this, m_gridBagLayout, this.m_constraints, accountDisplayString); - dataList.addAll( CommunicationArtifactViewerHelper.addPersonaRow(this, m_gridBagLayout, this.m_constraints, callLogViewData.getToAccount())); - + List contactNames = callLogViewData.getToContactNameList(); + for (String name : contactNames) { + CommunicationArtifactViewerHelper.addContactRow(this, m_gridBagLayout, m_constraints, name); + } + + dataList.addAll(CommunicationArtifactViewerHelper.addPersonaRow(this, m_gridBagLayout, this.m_constraints, callLogViewData.getToAccount())); + } else { CommunicationArtifactViewerHelper.addValue(this, m_gridBagLayout, this.m_constraints, Bundle.CallLogArtifactViewer_value_unknown()); } @@ -335,7 +353,7 @@ public class CallLogArtifactViewer extends javax.swing.JPanel implements Artifac CommunicationArtifactViewerHelper.addKey(this, m_gridBagLayout, this.m_constraints, Bundle.CallLogArtifactViewer_label_to()); CommunicationArtifactViewerHelper.addValue(this, m_gridBagLayout, this.m_constraints, otherParty); - dataList.addAll( CommunicationArtifactViewerHelper.addPersonaRow(this, m_gridBagLayout, this.m_constraints, otherParty)); + dataList.addAll(CommunicationArtifactViewerHelper.addPersonaRow(this, m_gridBagLayout, this.m_constraints, otherParty)); } updateMetadataView(callLogViewData); @@ -353,7 +371,7 @@ public class CallLogArtifactViewer extends javax.swing.JPanel implements Artifac this.setLayout(m_gridBagLayout); this.revalidate(); this.repaint(); - + return dataList; } @@ -441,7 +459,7 @@ public class CallLogArtifactViewer extends javax.swing.JPanel implements Artifac * local account, if it is known. If it is, it appends a "(Local)" suffix to * account display string. * - * @param accountIdentifier Account identifier to check. + * @param accountIdentifier Account identifier to check. * @param callLogViewDataNew Call log data which may have the lock account. * * @return Account string to display. @@ -474,7 +492,7 @@ public class CallLogArtifactViewer extends javax.swing.JPanel implements Artifac private void resetComponent() { // cancel any outstanding persona searching threads. - if(currentAccountFetcher != null && !currentAccountFetcher.isDone()) { + if (currentAccountFetcher != null && !currentAccountFetcher.isDone()) { currentAccountFetcher.cancel(true); currentAccountFetcher = null; } diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/CallLogViewData.java b/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/CallLogViewData.java index dabba1b0ca..f7888ed7ec 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/CallLogViewData.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/CallLogViewData.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; /** @@ -47,11 +48,14 @@ final class CallLogViewData { private String dataSourceName = null; + private List toContactNameList = null; + private List fromContactNameList = null; + /** * Constructor. * * @param fromAccount From account identifier, may be null; - * @param toAccount To account identifier, may be null; + * @param toAccount To account identifier, may be null; */ CallLogViewData(String fromAccount, String toAccount) { this(fromAccount, toAccount, null); @@ -61,8 +65,8 @@ final class CallLogViewData { * Constructor. * * @param fromAccount From account identifier, may be null; - * @param toAccount To account identifier, may be null; - * @param direction Direction, may be null. + * @param toAccount To account identifier, may be null; + * @param direction Direction, may be null. */ CallLogViewData(String fromAccount, String toAccount, String direction) { this.fromAccount = fromAccount; @@ -146,4 +150,28 @@ final class CallLogViewData { this.localAccountId = localAccountId; } + public void setToContactNameList(List contactNameList) { + if (contactNameList != null) { + this.toContactNameList = new ArrayList<>(contactNameList); + } else { + this.toContactNameList = new ArrayList<>(); + } + } + + public List getToContactNameList() { + return Collections.unmodifiableList(this.toContactNameList); + } + + public void setFromContactNameList(List contactNameList) { + if (contactNameList != null) { + this.fromContactNameList = new ArrayList<>(contactNameList); + } else { + this.fromContactNameList = new ArrayList<>(); + } + } + + public List getFromContactNameList() { + return Collections.unmodifiableList(this.fromContactNameList); + } + } diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/CommunicationArtifactViewerHelper.java b/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/CommunicationArtifactViewerHelper.java index 2ae186e044..071fc7011b 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/CommunicationArtifactViewerHelper.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/CommunicationArtifactViewerHelper.java @@ -378,7 +378,7 @@ final class CommunicationArtifactViewerHelper { "CommunicationArtifactViewerHelper_persona_button_view=View", "CommunicationArtifactViewerHelper_persona_button_create=Create" }) - + static List addPersonaRow(JPanel panel, GridBagLayout gridbagLayout, GridBagConstraints constraints, String accountIdentifier) { List dataList = new ArrayList<>(); @@ -428,6 +428,35 @@ final class CommunicationArtifactViewerHelper { return dataList; } + + /** + * Adds a contact row to the panel. + * + * @param panel Panel to update. + * @param gridbagLayout Layout to use. + * @param constraints Constrains to use. + * @param contactId Contact name to display. + * + * @return A JLabel with the contact information. + */ + @NbBundle.Messages({ + "#{0} - contact name", + "CommunicationArtifactViewerHelper_contact_label=Contact: {0}", + "CommunicationArtifactViewerHelper_contact_label_unknown=Unknown" + }) + static JLabel addContactRow(JPanel panel, GridBagLayout gridbagLayout, GridBagConstraints constraints, String contactId) { + // Increase the y value because we are not calling the addKey + constraints.gridy++; + //Don't change the origian constraints, just make a copy to modify + GridBagConstraints indentedConstraints = (GridBagConstraints) constraints.clone(); + + // Add an indent to match persona labels + indentedConstraints.insets = new java.awt.Insets(0, 2 * LEFT_INSET, 0, 0); + + String contactInfo = Bundle.CommunicationArtifactViewerHelper_contact_label(contactId != null && !contactId.isEmpty() ? contactId : Bundle.CommunicationArtifactViewerHelper_contact_label_unknown()); + + return addValueAtCol(panel, gridbagLayout, indentedConstraints, contactInfo, 1); + } /** * Event handler for mouse click event. Attaches a 'Copy' menu item to right diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/MessageAccountPanel.java b/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/MessageAccountPanel.java index 050fca83fe..e51577819a 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/MessageAccountPanel.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/MessageAccountPanel.java @@ -16,7 +16,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.sleuthkit.autopsy.contentviewers.artifactviewers; import java.awt.event.ActionEvent; @@ -39,6 +38,7 @@ import javax.swing.SwingUtilities; import javax.swing.SwingWorker; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; import org.sleuthkit.autopsy.centralrepository.datamodel.Persona; import org.sleuthkit.autopsy.centralrepository.datamodel.PersonaAccount; import org.sleuthkit.autopsy.centralrepository.persona.PersonaDetailsDialog; @@ -46,8 +46,10 @@ import org.sleuthkit.autopsy.centralrepository.persona.PersonaDetailsDialogCallb import org.sleuthkit.autopsy.centralrepository.persona.PersonaDetailsMode; import org.sleuthkit.autopsy.centralrepository.persona.PersonaDetailsPanel; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.guiutils.ContactCache; import org.sleuthkit.datamodel.Account; import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; import org.sleuthkit.datamodel.CommunicationsManager; import org.sleuthkit.datamodel.TskCoreException; @@ -113,13 +115,24 @@ final class MessageAccountPanel extends JPanel { return new ArrayList<>(); } - Collection personAccounts = PersonaAccount.getPersonaAccountsForAccount(account); - if (personAccounts != null && !personAccounts.isEmpty()) { - for (PersonaAccount personaAccount : PersonaAccount.getPersonaAccountsForAccount(account)) { - dataList.add(new AccountContainer(account, personaAccount)); + List contactList = ContactCache.getContacts(account); + BlackboardArtifact contact = null; + + if (contactList != null && !contactList.isEmpty()) { + contact = contactList.get(0); + } + + if (CentralRepository.isEnabled()) { + Collection personAccounts = PersonaAccount.getPersonaAccountsForAccount(account); + if (personAccounts != null && !personAccounts.isEmpty()) { + for (PersonaAccount personaAccount : PersonaAccount.getPersonaAccountsForAccount(account)) { + dataList.add(new AccountContainer(account, personaAccount, contact)); + } + } else { + dataList.add(new AccountContainer(account, null, contact)); } } else { - dataList.add(new AccountContainer(account, null)); + dataList.add(new AccountContainer(account, null, contact)); } } @@ -127,8 +140,7 @@ final class MessageAccountPanel extends JPanel { } @Messages({ - "MessageAccountPanel_no_matches=No matches found.", - }) + "MessageAccountPanel_no_matches=No matches found.",}) @Override protected void done() { try { @@ -199,6 +211,7 @@ final class MessageAccountPanel extends JPanel { for (AccountContainer o : data) { group.addGap(5) .addComponent(o.getAccountLabel()) + .addGroup(o.getContactLineVerticalGroup(layout)) .addGroup(o.getPersonLineVerticalGroup(layout)); } @@ -234,6 +247,7 @@ final class MessageAccountPanel extends JPanel { group.addGap(10); for (AccountContainer o : data) { pgroup.addGroup(o.getPersonaSequentialGroup(layout)); + pgroup.addGroup(o.getContactSequentialGroup(layout)); } group.addGap(10) .addGroup(pgroup) @@ -253,10 +267,13 @@ final class MessageAccountPanel extends JPanel { private final Account account; private Persona persona = null; + private final String contactName; private JLabel accountLabel; private JLabel personaHeader; private JLabel personaDisplayName; + private JLabel contactHeader; + private JLabel contactDisplayName; private JButton button; /** @@ -265,16 +282,22 @@ final class MessageAccountPanel extends JPanel { * @param account * @param personaAccount */ - AccountContainer(Account account, PersonaAccount personaAccount) { + AccountContainer(Account account, PersonaAccount personaAccount, BlackboardArtifact contactArtifact) throws TskCoreException { + if (contactArtifact != null && contactArtifact.getArtifactTypeID() != BlackboardArtifact.ARTIFACT_TYPE.TSK_CONTACT.getTypeID()) { + throw new IllegalArgumentException("Failed to create AccountContainer object, passed in artifact was not a TSK_CONTACT"); + } + this.account = account; this.persona = personaAccount != null ? personaAccount.getPersona() : null; + this.contactName = getNameFromContactArtifact(contactArtifact); } @Messages({ "MessageAccountPanel_persona_label=Persona:", "MessageAccountPanel_unknown_label=Unknown", "MessageAccountPanel_button_view_label=View", - "MessageAccountPanel_button_create_label=Create" + "MessageAccountPanel_button_create_label=Create", + "MessageAccountPanel_contact_label=Contact:" }) /** * Swing components will not be initialized until this method is called. @@ -282,16 +305,29 @@ final class MessageAccountPanel extends JPanel { private void initalizeSwingControls() { accountLabel = new JLabel(); personaHeader = new JLabel(Bundle.MessageAccountPanel_persona_label()); + contactHeader = new JLabel(Bundle.MessageAccountPanel_contact_label()); personaDisplayName = new JLabel(); + contactDisplayName = new JLabel(); button = new JButton(); button.addActionListener(new PersonaButtonListener(this)); accountLabel.setText(account.getTypeSpecificID()); - + contactDisplayName.setText(contactName); personaDisplayName.setText(persona != null ? persona.getName() : Bundle.MessageAccountPanel_unknown_label()); button.setText(persona != null ? Bundle.MessageAccountPanel_button_view_label() : Bundle.MessageAccountPanel_button_create_label()); } + private String getNameFromContactArtifact(BlackboardArtifact contactArtifact) throws TskCoreException { + if (contactArtifact != null) { + BlackboardAttribute attribute = contactArtifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME)); + if (attribute != null) { + return attribute.getValueString(); + } + } + + return Bundle.MessageAccountPanel_unknown_label(); + } + /** * Sets a new persona for this object and update the controls. * @@ -365,6 +401,17 @@ final class MessageAccountPanel extends JPanel { return group; } + + private SequentialGroup getContactSequentialGroup(GroupLayout layout) { + SequentialGroup group = layout.createSequentialGroup(); + + group + .addComponent(contactHeader) + .addPreferredGap(ComponentPlacement.RELATED) + .addComponent(contactDisplayName); + + return group; + } /** * Generates the vertical layout code for the persona line. @@ -379,6 +426,12 @@ final class MessageAccountPanel extends JPanel { .addComponent(personaDisplayName) .addComponent(button); } + + private ParallelGroup getContactLineVerticalGroup(GroupLayout layout) { + return layout.createParallelGroup(Alignment.BASELINE) + .addComponent(contactHeader) + .addComponent(contactDisplayName); + } } /** diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/MessageArtifactViewer.java b/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/MessageArtifactViewer.java index f8257afb64..812fb7290d 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/MessageArtifactViewer.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/MessageArtifactViewer.java @@ -447,10 +447,8 @@ public class MessageArtifactViewer extends javax.swing.JPanel implements Artifac resetComponent(); } - msgbodyTabbedPane.setEnabledAt(ACCT_TAB_INDEX, CentralRepository.isEnabled()); - if(CentralRepository.isEnabled()) { - accountsPanel.setArtifact(artifact); - } + msgbodyTabbedPane.setEnabledAt(ACCT_TAB_INDEX, true); + accountsPanel.setArtifact(artifact); } /** diff --git a/Core/src/org/sleuthkit/autopsy/guiutils/ContactCache.java b/Core/src/org/sleuthkit/autopsy/guiutils/ContactCache.java new file mode 100755 index 0000000000..bb0db49f3a --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/guiutils/ContactCache.java @@ -0,0 +1,192 @@ +/* + * 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.guiutils; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +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 static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME; +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. + * + */ +public 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 + */ + public static synchronized List getContacts(Account account) throws ExecutionException { + return getInstance().accountMap.get("realMap").get(account.getTypeSpecificID()); + } + + /** + * Returns a list of Contact TSK_NAME values for the given account type + * specific id. + * + * @param accountTypeSpecificID Account type specific id + * + * @return List of contact string names or empty list if none were found. + * + * @throws TskCoreException + */ + public static synchronized List getContactNameList(String accountTypeSpecificID) throws TskCoreException { + List contactList; + try { + contactList = getInstance().accountMap.get("realMap").get(accountTypeSpecificID); + } catch (ExecutionException ex) { + throw new TskCoreException("Unable to get contact list from cache", ex); + } + List contactNameList = new ArrayList<>(); + + if (contactList != null) { + for (BlackboardArtifact artifact : contactList) { + BlackboardAttribute attribute = artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.fromID(TSK_NAME.getTypeID()))); + if (attribute != null && !contactNameList.contains(attribute.getValueString())) { + contactNameList.add(attribute.getValueString()); + } + } + } + + return contactNameList; + } + + /** + * 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(); + } + } + }; + + 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.get(accountID); + if (artifactList == null) { + artifactList = new ArrayList<>(); + acctMap.put(accountID, artifactList); + } + if (!artifactList.contains(contactArtifact)) { + artifactList.add(contactArtifact); + } + } + } + + } + + return acctMap; + } +} diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Bundle.properties b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Bundle.properties index cec93716dc..283bbc6dc5 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Bundle.properties +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Bundle.properties @@ -2,7 +2,14 @@ OpenIDE-Module-Display-Category=Ingest Module OpenIDE-Module-Long-Description=Recent Activity ingest module.\n\n\The module extracts useful information about the recent user activity on the disk image being ingested, such as:\n\n- Recently open documents,\n- Web activity (sites visited, stored cookies, book marked sites, search engine queries, file downloads),\n- Recently attached devices,\n- Installed programs.\n\nThe module currently supports Windows only disk images.\nThe plugin is also fully functional when deployed on Windows version of Autopsy. OpenIDE-Module-Name=RecentActivity OpenIDE-Module-Short-Description=Recent Activity finder ingest module -Chrome.moduleName=Chrome +Browser.name.Microsoft.Edge=Microsoft Edge +Browser.name.Yandex=Yandex +Browser.name.Opera=Opera +Browser.name.SalamWeb=SalamWeb +Browser.name.UC.Browser=UC Browser +Browser.name.Brave=Brave +Browser.name.Google.Chrome=Google Chrome +Chrome.moduleName=Chromium Chrome.getHistory.errMsg.errGettingFiles=Error when trying to get Chrome history files. Chrome.getHistory.errMsg.couldntFindAnyFiles=Could not find any allocated Chrome history files. Chrome.getHistory.errMsg.errAnalyzingFile={0}: Error while trying to analyze file:{1} diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Bundle.properties-MERGED b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Bundle.properties-MERGED index 5ea28d7eec..af0dbf306b 100755 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Bundle.properties-MERGED +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Bundle.properties-MERGED @@ -5,15 +5,10 @@ ChromeCacheExtract_adding_artifacts_msg=Chrome Cache: Adding %d artifacts for an ChromeCacheExtract_adding_extracted_files_msg=Chrome Cache: Adding %d extracted files for analysis. ChromeCacheExtract_loading_files_msg=Chrome Cache: Loading files from %s. ChromeCacheExtractor.moduleName=ChromeCacheExtractor -# {0} - module name -# {1} - row number -# {2} - table length -# {3} - cache path ChromeCacheExtractor.progressMsg={0}: Extracting cache entry {1} of {2} entries from {3} DataSourceUsage_AndroidMedia=Android Media Card DataSourceUsage_DJU_Drone_DAT=DJI Internal SD Card DataSourceUsage_FlashDrive=Flash Drive -# {0} - OS name DataSourceUsageAnalyzer.customVolume.label=OS Drive ({0}) DataSourceUsageAnalyzer.parentModuleName=Recent Activity Extract.indexError.message=Failed to index artifact for keyword search. @@ -77,10 +72,17 @@ ExtractZone_progress_Msg=Extracting :Zone.Identifer files ExtractZone_Restricted=Restricted Sites Zone ExtractZone_Trusted=Trusted Sites Zone OpenIDE-Module-Display-Category=Ingest Module -OpenIDE-Module-Long-Description=Recent Activity ingest module.\n\n\The module extracts useful information about the recent user activity on the disk image being ingested, such as:\n\n- Recently open documents,\n- Web activity (sites visited, stored cookies, book marked sites, search engine queries, file downloads),\n- Recently attached devices,\n- Installed programs.\n\nThe module currently supports Windows only disk images.\nThe plugin is also fully functional when deployed on Windows version of Autopsy. +OpenIDE-Module-Long-Description=Recent Activity ingest module.\n\nThe module extracts useful information about the recent user activity on the disk image being ingested, such as:\n\n- Recently open documents,\n- Web activity (sites visited, stored cookies, book marked sites, search engine queries, file downloads),\n- Recently attached devices,\n- Installed programs.\n\nThe module currently supports Windows only disk images.\nThe plugin is also fully functional when deployed on Windows version of Autopsy. OpenIDE-Module-Name=RecentActivity OpenIDE-Module-Short-Description=Recent Activity finder ingest module -Chrome.moduleName=Chrome +Browser.name.Microsoft.Edge=Microsoft Edge +Browser.name.Yandex=Yandex +Browser.name.Opera=Opera +Browser.name.SalamWeb=SalamWeb +Browser.name.UC.Browser=UC Browser +Browser.name.Brave=Brave +Browser.name.Google.Chrome=Google Chrome +Chrome.moduleName=Chromium Chrome.getHistory.errMsg.errGettingFiles=Error when trying to get Chrome history files. Chrome.getHistory.errMsg.couldntFindAnyFiles=Could not find any allocated Chrome history files. Chrome.getHistory.errMsg.errAnalyzingFile={0}: Error while trying to analyze file:{1} @@ -148,14 +150,14 @@ Firefox.getDlV24.errMsg.errAnalyzeFile={0}: Error while trying to analyze file:{ Firefox.getDlV24.errMsg.errParsingArtifacts={0}: Error parsing {1} Firefox web download artifacts. Progress_Message_Analyze_Registry=Analyzing Registry Files Progress_Message_Analyze_Usage=Data Sources Usage Analysis -Progress_Message_Chrome_AutoFill=Chrome Auto Fill -Progress_Message_Chrome_Bookmarks=Chrome Bookmarks +Progress_Message_Chrome_AutoFill=Chrome Auto Fill Browser {0} +Progress_Message_Chrome_Bookmarks=Chrome Bookmarks Browser {0} Progress_Message_Chrome_Cache=Chrome Cache -Progress_Message_Chrome_Cookies=Chrome Cookies -Progress_Message_Chrome_Downloads=Chrome Downloads +Progress_Message_Chrome_Cookies=Chrome Cookies Browser {0} +Progress_Message_Chrome_Downloads=Chrome Downloads Browser {0} Progress_Message_Chrome_FormHistory=Chrome Form History -Progress_Message_Chrome_History=Chrome History -Progress_Message_Chrome_Logins=Chrome Logins +Progress_Message_Chrome_History=Chrome History Browser {0} +Progress_Message_Chrome_Logins=Chrome Logins Browser {0} Progress_Message_Edge_Bookmarks=Microsoft Edge Bookmarks Progress_Message_Edge_Cookies=Microsoft Edge Cookies Progress_Message_Edge_History=Microsoft Edge History @@ -209,7 +211,6 @@ Recently_Used_Artifacts_Winrar=Recently opened according to WinRAR MRU Registry_System_Bam=Recently Executed according to Background Activity Moderator (BAM) RegRipperFullNotFound=Full version RegRipper executable not found. RegRipperNotFound=Autopsy RegRipper executable not found. -# {0} - file name SearchEngineURLQueryAnalyzer.init.exception.msg=Unable to find {0}. SearchEngineURLQueryAnalyzer.moduleName.text=Search Engine SearchEngineURLQueryAnalyzer.engineName.none=NONE diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Chrome.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Chromium.java similarity index 86% rename from RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Chrome.java rename to RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Chromium.java index 2c7c5dfcb0..51ce1e71c8 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Chrome.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Chromium.java @@ -22,6 +22,7 @@ */ package org.sleuthkit.autopsy.recentactivity; +import com.google.common.collect.ImmutableMap; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonIOException; @@ -58,9 +59,9 @@ import org.sleuthkit.datamodel.TskData; import org.sleuthkit.datamodel.blackboardutils.WebBrowserArtifactsHelper; /** - * Chrome recent activity extraction + * Chromium recent activity extraction */ -class Chrome extends Extract { +class Chromium extends Extract { private static final String HISTORY_QUERY = "SELECT urls.url, urls.title, urls.visit_count, urls.typed_count, " //NON-NLS + "last_visit_time, urls.hidden, visits.visit_time, (SELECT urls.url FROM urls WHERE urls.id=visits.url) AS from_visit, visits.transition FROM urls, visits WHERE urls.id = visits.url"; //NON-NLS @@ -83,20 +84,31 @@ class Chrome extends Extract { private final Logger logger = Logger.getLogger(this.getClass().getName()); private Content dataSource; private IngestJobContext context; + + private static final Map BROWSERS_MAP = ImmutableMap.builder() + .put(NbBundle.getMessage(Chromium.class, "Browser.name.Microsoft.Edge"), "Microsoft/Edge") + .put(NbBundle.getMessage(Chromium.class, "Browser.name.Yandex"), "YandexBrowser") + .put(NbBundle.getMessage(Chromium.class, "Browser.name.Opera"), "Opera Software") + .put(NbBundle.getMessage(Chromium.class, "Browser.name.SalamWeb"), "SalamWeb") + .put(NbBundle.getMessage(Chromium.class, "Browser.name.UC.Browser"), "UCBrowser") + .put(NbBundle.getMessage(Chromium.class, "Browser.name.Brave"), "BraveSoftware") + .put(NbBundle.getMessage(Chromium.class, "Browser.name.Google.Chrome"), "Chrome") + .build(); + @Messages({ - "Progress_Message_Chrome_History=Chrome History", - "Progress_Message_Chrome_Bookmarks=Chrome Bookmarks", - "Progress_Message_Chrome_Cookies=Chrome Cookies", - "Progress_Message_Chrome_Downloads=Chrome Downloads", + "Progress_Message_Chrome_History=Chrome History Browser {0}", + "Progress_Message_Chrome_Bookmarks=Chrome Bookmarks Browser {0}", + "Progress_Message_Chrome_Cookies=Chrome Cookies Browser {0}", + "Progress_Message_Chrome_Downloads=Chrome Downloads Browser {0}", "Progress_Message_Chrome_FormHistory=Chrome Form History", - "Progress_Message_Chrome_AutoFill=Chrome Auto Fill", - "Progress_Message_Chrome_Logins=Chrome Logins", + "Progress_Message_Chrome_AutoFill=Chrome Auto Fill Browser {0}", + "Progress_Message_Chrome_Logins=Chrome Logins Browser {0}", "Progress_Message_Chrome_Cache=Chrome Cache", }) - Chrome() { - moduleName = NbBundle.getMessage(Chrome.class, "Chrome.moduleName"); + Chromium() { + moduleName = NbBundle.getMessage(Chromium.class, "Chrome.moduleName"); } @Override @@ -105,55 +117,60 @@ class Chrome extends Extract { this.context = context; dataFound = false; - progressBar.progress(Bundle.Progress_Message_Chrome_History()); - this.getHistory(); - if (context.dataSourceIngestIsCancelled()) { - return; - } - - progressBar.progress(Bundle.Progress_Message_Chrome_Bookmarks()); - this.getBookmark(); - if (context.dataSourceIngestIsCancelled()) { - return; - } - - progressBar.progress(Bundle.Progress_Message_Chrome_Cookies()); - this.getCookie(); - if (context.dataSourceIngestIsCancelled()) { - return; - } - - progressBar.progress(Bundle.Progress_Message_Chrome_Logins()); - this.getLogins(); - if (context.dataSourceIngestIsCancelled()) { - return; - } - - progressBar.progress(Bundle.Progress_Message_Chrome_AutoFill()); - this.getAutofill(); - if (context.dataSourceIngestIsCancelled()) { - return; - } - - progressBar.progress(Bundle.Progress_Message_Chrome_Downloads()); - this.getDownload(); - if (context.dataSourceIngestIsCancelled()) { - return; + for (Map.Entry browser : BROWSERS_MAP.entrySet()) { + String browserName = browser.getKey(); + String browserLocation = browser.getValue(); + progressBar.progress(NbBundle.getMessage(this.getClass(), "Progress_Message_Chrome_History", browserName)); + this.getHistory(browser.getKey(), browser.getValue()); + if (context.dataSourceIngestIsCancelled()) { + return; + } + + progressBar.progress(NbBundle.getMessage(this.getClass(), "Progress_Message_Chrome_Bookmarks", browserName)); + this.getBookmark(browser.getKey(), browser.getValue()); + if (context.dataSourceIngestIsCancelled()) { + return; + } + + progressBar.progress(NbBundle.getMessage(this.getClass(), "Progress_Message_Chrome_Cookies", browserName)); + this.getCookie(browser.getKey(), browser.getValue()); + if (context.dataSourceIngestIsCancelled()) { + return; + } + + progressBar.progress(NbBundle.getMessage(this.getClass(), "Progress_Message_Chrome_Logins", browserName)); + this.getLogins(browser.getKey(), browser.getValue()); + if (context.dataSourceIngestIsCancelled()) { + return; + } + + progressBar.progress(NbBundle.getMessage(this.getClass(), "Progress_Message_Chrome_AutoFill", browserName)); + this.getAutofill(browser.getKey(), browser.getValue()); + if (context.dataSourceIngestIsCancelled()) { + return; + } + + progressBar.progress(NbBundle.getMessage(this.getClass(), "Progress_Message_Chrome_Downloads", browserName)); + this.getDownload(browser.getKey(), browser.getValue()); + if (context.dataSourceIngestIsCancelled()) { + return; + } } progressBar.progress(Bundle.Progress_Message_Chrome_Cache()); ChromeCacheExtractor chromeCacheExtractor = new ChromeCacheExtractor(dataSource, context, progressBar); chromeCacheExtractor.processCaches(); + } /** * Query for history databases and add artifacts */ - private void getHistory() { + private void getHistory(String browser, String browserLocation) { FileManager fileManager = currentCase.getServices().getFileManager(); List historyFiles; try { - historyFiles = fileManager.findFiles(dataSource, "History", "Chrome"); //NON-NLS + historyFiles = fileManager.findFiles(dataSource, "%History%", browserLocation); //NON-NLS } catch (TskCoreException ex) { String msg = NbBundle.getMessage(this.getClass(), "Chrome.getHistory.errMsg.errGettingFiles"); logger.log(Level.SEVERE, msg, ex); @@ -179,10 +196,11 @@ class Chrome extends Extract { dataFound = true; Collection bbartifacts = new ArrayList<>(); int j = 0; - while (j < historyFiles.size()) { - String temps = RAImageIngestModule.getRATempPath(currentCase, "chrome") + File.separator + historyFiles.get(j).getName() + j + ".db"; //NON-NLS - final AbstractFile historyFile = historyFiles.get(j++); - if (historyFile.getSize() == 0) { + while (j < allocatedHistoryFiles.size()) { + String temps = RAImageIngestModule.getRATempPath(currentCase, browser) + File.separator + allocatedHistoryFiles.get(j).getName() + j + ".db"; //NON-NLS + final AbstractFile historyFile = allocatedHistoryFiles.get(j++); + if ((historyFile.getSize() == 0) || (historyFile.getName().toLowerCase().contains("-slack")) + || (historyFile.getName().toLowerCase().contains("cache")) || (historyFile.getName().toLowerCase().contains("media"))) { continue; } try { @@ -223,8 +241,7 @@ class Chrome extends Extract { RecentActivityExtracterModuleFactory.getModuleName(), ((result.get("title").toString() != null) ? result.get("title").toString() : ""))); //NON-NLS bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_PROG_NAME, - RecentActivityExtracterModuleFactory.getModuleName(), - NbBundle.getMessage(this.getClass(), "Chrome.moduleName"))); + RecentActivityExtracterModuleFactory.getModuleName(), browser)); bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_DOMAIN, RecentActivityExtracterModuleFactory.getModuleName(), (NetworkUtils.extractDomain((result.get("url").toString() != null) ? result.get("url").toString() : "")))); //NON-NLS @@ -245,11 +262,11 @@ class Chrome extends Extract { /** * Search for bookmark files and make artifacts. */ - private void getBookmark() { + private void getBookmark(String browser, String browserLocation) { FileManager fileManager = currentCase.getServices().getFileManager(); List bookmarkFiles; try { - bookmarkFiles = fileManager.findFiles(dataSource, "Bookmarks", "Chrome"); //NON-NLS + bookmarkFiles = fileManager.findFiles(dataSource, "%Bookmarks%", browserLocation); //NON-NLS } catch (TskCoreException ex) { String msg = NbBundle.getMessage(this.getClass(), "Chrome.getBookmark.errMsg.errGettingFiles"); logger.log(Level.SEVERE, msg, ex); @@ -268,10 +285,12 @@ class Chrome extends Extract { while (j < bookmarkFiles.size()) { AbstractFile bookmarkFile = bookmarkFiles.get(j++); - if (bookmarkFile.getSize() == 0) { + if ((bookmarkFile.getSize() == 0) || (bookmarkFile.getName().toLowerCase().contains("-slack")) + || (bookmarkFile.getName().toLowerCase().contains("extras")) || (bookmarkFile.getName().toLowerCase().contains("log")) + || (bookmarkFile.getName().toLowerCase().contains("backup")) || (bookmarkFile.getName().toLowerCase().contains("visualized"))) { continue; } - String temps = RAImageIngestModule.getRATempPath(currentCase, "chrome") + File.separator + bookmarkFile.getName() + j + ".db"; //NON-NLS + String temps = RAImageIngestModule.getRATempPath(currentCase, browser) + File.separator + bookmarkFile.getName() + j + ".db"; //NON-NLS try { ContentUtils.writeToFile(bookmarkFile, new File(temps), context::dataSourceIngestIsCancelled); } catch (ReadContentInputStreamException ex) { @@ -359,8 +378,7 @@ class Chrome extends Extract { bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_DATETIME_CREATED, RecentActivityExtracterModuleFactory.getModuleName(), (date / 1000000) - Long.valueOf("11644473600"))); bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_PROG_NAME, - RecentActivityExtracterModuleFactory.getModuleName(), - NbBundle.getMessage(this.getClass(), "Chrome.moduleName"))); + RecentActivityExtracterModuleFactory.getModuleName(), browser)); bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_DOMAIN, RecentActivityExtracterModuleFactory.getModuleName(), domain)); bbart.addAttributes(bbattributes); @@ -381,12 +399,12 @@ class Chrome extends Extract { /** * Queries for cookie files and adds artifacts */ - private void getCookie() { + private void getCookie(String browser, String browserLocation) { FileManager fileManager = currentCase.getServices().getFileManager(); List cookiesFiles; try { - cookiesFiles = fileManager.findFiles(dataSource, "Cookies", "Chrome"); //NON-NLS + cookiesFiles = fileManager.findFiles(dataSource, "%Cookies%", browserLocation); //NON-NLS } catch (TskCoreException ex) { String msg = NbBundle.getMessage(this.getClass(), "Chrome.getCookie.errMsg.errGettingFiles"); logger.log(Level.SEVERE, msg, ex); @@ -404,10 +422,10 @@ class Chrome extends Extract { int j = 0; while (j < cookiesFiles.size()) { AbstractFile cookiesFile = cookiesFiles.get(j++); - if (cookiesFile.getSize() == 0) { + if ((cookiesFile.getSize() == 0) || (cookiesFile.getName().toLowerCase().contains("-slack"))) { continue; } - String temps = RAImageIngestModule.getRATempPath(currentCase, "chrome") + File.separator + cookiesFile.getName() + j + ".db"; //NON-NLS + String temps = RAImageIngestModule.getRATempPath(currentCase, browser) + File.separator + cookiesFile.getName() + j + ".db"; //NON-NLS try { ContentUtils.writeToFile(cookiesFile, new File(temps), context::dataSourceIngestIsCancelled); } catch (ReadContentInputStreamException ex) { @@ -447,8 +465,7 @@ class Chrome extends Extract { RecentActivityExtracterModuleFactory.getModuleName(), ((result.get("value").toString() != null) ? result.get("value").toString() : ""))); //NON-NLS bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_PROG_NAME, - RecentActivityExtracterModuleFactory.getModuleName(), - NbBundle.getMessage(this.getClass(), "Chrome.moduleName"))); + RecentActivityExtracterModuleFactory.getModuleName(), browser)); String domain = result.get("host_key").toString(); //NON-NLS domain = domain.replaceFirst("^\\.+(?!$)", ""); bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_DOMAIN, @@ -471,11 +488,11 @@ class Chrome extends Extract { /** * Queries for download files and adds artifacts */ - private void getDownload() { + private void getDownload(String browser, String browserLocation) { FileManager fileManager = currentCase.getServices().getFileManager(); List downloadFiles; try { - downloadFiles = fileManager.findFiles(dataSource, "History", "Chrome"); //NON-NLS + downloadFiles = fileManager.findFiles(dataSource, "%History%", browserLocation); //NON-NLS } catch (TskCoreException ex) { String msg = NbBundle.getMessage(this.getClass(), "Chrome.getDownload.errMsg.errGettingFiles"); logger.log(Level.SEVERE, msg, ex); @@ -493,10 +510,12 @@ class Chrome extends Extract { int j = 0; while (j < downloadFiles.size()) { AbstractFile downloadFile = downloadFiles.get(j++); - if (downloadFile.getSize() == 0) { + if ((downloadFile.getSize() == 0) || (downloadFile.getName().toLowerCase().contains("-slack")) + || (downloadFile.getName().toLowerCase().contains("cache"))) { continue; } - String temps = RAImageIngestModule.getRATempPath(currentCase, "chrome") + File.separator + downloadFile.getName() + j + ".db"; //NON-NLS + + String temps = RAImageIngestModule.getRATempPath(currentCase, browser) + File.separator + downloadFile.getName() + j + ".db"; //NON-NLS try { ContentUtils.writeToFile(downloadFile, new File(temps), context::dataSourceIngestIsCancelled); } catch (ReadContentInputStreamException ex) { @@ -552,8 +571,7 @@ class Chrome extends Extract { bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_DOMAIN, RecentActivityExtracterModuleFactory.getModuleName(), domain)); bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_PROG_NAME, - RecentActivityExtracterModuleFactory.getModuleName(), - NbBundle.getMessage(this.getClass(), "Chrome.moduleName"))); + RecentActivityExtracterModuleFactory.getModuleName(), browser)); BlackboardArtifact webDownloadArtifact = createArtifactWithAttributes(ARTIFACT_TYPE.TSK_WEB_DOWNLOAD, downloadFile, bbattributes); if (webDownloadArtifact != null) { @@ -561,7 +579,7 @@ class Chrome extends Extract { // find the downloaded file and create a TSK_ASSOCIATED_OBJECT for it, associating it with the TSK_WEB_DOWNLOAD artifact. try { - String normalizedFullPath = FilenameUtils.normalize(fullPath, true); + String normalizedFullPath = FilenameUtils.normalize(fullPath, true); for (AbstractFile downloadedFile : fileManager.findFiles(dataSource, FilenameUtils.getName(normalizedFullPath), FilenameUtils.getPath(normalizedFullPath))) { BlackboardArtifact associatedObjectArtifact = downloadedFile.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_ASSOCIATED_OBJECT); associatedObjectArtifact.addAttribute( @@ -588,12 +606,12 @@ class Chrome extends Extract { /** * Gets user logins from Login Data sqlite database */ - private void getLogins() { + private void getLogins(String browser, String browserLocation) { FileManager fileManager = currentCase.getServices().getFileManager(); List loginDataFiles; try { - loginDataFiles = fileManager.findFiles(dataSource, "Login Data", "Chrome"); //NON-NLS + loginDataFiles = fileManager.findFiles(dataSource, "%Login Data%", browserLocation); //NON-NLS } catch (TskCoreException ex) { String msg = NbBundle.getMessage(this.getClass(), "Chrome.getLogin.errMsg.errGettingFiles"); logger.log(Level.SEVERE, msg, ex); @@ -611,10 +629,10 @@ class Chrome extends Extract { int j = 0; while (j < loginDataFiles.size()) { AbstractFile loginDataFile = loginDataFiles.get(j++); - if (loginDataFile.getSize() == 0) { + if ((loginDataFile.getSize() == 0) || (loginDataFile.getName().toLowerCase().contains("-slack"))) { continue; } - String temps = RAImageIngestModule.getRATempPath(currentCase, "chrome") + File.separator + loginDataFile.getName() + j + ".db"; //NON-NLS + String temps = RAImageIngestModule.getRATempPath(currentCase, browser) + File.separator + loginDataFile.getName() + j + ".db"; //NON-NLS try { ContentUtils.writeToFile(loginDataFile, new File(temps), context::dataSourceIngestIsCancelled); } catch (ReadContentInputStreamException ex) { @@ -661,6 +679,9 @@ class Chrome extends Extract { RecentActivityExtracterModuleFactory.getModuleName(), ((result.get("signon_realm").toString() != null) ? result.get("signon_realm").toString() : ""))); //NON-NLS + bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_PROG_NAME, + RecentActivityExtracterModuleFactory.getModuleName(), browser)); + BlackboardArtifact bbart = createArtifactWithAttributes(ARTIFACT_TYPE.TSK_SERVICE_ACCOUNT, loginDataFile, bbattributes); if (bbart != null) { bbartifacts.add(bbart); @@ -679,12 +700,12 @@ class Chrome extends Extract { * Gets and parses Autofill data from 'Web Data' database, * and creates TSK_WEB_FORM_AUTOFILL, TSK_WEB_FORM_ADDRESS artifacts */ - private void getAutofill() { + private void getAutofill(String browser, String browserLocation) { FileManager fileManager = currentCase.getServices().getFileManager(); List webDataFiles; try { - webDataFiles = fileManager.findFiles(dataSource, "Web Data", "Chrome"); //NON-NLS + webDataFiles = fileManager.findFiles(dataSource, "%Web Data%", browserLocation); //NON-NLS } catch (TskCoreException ex) { String msg = NbBundle.getMessage(this.getClass(), "Chrome.getAutofills.errMsg.errGettingFiles"); logger.log(Level.SEVERE, msg, ex); @@ -702,10 +723,10 @@ class Chrome extends Extract { int j = 0; while (j < webDataFiles.size()) { AbstractFile webDataFile = webDataFiles.get(j++); - if (webDataFile.getSize() == 0) { + if ((webDataFile.getSize() == 0) || (webDataFile.getName().toLowerCase().contains("-slack"))) { continue; } - String tempFilePath = RAImageIngestModule.getRATempPath(currentCase, "chrome") + File.separator + webDataFile.getName() + j + ".db"; //NON-NLS + String tempFilePath = RAImageIngestModule.getRATempPath(currentCase, browser) + File.separator + webDataFile.getName() + j + ".db"; //NON-NLS try { ContentUtils.writeToFile(webDataFile, new File(tempFilePath), context::dataSourceIngestIsCancelled); } catch (ReadContentInputStreamException ex) { @@ -731,7 +752,7 @@ class Chrome extends Extract { boolean isSchemaV8X = Util.checkColumn("date_created", "autofill", tempFilePath); // get form autofill artifacts - bbartifacts.addAll(getFormAutofillArtifacts(webDataFile, tempFilePath, isSchemaV8X)); + bbartifacts.addAll(getFormAutofillArtifacts(webDataFile, tempFilePath, isSchemaV8X, browser)); try { // get form address atifacts getFormAddressArtifacts(webDataFile, tempFilePath, isSchemaV8X); @@ -757,7 +778,7 @@ class Chrome extends Extract { * * @return collection of TSK_WEB_FORM_AUTOFILL artifacts */ - private Collection getFormAutofillArtifacts (AbstractFile webDataFile, String dbFilePath , boolean isSchemaV8X ) { + private Collection getFormAutofillArtifacts (AbstractFile webDataFile, String dbFilePath , boolean isSchemaV8X, String browser ) { Collection bbartifacts = new ArrayList<>(); @@ -783,7 +804,7 @@ class Chrome extends Extract { RecentActivityExtracterModuleFactory.getModuleName(), (Integer.valueOf(result.get("count").toString())))); //NON-NLS - bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_DATETIME_CREATED, + bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_DATETIME_CREATED, RecentActivityExtracterModuleFactory.getModuleName(), Long.valueOf(result.get("date_created").toString()))); //NON-NLS @@ -794,6 +815,9 @@ class Chrome extends Extract { Long.valueOf(result.get("date_last_used").toString()))); //NON-NLS } + bbattributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_PROG_NAME, + RecentActivityExtracterModuleFactory.getModuleName(), browser)); + // Add an artifact BlackboardArtifact bbart = createArtifactWithAttributes(ARTIFACT_TYPE.TSK_WEB_FORM_AUTOFILL, webDataFile, bbattributes); if (bbart != null) { @@ -886,7 +910,7 @@ class Chrome extends Extract { use_count, otherAttributes); } } - + private boolean isChromePreVersion30(String temps) { String query = "PRAGMA table_info(downloads)"; //NON-NLS List> columns = this.dbConnect(temps, query); diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractRegistry.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractRegistry.java index fd3584d843..20acab7a87 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractRegistry.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ExtractRegistry.java @@ -83,9 +83,12 @@ import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_OS_AC import org.sleuthkit.datamodel.BlackboardAttribute; import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT; import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_COMMENT; +import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME; import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_ACCESSED; import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_CREATED; import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_MODIFIED; +import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DEVICE_ID; +import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME; import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH; import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_USER_ID; import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_USER_NAME; @@ -353,12 +356,10 @@ class ExtractRegistry extends Extract { } catch (IOException | TskCoreException ex) { logger.log(Level.WARNING, String.format("Unable to get shell bags from file %s", regOutputFiles.fullPlugins), ex); } - } else if (regFileNameLocal.toLowerCase().contains("system")) { - try { - createSystemArtifacts(regOutputFiles.fullPlugins, regFile); - } catch (IOException ex) { - logger.log(Level.WARNING, String.format("Unable to get artifacts from file %s", regOutputFiles.fullPlugins), ex); - } + } else if (regFileNameLocal.toLowerCase().contains("system") && parseSystemPluginOutput(regOutputFiles.fullPlugins, regFile) == false) { + this.addErrorMessage( + NbBundle.getMessage(this.getClass(), "ExtractRegistry.analyzeRegFiles.failedParsingResults", + this.getName(), regFileName)); } try { Report report = currentCase.addReport(regOutputFiles.fullPlugins, @@ -952,6 +953,119 @@ class ExtractRegistry extends Extract { return false; } + + private boolean parseSystemPluginOutput(String regfilePath, AbstractFile regAbstractFile) { + File regfile = new File(regfilePath); + try (BufferedReader reader = new BufferedReader(new FileReader(regfile))) { + String line = reader.readLine(); + while (line != null) { + line = line.trim(); + + if (line.toLowerCase().matches("^bam v.*")) { + parseBamKey(regAbstractFile, reader, Bundle.Registry_System_Bam()); + } else if (line.toLowerCase().matches("^bthport v..*")) { + parseBlueToothDevices(regAbstractFile, reader); + } + line = reader.readLine(); + } + return true; + } catch (FileNotFoundException ex) { + logger.log(Level.WARNING, "Error finding the registry file.", ex); //NON-NLS + } catch (IOException ex) { + logger.log(Level.WARNING, "Error reading the system hive: {0}", ex); //NON-NLS + } + + return false; + + } + + /** + * Create recently used artifacts to parse the regripper plugin output, this + * format is used in several diffent plugins + * + * @param regFile registry file the artifact is associated with + * + * @param reader buffered reader to parse adobemru records + * + * @param comment string that will populate attribute TSK_COMMENT + * + * @throws FileNotFound and IOException + */ + private void parseBlueToothDevices(AbstractFile regFile, BufferedReader reader) throws FileNotFoundException, IOException { + List bbartifacts = new ArrayList<>(); + String line = reader.readLine(); + while ((line != null) && (!line.contains(SECTION_DIVIDER))) { + line = reader.readLine(); + + if (line != null) { + line = line.trim(); + } + + if ((line != null) && (line.toLowerCase().contains("device unique id"))) { + // Columns are seperated by colons : + // Data : Values + // Record is 4 lines in length (Device Unique Id, Name, Last Seen, LastConnected + while (line != null && !line.contains(SECTION_DIVIDER) && !line.isEmpty() && !line.toLowerCase().contains("radio support not found")) { + Collection attributes = new ArrayList<>(); + addBlueToothAttribute(line, attributes, TSK_DEVICE_ID); + line = reader.readLine(); + // Name may not exist, check for it to make sure. + if ((line != null) && (line.toLowerCase().contains("name"))) { + addBlueToothAttribute(line, attributes, TSK_NAME); + line = reader.readLine(); + } + addBlueToothAttribute(line, attributes, TSK_DATETIME); + line = reader.readLine(); + addBlueToothAttribute(line, attributes, TSK_DATETIME_ACCESSED); + BlackboardArtifact bba = createArtifactWithAttributes(ARTIFACT_TYPE.TSK_BLUETOOTH_PAIRING, regFile, attributes); + if(bba != null) { + bbartifacts.add(bba); + } + // Read blank line between records then next read line is start of next block + reader.readLine(); + line = reader.readLine(); + } + + if (line != null) { + line = line.trim(); + } + } + } + if (!bbartifacts.isEmpty()) { + postArtifacts(bbartifacts); + } + } + + + private void addBlueToothAttribute(String line, Collection attributes, ATTRIBUTE_TYPE attributeType) { + if (line == null) { + return; + } + + String tokens[] = line.split(": "); + if (tokens.length > 1 && !tokens[1].isEmpty()) { + String tokenString = tokens[1]; + if (attributeType.getDisplayName().toLowerCase().contains("date")) { + String dateString = tokenString.toLowerCase().replace(" z", ""); + // date format for plugin Tue Jun 23 10:27:54 2020 Z + SimpleDateFormat dateFormat = new SimpleDateFormat("EEE MMM d HH:mm:ss yyyy", US); + Long dateLong = Long.valueOf(0); + try { + Date newDate = dateFormat.parse(dateString); + dateLong = newDate.getTime()/1000; + } catch (ParseException ex) { + // catching error and displaying date that could not be parsed + // we set the timestamp to 0 and continue on processing + logger.log(Level.WARNING, String.format("Failed to parse date/time %s for Bluetooth Last Seen attribute.", dateString), ex); //NON-NLS + } + attributes.add(new BlackboardAttribute(attributeType, getName(), dateLong)); + } else { + attributes.add(new BlackboardAttribute(attributeType, getName(), tokenString)); + } + } +} + + /** * Parse the output of the SAM regripper plugin to get additional Account * information @@ -1280,30 +1394,6 @@ class ExtractRegistry extends Extract { } } - /** - * Create artifacts from the System registry Hive - * - * @param regFileName name of the regripper output file - * - * @param regFile registry file the artifact is associated with - * - * @throws FileNotFound and IOException - */ - private void createSystemArtifacts(String regFileName, AbstractFile regFile) throws FileNotFoundException, IOException { - File regfile = new File(regFileName); - try (BufferedReader reader = new BufferedReader(new FileReader(regfile))) { - String line = reader.readLine(); - while (line != null) { - line = line.trim(); - - if (line.matches("^bam v.*")) { - parseBamKey(regFile, reader, Bundle.Registry_System_Bam()); - } - line = reader.readLine(); - } - } - } - /** * Create artifacts from BAM Regripper Plugin records * @@ -2031,4 +2121,4 @@ class ExtractRegistry extends Extract { public String autopsyPlugins = ""; public String fullPlugins = ""; } -} \ No newline at end of file +} diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/RAImageIngestModule.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/RAImageIngestModule.java index e9519abf13..488448d434 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/RAImageIngestModule.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/RAImageIngestModule.java @@ -70,7 +70,7 @@ public final class RAImageIngestModule implements DataSourceIngestModule { Extract registry = new ExtractRegistry(); Extract recentDocuments = new RecentDocumentsByLnk(); - Extract chrome = new Chrome(); + Extract chrome = new Chromium(); Extract firefox = new Firefox(); Extract SEUQA = new SearchEngineURLQueryAnalyzer(); Extract osExtract = new ExtractOs(); diff --git a/thirdparty/rr-full/plugins/system b/thirdparty/rr-full/plugins/system index 568b92c648..37c447961a 100644 --- a/thirdparty/rr-full/plugins/system +++ b/thirdparty/rr-full/plugins/system @@ -5,6 +5,7 @@ auditfail backuprestore bam bam_tln +btconfig bthport comfoo compname