diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java b/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java index 9b71d51b06..abd2edbccf 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/CaseMetadata.java @@ -19,6 +19,7 @@ package org.sleuthkit.autopsy.casemodule; import java.io.BufferedWriter; +import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; @@ -189,6 +190,27 @@ public final class CaseMetadata { this.metadataFilePath = metadataFilePath; readFromFile(); } + + /** + * Locate the case meta data file in the supplied directory. If the file does + * not exist, null is returned. + * + * @param directoryPath Directory path to search + * @return case meta data file path or null + */ + public static Path getCaseMetadataFile(Path directoryPath) { + final File[] caseFiles = directoryPath.toFile().listFiles(); + if(caseFiles != null) { + for (File file : caseFiles) { + final String fileName = file.getName().toLowerCase(); + if (fileName.endsWith(CaseMetadata.getFileExtension())) { + return file.toPath(); + } + } + } + + return null; + } /** * Gets the full path to the case metadata file. diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/Bundle.properties-MERGED index 3734fc5a3e..fc30931457 100755 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/Bundle.properties-MERGED @@ -9,13 +9,13 @@ DataContentViewerOtherCases.correlatedArtifacts.byType={0}% of data sources have DataContentViewerOtherCases.correlatedArtifacts.failed=Failed to get frequency details. DataContentViewerOtherCases.correlatedArtifacts.isEmpty=There are no files or artifacts to correlate. DataContentViewerOtherCases.correlatedArtifacts.title=Attribute Frequency +DataContentViewerOtherCases.dataSources.header.text=Data Source Name DataContentViewerOtherCases.earliestCaseNotAvailable=\ Not Enabled. DataContentViewerOtherCases.foundIn.text=Found %d instances in %d cases and %d data sources. DataContentViewerOtherCases.noOpenCase.errMsg=No open case available. DataContentViewerOtherCases.selectAllMenuItem.text=Select All DataContentViewerOtherCases.showCaseDetailsMenuItem.text=Show Case Details DataContentViewerOtherCases.table.noArtifacts=Item has no attributes with which to search. -DataContentViewerOtherCases.table.nodbconnection=Cannot connect to central repository database. DataContentViewerOtherCases.table.noResultsFound=No results found. DataContentViewerOtherCases.table.toolTip.text=Click column name to sort. Right-click on the table for more options. DataContentViewerOtherCases.exportToCSVMenuItem.text=Export Selected Rows to CSV @@ -26,12 +26,13 @@ DataContentViewerOtherCases.earliestCaseLabel.text=Central Repository Starting D DataContentViewerOtherCases.foundInLabel.text= DataContentViewerOtherCases.title=Other Occurrences DataContentViewerOtherCases.toolTip=Displays instances of the selected file/artifact from other occurrences. -DataContentViewerOtherCasesTableModel.attribute=Matched Attribute -DataContentViewerOtherCasesTableModel.case=Case -DataContentViewerOtherCasesTableModel.comment=Comment -DataContentViewerOtherCasesTableModel.dataSource=Data Source -DataContentViewerOtherCasesTableModel.device=Device -DataContentViewerOtherCasesTableModel.known=Known -DataContentViewerOtherCasesTableModel.noData=No Data. -DataContentViewerOtherCasesTableModel.path=Path -DataContentViewerOtherCasesTableModel.value=Attribute Value +OtherOccurrencesCasesTableModel.case=Case +OtherOccurrencesCasesTableModel.noData=No Data. +OtherOccurrencesFilesTableModel.attribute=Matched Attribute +OtherOccurrencesFilesTableModel.comment=Comment +OtherOccurrencesFilesTableModel.dataSource=Data Source +OtherOccurrencesFilesTableModel.device=Device +OtherOccurrencesFilesTableModel.known=Known +OtherOccurrencesFilesTableModel.noData=No Data. +OtherOccurrencesFilesTableModel.path=Path +OtherOccurrencesFilesTableModel.value=Attribute Value diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/CorrelationCaseWrapper.java b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/CorrelationCaseWrapper.java new file mode 100644 index 0000000000..cee853eadb --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/CorrelationCaseWrapper.java @@ -0,0 +1,60 @@ +/* + * Central Repository + * + * Copyright 2019 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.centralrepository.contentviewer; + +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationCase; + +/** + * Class to wrap CorrelationCases or a text message + */ +class CorrelationCaseWrapper { + + private final CorrelationCase corCase; + private final String message; + + CorrelationCaseWrapper(CorrelationCase corrCase) { + corCase = corrCase; + message = corrCase.getDisplayName(); + } + + CorrelationCaseWrapper(String msg) { + corCase = null; + message = msg; + } + + /** + * Get the correlation case this is wrapping or null if it only has a + * message. + * + * @return CorrelationCase or Null + */ + CorrelationCase getCorrelationCase() { + return corCase; + } + + /** + * Get the message this is wrapping, if a correlation case is being wrapped + * this will be it's display name. + * + * @return the message or Correlation Case display name + */ + String getMessage() { + return message; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.form b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.form index 53f6f6fc4b..9b30385d0f 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.form +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.form @@ -68,12 +68,12 @@ - + - + @@ -81,25 +81,28 @@ - + - + - + + + + - + - + @@ -117,67 +120,37 @@ - - - - - - - - + + + + + + + + + + + + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -202,6 +175,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.java b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.java index 308e14be63..34254f902b 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCases.java @@ -19,7 +19,6 @@ package org.sleuthkit.autopsy.centralrepository.contentviewer; import java.awt.Component; -import java.awt.FontMetrics; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.BufferedWriter; @@ -48,9 +47,8 @@ import static javax.swing.JOptionPane.DEFAULT_OPTION; import static javax.swing.JOptionPane.PLAIN_MESSAGE; import static javax.swing.JOptionPane.ERROR_MESSAGE; import javax.swing.JPanel; -import javax.swing.RowSorter; -import javax.swing.SortOrder; import javax.swing.filechooser.FileNameExtensionFilter; +import javax.swing.table.DefaultTableModel; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import javax.swing.table.TableModel; @@ -86,18 +84,23 @@ import org.sleuthkit.datamodel.TskData; @SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives @ServiceProvider(service = DataContentViewer.class, position = 9) @Messages({"DataContentViewerOtherCases.title=Other Occurrences", - "DataContentViewerOtherCases.toolTip=Displays instances of the selected file/artifact from other occurrences.",}) + "DataContentViewerOtherCases.toolTip=Displays instances of the selected file/artifact from other occurrences.", + "DataContentViewerOtherCases.table.noArtifacts=Item has no attributes with which to search.", + "DataContentViewerOtherCases.table.noResultsFound=No results found."}) public class DataContentViewerOtherCases extends JPanel implements DataContentViewer { private static final long serialVersionUID = -1L; private static final Logger LOGGER = Logger.getLogger(DataContentViewerOtherCases.class.getName()); - + private static final CorrelationCaseWrapper NO_ARTIFACTS_CASE = new CorrelationCaseWrapper(Bundle.DataContentViewerOtherCases_table_noArtifacts()); + private static final CorrelationCaseWrapper NO_RESULTS_CASE = new CorrelationCaseWrapper(Bundle.DataContentViewerOtherCases_table_noArtifacts()); private static final int DEFAULT_MIN_CELL_WIDTH = 15; - private static final int CELL_TEXT_WIDTH_PADDING = 5; - private final DataContentViewerOtherCasesTableModel tableModel; + private final OtherOccurrencesFilesTableModel tableModel; + private final OtherOccurrencesCasesTableModel casesTableModel; private final Collection correlationAttributes; + private String dataSourceName = ""; + private String deviceId = ""; /** * Could be null. */ @@ -107,7 +110,8 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi * Creates new form DataContentViewerOtherCases */ public DataContentViewerOtherCases() { - this.tableModel = new DataContentViewerOtherCasesTableModel(); + this.tableModel = new OtherOccurrencesFilesTableModel(); + this.casesTableModel = new OtherOccurrencesCasesTableModel(); this.correlationAttributes = new ArrayList<>(); initComponents(); @@ -121,9 +125,9 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi public void actionPerformed(ActionEvent e) { JMenuItem jmi = (JMenuItem) e.getSource(); if (jmi.equals(selectAllMenuItem)) { - otherCasesTable.selectAll(); + filesTable.selectAll(); } else if (jmi.equals(showCaseDetailsMenuItem)) { - showCaseDetails(otherCasesTable.getSelectedRow()); + showCaseDetails(filesTable.getSelectedRow()); } else if (jmi.equals(exportToCSVMenuItem)) { try { saveToCSV(); @@ -142,22 +146,24 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi showCommonalityMenuItem.addActionListener(actList); // Set background of every nth row as light grey. - TableCellRenderer renderer = new DataContentViewerOtherCasesTableCellRenderer(); - otherCasesTable.setDefaultRenderer(Object.class, renderer); - + TableCellRenderer renderer = new OtherOccurrencesFilesTableCellRenderer(); + filesTable.setDefaultRenderer(Object.class, renderer); + // Configure column sorting. - TableRowSorter sorter = new TableRowSorter<>(otherCasesTable.getModel()); - otherCasesTable.setRowSorter(sorter); - List sortKeys = new ArrayList<>(); - - int caseNameColumnIndex = DataContentViewerOtherCasesTableModel.TableColumns.CASE_NAME.ordinal(); - sortKeys.add(new RowSorter.SortKey(caseNameColumnIndex, SortOrder.ASCENDING)); - - int dataSourceColumnIndex = DataContentViewerOtherCasesTableModel.TableColumns.DATA_SOURCE.ordinal(); - sortKeys.add(new RowSorter.SortKey(dataSourceColumnIndex, SortOrder.ASCENDING)); - - sorter.setSortKeys(sortKeys); - sorter.sort(); + TableRowSorter sorter = new TableRowSorter<>(filesTable.getModel()); + filesTable.setRowSorter(sorter); + casesTable.getSelectionModel().addListSelectionListener((e) -> { + if (Case.isCaseOpen()) { + updateOnCaseSelection(); + } + }); + dataSourcesTable.getSelectionModel().addListSelectionListener((e) -> { + if (Case.isCaseOpen()) { + updateOnDataSourceSelection(); + } + }); + casesTable.getRowSorter().toggleSortOrder(0); + dataSourcesTable.getRowSorter().toggleSortOrder(0); } @Messages({"DataContentViewerOtherCases.correlatedArtifacts.isEmpty=There are no files or artifacts to correlate.", @@ -217,7 +223,7 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi try { if (-1 != selectedRowViewIdx) { EamDb dbManager = EamDb.getInstance(); - int selectedRowModelIdx = otherCasesTable.convertRowIndexToModel(selectedRowViewIdx); + int selectedRowModelIdx = filesTable.convertRowIndexToModel(selectedRowViewIdx); OtherOccurrenceNodeInstanceData nodeData = (OtherOccurrenceNodeInstanceData) tableModel.getRow(selectedRowModelIdx); CorrelationCase eamCasePartial = nodeData.getCorrelationAttributeInstance().getCorrelationCase(); if (eamCasePartial == null) { @@ -259,14 +265,14 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi } private void saveToCSV() throws NoCurrentCaseException { - if (0 != otherCasesTable.getSelectedRowCount()) { + if (0 != filesTable.getSelectedRowCount()) { Calendar now = Calendar.getInstance(); String fileName = String.format("%1$tY%1$tm%1$te%1$tI%1$tM%1$tS_other_data_sources.csv", now); CSVFileChooser.setCurrentDirectory(new File(Case.getCurrentCaseThrows().getExportDirectory())); CSVFileChooser.setSelectedFile(new File(fileName)); CSVFileChooser.setFileFilter(new FileNameExtensionFilter("csv file", "csv")); - int returnVal = CSVFileChooser.showSaveDialog(otherCasesTable); + int returnVal = CSVFileChooser.showSaveDialog(filesTable); if (returnVal == JFileChooser.APPROVE_OPTION) { File selectedFile = CSVFileChooser.getSelectedFile(); @@ -281,7 +287,7 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi private void writeSelectedRowsToFileAsCSV(File destFile) { StringBuilder content; - int[] selectedRowViewIndices = otherCasesTable.getSelectedRows(); + int[] selectedRowViewIndices = filesTable.getSelectedRows(); int colCount = tableModel.getColumnCount(); try (BufferedWriter writer = Files.newBufferedWriter(destFile.toPath())) { @@ -302,7 +308,7 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi for (int rowViewIdx : selectedRowViewIndices) { content = new StringBuilder(""); for (int colIdx = 0; colIdx < colCount; colIdx++) { - int rowModelIdx = otherCasesTable.convertRowIndexToModel(rowViewIdx); + int rowModelIdx = filesTable.convertRowIndexToModel(rowViewIdx); content.append('"').append(tableModel.getValueAt(rowModelIdx, colIdx)).append('"'); if (colIdx < (colCount - 1)) { content.append(","); @@ -322,6 +328,8 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi */ private void reset() { // start with empty table + casesTableModel.clearTable(); + ((DefaultTableModel) dataSourcesTable.getModel()).setRowCount(0); tableModel.clearTable(); correlationAttributes.clear(); earliestCaseDate.setText(Bundle.DataContentViewerOtherCases_earliestCaseNotAvailable()); @@ -357,40 +365,6 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi public int isPreferred(Node node) { return 1; } - - /** - * Set the number of unique cases and data sources. - */ - @Messages({ - "DataContentViewerOtherCases.foundIn.text=Found %d instances in %d cases and %d data sources." - }) - private void setOccurrenceCounts() { - DataContentViewerOtherCasesTableModel model = (DataContentViewerOtherCasesTableModel) otherCasesTable.getModel(); - - int caseColumnIndex = DataContentViewerOtherCasesTableModel.TableColumns.CASE_NAME.ordinal(); - int deviceColumnIndex = DataContentViewerOtherCasesTableModel.TableColumns.DEVICE.ordinal(); - - /* - * We need a unique set of data sources. We rely on device ID for this. - * To mitigate edge cases where a device ID could be duplicated in the - * same case (e.g. "report.xml"), we put the device ID and case name in - * a key-value pair. - * - * Note: Relying on the case name isn't a fool-proof way of determining - * a case to be unique. We should improve this in the future. - */ - Set cases = new HashSet<>(); - Map devices = new HashMap<>(); - - for (int i=0; i < model.getRowCount(); i++) { - String caseName = (String) model.getValueAt(i, caseColumnIndex); - String deviceId = (String) model.getValueAt(i, deviceColumnIndex); - cases.add(caseName); - devices.put(deviceId, caseName); - } - - foundInLabel.setText(String.format(Bundle.DataContentViewerOtherCases_foundIn_text(), model.getRowCount(), cases.size(), devices.size())); - } /** * Get the associated BlackboardArtifact from a node, if it exists. @@ -399,9 +373,12 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi * * @return The associated BlackboardArtifact, or null */ - private BlackboardArtifact getBlackboardArtifactFromNode(Node node) { - BlackboardArtifactTag nodeBbArtifactTag = node.getLookup().lookup(BlackboardArtifactTag.class); - BlackboardArtifact nodeBbArtifact = node.getLookup().lookup(BlackboardArtifact.class); + private BlackboardArtifact + getBlackboardArtifactFromNode(Node node) { + BlackboardArtifactTag nodeBbArtifactTag = node.getLookup().lookup(BlackboardArtifactTag.class + ); + BlackboardArtifact nodeBbArtifact = node.getLookup().lookup(BlackboardArtifact.class + ); if (nodeBbArtifactTag != null) { return nodeBbArtifactTag.getArtifact(); @@ -410,6 +387,7 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi } return null; + } /** @@ -420,10 +398,14 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi * @return The associated AbstractFile, or null */ private AbstractFile getAbstractFileFromNode(Node node) { - BlackboardArtifactTag nodeBbArtifactTag = node.getLookup().lookup(BlackboardArtifactTag.class); - ContentTag nodeContentTag = node.getLookup().lookup(ContentTag.class); - BlackboardArtifact nodeBbArtifact = node.getLookup().lookup(BlackboardArtifact.class); - AbstractFile nodeAbstractFile = node.getLookup().lookup(AbstractFile.class); + BlackboardArtifactTag nodeBbArtifactTag = node.getLookup().lookup(BlackboardArtifactTag.class + ); + ContentTag nodeContentTag = node.getLookup().lookup(ContentTag.class + ); + BlackboardArtifact nodeBbArtifact = node.getLookup().lookup(BlackboardArtifact.class + ); + AbstractFile nodeAbstractFile = node.getLookup().lookup(AbstractFile.class + ); if (nodeBbArtifactTag != null) { Content content = nodeBbArtifactTag.getContent(); @@ -489,7 +471,7 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi CorrelationDataSource.fromTSKDataSource(corCase, file.getDataSource()), file.getParentPath() + file.getName(), "", - file.getKnown(), + file.getKnown(), file.getId())); } catch (CorrelationAttributeNormalizationException ex) { LOGGER.log(Level.INFO, String.format("Unable to check create CorrelationAttribtueInstance for value %s and type %s.", md5, aType.toString()), ex); @@ -574,7 +556,7 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi private Map getCorrelatedInstances(CorrelationAttributeInstance corAttr, String dataSourceName, String deviceId) { // @@@ Check exception try { - final Case openCase = Case.getCurrentCase(); + final Case openCase = Case.getCurrentCaseThrows(); String caseUUID = openCase.getName(); HashMap nodeDataMap = new HashMap<>(); @@ -709,14 +691,13 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi if (EamDb.isEnabled()) { return !getCorrelationAttributesFromNode(node).isEmpty(); } else { - return this.file != null + return this.file != null && this.file.getSize() > 0 && ((this.file.getMd5Hash() != null) && (!this.file.getMd5Hash().isEmpty())); } } @Override - @Messages({"DataContentViewerOtherCases.table.nodbconnection=Cannot connect to central repository database."}) public void setNode(Node node) { reset(); // reset the table to empty. @@ -726,6 +707,7 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi //could be null this.file = this.getAbstractFileFromNode(node); populateTable(node); + } /** @@ -735,12 +717,10 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi * @param node The node being viewed. */ @Messages({ - "DataContentViewerOtherCases.table.noArtifacts=Item has no attributes with which to search.", - "DataContentViewerOtherCases.table.noResultsFound=No results found." + "DataContentViewerOtherCases.dataSources.header.text=Data Source Name", + "DataContentViewerOtherCases.foundIn.text=Found %d instances in %d cases and %d data sources." }) private void populateTable(Node node) { - String dataSourceName = ""; - String deviceId = ""; try { if (this.file != null) { Content dataSource = this.file.getDataSource(); @@ -754,43 +734,129 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi // get the attributes we can correlate on correlationAttributes.addAll(getCorrelationAttributesFromNode(node)); + Map caseNames = new HashMap<>(); + int totalCount = 0; + Set dataSources = new HashSet<>(); for (CorrelationAttributeInstance corAttr : correlationAttributes) { Map correlatedNodeDataMap = new HashMap<>(0); // get correlation and reference set instances from DB correlatedNodeDataMap.putAll(getCorrelatedInstances(corAttr, dataSourceName, deviceId)); - - correlatedNodeDataMap.values().forEach((nodeData) -> { - tableModel.addNodeData(nodeData); - }); + for (OtherOccurrenceNodeInstanceData nodeData : correlatedNodeDataMap.values()) { + if (nodeData.isCentralRepoNode()) { + try { + dataSources.add(makeDataSourceString(nodeData.getCorrelationAttributeInstance().getCorrelationCase().getCaseUUID(), nodeData.getDeviceID(), nodeData.getDataSourceName())); + caseNames.put(nodeData.getCorrelationAttributeInstance().getCorrelationCase().getCaseUUID(), nodeData.getCorrelationAttributeInstance().getCorrelationCase()); + } catch (EamDbException ex) { + LOGGER.log(Level.WARNING, "Unable to get correlation case for displaying other occurrence for case: " + nodeData.getCaseName()); + } + } else { + try { + dataSources.add(makeDataSourceString(Case.getCurrentCaseThrows().getName(), nodeData.getDeviceID(), nodeData.getDataSourceName())); + caseNames.put(Case.getCurrentCaseThrows().getName(), new CorrelationCase(Case.getCurrentCaseThrows().getName(), Case.getCurrentCaseThrows().getDisplayName())); + } catch (NoCurrentCaseException ex) { + LOGGER.log(Level.WARNING, "No current case open for other occurrences"); + } + } + totalCount++; + } } - + for (CorrelationCase corCase : caseNames.values()) { + casesTableModel.addCorrelationCase(new CorrelationCaseWrapper(corCase)); + } + int caseCount = casesTableModel.getRowCount(); if (correlationAttributes.isEmpty()) { - tableModel.addNodeData(new OtherOccurrenceNodeMessageData(Bundle.DataContentViewerOtherCases_table_noArtifacts())); - setColumnWidthToText(0, Bundle.DataContentViewerOtherCases_table_noArtifacts()); - } else if (0 == tableModel.getRowCount()) { - tableModel.addNodeData(new OtherOccurrenceNodeMessageData(Bundle.DataContentViewerOtherCases_table_noResultsFound())); - setColumnWidthToText(0, Bundle.DataContentViewerOtherCases_table_noResultsFound()); - } else { - setColumnWidths(); + casesTableModel.addCorrelationCase(NO_ARTIFACTS_CASE); + } else if (caseCount == 0) { + casesTableModel.addCorrelationCase(NO_RESULTS_CASE); } - + setColumnWidths(); setEarliestCaseDate(); - setOccurrenceCounts(); + foundInLabel.setText(String.format(Bundle.DataContentViewerOtherCases_foundIn_text(), totalCount, caseCount, dataSources.size())); + if (caseCount > 0) { + casesTable.setRowSelectionInterval(0, 0); + } } /** - * Adjust a given column for the text provided. - * - * @param columnIndex The index of the column to adjust. - * @param text The text whose length will be used to adjust the - * column width. + * Create a unique string to be used as a key for deduping data sources as + * best as possible */ - private void setColumnWidthToText(int columnIndex, String text) { - TableColumn column = otherCasesTable.getColumnModel().getColumn(columnIndex); - FontMetrics fontMetrics = otherCasesTable.getFontMetrics(otherCasesTable.getFont()); - int stringWidth = fontMetrics.stringWidth(text); - column.setMinWidth(stringWidth + CELL_TEXT_WIDTH_PADDING); + private String makeDataSourceString(String caseUUID, String deviceId, String dataSourceName) { + return caseUUID + deviceId + dataSourceName; + } + + /** + * Updates diplayed information to be correct for the current case selection + */ + private void updateOnCaseSelection() { + int[] selectedCaseIndexes = casesTable.getSelectedRows(); + DefaultTableModel dataSourceModel = (DefaultTableModel) dataSourcesTable.getModel(); + dataSourceModel.setRowCount(0); + tableModel.clearTable(); + for (CorrelationAttributeInstance corAttr : correlationAttributes) { + Map correlatedNodeDataMap = new HashMap<>(0); + + // get correlation and reference set instances from DB + correlatedNodeDataMap.putAll(getCorrelatedInstances(corAttr, dataSourceName, deviceId)); + for (OtherOccurrenceNodeInstanceData nodeData : correlatedNodeDataMap.values()) { + for (int selectedRow : selectedCaseIndexes) { + try { + if (nodeData.isCentralRepoNode()) { + if (casesTableModel.getCorrelationCase(casesTable.convertRowIndexToModel(selectedRow)) != null + && ((CorrelationCase) casesTableModel.getCorrelationCase(casesTable.convertRowIndexToModel(selectedRow))).getCaseUUID().equals(nodeData.getCorrelationAttributeInstance().getCorrelationCase().getCaseUUID())) { + dataSourceModel.addRow(new Object[]{nodeData.getDataSourceName(), nodeData.getDeviceID()}); + } + } else { + dataSourceModel.addRow(new Object[]{nodeData.getDataSourceName(), nodeData.getDeviceID()}); + } + } catch (EamDbException ex) { + LOGGER.log(Level.WARNING, "Unable to get correlation attribute instance from OtherOccurrenceNodeInstanceData for case " + nodeData.getCaseName()); + } + } + } + } + if (dataSourcesTable.getRowCount() > 0) { + dataSourcesTable.setRowSelectionInterval(0, 0); + } + } + + /** + * Updates diplayed information to be correct for the current data source + * selection + */ + private void updateOnDataSourceSelection() { + int[] selectedCaseIndexes = casesTable.getSelectedRows(); + DefaultTableModel dataSourceModel = (DefaultTableModel) dataSourcesTable.getModel(); + int[] selectedDataSources = dataSourcesTable.getSelectedRows(); + tableModel.clearTable(); + for (CorrelationAttributeInstance corAttr : correlationAttributes) { + Map correlatedNodeDataMap = new HashMap<>(0); + + // get correlation and reference set instances from DB + correlatedNodeDataMap.putAll(getCorrelatedInstances(corAttr, dataSourceName, deviceId)); + for (OtherOccurrenceNodeInstanceData nodeData : correlatedNodeDataMap.values()) { + for (int selectedCaseRow : selectedCaseIndexes) { + for (int selectedDataSourceRow : selectedDataSources) { + try { + if (nodeData.isCentralRepoNode()) { + if (casesTableModel.getCorrelationCase(casesTable.convertRowIndexToModel(selectedCaseRow)) != null + && ((CorrelationCase) casesTableModel.getCorrelationCase(casesTable.convertRowIndexToModel(selectedCaseRow))).getCaseUUID().equals(nodeData.getCorrelationAttributeInstance().getCorrelationCase().getCaseUUID()) + && dataSourceModel.getValueAt(dataSourcesTable.convertRowIndexToModel(selectedDataSourceRow), 1).toString().equals(nodeData.getDeviceID())) { + tableModel.addNodeData(nodeData); + } + } else { + if (dataSourceModel.getValueAt(dataSourcesTable.convertRowIndexToModel(selectedDataSourceRow), 1).toString().equals(nodeData.getDeviceID())) { + tableModel.addNodeData(nodeData); + } + } + } catch (EamDbException ex) { + LOGGER.log(Level.WARNING, "Unable to get correlation attribute instance from OtherOccurrenceNodeInstanceData for case " + nodeData.getCaseName()); + } + } + } + } + } } /** @@ -798,13 +864,20 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi */ private void setColumnWidths() { for (int idx = 0; idx < tableModel.getColumnCount(); idx++) { - TableColumn column = otherCasesTable.getColumnModel().getColumn(idx); + TableColumn column = filesTable.getColumnModel().getColumn(idx); column.setMinWidth(DEFAULT_MIN_CELL_WIDTH); int columnWidth = tableModel.getColumnPreferredWidth(idx); if (columnWidth > 0) { column.setPreferredWidth(columnWidth); } } + for (int idx = 0; idx < dataSourcesTable.getColumnCount(); idx++) { + if (dataSourcesTable.getColumnModel().getColumn(idx).getHeaderValue().toString().equals(Bundle.DataContentViewerOtherCases_dataSources_header_text())) { + dataSourcesTable.getColumnModel().getColumn(idx).setPreferredWidth(100); + } else { + dataSourcesTable.getColumnModel().getColumn(idx).setPreferredWidth(210); + } + } } /** @@ -824,11 +897,17 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi CSVFileChooser = new javax.swing.JFileChooser(); otherCasesPanel = new javax.swing.JPanel(); tableContainerPanel = new javax.swing.JPanel(); - tableScrollPane = new javax.swing.JScrollPane(); - otherCasesTable = new javax.swing.JTable(); earliestCaseLabel = new javax.swing.JLabel(); earliestCaseDate = new javax.swing.JLabel(); foundInLabel = new javax.swing.JLabel(); + jSplitPane2 = new javax.swing.JSplitPane(); + jSplitPane3 = new javax.swing.JSplitPane(); + caseScrollPane = new javax.swing.JScrollPane(); + casesTable = new javax.swing.JTable(); + dataSourceScrollPane = new javax.swing.JScrollPane(); + dataSourcesTable = new javax.swing.JTable(); + propertiesTableScrollPane = new javax.swing.JScrollPane(); + filesTable = new javax.swing.JTable(); rightClickPopupMenu.addPopupMenuListener(new javax.swing.event.PopupMenuListener() { public void popupMenuCanceled(javax.swing.event.PopupMenuEvent evt) { @@ -856,19 +935,10 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi setOpaque(false); setPreferredSize(new java.awt.Dimension(1500, 44)); - otherCasesPanel.setPreferredSize(new java.awt.Dimension(1500, 144)); + otherCasesPanel.setPreferredSize(new java.awt.Dimension(921, 62)); tableContainerPanel.setPreferredSize(new java.awt.Dimension(1500, 63)); - tableScrollPane.setPreferredSize(new java.awt.Dimension(1500, 30)); - - otherCasesTable.setAutoCreateRowSorter(true); - otherCasesTable.setModel(tableModel); - otherCasesTable.setToolTipText(org.openide.util.NbBundle.getMessage(DataContentViewerOtherCases.class, "DataContentViewerOtherCases.table.toolTip.text")); // NOI18N - otherCasesTable.setComponentPopupMenu(rightClickPopupMenu); - otherCasesTable.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_INTERVAL_SELECTION); - tableScrollPane.setViewportView(otherCasesTable); - org.openide.awt.Mnemonics.setLocalizedText(earliestCaseLabel, org.openide.util.NbBundle.getMessage(DataContentViewerOtherCases.class, "DataContentViewerOtherCases.earliestCaseLabel.text")); // NOI18N earliestCaseLabel.setToolTipText(org.openide.util.NbBundle.getMessage(DataContentViewerOtherCases.class, "DataContentViewerOtherCases.earliestCaseLabel.toolTipText")); // NOI18N @@ -876,45 +946,93 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi org.openide.awt.Mnemonics.setLocalizedText(foundInLabel, org.openide.util.NbBundle.getMessage(DataContentViewerOtherCases.class, "DataContentViewerOtherCases.foundInLabel.text")); // NOI18N + jSplitPane2.setDividerLocation(470); + + jSplitPane3.setDividerLocation(150); + + casesTable.setAutoCreateRowSorter(true); + casesTable.setModel(casesTableModel); + caseScrollPane.setViewportView(casesTable); + + jSplitPane3.setLeftComponent(caseScrollPane); + + dataSourcesTable.setAutoCreateRowSorter(true); + dataSourcesTable.setModel(new javax.swing.table.DefaultTableModel( + new Object [][] { + + }, + new String [] { + "Data Source Name", "Device ID" + } + ) { + boolean[] canEdit = new boolean [] { + false, false + }; + + public boolean isCellEditable(int rowIndex, int columnIndex) { + return canEdit [columnIndex]; + } + }); + dataSourceScrollPane.setViewportView(dataSourcesTable); + + jSplitPane3.setRightComponent(dataSourceScrollPane); + + jSplitPane2.setLeftComponent(jSplitPane3); + + propertiesTableScrollPane.setPreferredSize(new java.awt.Dimension(1000, 30)); + + filesTable.setAutoCreateRowSorter(true); + filesTable.setModel(tableModel); + filesTable.setToolTipText(org.openide.util.NbBundle.getMessage(DataContentViewerOtherCases.class, "DataContentViewerOtherCases.table.toolTip.text")); // NOI18N + filesTable.setComponentPopupMenu(rightClickPopupMenu); + filesTable.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_INTERVAL_SELECTION); + propertiesTableScrollPane.setViewportView(filesTable); + + jSplitPane2.setRightComponent(propertiesTableScrollPane); + javax.swing.GroupLayout tableContainerPanelLayout = new javax.swing.GroupLayout(tableContainerPanel); tableContainerPanel.setLayout(tableContainerPanelLayout); tableContainerPanelLayout.setHorizontalGroup( tableContainerPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(tableScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 1508, Short.MAX_VALUE) .addGroup(tableContainerPanelLayout.createSequentialGroup() - .addComponent(earliestCaseLabel) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addComponent(earliestCaseDate) - .addGap(66, 66, 66) - .addComponent(foundInLabel) - .addGap(0, 1157, Short.MAX_VALUE)) + .addGroup(tableContainerPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(tableContainerPanelLayout.createSequentialGroup() + .addComponent(earliestCaseLabel) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(earliestCaseDate) + .addGap(66, 66, 66) + .addComponent(foundInLabel)) + .addComponent(jSplitPane2, javax.swing.GroupLayout.DEFAULT_SIZE, 911, Short.MAX_VALUE)) + .addContainerGap()) ); tableContainerPanelLayout.setVerticalGroup( tableContainerPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, tableContainerPanelLayout.createSequentialGroup() - .addComponent(tableScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 71, Short.MAX_VALUE) + .addComponent(jSplitPane2, javax.swing.GroupLayout.DEFAULT_SIZE, 31, Short.MAX_VALUE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(tableContainerPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(earliestCaseLabel) .addComponent(earliestCaseDate) .addComponent(foundInLabel)) - .addGap(6, 6, 6)) + .addContainerGap()) ); javax.swing.GroupLayout otherCasesPanelLayout = new javax.swing.GroupLayout(otherCasesPanel); otherCasesPanel.setLayout(otherCasesPanelLayout); otherCasesPanelLayout.setHorizontalGroup( otherCasesPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGap(0, 1500, Short.MAX_VALUE) + .addGap(0, 921, Short.MAX_VALUE) .addGroup(otherCasesPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(tableContainerPanel, javax.swing.GroupLayout.Alignment.TRAILING, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .addGroup(otherCasesPanelLayout.createSequentialGroup() + .addComponent(tableContainerPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGap(0, 0, 0))) ); otherCasesPanelLayout.setVerticalGroup( otherCasesPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGap(0, 61, Short.MAX_VALUE) + .addGap(0, 62, Short.MAX_VALUE) .addGroup(otherCasesPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(otherCasesPanelLayout.createSequentialGroup() - .addComponent(tableContainerPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 53, Short.MAX_VALUE) + .addComponent(tableContainerPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 62, Short.MAX_VALUE) .addGap(0, 0, 0))) ); @@ -922,19 +1040,19 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi this.setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(otherCasesPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(otherCasesPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 1500, Short.MAX_VALUE) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(otherCasesPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 53, Short.MAX_VALUE) + .addComponent(otherCasesPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) ); }// //GEN-END:initComponents private void rightClickPopupMenuPopupMenuWillBecomeVisible(javax.swing.event.PopupMenuEvent evt) {//GEN-FIRST:event_rightClickPopupMenuPopupMenuWillBecomeVisible boolean enableCentralRepoActions = false; - if (EamDb.isEnabled() && otherCasesTable.getSelectedRowCount() == 1) { - int rowIndex = otherCasesTable.getSelectedRow(); + if (EamDb.isEnabled() && filesTable.getSelectedRowCount() == 1) { + int rowIndex = filesTable.getSelectedRow(); OtherOccurrenceNodeData selectedNode = (OtherOccurrenceNodeData) tableModel.getRow(rowIndex); if (selectedNode instanceof OtherOccurrenceNodeInstanceData) { OtherOccurrenceNodeInstanceData instanceData = (OtherOccurrenceNodeInstanceData) selectedNode; @@ -947,18 +1065,24 @@ public class DataContentViewerOtherCases extends JPanel implements DataContentVi // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JFileChooser CSVFileChooser; + private javax.swing.JScrollPane caseScrollPane; + private javax.swing.JTable casesTable; + private javax.swing.JScrollPane dataSourceScrollPane; + private javax.swing.JTable dataSourcesTable; private javax.swing.JLabel earliestCaseDate; private javax.swing.JLabel earliestCaseLabel; private javax.swing.JMenuItem exportToCSVMenuItem; + private javax.swing.JTable filesTable; private javax.swing.JLabel foundInLabel; + private javax.swing.JSplitPane jSplitPane2; + private javax.swing.JSplitPane jSplitPane3; private javax.swing.JPanel otherCasesPanel; - private javax.swing.JTable otherCasesTable; + private javax.swing.JScrollPane propertiesTableScrollPane; private javax.swing.JPopupMenu rightClickPopupMenu; private javax.swing.JMenuItem selectAllMenuItem; private javax.swing.JMenuItem showCaseDetailsMenuItem; private javax.swing.JMenuItem showCommonalityMenuItem; private javax.swing.JPanel tableContainerPanel; - private javax.swing.JScrollPane tableScrollPane; // End of variables declaration//GEN-END:variables /** diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesCasesTableModel.java b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesCasesTableModel.java new file mode 100644 index 0000000000..07ecc0878a --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesCasesTableModel.java @@ -0,0 +1,149 @@ +/* + * Central Repository + * + * Copyright 2019 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.centralrepository.contentviewer; + +import java.util.ArrayList; +import java.util.List; +import javax.swing.table.AbstractTableModel; +import org.openide.util.NbBundle.Messages; + +/** + * Model for cells in the cases section of the other occurrences data content + * viewer + */ +public class OtherOccurrencesCasesTableModel extends AbstractTableModel { + + private static final long serialVersionUID = 1L; + private final List correlationCaseList = new ArrayList<>(); + + OtherOccurrencesCasesTableModel() { + } + + @Override + public int getColumnCount() { + return TableColumns.values().length; + } + + /** + * Get the preferred width that has been configured for this column. + * + * A value of 0 means that no preferred width has been defined for this + * column. + * + * @param colIdx Column index + * + * @return preferred column width >= 0 + */ + public int getColumnPreferredWidth(int colIdx) { + return TableColumns.values()[colIdx].columnWidth(); + } + + @Override + public int getRowCount() { + return correlationCaseList.size(); + } + + @Override + public String getColumnName(int colIdx) { + return TableColumns.values()[colIdx].columnName(); + } + + @Override + public Object getValueAt(int rowIdx, int colIdx) { + if (0 == correlationCaseList.size()) { + return Bundle.OtherOccurrencesCasesTableModel_noData(); + } + + CorrelationCaseWrapper caseWrapper = correlationCaseList.get(rowIdx); + TableColumns columnId = TableColumns.values()[colIdx]; + return mapCorrelationCase(caseWrapper, columnId); + } + + /** + * Map a column ID to the value in that cell for correlation case wrapper. + * + * @param correlationCaseWrapper The correlation case wrapper + * @param columnId The ID of the cell column. + * + * @return The value in the cell. + */ + @Messages({"OtherOccurrencesCasesTableModel.noData=No Data."}) + private Object mapCorrelationCase(CorrelationCaseWrapper correlationCaseWrapper, TableColumns columnId) { + String value = Bundle.OtherOccurrencesCasesTableModel_noData(); + + switch (columnId) { + case CASE_NAME: + value = correlationCaseWrapper.getMessage(); + break; + default: //Use default "No data" value. + break; + } + return value; + } + + Object getCorrelationCase(int rowIdx) { + return correlationCaseList.get(rowIdx).getCorrelationCase(); + } + + @Override + public Class getColumnClass(int colIdx) { + return String.class; + } + + /** + * Add one correlated instance object to the table + * + * @param newCorrelationCaseWrapper data to add to the table + */ + void addCorrelationCase(CorrelationCaseWrapper newCorrelationCaseWrapper) { + correlationCaseList.add(newCorrelationCaseWrapper); + fireTableDataChanged(); + } + + /** + * Clear the correlation case table. + */ + void clearTable() { + correlationCaseList.clear(); + fireTableDataChanged(); + } + + @Messages({"OtherOccurrencesCasesTableModel.case=Case",}) + enum TableColumns { + // Ordering here determines displayed column order in Content Viewer. + // If order is changed, update the CellRenderer to ensure correct row coloring. + CASE_NAME(Bundle.OtherOccurrencesCasesTableModel_case(), 100); + + private final String columnName; + private final int columnWidth; + + TableColumns(String columnName, int columnWidth) { + this.columnName = columnName; + this.columnWidth = columnWidth; + } + + public String columnName() { + return columnName; + } + + public int columnWidth() { + return columnWidth; + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCasesTableCellRenderer.java b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesFilesTableCellRenderer.java similarity index 82% rename from Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCasesTableCellRenderer.java rename to Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesFilesTableCellRenderer.java index 9ba05589ce..2f258d3e7b 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCasesTableCellRenderer.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesFilesTableCellRenderer.java @@ -1,7 +1,7 @@ /* * Central Repository * - * Copyright 2015-2017 Basis Technology Corp. + * Copyright 2015-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -20,16 +20,16 @@ package org.sleuthkit.autopsy.centralrepository.contentviewer; import java.awt.Color; import java.awt.Component; -import javax.swing.JLabel; +import javax.swing.JComponent; import javax.swing.JTable; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.TableCellRenderer; import org.sleuthkit.datamodel.TskData; /** - * Renderer for cells in data content viewer table + * Renderer for cells in the files section of the other occurrences data content viewer */ -public class DataContentViewerOtherCasesTableCellRenderer implements TableCellRenderer { +public class OtherOccurrencesFilesTableCellRenderer implements TableCellRenderer { public static final DefaultTableCellRenderer DEFAULT_RENDERER = new DefaultTableCellRenderer(); @@ -43,20 +43,19 @@ public class DataContentViewerOtherCasesTableCellRenderer implements TableCellRe int column) { Component renderer = DEFAULT_RENDERER.getTableCellRendererComponent( table, value, isSelected, hasFocus, row, column); - ((JLabel) renderer).setOpaque(true); + ((JComponent) renderer).setOpaque(true); Color foreground, background; if (isSelected) { foreground = Color.WHITE; - background = Color.BLUE; + background = new Color(51,153,255); } else { String known_status = (String) table.getModel().getValueAt(table.convertRowIndexToModel(row), - table.getColumn(DataContentViewerOtherCasesTableModel.TableColumns.KNOWN.columnName()).getModelIndex()); + table.getColumn(OtherOccurrencesFilesTableModel.TableColumns.KNOWN.columnName()).getModelIndex()); if (known_status.equals(TskData.FileKnown.BAD.getName())) { foreground = Color.WHITE; background = Color.RED; } else if (known_status.equals(TskData.FileKnown.UNKNOWN.getName())) { foreground = Color.BLACK; - //background = Color.YELLOW; background = Color.WHITE; } else { foreground = Color.BLACK; diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCasesTableModel.java b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesFilesTableModel.java similarity index 66% rename from Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCasesTableModel.java rename to Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesFilesTableModel.java index 1a3527d940..e6c0a4215f 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/DataContentViewerOtherCasesTableModel.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/contentviewer/OtherOccurrencesFilesTableModel.java @@ -1,7 +1,7 @@ /* * Central Repository * - * Copyright 2015-2018 Basis Technology Corp. + * Copyright 2015-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,33 +24,29 @@ import javax.swing.table.AbstractTableModel; import org.openide.util.NbBundle.Messages; /** - * Model for cells in data content viewer table + * Model for cells in the files section of the other occurrences data content viewer */ -public class DataContentViewerOtherCasesTableModel extends AbstractTableModel { +public class OtherOccurrencesFilesTableModel extends AbstractTableModel { private static final long serialVersionUID = 1L; - @Messages({"DataContentViewerOtherCasesTableModel.case=Case", - "DataContentViewerOtherCasesTableModel.device=Device", - "DataContentViewerOtherCasesTableModel.dataSource=Data Source", - "DataContentViewerOtherCasesTableModel.path=Path", - "DataContentViewerOtherCasesTableModel.attribute=Matched Attribute", - "DataContentViewerOtherCasesTableModel.value=Attribute Value", - "DataContentViewerOtherCasesTableModel.known=Known", - "DataContentViewerOtherCasesTableModel.comment=Comment", - "DataContentViewerOtherCasesTableModel.noData=No Data.",}) + @Messages({"OtherOccurrencesFilesTableModel.device=Device", + "OtherOccurrencesFilesTableModel.dataSource=Data Source", + "OtherOccurrencesFilesTableModel.path=Path", + "OtherOccurrencesFilesTableModel.attribute=Matched Attribute", + "OtherOccurrencesFilesTableModel.value=Attribute Value", + "OtherOccurrencesFilesTableModel.known=Known", + "OtherOccurrencesFilesTableModel.comment=Comment", + "OtherOccurrencesFilesTableModel.noData=No Data.",}) enum TableColumns { // Ordering here determines displayed column order in Content Viewer. // If order is changed, update the CellRenderer to ensure correct row coloring. - CASE_NAME(Bundle.DataContentViewerOtherCasesTableModel_case(), 100), - DATA_SOURCE(Bundle.DataContentViewerOtherCasesTableModel_dataSource(), 100), - ATTRIBUTE(Bundle.DataContentViewerOtherCasesTableModel_attribute(), 125), - VALUE(Bundle.DataContentViewerOtherCasesTableModel_value(), 200), - KNOWN(Bundle.DataContentViewerOtherCasesTableModel_known(), 50), - FILE_PATH(Bundle.DataContentViewerOtherCasesTableModel_path(), 450), - COMMENT(Bundle.DataContentViewerOtherCasesTableModel_comment(), 200), - DEVICE(Bundle.DataContentViewerOtherCasesTableModel_device(), 250); - + ATTRIBUTE(Bundle.OtherOccurrencesFilesTableModel_attribute(), 75), + VALUE(Bundle.OtherOccurrencesFilesTableModel_value(), 190), + KNOWN(Bundle.OtherOccurrencesFilesTableModel_known(), 25), + FILE_PATH(Bundle.OtherOccurrencesFilesTableModel_path(), 470), + COMMENT(Bundle.OtherOccurrencesFilesTableModel_comment(), 190); + private final String columnName; private final int columnWidth; @@ -68,10 +64,10 @@ public class DataContentViewerOtherCasesTableModel extends AbstractTableModel { } }; - private final List nodeDataList; + private final List nodeDataList = new ArrayList<>(); - DataContentViewerOtherCasesTableModel() { - nodeDataList = new ArrayList<>(); + OtherOccurrencesFilesTableModel() { + } @Override @@ -106,7 +102,7 @@ public class DataContentViewerOtherCasesTableModel extends AbstractTableModel { @Override public Object getValueAt(int rowIdx, int colIdx) { if (0 == nodeDataList.size()) { - return Bundle.DataContentViewerOtherCasesTableModel_noData(); + return Bundle.OtherOccurrencesFilesTableModel_noData(); } OtherOccurrenceNodeData nodeData = nodeDataList.get(rowIdx); @@ -126,7 +122,7 @@ public class DataContentViewerOtherCasesTableModel extends AbstractTableModel { * @return The value in the cell. */ private Object mapNodeMessageData(OtherOccurrenceNodeMessageData nodeData, TableColumns columnId) { - if (columnId == TableColumns.CASE_NAME) { + if (columnId == TableColumns.ATTRIBUTE) { return nodeData.getDisplayMessage(); } return ""; @@ -141,24 +137,9 @@ public class DataContentViewerOtherCasesTableModel extends AbstractTableModel { * @return The value in the cell. */ private Object mapNodeInstanceData(OtherOccurrenceNodeInstanceData nodeData, TableColumns columnId) { - String value = Bundle.DataContentViewerOtherCasesTableModel_noData(); + String value = Bundle.OtherOccurrencesFilesTableModel_noData(); switch (columnId) { - case CASE_NAME: - if (null != nodeData.getCaseName()) { - value = nodeData.getCaseName(); - } - break; - case DEVICE: - if (null != nodeData.getDeviceID()) { - value = nodeData.getDeviceID(); - } - break; - case DATA_SOURCE: - if (null != nodeData.getDataSourceName()) { - value = nodeData.getDataSourceName(); - } - break; case FILE_PATH: value = nodeData.getFilePath(); break; @@ -174,7 +155,7 @@ public class DataContentViewerOtherCasesTableModel extends AbstractTableModel { case COMMENT: value = nodeData.getComment(); break; - default: // This shouldn't occur! Use default "No data" value. + default: //Use default "No data" value. break; } return value; diff --git a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationDataSource.java b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationDataSource.java index 03175347ca..8b8ed63699 100644 --- a/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationDataSource.java +++ b/Core/src/org/sleuthkit/autopsy/centralrepository/datamodel/CorrelationDataSource.java @@ -168,7 +168,7 @@ public class CorrelationDataSource implements Serializable { * * @return the ID or -1 if unknown */ - int getID() { + public int getID() { return dataSourceID; } diff --git a/Core/src/org/sleuthkit/autopsy/commandlineingest/AddDataSourceCallback.java b/Core/src/org/sleuthkit/autopsy/commandlineingest/AddDataSourceCallback.java deleted file mode 100755 index b4a000b0f3..0000000000 --- a/Core/src/org/sleuthkit/autopsy/commandlineingest/AddDataSourceCallback.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2019-2019 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.commandlineingest; - -import java.util.List; -import java.util.UUID; -import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback; -import org.sleuthkit.datamodel.Content; - - -/** - * A "callback" that collects the results of running a data source processor on - * a data source and unblocks the job processing thread when the data source - * processor finishes running in its own thread. - */ -class AddDataSourceCallback extends DataSourceProcessorCallback { - - private final Case caseForJob; - private final DataSource dataSourceInfo; - private final UUID taskId; - private final Object lock; - - /** - * Constructs a "callback" that collects the results of running a data - * source processor on a data source and unblocks the job processing thread - * when the data source processor finishes running in its own thread. - * - * @param caseForJob The case for the current job. - * @param dataSourceInfo The data source - * @param taskId The task id to associate with ingest job events. - */ - AddDataSourceCallback(Case caseForJob, DataSource dataSourceInfo, UUID taskId, Object lock) { - this.caseForJob = caseForJob; - this.dataSourceInfo = dataSourceInfo; - this.taskId = taskId; - this.lock = lock; - } - - /** - * Called by the data source processor when it finishes running in its own - * thread. - * - * @param result The result code for the processing of the data source. - * @param errorMessages Any error messages generated during the processing - * of the data source. - * @param dataSourceContent The content produced by processing the data - * source. - */ - @Override - public void done(DataSourceProcessorCallback.DataSourceProcessorResult result, List errorMessages, List dataSourceContent) { - if (!dataSourceContent.isEmpty()) { - caseForJob.notifyDataSourceAdded(dataSourceContent.get(0), taskId); - } else { - caseForJob.notifyFailedAddingDataSource(taskId); - } - dataSourceInfo.setDataSourceProcessorOutput(result, errorMessages, dataSourceContent); - dataSourceContent.addAll(dataSourceContent); - synchronized (lock) { - lock.notifyAll(); - } - } - - /** - * Called by the data source processor when it finishes running in its own - * thread, if that thread is the AWT (Abstract Window Toolkit) event - * dispatch thread (EDT). - * - * @param result The result code for the processing of the data source. - * @param errorMessages Any error messages generated during the processing - * of the data source. - * @param dataSourceContent The content produced by processing the data - * source. - */ - @Override - public void doneEDT(DataSourceProcessorCallback.DataSourceProcessorResult result, List errorMessages, List dataSources) { - done(result, errorMessages, dataSources); - } - -} diff --git a/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineIngestManager.java b/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineIngestManager.java index 13b76a5eca..8039aa0479 100755 --- a/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineIngestManager.java +++ b/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineIngestManager.java @@ -43,6 +43,9 @@ import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgress import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.TimeStampUtils; import org.sleuthkit.autopsy.datasourceprocessors.AutoIngestDataSourceProcessor; +import org.sleuthkit.autopsy.datasourceprocessors.AutoIngestDataSource; +import org.sleuthkit.autopsy.datasourceprocessors.AddDataSourceCallback; +import org.sleuthkit.autopsy.datasourceprocessors.DataSourceProcessorUtility; import org.sleuthkit.autopsy.events.AutopsyEvent; import org.sleuthkit.autopsy.ingest.IngestJob; import org.sleuthkit.autopsy.ingest.IngestJobSettings; @@ -177,7 +180,7 @@ public class CommandLineIngestManager { return; } - DataSource dataSource = new DataSource("", Paths.get(dataSourcePath)); + AutoIngestDataSource dataSource = new AutoIngestDataSource("", Paths.get(dataSourcePath)); try { // run data source processor runDataSourceProcessor(caseForJob, dataSource); @@ -228,7 +231,7 @@ public class CommandLineIngestManager { * @param dataSource DataSource object * @return object ID */ - private Long getDataSourceId(DataSource dataSource) { + private Long getDataSourceId(AutoIngestDataSource dataSource) { Content content = dataSource.getContent().get(0); return content.getId(); } @@ -271,7 +274,7 @@ public class CommandLineIngestManager { * task is interrupted while blocked, i.e., if auto ingest is shutting * down. */ - private void runDataSourceProcessor(Case caseForJob, DataSource dataSource) throws InterruptedException, AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException { + private void runDataSourceProcessor(Case caseForJob, AutoIngestDataSource dataSource) throws InterruptedException, AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException { LOGGER.log(Level.INFO, "Adding data source {0} ", dataSource.getPath().toString()); @@ -329,7 +332,7 @@ public class CommandLineIngestManager { * * @param dataSource The data source. */ - private void logDataSourceProcessorResult(DataSource dataSource) { + private void logDataSourceProcessorResult(AutoIngestDataSource dataSource) { DataSourceProcessorCallback.DataSourceProcessorResult resultCode = dataSource.getResultDataSourceProcessorResultCode(); if (null != resultCode) { @@ -376,7 +379,7 @@ public class CommandLineIngestManager { * task is interrupted while blocked, i.e., if auto ingest is shutting * down. */ - private void analyze(DataSource dataSource) throws AnalysisStartupException, InterruptedException { + private void analyze(AutoIngestDataSource dataSource) throws AnalysisStartupException, InterruptedException { LOGGER.log(Level.INFO, "Starting ingest modules analysis for {0} ", dataSource.getPath()); IngestJobEventListener ingestJobEventListener = new IngestJobEventListener(); diff --git a/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineIngestSettingsPanel.java b/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineIngestSettingsPanel.java index 20811d0fb8..5d3a0787ce 100755 --- a/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineIngestSettingsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/commandlineingest/CommandLineIngestSettingsPanel.java @@ -193,7 +193,12 @@ public class CommandLineIngestSettingsPanel extends javax.swing.JPanel { if (outputPath.isEmpty()) { jLabelInvalidResultsFolder.setVisible(true); jLabelInvalidResultsFolder.setText(NbBundle.getMessage(CommandLineIngestSettingsPanel.class, "CommandLineIngestSettingsPanel.ResultsDirectoryUnspecified")); - return false; + /* + NOTE: JIRA-4850: Returning false disables OK and Apply buttons for the entire + Tools->Options bar until the path is set. It was decided to only validate + the path if the path is set. + */ + return true; } if (!isFolderPathValid(outputPath)) { diff --git a/Core/src/org/sleuthkit/autopsy/commandlineingest/DataSource.java b/Core/src/org/sleuthkit/autopsy/commandlineingest/DataSource.java deleted file mode 100755 index 527a4e57f3..0000000000 --- a/Core/src/org/sleuthkit/autopsy/commandlineingest/DataSource.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2019-2019 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.commandlineingest; - -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback.DataSourceProcessorResult; -import org.sleuthkit.datamodel.Content; - -class DataSource { - - private final String deviceId; - private final Path path; - private DataSourceProcessorResult resultCode; - private List errorMessages; - private List content; - - DataSource(String deviceId, Path path) { - this.deviceId = deviceId; - this.path = path; - } - - String getDeviceId() { - return deviceId; - } - - Path getPath() { - return this.path; - } - - synchronized void setDataSourceProcessorOutput(DataSourceProcessorResult result, List errorMessages, List content) { - this.resultCode = result; - this.errorMessages = new ArrayList<>(errorMessages); - this.content = new ArrayList<>(content); - } - - synchronized DataSourceProcessorResult getResultDataSourceProcessorResultCode() { - return resultCode; - } - - synchronized List getDataSourceProcessorErrorMessages() { - return new ArrayList<>(errorMessages); - } - - synchronized List getContent() { - return new ArrayList<>(content); - } - -} diff --git a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/AbstractCommonAttributeSearcher.java b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/AbstractCommonAttributeSearcher.java index 54c6246ee7..70ac378131 100644 --- a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/AbstractCommonAttributeSearcher.java +++ b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/AbstractCommonAttributeSearcher.java @@ -26,6 +26,8 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.centralrepository.datamodel.EamDbException; @@ -130,12 +132,12 @@ public abstract class AbstractCommonAttributeSearcher { } } - static Map collateMatchesByNumberOfInstances(Map commonFiles) { + static TreeMap collateMatchesByNumberOfInstances(Map commonFiles) { //collate matches by number of matching instances - doing this in sql doesnt seem efficient - Map instanceCollatedCommonFiles = new TreeMap<>(); + TreeMap instanceCollatedCommonFiles = new TreeMap<>(); for (CommonAttributeValue md5Metadata : commonFiles.values()) { - Integer size = md5Metadata.getInstanceCount(); + Integer size = md5Metadata.getNumberOfDataSourcesInCurrentCase(); if (instanceCollatedCommonFiles.containsKey(size)) { instanceCollatedCommonFiles.get(size).addMetadataToList(md5Metadata); @@ -147,7 +149,71 @@ public abstract class AbstractCommonAttributeSearcher { } return instanceCollatedCommonFiles; } + + /* + * The set of the MIME types that will be checked for extension mismatches + * when checkType is ONLY_MEDIA. ".jpg", ".jpeg", ".png", ".psd", ".nef", + * ".tiff", ".bmp", ".tec" ".aaf", ".3gp", ".asf", ".avi", ".m1v", ".m2v", + * //NON-NLS ".m4v", ".mp4", ".mov", ".mpeg", ".mpg", ".mpe", ".mp4", ".rm", + * ".wmv", ".mpv", ".flv", ".swf" + */ + static final Set MEDIA_PICS_VIDEO_MIME_TYPES = Stream.of( + "image/bmp", //NON-NLS + "image/gif", //NON-NLS + "image/jpeg", //NON-NLS + "image/png", //NON-NLS + "image/tiff", //NON-NLS + "image/vnd.adobe.photoshop", //NON-NLS + "image/x-raw-nikon", //NON-NLS + "image/x-ms-bmp", //NON-NLS + "image/x-icon", //NON-NLS + "video/webm", //NON-NLS + "video/3gpp", //NON-NLS + "video/3gpp2", //NON-NLS + "video/ogg", //NON-NLS + "video/mpeg", //NON-NLS + "video/mp4", //NON-NLS + "video/quicktime", //NON-NLS + "video/x-msvideo", //NON-NLS + "video/x-flv", //NON-NLS + "video/x-m4v", //NON-NLS + "video/x-ms-wmv", //NON-NLS + "application/vnd.ms-asf", //NON-NLS + "application/vnd.rn-realmedia", //NON-NLS + "application/x-shockwave-flash" //NON-NLS + ).collect(Collectors.toSet()); + /* + * The set of the MIME types that will be checked for extension mismatches + * when checkType is ONLY_TEXT_FILES. ".doc", ".docx", ".odt", ".xls", + * ".xlsx", ".ppt", ".pptx" ".txt", ".rtf", ".log", ".text", ".xml" ".html", + * ".htm", ".css", ".js", ".php", ".aspx" ".pdf" + * //ignore text/plain due to large number of results with that type + */ + static final Set TEXT_FILES_MIME_TYPES = Stream.of( + "application/rtf", //NON-NLS + "application/pdf", //NON-NLS + "text/css", //NON-NLS + "text/html", //NON-NLS + "text/csv", //NON-NLS + "application/json", //NON-NLS + "application/javascript", //NON-NLS + "application/xml", //NON-NLS + "text/calendar", //NON-NLS + "application/x-msoffice", //NON-NLS + "application/x-ooxml", //NON-NLS + "application/msword", //NON-NLS + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", //NON-NLS + "application/vnd.ms-powerpoint", //NON-NLS + "application/vnd.openxmlformats-officedocument.presentationml.presentation", //NON-NLS + "application/vnd.ms-excel", //NON-NLS + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", //NON-NLS + "application/vnd.oasis.opendocument.presentation", //NON-NLS + "application/vnd.oasis.opendocument.spreadsheet", //NON-NLS + "application/vnd.oasis.opendocument.text" //NON-NLS + ).collect(Collectors.toSet()); + + /** * @return the filterByMedia */ diff --git a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/Bundle.properties b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/Bundle.properties index cf530cf013..563c433322 100644 --- a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/Bundle.properties @@ -44,5 +44,5 @@ CommonAttributePanel.resultsDisplayLabel.text_2=Display results organized by: CommonAttributePanel.organizeByCaseRadio.text=Case CommonAttributePanel.organizeByCountRadio.text=Number of occurrences CommonAttributePanel.caseResultsRadioButton.text=Case -CommonAttributePanel.countResultsRadioButton.text=Number of occurrences +CommonAttributePanel.countResultsRadioButton.text=Number of data sources CommonAttributePanel.displayResultsLabel.text_2=Display results organized by: diff --git a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/Bundle.properties-MERGED index 10370577d7..d164529639 100755 --- a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/Bundle.properties-MERGED @@ -60,7 +60,7 @@ CommonFilesSearchResultsViewerTable.noDescText=\ CommonFilesSearchResultsViewerTable.pathColLbl=Parent Path CommonFilesSearchResultsViewerTable.valueColLbl=Value InstanceCountNode.createSheet.noDescription=\ -InstanceCountNode.displayName=Files with %s instances (%s) +InstanceCountNode.displayName=Exists in %s data sources (%s) IntraCasePanel.selectDataSourceComboBox.actionCommand= CommonAttributePanel.jCheckBox1.text=Hide files found in over CommonAttributePanel.jLabel1.text=% of data sources in central repository. @@ -101,7 +101,7 @@ CommonAttributePanel.resultsDisplayLabel.text_2=Display results organized by: CommonAttributePanel.organizeByCaseRadio.text=Case CommonAttributePanel.organizeByCountRadio.text=Number of occurrences CommonAttributePanel.caseResultsRadioButton.text=Case -CommonAttributePanel.countResultsRadioButton.text=Number of occurrences +CommonAttributePanel.countResultsRadioButton.text=Number of data sources CommonAttributePanel.displayResultsLabel.text_2=Display results organized by: # {0} - case name # {1} - attr type diff --git a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/CommonAttributeCountSearchResults.java b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/CommonAttributeCountSearchResults.java index c1d2dcb481..8265a8db25 100644 --- a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/CommonAttributeCountSearchResults.java +++ b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/CommonAttributeCountSearchResults.java @@ -25,6 +25,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.TreeMap; import java.util.logging.Level; import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeNormalizationException; @@ -58,7 +59,7 @@ final public class CommonAttributeCountSearchResults { */ CommonAttributeCountSearchResults(Map metadata, int percentageThreshold, CorrelationAttributeInstance.Type resultType) { //wrap in a new object in case any client code has used an unmodifiable collection - this.instanceCountToAttributeValues = new HashMap<>(metadata); + this.instanceCountToAttributeValues = new TreeMap<>(metadata); this.percentageThreshold = percentageThreshold; this.resultTypeId = resultType.getId(); } @@ -73,7 +74,7 @@ final public class CommonAttributeCountSearchResults { */ CommonAttributeCountSearchResults(Map metadata, int percentageThreshold) { //wrap in a new object in case any client code has used an unmodifiable collection - this.instanceCountToAttributeValues = new HashMap<>(metadata); + this.instanceCountToAttributeValues = new TreeMap<>(metadata); this.percentageThreshold = percentageThreshold; this.resultTypeId = CorrelationAttributeInstance.FILES_TYPE_ID; } diff --git a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/CommonAttributePanel.java b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/CommonAttributePanel.java index 72086983de..a9c9ba1762 100644 --- a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/CommonAttributePanel.java +++ b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/CommonAttributePanel.java @@ -294,7 +294,11 @@ final class CommonAttributePanel extends javax.swing.JDialog implements Observer DataResultTopComponent.createInstance(tabTitle, Bundle.CommonAttributePanel_search_results_pathText(), commonFilesNode, 1); } else { // -3969 - Node commonFilesNode = new CommonAttributeSearchResultRootNode(metadata); + CorrelationAttributeInstance.Type correlationType = null; + if (interCaseRadio.isSelected()){ + correlationType = interCasePanel.getSelectedCorrelationType(); + } + Node commonFilesNode = new CommonAttributeSearchResultRootNode(metadata, correlationType); DataResultFilterNode dataResultFilterNode = new DataResultFilterNode(commonFilesNode, ExplorerManager.find(CommonAttributePanel.this)); TableFilterNode tableFilterWithDescendantsNode = new TableFilterNode(dataResultFilterNode, 3); DataResultViewerTable table = new CommonAttributesSearchResultsViewerTable(); diff --git a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/CommonAttributeSearchResultRootNode.java b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/CommonAttributeSearchResultRootNode.java index 6576c9219f..27156f97fa 100644 --- a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/CommonAttributeSearchResultRootNode.java +++ b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/CommonAttributeSearchResultRootNode.java @@ -25,6 +25,7 @@ import org.openide.nodes.ChildFactory; import org.openide.nodes.Children; import org.openide.nodes.Node; import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; import org.sleuthkit.autopsy.datamodel.DisplayableItemNode; import org.sleuthkit.autopsy.datamodel.DisplayableItemNodeVisitor; @@ -35,8 +36,8 @@ import org.sleuthkit.autopsy.datamodel.DisplayableItemNodeVisitor; */ final public class CommonAttributeSearchResultRootNode extends DisplayableItemNode { - CommonAttributeSearchResultRootNode(CommonAttributeCountSearchResults metadataList) { - super(Children.create(new InstanceCountNodeFactory(metadataList), true)); + CommonAttributeSearchResultRootNode(CommonAttributeCountSearchResults metadataList, CorrelationAttributeInstance.Type type) { + super(Children.create(new InstanceCountNodeFactory(metadataList, type), true)); } CommonAttributeSearchResultRootNode(CommonAttributeCaseSearchResults metadataList) { @@ -73,6 +74,7 @@ final public class CommonAttributeSearchResultRootNode extends DisplayableItemNo private static final Logger LOGGER = Logger.getLogger(InstanceCountNodeFactory.class.getName()); private final CommonAttributeCountSearchResults searchResults; + private final CorrelationAttributeInstance.Type type; /** * Build a factory which converts a @@ -81,8 +83,9 @@ final public class CommonAttributeSearchResultRootNode extends DisplayableItemNo * * @param searchResults */ - InstanceCountNodeFactory(CommonAttributeCountSearchResults searchResults) { + InstanceCountNodeFactory(CommonAttributeCountSearchResults searchResults, CorrelationAttributeInstance.Type type) { this.searchResults = searchResults; + this.type = type; } @Override @@ -94,7 +97,7 @@ final public class CommonAttributeSearchResultRootNode extends DisplayableItemNo @Override protected Node createNodeForKey(Integer instanceCount) { CommonAttributeValueList attributeValues = this.searchResults.getAttributeValuesForInstanceCount(instanceCount); - return new InstanceCountNode(instanceCount, attributeValues); + return new InstanceCountNode(instanceCount, attributeValues, type); } } diff --git a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/CommonAttributeValue.java b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/CommonAttributeValue.java index d86eff5a4f..c4fe25f14b 100644 --- a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/CommonAttributeValue.java +++ b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/CommonAttributeValue.java @@ -2,7 +2,7 @@ * * Autopsy Forensic Browser * - * Copyright 2018 Basis Technology Corp. + * Copyright 2018-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,10 +22,13 @@ package org.sleuthkit.autopsy.commonpropertiessearch; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import org.sleuthkit.datamodel.AbstractFile; /** * Defines a value that was in the common file search results as well as @@ -35,6 +38,7 @@ final public class CommonAttributeValue { private final String value; private final List fileInstances; + private final Map fileNames = new HashMap<>(); CommonAttributeValue(String value) { this.value = value; @@ -45,6 +49,23 @@ final public class CommonAttributeValue { return this.value; } + /** + * Get the file name of the first available instance of this value. + * + * @return the file name of an instance of this file + */ + String getTokenFileName() { + String tokenFileName = null; + int maxValue = 0; + for (String key : fileNames.keySet()){ + if (fileNames.get(key) > maxValue){ + maxValue = fileNames.get(key); + tokenFileName = key; + } + } + return tokenFileName; + } + /** * concatenate cases this value was seen into a single string * @@ -54,16 +75,43 @@ final public class CommonAttributeValue { return this.fileInstances.stream().map(AbstractCommonAttributeInstance::getCaseName).collect(Collectors.joining(", ")); } - public String getDataSources() { + /** + * Get the set of data sources names this value exists in + * + * @return a set of data source names + */ + public Set getDataSources() { Set sources = new HashSet<>(); for (AbstractCommonAttributeInstance data : this.fileInstances) { sources.add(data.getDataSource()); } + return sources; + } - return String.join(", ", sources); + /** + * Get the number of unique data sources in the current case which the value + * appeared in. + * + * @return the number of unique data sources in the current case which + * contained the value + */ + int getNumberOfDataSourcesInCurrentCase() { + Set dataSourceIds = new HashSet<>(); + for (AbstractCommonAttributeInstance data : this.fileInstances) { + AbstractFile file = data.getAbstractFile(); + if (file != null) { + dataSourceIds.add(file.getDataSourceObjectId()); + } + } + return dataSourceIds.size(); } void addInstance(AbstractCommonAttributeInstance metadata) { + if (metadata.getAbstractFile() != null) { + Integer currentValue = fileNames.get(metadata.getAbstractFile().getName()); + currentValue = currentValue == null ? 1 : currentValue+1; + fileNames.put(metadata.getAbstractFile().getName(), currentValue); + } this.fileInstances.add(metadata); } diff --git a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/CommonAttributeValueNode.java b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/CommonAttributeValueNode.java index d149847d78..ed8c395b5f 100644 --- a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/CommonAttributeValueNode.java +++ b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/CommonAttributeValueNode.java @@ -2,7 +2,7 @@ * * Autopsy Forensic Browser * - * Copyright 2018 Basis Technology Corp. + * Copyright 2018-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,6 +25,8 @@ import org.openide.nodes.Children; import org.openide.nodes.Node; import org.openide.nodes.Sheet; import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; +import static org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance.FILES_TYPE_ID; import org.sleuthkit.autopsy.datamodel.AbstractAbstractFileNode; import org.sleuthkit.autopsy.datamodel.DisplayableItemNode; import org.sleuthkit.autopsy.datamodel.DisplayableItemNodeVisitor; @@ -49,15 +51,19 @@ public class CommonAttributeValueNode extends DisplayableItemNode { * * @param data the common feature, and the children */ - public CommonAttributeValueNode(CommonAttributeValue data) { + public CommonAttributeValueNode(CommonAttributeValue data, CorrelationAttributeInstance.Type type) { super(Children.create( new FileInstanceNodeFactory(data), true)); this.commonFileCount = data.getInstanceCount(); this.cases = data.getCases(); - // @@ We seem to be doing this string concat twice. We also do it in getDataSources() this.dataSources = String.join(", ", data.getDataSources()); this.value = data.getValue(); - this.setDisplayName(String.format(Bundle.CommonAttributeValueNode_CommonAttributeValueNode_format(), this.value)); + //if the type is null (indicating intra-case) or files then make the node name the representitive file name + if (type == null || type.getId() == FILES_TYPE_ID) { + this.setDisplayName(data.getTokenFileName()); + } else { + this.setDisplayName(String.format(Bundle.CommonAttributeValueNode_CommonAttributeValueNode_format(), this.value)); + } this.setIconBaseWithExtension("org/sleuthkit/autopsy/images/fileset-icon-16.png"); //NON-NLS } diff --git a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/InstanceCountNode.java b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/InstanceCountNode.java index 29f817210d..973d7711c4 100644 --- a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/InstanceCountNode.java +++ b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/InstanceCountNode.java @@ -2,7 +2,7 @@ * * Autopsy Forensic Browser * - * Copyright 2018 Basis Technology Corp. + * Copyright 2018-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,6 +28,7 @@ import org.openide.nodes.Children; import org.openide.nodes.Node; import org.openide.nodes.Sheet; import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; import org.sleuthkit.autopsy.core.UserPreferences; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.datamodel.AbstractAbstractFileNode; @@ -45,6 +46,7 @@ public final class InstanceCountNode extends DisplayableItemNode { final private int instanceCount; final private CommonAttributeValueList attributeValues; + final private CorrelationAttributeInstance.Type type; /** * Create a node with the given number of instances, and the given selection @@ -54,11 +56,11 @@ public final class InstanceCountNode extends DisplayableItemNode { * @param attributeValues */ @NbBundle.Messages({ - "InstanceCountNode.displayName=Files with %s instances (%s)" + "InstanceCountNode.displayName=Exists in %s data sources (%s)" }) - public InstanceCountNode(int instanceCount, CommonAttributeValueList attributeValues) { - super(Children.create(new CommonAttributeValueNodeFactory(attributeValues.getMetadataList()), false)); - + public InstanceCountNode(int instanceCount, CommonAttributeValueList attributeValues, CorrelationAttributeInstance.Type type) { + super(Children.create(new CommonAttributeValueNodeFactory(attributeValues.getMetadataList(), type), false)); + this.type = type; this.instanceCount = instanceCount; this.attributeValues = attributeValues; @@ -81,7 +83,7 @@ public final class InstanceCountNode extends DisplayableItemNode { */ void createChildren() { attributeValues.displayDelayedMetadata(); - setChildren(Children.create(new CommonAttributeValueNodeFactory(attributeValues.getMetadataList()), false)); + setChildren(Children.create(new CommonAttributeValueNodeFactory(attributeValues.getMetadataList(), type), false)); } /** @@ -146,10 +148,11 @@ public final class InstanceCountNode extends DisplayableItemNode { */ // maps sting version of value to value Object (??) private final Map metadata; + private final CorrelationAttributeInstance.Type type; - CommonAttributeValueNodeFactory(List attributeValues) { + CommonAttributeValueNodeFactory(List attributeValues, CorrelationAttributeInstance.Type type) { this.metadata = new HashMap<>(); - + this.type = type; Iterator iterator = attributeValues.iterator(); while (iterator.hasNext()) { CommonAttributeValue attributeValue = iterator.next(); @@ -167,7 +170,7 @@ public final class InstanceCountNode extends DisplayableItemNode { @Override protected Node createNodeForKey(String attributeValue) { CommonAttributeValue md5Metadata = this.metadata.get(attributeValue); - return new CommonAttributeValueNode(md5Metadata); + return new CommonAttributeValueNode(md5Metadata, type); } } } diff --git a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/InterCasePanel.form b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/InterCasePanel.form index f878323524..93a06ca05c 100644 --- a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/InterCasePanel.form +++ b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/InterCasePanel.form @@ -134,7 +134,6 @@ - @@ -152,6 +151,7 @@ + diff --git a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/InterCasePanel.java b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/InterCasePanel.java index 2c6100a42c..c437bb0a3a 100644 --- a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/InterCasePanel.java +++ b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/InterCasePanel.java @@ -2,7 +2,7 @@ * * Autopsy Forensic Browser * - * Copyright 2018 Basis Technology Corp. + * Copyright 2018-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -164,7 +164,6 @@ public final class InterCasePanel extends javax.swing.JPanel { categoriesLabel.setName(""); // NOI18N buttonGroup.add(allFileCategoriesRadioButton); - allFileCategoriesRadioButton.setSelected(true); org.openide.awt.Mnemonics.setLocalizedText(allFileCategoriesRadioButton, org.openide.util.NbBundle.getMessage(InterCasePanel.class, "InterCasePanel.allFileCategoriesRadioButton.text")); // NOI18N allFileCategoriesRadioButton.setToolTipText(org.openide.util.NbBundle.getMessage(InterCasePanel.class, "InterCasePanel.allFileCategoriesRadioButton.toolTipText")); // NOI18N allFileCategoriesRadioButton.setEnabled(false); @@ -175,6 +174,7 @@ public final class InterCasePanel extends javax.swing.JPanel { }); buttonGroup.add(selectedFileCategoriesButton); + selectedFileCategoriesButton.setSelected(true); org.openide.awt.Mnemonics.setLocalizedText(selectedFileCategoriesButton, org.openide.util.NbBundle.getMessage(InterCasePanel.class, "InterCasePanel.selectedFileCategoriesButton.text")); // NOI18N selectedFileCategoriesButton.setToolTipText(org.openide.util.NbBundle.getMessage(InterCasePanel.class, "InterCasePanel.selectedFileCategoriesButton.toolTipText")); // NOI18N selectedFileCategoriesButton.setEnabled(false); diff --git a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/InterCaseSearchResultsProcessor.java b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/InterCaseSearchResultsProcessor.java index 52f2cec276..a6dd6833b3 100644 --- a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/InterCaseSearchResultsProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/InterCaseSearchResultsProcessor.java @@ -29,7 +29,9 @@ import java.util.HashSet; import java.util.Set; import java.util.List; import java.util.Map; +import java.util.TreeMap; import java.util.logging.Level; +import java.util.stream.Collectors; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance; import org.sleuthkit.autopsy.centralrepository.datamodel.CorrelationAttributeInstance.Type; @@ -173,7 +175,7 @@ final class InterCaseSearchResultsProcessor { } catch (EamDbException | TskCoreException ex) { LOGGER.log(Level.SEVERE, "Error accessing EamDb processing CaseInstancesTable.", ex); } - return new HashMap<>(); + return new TreeMap<>(); } /** @@ -205,7 +207,7 @@ final class InterCaseSearchResultsProcessor { } catch (EamDbException | TskCoreException ex) { LOGGER.log(Level.SEVERE, "Error accessing EamDb processing CaseInstancesTable.", ex); } - return new HashMap<>(); + return new TreeMap<>(); } /** @@ -248,7 +250,7 @@ final class InterCaseSearchResultsProcessor { */ private class InterCaseByCountCallback implements CaseDbAccessManager.CaseDbAccessQueryCallback, InstanceTableCallback { - private final Map instanceCollatedCommonFiles = new HashMap<>(); + private final TreeMap instanceCollatedCommonFiles = new TreeMap<>(); private final int caseID; private final int targetCase; @@ -284,7 +286,7 @@ final class InterCaseSearchResultsProcessor { } else { instances = EamDb.getInstance().getArtifactInstancesByTypeValuesAndCases(correlationType, Arrays.asList(corValue), targetCases); } - int size = instances.size(); + int size = instances.stream().map(instance -> instance.getCorrelationDataSource().getID()).collect(Collectors.toSet()).size(); if (size > 1) { CommonAttributeValue commonAttributeValue = new CommonAttributeValue(corValue); boolean anotherCase = false; @@ -311,7 +313,7 @@ final class InterCaseSearchResultsProcessor { } Map getInstanceCollatedCommonFiles() { - return Collections.unmodifiableMap(instanceCollatedCommonFiles); + return Collections.unmodifiableSortedMap(instanceCollatedCommonFiles); } } diff --git a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/IntraCaseCommonAttributeSearcher.java b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/IntraCaseCommonAttributeSearcher.java index 4e7d1fe2e8..f8b1000a59 100644 --- a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/IntraCaseCommonAttributeSearcher.java +++ b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/IntraCaseCommonAttributeSearcher.java @@ -2,7 +2,7 @@ * * Autopsy Forensic Browser * - * Copyright 2018 Basis Technology Corp. + * Copyright 2018-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/IntraCasePanel.form b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/IntraCasePanel.form index d4a11098bb..2d79515463 100644 --- a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/IntraCasePanel.form +++ b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/IntraCasePanel.form @@ -99,6 +99,7 @@ + @@ -116,7 +117,6 @@ - @@ -128,7 +128,6 @@ - @@ -139,7 +138,6 @@ - diff --git a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/IntraCasePanel.java b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/IntraCasePanel.java index d0c7ab1579..69e02eb7a7 100644 --- a/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/IntraCasePanel.java +++ b/Core/src/org/sleuthkit/autopsy/commonpropertiessearch/IntraCasePanel.java @@ -2,7 +2,7 @@ * * Autopsy Forensic Browser * - * Copyright 2018 Basis Technology Corp. + * Copyright 2018-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -154,6 +154,7 @@ public final class IntraCasePanel extends javax.swing.JPanel { categoriesLabel.setName(""); // NOI18N buttonGroup.add(selectedFileCategoriesButton); + selectedFileCategoriesButton.setSelected(true); org.openide.awt.Mnemonics.setLocalizedText(selectedFileCategoriesButton, org.openide.util.NbBundle.getMessage(IntraCasePanel.class, "IntraCasePanel.selectedFileCategoriesButton.text")); // NOI18N selectedFileCategoriesButton.setToolTipText(org.openide.util.NbBundle.getMessage(IntraCasePanel.class, "IntraCasePanel.selectedFileCategoriesButton.toolTipText")); // NOI18N selectedFileCategoriesButton.addActionListener(new java.awt.event.ActionListener() { @@ -164,7 +165,6 @@ public final class IntraCasePanel extends javax.swing.JPanel { pictureVideoCheckbox.setSelected(true); org.openide.awt.Mnemonics.setLocalizedText(pictureVideoCheckbox, org.openide.util.NbBundle.getMessage(IntraCasePanel.class, "IntraCasePanel.pictureVideoCheckbox.text")); // NOI18N - pictureVideoCheckbox.setEnabled(false); pictureVideoCheckbox.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { pictureVideoCheckboxActionPerformed(evt); @@ -173,7 +173,6 @@ public final class IntraCasePanel extends javax.swing.JPanel { documentsCheckbox.setSelected(true); org.openide.awt.Mnemonics.setLocalizedText(documentsCheckbox, org.openide.util.NbBundle.getMessage(IntraCasePanel.class, "IntraCasePanel.documentsCheckbox.text")); // NOI18N - documentsCheckbox.setEnabled(false); documentsCheckbox.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { documentsCheckboxActionPerformed(evt); @@ -181,7 +180,6 @@ public final class IntraCasePanel extends javax.swing.JPanel { }); buttonGroup.add(allFileCategoriesRadioButton); - allFileCategoriesRadioButton.setSelected(true); org.openide.awt.Mnemonics.setLocalizedText(allFileCategoriesRadioButton, org.openide.util.NbBundle.getMessage(IntraCasePanel.class, "IntraCasePanel.allFileCategoriesRadioButton.text")); // NOI18N allFileCategoriesRadioButton.setToolTipText(org.openide.util.NbBundle.getMessage(IntraCasePanel.class, "IntraCasePanel.allFileCategoriesRadioButton.toolTipText")); // NOI18N allFileCategoriesRadioButton.addActionListener(new java.awt.event.ActionListener() { diff --git a/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties b/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties index 358971eaa8..6665c2e958 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties @@ -38,3 +38,4 @@ VisualizationPanel.organicLayoutButton.text=Organic VisualizationPanel.fastOrganicLayoutButton.text=Fast Organic VisualizationPanel.hierarchyLayoutButton.text=Hierarchical VisualizationPanel.clearVizButton.text_1=Clear Viz. +VisualizationPanel.snapshotButton.text_1=Snapshot Report diff --git a/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties-MERGED index 381ec7b337..e4daded09b 100755 --- a/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/communications/Bundle.properties-MERGED @@ -32,6 +32,23 @@ ResetAndPinAccountsAction.singularText=Visualize Only Selected Account UnpinAccountsAction.pluralText=Remove Selected Accounts UnpinAccountsAction.singularText=Remove Selected Account VisalizationPanel.paintingError=Problem painting visualization. +# {0} - default name +VisualizationPane_accept_defaultName=Report name was empty. Press OK to accept default report name: {0} +VisualizationPane_blank_report_title=Blank Report Name +VisualizationPane_DisplayName=Open Report +VisualizationPane_fileName_prompt=Enter name for the Communications Snapshot Report: +VisualizationPane_MessageBoxTitle=Open Report Failure +VisualizationPane_MissingReportFileMessage=The report file no longer exists. +VisualizationPane_NoAssociatedEditorMessage=There is no associated editor for reports of this type or the associated application failed to launch. +VisualizationPane_NoOpenInEditorSupportMessage=This platform (operating system) does not support opening a file in an editor this way. +VisualizationPane_Open_Report=Open Report +# {0} - report name +VisualizationPane_overrite_exiting=Overwrite existing report?\n{0} +VisualizationPane_Report_OK_Button=OK +# {0} - report path +VisualizationPane_Report_Success=Report Successfully create at:\n{0} +VisualizationPane_ReportFileOpenPermissionDeniedMessage=Permission to open the report file was denied. +VisualizationPane_reportName=Communications Snapshot VisualizationPanel.cancelButton.text=Cancel VisualizationPanel.computingLayout=Computing Layout VisualizationPanel.jButton1.text=Fast Organic @@ -65,3 +82,8 @@ VisualizationPanel.organicLayoutButton.text=Organic VisualizationPanel.fastOrganicLayoutButton.text=Fast Organic VisualizationPanel.hierarchyLayoutButton.text=Hierarchical VisualizationPanel.clearVizButton.text_1=Clear Viz. +VisualizationPanel.snapshotButton.text_1=Snapshot Report +VisualizationPanel_action_dialogs_title=Communications +VisualizationPanel_action_name_text=Snapshot Report +VisualizationPanel_module_name=Communications +VisualizationPanel_snapshot_report_failure=Snapshot report not created. An error occurred during creation. diff --git a/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.form b/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.form index 49b86ae014..4e0a73a6c7 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.form +++ b/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.form @@ -11,7 +11,7 @@ - + @@ -49,9 +49,9 @@ - + - + @@ -120,6 +120,10 @@ + + + + @@ -143,6 +147,8 @@ + + @@ -310,6 +316,24 @@ + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.java b/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.java index 8f5e6e5c94..f3d4f95bb2 100644 --- a/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.java +++ b/Core/src/org/sleuthkit/autopsy/communications/VisualizationPanel.java @@ -28,6 +28,7 @@ import com.mxgraph.model.mxCell; import com.mxgraph.model.mxICell; import com.mxgraph.swing.handler.mxRubberband; import com.mxgraph.swing.mxGraphComponent; +import com.mxgraph.util.mxCellRenderer; import com.mxgraph.util.mxEvent; import com.mxgraph.util.mxEventObject; import com.mxgraph.util.mxEventSource; @@ -41,19 +42,28 @@ import com.mxgraph.view.mxGraphView; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Cursor; +import java.awt.Desktop; import java.awt.Dimension; import java.awt.Font; import java.awt.Frame; import java.awt.Graphics; +import java.awt.GridLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.MouseWheelEvent; +import java.awt.image.BufferedImage; import java.beans.PropertyChangeEvent; import java.beans.PropertyVetoException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.text.DecimalFormat; +import java.text.SimpleDateFormat; import java.util.Arrays; +import java.util.Date; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; @@ -75,14 +85,17 @@ import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JMenuItem; +import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JSplitPane; import javax.swing.JTextArea; +import javax.swing.JTextField; import javax.swing.JToolBar; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.SwingWorker; +import org.apache.commons.lang3.StringUtils; import org.controlsfx.control.Notifications; import org.jdesktop.layout.GroupLayout; import org.jdesktop.layout.LayoutStyle; @@ -92,8 +105,11 @@ import org.openide.nodes.Node; import org.openide.util.Lookup; import org.openide.util.NbBundle; import org.openide.util.lookup.ProxyLookup; +import org.openide.windows.WindowManager; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.communications.snapshot.CommSnapShotReportWriter; +import org.sleuthkit.autopsy.coreutils.FileUtil; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.progress.ModalDialogProgressIndicator; @@ -101,7 +117,6 @@ import org.sleuthkit.datamodel.CommunicationsFilter; import org.sleuthkit.datamodel.CommunicationsManager; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.TskCoreException; - /** * A panel that goes in the Visualize tab of the Communications Visualization * Tool. Hosts an JGraphX mxGraphComponent that implements the communications @@ -172,7 +187,7 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider public void paint(Graphics graphics) { try { super.paint(graphics); - } catch (NullPointerException ex) { //NOPMD + } catch (NullPointerException ex) { //NOPMD /* We can't find the underlying cause of the NPE in * jgraphx, but it doesn't seem to cause any * noticeable problems, so we are just logging it @@ -387,6 +402,8 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider zoomLabel = new JLabel(); clearVizButton = new JButton(); jSeparator2 = new JToolBar.Separator(); + snapshotButton = new JButton(); + jSeparator3 = new JToolBar.Separator(); notificationsJFXPanel = new JFXPanel(); setLayout(new BorderLayout()); @@ -406,9 +423,9 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider placeHolderPanel.setLayout(placeHolderPanelLayout); placeHolderPanelLayout.setHorizontalGroup(placeHolderPanelLayout.createParallelGroup(GroupLayout.LEADING) .add(placeHolderPanelLayout.createSequentialGroup() - .addContainerGap(71, Short.MAX_VALUE) + .addContainerGap(268, Short.MAX_VALUE) .add(jTextArea1, GroupLayout.PREFERRED_SIZE, 424, GroupLayout.PREFERRED_SIZE) - .addContainerGap(248, Short.MAX_VALUE)) + .addContainerGap(445, Short.MAX_VALUE)) ); placeHolderPanelLayout.setVerticalGroup(placeHolderPanelLayout.createParallelGroup(GroupLayout.LEADING) .add(placeHolderPanelLayout.createSequentialGroup() @@ -505,6 +522,16 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider jSeparator2.setOrientation(SwingConstants.VERTICAL); + snapshotButton.setIcon(new ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/report/images/image.png"))); // NOI18N + snapshotButton.setText(NbBundle.getMessage(VisualizationPanel.class, "VisualizationPanel.snapshotButton.text_1")); // NOI18N + snapshotButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent evt) { + snapshotButtonActionPerformed(evt); + } + }); + + jSeparator3.setOrientation(SwingConstants.VERTICAL); + GroupLayout toolbarLayout = new GroupLayout(toolbar); toolbar.setLayout(toolbarLayout); toolbarLayout.setHorizontalGroup(toolbarLayout.createParallelGroup(GroupLayout.LEADING) @@ -537,6 +564,10 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider .add(zoomActualButton, GroupLayout.PREFERRED_SIZE, 33, GroupLayout.PREFERRED_SIZE) .addPreferredGap(LayoutStyle.RELATED) .add(fitZoomButton, GroupLayout.PREFERRED_SIZE, 32, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.RELATED) + .add(jSeparator3, GroupLayout.PREFERRED_SIZE, 10, GroupLayout.PREFERRED_SIZE) + .addPreferredGap(LayoutStyle.RELATED) + .add(snapshotButton) .addContainerGap(GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) ); toolbarLayout.setVerticalGroup(toolbarLayout.createParallelGroup(GroupLayout.LEADING) @@ -556,7 +587,9 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider .add(jLabel2) .add(zoomLabel) .add(clearVizButton) - .add(jSeparator2, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) + .add(jSeparator2, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .add(snapshotButton) + .add(jSeparator3, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) .add(3, 3, 3)) ); @@ -648,6 +681,24 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider setCursor(Cursor.getDefaultCursor()); }//GEN-LAST:event_clearVizButtonActionPerformed + @NbBundle.Messages({ + "VisualizationPanel_snapshot_report_failure=Snapshot report not created. An error occurred during creation." + }) + private void snapshotButtonActionPerformed(ActionEvent evt) {//GEN-FIRST:event_snapshotButtonActionPerformed + try { + handleSnapshotEvent(); + } catch (NoCurrentCaseException | IOException ex) { + logger.log(Level.SEVERE, "Unable to create communications snapsot report", ex); //NON-NLS + + Platform.runLater(() + -> Notifications.create().owner(notificationsJFXPanel.getScene().getWindow()) + .text(Bundle.VisualizationPanel_snapshot_report_failure()) + .showWarning()); + } catch( TskCoreException ex) { + logger.log(Level.WARNING, "Unable to add report to currenct case", ex); //NON-NLS + } + }//GEN-LAST:event_snapshotButtonActionPerformed + private void fitGraph() { graphComponent.zoomTo(1, true); mxPoint translate = graph.getView().getTranslate(); @@ -674,7 +725,129 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider graphComponent.zoom((heightFactor + widthFactor) / 2.0); } + + /** + * Handle the ActionPerformed event from the Snapshot button. + * + * @throws NoCurrentCaseException + * @throws IOException + */ + @NbBundle.Messages({ + "VisualizationPanel_action_dialogs_title=Communications", + "VisualizationPanel_module_name=Communications", + "VisualizationPanel_action_name_text=Snapshot Report", + "VisualizationPane_fileName_prompt=Enter name for the Communications Snapshot Report:", + "VisualizationPane_reportName=Communications Snapshot", + "# {0} - default name", + "VisualizationPane_accept_defaultName=Report name was empty. Press OK to accept default report name: {0}", + "VisualizationPane_blank_report_title=Blank Report Name", + "# {0} - report name", + "VisualizationPane_overrite_exiting=Overwrite existing report?\n{0}" + }) + private void handleSnapshotEvent() throws NoCurrentCaseException, IOException, TskCoreException { + Case currentCase = Case.getCurrentCaseThrows(); + Date generationDate = new Date(); + final String defaultReportName = FileUtil.escapeFileName(currentCase.getDisplayName() + " " + new SimpleDateFormat("MMddyyyyHHmmss").format(generationDate)); //NON_NLS + + final JTextField text = new JTextField(50); + final JPanel panel = new JPanel(new GridLayout(2, 1)); + panel.add(new JLabel(Bundle.VisualizationPane_fileName_prompt())); + panel.add(text); + + text.setText(defaultReportName); + + int result = JOptionPane.showConfirmDialog(graphComponent, panel, + Bundle.VisualizationPanel_action_dialogs_title(), JOptionPane.OK_CANCEL_OPTION); + + if (result == JOptionPane.OK_OPTION) { + String enteredReportName = text.getText(); + + if(enteredReportName.trim().isEmpty()){ + result = JOptionPane.showConfirmDialog(graphComponent, Bundle.VisualizationPane_accept_defaultName(defaultReportName), Bundle.VisualizationPane_blank_report_title(), JOptionPane.OK_CANCEL_OPTION); + if(result != JOptionPane.OK_OPTION) { + return; + } + } + + String reportName = StringUtils.defaultIfBlank(enteredReportName, defaultReportName); + Path reportPath = Paths.get(currentCase.getReportDirectory(), reportName); + if (Files.exists(reportPath)) { + result = JOptionPane.showConfirmDialog(graphComponent, Bundle.VisualizationPane_overrite_exiting(reportName), + Bundle.VisualizationPanel_action_dialogs_title(), JOptionPane.OK_CANCEL_OPTION); + + if (result == JOptionPane.OK_OPTION) { + FileUtil.deleteFileDir(reportPath.toFile()); + createReport(currentCase, reportName); + } + } else { + createReport(currentCase, reportName); + currentCase.addReport(reportPath.toString(), Bundle.VisualizationPanel_module_name(), reportName); + + } + } + } + + /** + * Create the Snapshot Report. + * + * @param currentCase The current case + * @param reportName User selected name for the report + * + * @throws IOException + */ + @NbBundle.Messages({ + "VisualizationPane_DisplayName=Open Report", + "VisualizationPane_NoAssociatedEditorMessage=There is no associated editor for reports of this type or the associated application failed to launch.", + "VisualizationPane_MessageBoxTitle=Open Report Failure", + "VisualizationPane_NoOpenInEditorSupportMessage=This platform (operating system) does not support opening a file in an editor this way.", + "VisualizationPane_MissingReportFileMessage=The report file no longer exists.", + "VisualizationPane_ReportFileOpenPermissionDeniedMessage=Permission to open the report file was denied.", + "# {0} - report path", + "VisualizationPane_Report_Success=Report Successfully create at:\n{0}", + "VisualizationPane_Report_OK_Button=OK", + "VisualizationPane_Open_Report=Open Report",}) + private void createReport(Case currentCase, String reportName) throws IOException { + + // Create the report. + Path reportFolderPath = Paths.get(currentCase.getReportDirectory(), reportName, Bundle.VisualizationPane_reportName()); //NON_NLS + BufferedImage image = mxCellRenderer.createBufferedImage(graph, null, graph.getView().getScale(), Color.WHITE, true, null); + Path reportPath = new CommSnapShotReportWriter(currentCase, reportFolderPath, reportName, new Date(), image, currentFilter).writeReport(); + + // Report success to the user and offer to open the report. + String message = Bundle.VisualizationPane_Report_Success(reportPath.toAbsolutePath()); + String[] buttons = {Bundle.VisualizationPane_Open_Report(), Bundle.VisualizationPane_Report_OK_Button()}; + + int result = JOptionPane.showOptionDialog(graphComponent, message, + Bundle.VisualizationPanel_action_dialogs_title(), + JOptionPane.OK_CANCEL_OPTION, JOptionPane.INFORMATION_MESSAGE, + null, buttons, buttons[1]); + if (result == JOptionPane.YES_NO_OPTION) { + try { + Desktop.getDesktop().open(reportPath.toFile()); + } catch (IOException ex) { + JOptionPane.showMessageDialog(WindowManager.getDefault().getMainWindow(), + Bundle.VisualizationPane_NoAssociatedEditorMessage(), + Bundle.VisualizationPane_MessageBoxTitle(), + JOptionPane.ERROR_MESSAGE); + } catch (UnsupportedOperationException ex) { + JOptionPane.showMessageDialog(WindowManager.getDefault().getMainWindow(), + Bundle.VisualizationPane_NoOpenInEditorSupportMessage(), + Bundle.VisualizationPane_MessageBoxTitle(), + JOptionPane.ERROR_MESSAGE); + } catch (IllegalArgumentException ex) { + JOptionPane.showMessageDialog(WindowManager.getDefault().getMainWindow(), + Bundle.VisualizationPane_MissingReportFileMessage(), + Bundle.VisualizationPane_MessageBoxTitle(), + JOptionPane.ERROR_MESSAGE); + } catch (SecurityException ex) { + JOptionPane.showMessageDialog(WindowManager.getDefault().getMainWindow(), + Bundle.VisualizationPane_ReportFileOpenPermissionDeniedMessage(), + Bundle.VisualizationPane_MessageBoxTitle(), + JOptionPane.ERROR_MESSAGE); + } + } + } // Variables declaration - do not modify//GEN-BEGIN:variables private JPanel borderLayoutPanel; @@ -687,10 +860,12 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider private JLabel jLabel2; private JToolBar.Separator jSeparator1; private JToolBar.Separator jSeparator2; + private JToolBar.Separator jSeparator3; private JTextArea jTextArea1; private JFXPanel notificationsJFXPanel; private JButton organicLayoutButton; private JPanel placeHolderPanel; + private JButton snapshotButton; private JSplitPane splitPane; private JPanel toolbar; private JButton zoomActualButton; @@ -1001,4 +1176,4 @@ final public class VisualizationPanel extends JPanel implements Lookup.Provider lockedVertexModel.lock(selectedVertices); } } -} +} \ No newline at end of file diff --git a/Core/src/org/sleuthkit/autopsy/communications/snapshot/CommSnapShotReportWriter.java b/Core/src/org/sleuthkit/autopsy/communications/snapshot/CommSnapShotReportWriter.java new file mode 100755 index 0000000000..7b4dee6d19 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/communications/snapshot/CommSnapShotReportWriter.java @@ -0,0 +1,175 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 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.snapshot; + +import java.util.List; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.nio.file.Path; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.Set; +import javax.imageio.ImageIO; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.report.uisnapshot.UiSnapShotReportWriter; +import org.sleuthkit.datamodel.Account; +import org.sleuthkit.datamodel.CommunicationsFilter; +import org.sleuthkit.datamodel.CommunicationsFilter.AccountTypeFilter; +import org.sleuthkit.datamodel.CommunicationsFilter.DateRangeFilter; +import org.sleuthkit.datamodel.CommunicationsFilter.DeviceFilter; +import org.sleuthkit.datamodel.CommunicationsFilter.SubFilter; +import org.sleuthkit.datamodel.DataSource; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Generate and write the Communication snapshot report to disk. + */ +public class CommSnapShotReportWriter extends UiSnapShotReportWriter { + + private final BufferedImage image; + private final CommunicationsFilter filter; + + /** + * Constructor + * + * @param currentCase The Case to write a report for. + * @param reportFolderPath The Path to the folder that will contain the + * report. + * @param reportName The name of the report. + * @param generationDate The generation Date of the report. + * @param snapshot A snapshot of the view to include in the report. + */ + public CommSnapShotReportWriter(Case currentCase, Path reportFolderPath, String reportName, Date generationDate, BufferedImage snapshot, CommunicationsFilter filter) { + + super(currentCase, reportFolderPath, reportName, generationDate); + + this.image = snapshot; + this.filter = filter; + + } + + /** + * Generate and write the html page that shows the snapshot and the state of + * the CommunicationFilters + * + * @throws IOException If there is a problem writing the html file to disk. + */ + @Override + protected void writeSnapShotHTMLFile() throws IOException { + SimpleDateFormat formatter = new SimpleDateFormat("MMMMM dd, yyyy"); //NON-NLS + + ImageIO.write(image, "png", getReportFolderPath().resolve("snapshot.png").toFile()); //NON-NLS + + //make a map of context objects to resolve template paramaters against + HashMap snapShotContext = new HashMap<>(); + snapShotContext.put("reportTitle", getReportName()); //NON-NLS + + List filters = filter.getAndFilters(); + + for (SubFilter filter : filters) { + if (filter instanceof DateRangeFilter) { + long startDate = ((DateRangeFilter) filter).getStartDate(); + long endDate = ((DateRangeFilter) filter).getEndDate(); + + if (startDate > 0) { + + snapShotContext.put("startTime", formatter.format(new Date((Instant.ofEpochSecond(startDate)).toEpochMilli()))); //NON-NLS + } + + if (endDate > 0) { + snapShotContext.put("endTime", formatter.format(new Date((Instant.ofEpochSecond(endDate)).toEpochMilli()))); //NON-NLS + } + } else if (filter instanceof AccountTypeFilter) { + + Set selectedAccounts = ((AccountTypeFilter) filter).getAccountTypes(); + ArrayList fullAccountList = new ArrayList<>(); + for (Account.Type type : Account.Type.PREDEFINED_ACCOUNT_TYPES) { + if (type == Account.Type.CREDIT_CARD) { + continue; + } + + fullAccountList.add(new ReportWriterHelper(type.getDisplayName(), selectedAccounts.contains(type))); + } + + snapShotContext.put("accounts", fullAccountList); + } else if (filter instanceof DeviceFilter) { + Collection ids = ((DeviceFilter) filter).getDevices(); + ArrayList list = new ArrayList<>(); + try { + final SleuthkitCase sleuthkitCase = getCurrentCase().getSleuthkitCase(); + for (DataSource dataSource : sleuthkitCase.getDataSources()) { + boolean selected = ids.contains(dataSource.getDeviceId()); + String dsName = sleuthkitCase.getContentById(dataSource.getId()).getName(); + list.add(new ReportWriterHelper(dsName, selected)); + } + } catch (TskCoreException ex) { + + } + + snapShotContext.put("devices", list); + } + } + + fillTemplateAndWrite("/org/sleuthkit/autopsy/communications/snapshot/comm_snapshot_template.html", "Snapshot", snapShotContext, getReportFolderPath().resolve("snapshot.html")); //NON-NLS + } + + /** + * Helper class for use with the html template + */ + private final class ReportWriterHelper { + + private final String label; + private final boolean selected; + + /** + * Helper class for use with the html template. + * + * @param label Display label + * @param selected Boolean selected state + */ + ReportWriterHelper(String label, boolean selected) { + this.label = label; + this.selected = selected; + } + + /** + * Returns the display label + * + * @return The display label + */ + public String getLabel(){ + return label; + } + + /** + * Returns the selection state + * + * @return The selection state + */ + public boolean isSelected(){ + return selected; + } + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/communications/snapshot/comm_snapshot_template.html b/Core/src/org/sleuthkit/autopsy/communications/snapshot/comm_snapshot_template.html new file mode 100644 index 0000000000..078f6fc978 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/communications/snapshot/comm_snapshot_template.html @@ -0,0 +1,21 @@ + + + Communications Snapshot: {{reportTitle}} + + + + +
+ Snapshot + + + + + + {{#devices}}{{/devices}} + + {{#accounts}}{{/accounts}} +
Date Range
Start:{{startTime}}
End:{{endTime}}
Devices:
{{label}}
Account Types
{{label}}
+
+ + \ No newline at end of file diff --git a/Core/src/org/sleuthkit/autopsy/core/layer.xml b/Core/src/org/sleuthkit/autopsy/core/layer.xml index c25f37199e..02ea2b02cb 100644 --- a/Core/src/org/sleuthkit/autopsy/core/layer.xml +++ b/Core/src/org/sleuthkit/autopsy/core/layer.xml @@ -342,9 +342,9 @@ - + - + diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AddArchiveTask.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AddArchiveTask.java deleted file mode 100644 index e8e38dc743..0000000000 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AddArchiveTask.java +++ /dev/null @@ -1,374 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2011-2018 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.experimental.autoingest; - -import java.io.File; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.UUID; -import java.util.logging.Level; -import java.util.stream.Collectors; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.FilenameUtils; -import org.openide.util.Lookup; -import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.casemodule.LocalFilesDSProcessor; -import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback; -import static org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS; -import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgressMonitor; -import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.datasourceprocessors.AutoIngestDataSourceProcessor; -import org.sleuthkit.autopsy.coreutils.TimeStampUtils; -import org.sleuthkit.autopsy.datasourceprocessors.RawDSProcessor; -import org.sleuthkit.datamodel.Content; -import org.sleuthkit.datamodel.DataSource; - -/* - * A runnable that adds an archive data source as well as data sources contained - * in the archive to the case database. - */ -class AddArchiveTask implements Runnable { - - private final Logger logger = Logger.getLogger(AddArchiveTask.class.getName()); - private final String deviceId; - private final String archivePath; - private final DataSourceProcessorProgressMonitor progressMonitor; - private final DataSourceProcessorCallback callback; - private boolean criticalErrorOccurred; - private final Object archiveDspLock; - - private static final String ARCHIVE_EXTRACTOR_MODULE_OUTPUT_DIR = "Archive Extractor"; - - /** - * Constructs a runnable task that adds an archive as well as data sources - * contained in the archive to the case database. - * - * @param deviceId An ASCII-printable identifier for the device - * associated with the data source that is intended - * to be unique across multiple cases (e.g., a UUID). - * @param archivePath Path to the archive file. - * @param progressMonitor Progress monitor to report progress during - * processing. - * @param callback Callback to call when processing is done. - */ - AddArchiveTask(String deviceId, String archivePath, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) { - this.deviceId = deviceId; - this.archivePath = archivePath; - this.callback = callback; - this.progressMonitor = progressMonitor; - this.archiveDspLock = new Object(); - } - - /** - * Adds the archive to the case database. - */ - @Override - public void run() { - progressMonitor.setIndeterminate(true); - List errorMessages = new ArrayList<>(); - List newDataSources = new ArrayList<>(); - DataSourceProcessorCallback.DataSourceProcessorResult result; - if (!ArchiveUtil.isArchive(Paths.get(archivePath))) { - criticalErrorOccurred = true; - logger.log(Level.SEVERE, String.format("Input data source is not a valid datasource: %s", archivePath)); //NON-NLS - errorMessages.add("Input data source is not a valid datasource: " + archivePath); - result = DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS; - callback.done(result, errorMessages, newDataSources); - } - - logger.log(Level.INFO, "Using Archive Extractor DSP to process archive {0} ", archivePath); - - // extract the archive and pass the extracted folder as input - try { - Case currentCase = Case.getCurrentCaseThrows(); - - // create folder to extract archive to - Path destinationFolder = createDirectoryForFile(archivePath, currentCase.getModuleDirectory()); - if (destinationFolder.toString().isEmpty()) { - // unable to create directory - criticalErrorOccurred = true; - errorMessages.add(String.format("Unable to create directory {0} to extract archive {1} ", new Object[]{destinationFolder.toString(), archivePath})); - logger.log(Level.SEVERE, "Unable to create directory {0} to extract archive {1} ", new Object[]{destinationFolder.toString(), archivePath}); - return; - } - - // extract contents of ZIP archive into destination folder - List extractedFiles = new ArrayList<>(); - int numExtractedFilesRemaining = 0; - try { - progressMonitor.setProgressText(String.format("Extracting archive contents to: %s", destinationFolder.toString())); - extractedFiles = ArchiveUtil.unpackArchiveFile(archivePath, destinationFolder.toString()); - numExtractedFilesRemaining = extractedFiles.size(); - } catch (ArchiveUtil.ArchiveExtractionException ex) { - // delete extracted contents - logger.log(Level.SEVERE,"Exception while extracting archive contents into {0}. Deleteing the directory", destinationFolder.toString()); - FileUtils.deleteDirectory(destinationFolder.toFile()); - throw ex; - } - - // lookup all AutomatedIngestDataSourceProcessors so that we only do it once. - // LocalDisk, LocalFiles, and ArchiveDSP are removed from the list. - List processorCandidates = getListOfValidDataSourceProcessors(); - - // do processing - for (String file : extractedFiles) { - - // we only care about files, skip directories - File fileObject = new File(file); - if (fileObject.isDirectory()) { - numExtractedFilesRemaining--; - continue; - } - - // identify all "valid" DSPs that can process this file - List validDataSourceProcessors = getDataSourceProcessorsForFile(Paths.get(file), errorMessages, processorCandidates); - if (validDataSourceProcessors.isEmpty()) { - continue; - } - - // identified a "valid" data source within the archive - progressMonitor.setProgressText(String.format("Adding: %s", file)); - - /* - * NOTE: we have to move the valid data sources to a separate - * folder and then add the data source from that folder. This is - * necessary because after all valid data sources have been - * identified, we are going to add the remaining extracted - * contents of the archive as a single logical file set. Hence, - * if we do not move the data sources out of the extracted - * contents folder, those data source files will get added twice - * and can potentially result in duplicate keyword hits. - */ - Path newFolder = createDirectoryForFile(file, currentCase.getModuleDirectory()); - if (newFolder.toString().isEmpty()) { - // unable to create directory - criticalErrorOccurred = true; - errorMessages.add(String.format("Unable to create directory {0} to extract content of archive {1} ", new Object[]{newFolder.toString(), archivePath})); - logger.log(Level.SEVERE, "Unable to create directory {0} to extract content of archive {1} ", new Object[]{newFolder.toString(), archivePath}); - return; - } - - // Copy it to a different folder - FileUtils.copyFileToDirectory(fileObject, newFolder.toFile()); - Path newFilePath = Paths.get(newFolder.toString(), FilenameUtils.getName(file)); - - // Try each DSP in decreasing order of confidence - boolean success = false; - for (AutoIngestDataSourceProcessor selectedProcessor : validDataSourceProcessors) { - - logger.log(Level.INFO, "Using {0} to process extracted file {1} ", new Object[]{selectedProcessor.getDataSourceType(), file}); - synchronized (archiveDspLock) { - UUID taskId = UUID.randomUUID(); - currentCase.notifyAddingDataSource(taskId); - AutoIngestDataSource internalDataSource = new AutoIngestDataSource(deviceId, newFilePath); - DataSourceProcessorCallback internalArchiveDspCallBack = new AddDataSourceCallback(currentCase, internalDataSource, taskId, archiveDspLock); - selectedProcessor.process(deviceId, newFilePath, progressMonitor, internalArchiveDspCallBack); - archiveDspLock.wait(); - - // at this point we got the content object(s) from the current DSP. - // check whether the data source was processed successfully - if ((internalDataSource.getResultDataSourceProcessorResultCode() == CRITICAL_ERRORS) - || internalDataSource.getContent().isEmpty()) { - // move onto the the next DSP that can process this data source - for (String errorMessage : internalDataSource.getDataSourceProcessorErrorMessages()) { - logger.log(Level.SEVERE, "Data source processor {0} was unable to process {1}: {2}", new Object[]{selectedProcessor.getDataSourceType(), internalDataSource.getPath(), errorMessage}); - } - continue; - } - - // if we are here it means the data source was added successfully - success = true; - newDataSources.addAll(internalDataSource.getContent()); - - // update data source info - for (Content c:internalDataSource.getContent()) { - if (c instanceof DataSource) { - DataSource ds = (DataSource) c; - - // Read existing aquisition details and update them - String details = "Extracted from archive: " + archivePath.toString(); - String existingDetails = ds.getAcquisitionDetails(); - if (existingDetails != null && !existingDetails.isEmpty()) { - ds.setAcquisitionDetails(existingDetails + System.getProperty("line.separator") + details); - } else { - ds.setAcquisitionDetails(details); - } - - // Update the names for all new data sources to be the root archive plus the name of the data source - String newName = Paths.get(archivePath).getFileName() + "/" + ds.getName(); - ds.setDisplayName(newName); - currentCase.notifyDataSourceNameChanged(c, newName); - } - } - - // skip all other DSPs for this data source - break; - } - } - - if (success) { - // one of the DSPs successfully processed the data source. delete the - // copy of the data source in the original extracted archive folder. - // otherwise the data source is going to be added again as a logical file. - numExtractedFilesRemaining--; - FileUtils.deleteQuietly(fileObject); - } else { - // none of the DSPs were able to process the data source. delete the - // copy of the data source in the temporary folder. the data source is - // going to be added as a logical file with the rest of the extracted contents. - FileUtils.deleteQuietly(newFolder.toFile()); - } - } - - // after all archive contents have been examined (and moved to separate folders if necessary), - // add remaining extracted contents as one logical file set - if (numExtractedFilesRemaining > 0) { - progressMonitor.setProgressText(String.format("Adding: %s", destinationFolder.toString())); - logger.log(Level.INFO, "Adding directory {0} as logical file set", destinationFolder.toString()); - synchronized (archiveDspLock) { - UUID taskId = UUID.randomUUID(); - currentCase.notifyAddingDataSource(taskId); - AutoIngestDataSource internalDataSource = new AutoIngestDataSource(deviceId, destinationFolder); - DataSourceProcessorCallback internalArchiveDspCallBack = new AddDataSourceCallback(currentCase, internalDataSource, taskId, archiveDspLock); - - // folder where archive was extracted to - List pathsList = new ArrayList<>(); - pathsList.add(destinationFolder.toString()); - - // use archive file name as the name of the logical file set - String archiveFileName = FilenameUtils.getName(archivePath); - - LocalFilesDSProcessor localFilesDSP = new LocalFilesDSProcessor(); - localFilesDSP.run(deviceId, archiveFileName, pathsList, progressMonitor, internalArchiveDspCallBack); - - archiveDspLock.wait(); - - // at this point we got the content object(s) from the current DSP. - newDataSources.addAll(internalDataSource.getContent()); - - for (Content c : internalDataSource.getContent()) { - if (c instanceof DataSource) { - DataSource ds = (DataSource) c; - // This is a new data source so just write the aquisition details - String details = "Extracted from archive: " + archivePath.toString(); - ds.setAcquisitionDetails(details); - } - } - } - } - } catch (Exception ex) { - criticalErrorOccurred = true; - errorMessages.add(ex.getMessage()); - logger.log(Level.SEVERE, String.format("Critical error occurred while extracting archive %s", archivePath), ex); //NON-NLS - } finally { - logger.log(Level.INFO, "Finished processing of archive {0}", archivePath); - progressMonitor.setProgress(100); - if (criticalErrorOccurred) { - result = DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS; - } else if (!errorMessages.isEmpty()) { - result = DataSourceProcessorCallback.DataSourceProcessorResult.NONCRITICAL_ERRORS; - } else { - result = DataSourceProcessorCallback.DataSourceProcessorResult.NO_ERRORS; - } - callback.done(result, errorMessages, newDataSources); - } - } - - /** - * Get a list of data source processors. LocalFiles, RawDSProcessor, and - * ArchiveDSP are removed from the list. - * - * @return List of data source processors - */ - private List getListOfValidDataSourceProcessors() { - - Collection processorCandidates = Lookup.getDefault().lookupAll(AutoIngestDataSourceProcessor.class); - - List validDataSourceProcessors = processorCandidates.stream().collect(Collectors.toList()); - - for (Iterator iterator = validDataSourceProcessors.iterator(); iterator.hasNext();) { - AutoIngestDataSourceProcessor selectedProcessor = iterator.next(); - - // skip local files, only looking for "valid" data sources. - // also skip RawDSP as we don't want to add random "bin" and "raw" files that may be inside archive - // as individual data sources. - // also skip nested archive files, those will be ingested as logical files and extracted during ingest - if ((selectedProcessor instanceof LocalFilesDSProcessor) - || (selectedProcessor instanceof RawDSProcessor) - || (selectedProcessor instanceof ArchiveExtractorDSProcessor)) { - iterator.remove(); - } - } - - return validDataSourceProcessors; - } - - /** - * Get a list of data source processors that can process the data source of - * interest. The list is sorted by confidence in decreasing order. - * - * @param dataSourcePath Full path to the data source - * @param errorMessages List for error messages - * @param errorMessages List of AutoIngestDataSourceProcessor to try - * - * @return Ordered list of applicable DSPs - */ - private List getDataSourceProcessorsForFile(Path dataSourcePath, List errorMessages, - List processorCandidates) { - - // Get an ordered list of data source processors to try - List validDataSourceProcessorsForFile = Collections.emptyList(); - try { - validDataSourceProcessorsForFile = DataSourceProcessorUtility.getOrderedListOfDataSourceProcessors(dataSourcePath, processorCandidates); - } catch (AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException ex) { - criticalErrorOccurred = true; - errorMessages.add(ex.getMessage()); - logger.log(Level.SEVERE, String.format("Critical error occurred while extracting archive %s", archivePath), ex); //NON-NLS - return Collections.emptyList(); - } - return validDataSourceProcessorsForFile; - } - - /** - * Create a directory in ModuleOutput folder based on input file name. A - * time stamp is appended to the directory name. - * - * @param fileName File name - * @param baseDirectory Base directory. Typically the case output directory. - * - * @return Full path to the new directory - */ - private Path createDirectoryForFile(String fileName, String baseDirectory) { - // get file name without full path or extension - String fileNameNoExt = FilenameUtils.getBaseName(fileName); - - // create folder to extract archive to - Path newFolder = Paths.get(baseDirectory, ARCHIVE_EXTRACTOR_MODULE_OUTPUT_DIR, fileNameNoExt + "_" + TimeStampUtils.createTimeStamp()); - if (newFolder.toFile().mkdirs() == false) { - // unable to create directory - return Paths.get(""); - } - return newFolder; - } -} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ArchiveExtractorDSProcessor.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ArchiveExtractorDSProcessor.java deleted file mode 100644 index a30fbf0c75..0000000000 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ArchiveExtractorDSProcessor.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2011-2017 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.experimental.autoingest; - -import com.google.common.util.concurrent.ThreadFactoryBuilder; -import java.nio.file.Path; -import java.util.UUID; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import javax.swing.JPanel; -import org.openide.util.NbBundle; -import org.openide.util.lookup.ServiceProvider; -import org.openide.util.lookup.ServiceProviders; -import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback; -import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorProgressMonitor; -import org.sleuthkit.autopsy.datasourceprocessors.AutoIngestDataSourceProcessor; - -/** - * A data source processor that handles archive files. Implements the - * DataSourceProcessor service provider interface to allow integration with the - * add data source wizard. It also provides a run method overload to allow it to - * be used independently of the wizard. - */ -@ServiceProviders(value={ - @ServiceProvider(service=AutoIngestDataSourceProcessor.class)} -) -@NbBundle.Messages({ - "ArchiveDSP.dsType.text=Archive file"}) -public class ArchiveExtractorDSProcessor implements AutoIngestDataSourceProcessor { - - private final static String DATA_SOURCE_TYPE = Bundle.ArchiveDSP_dsType_text(); - - private final ArchiveFilePanel configPanel; - private String deviceId; - private String archivePath; - private boolean setDataSourceOptionsCalled; - - private final ExecutorService jobProcessingExecutor; - private static final String ARCHIVE_DSP_THREAD_NAME = "Archive-DSP-%d"; - private AddArchiveTask addArchiveTask; - - /** - * Constructs an archive data source processor that - * implements the DataSourceProcessor service provider interface to allow - * integration with the add data source wizard. It also provides a run - * method overload to allow it to be used independently of the wizard. - */ - public ArchiveExtractorDSProcessor() { - configPanel = ArchiveFilePanel.createInstance(ArchiveExtractorDSProcessor.class.getName(), ArchiveUtil.getArchiveFilters()); - jobProcessingExecutor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat(ARCHIVE_DSP_THREAD_NAME).build()); - } - - @Override - public int canProcess(Path dataSourcePath) throws AutoIngestDataSourceProcessorException { - // check whether this is an archive - if (ArchiveUtil.isArchive(dataSourcePath)){ - // return "high confidence" value - return 100; - } - return 0; - } - - @Override - public void process(String deviceId, Path dataSourcePath, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callBack) { - run(deviceId, dataSourcePath.toString(), progressMonitor, callBack); - } - - @Override - public String getDataSourceType() { - return DATA_SOURCE_TYPE; - } - - /** - * Gets the panel that allows a user to select a data source and do any - * configuration required by the data source. The panel is less than 544 - * pixels wide and less than 173 pixels high. - * - * @return A selection and configuration panel for this data source - * processor. - */ - @Override - public JPanel getPanel() { - configPanel.readSettings(); - configPanel.select(); - return configPanel; - } - - /** - * Indicates whether the settings in the selection and configuration panel - * are valid and complete. - * - * @return True if the settings are valid and complete and the processor is - * ready to have its run method called, false otherwise. - */ - @Override - public boolean isPanelValid() { - return configPanel.validatePanel(); - } - - /** - * Adds a data source to the case database using a background task in a - * separate thread and the settings provided by the selection and - * configuration panel. Returns as soon as the background task is started. - * The background task uses a callback object to signal task completion and - * return results. - * - * This method should not be called unless isPanelValid returns true. - * - * @param progressMonitor Progress monitor that will be used by the - * background task to report progress. - * @param callback Callback that will be used by the background task - * to return results. - */ - @Override - public void run(DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) { - if (!setDataSourceOptionsCalled) { - configPanel.storeSettings(); - deviceId = UUID.randomUUID().toString(); - archivePath = configPanel.getContentPaths(); - } - run(deviceId, archivePath, progressMonitor, callback); - } - - /** - * Adds a data source to the case database using a background task in a - * separate thread and the given settings instead of those provided by the - * selection and configuration panel. Returns as soon as the background task - * is started and uses the callback object to signal task completion and - * return results. - * - * @param deviceId An ASCII-printable identifier for the device - * associated with the data source that is - * intended to be unique across multiple cases - * (e.g., a UUID). - * @param archivePath Path to the archive file. - * @param progressMonitor Progress monitor for reporting progress - * during processing. - * @param callback Callback to call when processing is done. - */ - public void run(String deviceId, String archivePath, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) { - addArchiveTask = new AddArchiveTask(deviceId, archivePath, progressMonitor, callback); - jobProcessingExecutor.submit(addArchiveTask); - } - - /** - * This DSP is a service to AutoIngestDataSourceProcessor only. Hence it is - * only used by AIM. AIM currently doesn't support DSP cancellation. - */ - @Override - public void cancel() { - } - - @Override - public void reset() { - deviceId = null; - archivePath = null; - configPanel.reset(); - setDataSourceOptionsCalled = false; - } -} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ArchiveFilePanel.form b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ArchiveFilePanel.form deleted file mode 100644 index af9347df0c..0000000000 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ArchiveFilePanel.form +++ /dev/null @@ -1,94 +0,0 @@ - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ArchiveFilePanel.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ArchiveFilePanel.java deleted file mode 100644 index cbe39cac1b..0000000000 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ArchiveFilePanel.java +++ /dev/null @@ -1,289 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2011-2018 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.experimental.autoingest; - -import java.io.File; -import java.util.List; -import java.util.logging.Level; -import javax.swing.JFileChooser; -import javax.swing.JPanel; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; -import javax.swing.filechooser.FileFilter; -import org.apache.commons.lang3.StringUtils; -import org.openide.util.NbBundle; -import org.sleuthkit.autopsy.casemodule.Case; -import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; -import static org.sleuthkit.autopsy.experimental.autoingest.Bundle.*; -import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessor; -import org.sleuthkit.autopsy.coreutils.DriveUtils; -import org.sleuthkit.autopsy.coreutils.Logger; -import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; -import org.sleuthkit.autopsy.coreutils.ModuleSettings; -import org.sleuthkit.autopsy.coreutils.PathValidator; - -/** - * Panel for adding an archive file which is supported by 7zip library (e.g. - * "zip", "rar", "arj", "7z", "7zip", "gzip, etc). Allows the user to select a - * file. - */ -@SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives -class ArchiveFilePanel extends JPanel implements DocumentListener { - - private static final Logger logger = Logger.getLogger(ArchiveFilePanel.class.getName()); - private static final String PROP_LAST_ARCHIVE_PATH = "LBL_LastImage_PATH"; //NON-NLS - - private final JFileChooser fileChooser = new JFileChooser(); - - /** - * Externally supplied name is used to store settings - */ - private final String contextName; - - /** - * Creates new form ArchiveFilePanel - * - * @param context A string context name used to read/store last - * used settings. - * @param fileChooserFilters A list of filters to be used with the - * FileChooser. - */ - private ArchiveFilePanel(String context, List fileChooserFilters) { - this.contextName = context; - initComponents(); - - errorLabel.setVisible(false); - - fileChooser.setDragEnabled(false); - fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); - fileChooser.setMultiSelectionEnabled(false); - fileChooserFilters.forEach(fileChooser::addChoosableFileFilter); - if (fileChooserFilters.isEmpty() == false) { - fileChooser.setFileFilter(fileChooserFilters.get(0)); - } - } - - /** - * Creates and returns an instance of a ArchiveFilePanel. - * - * @param context A string context name used to read/store last - * used settings. - * @param fileChooserFilters A list of filters to be used with the - * FileChooser. - * - * @return instance of the ArchiveFilePanel - */ - public static synchronized ArchiveFilePanel createInstance(String context, List fileChooserFilters) { - ArchiveFilePanel instance = new ArchiveFilePanel(context, fileChooserFilters); - // post-constructor initialization of listener support without leaking references of uninitialized objects - instance.pathTextField.getDocument().addDocumentListener(instance); - return instance; - } - - /** - * 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. - */ - // //GEN-BEGIN:initComponents - private void initComponents() { - - pathLabel = new javax.swing.JLabel(); - browseButton = new javax.swing.JButton(); - pathTextField = new javax.swing.JTextField(); - errorLabel = new javax.swing.JLabel(); - - setMinimumSize(new java.awt.Dimension(0, 65)); - setPreferredSize(new java.awt.Dimension(403, 65)); - - org.openide.awt.Mnemonics.setLocalizedText(pathLabel, org.openide.util.NbBundle.getMessage(ArchiveFilePanel.class, "ArchiveFilePanel.pathLabel.text")); // NOI18N - - org.openide.awt.Mnemonics.setLocalizedText(browseButton, org.openide.util.NbBundle.getMessage(ArchiveFilePanel.class, "ArchiveFilePanel.browseButton.text")); // NOI18N - browseButton.addActionListener(new java.awt.event.ActionListener() { - public void actionPerformed(java.awt.event.ActionEvent evt) { - browseButtonActionPerformed(evt); - } - }); - - pathTextField.setText(org.openide.util.NbBundle.getMessage(ArchiveFilePanel.class, "ArchiveFilePanel.pathTextField.text")); // NOI18N - - errorLabel.setForeground(new java.awt.Color(255, 0, 0)); - org.openide.awt.Mnemonics.setLocalizedText(errorLabel, org.openide.util.NbBundle.getMessage(ArchiveFilePanel.class, "ArchiveFilePanel.errorLabel.text")); // NOI18N - - javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); - this.setLayout(layout); - layout.setHorizontalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addComponent(pathTextField) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) - .addComponent(browseButton) - .addGap(2, 2, 2)) - .addGroup(layout.createSequentialGroup() - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addComponent(pathLabel) - .addComponent(errorLabel)) - .addGap(0, 277, Short.MAX_VALUE)) - ); - layout.setVerticalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addComponent(pathLabel) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(browseButton) - .addComponent(pathTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) - .addGap(3, 3, 3) - .addComponent(errorLabel) - .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) - ); - }// //GEN-END:initComponents - - private void browseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_browseButtonActionPerformed - String oldText = getContentPaths(); - // set the current directory of the FileChooser if the ArchivePath Field is valid - File currentDir = new File(oldText); - if (currentDir.exists()) { - fileChooser.setCurrentDirectory(currentDir); - } - - if (fileChooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) { - String path = fileChooser.getSelectedFile().getPath(); - setContentPath(path); - } - - updateHelper(); - }//GEN-LAST:event_browseButtonActionPerformed - - // Variables declaration - do not modify//GEN-BEGIN:variables - private javax.swing.JButton browseButton; - private javax.swing.JLabel errorLabel; - private javax.swing.JLabel pathLabel; - private javax.swing.JTextField pathTextField; - // End of variables declaration//GEN-END:variables - - /** - * Get the path of the user selected archive. - * - * @return the archive path - */ - public String getContentPaths() { - return pathTextField.getText(); - } - - /** - * Set the path of the archive file. - * - * @param s path of the archive file - */ - public void setContentPath(String s) { - pathTextField.setText(s); - } - - public void reset() { - //reset the UI elements to default - pathTextField.setText(null); - } - - /** - * Should we enable the next button of the wizard? - * - * @return true if a proper archive has been selected, false otherwise - */ - @NbBundle.Messages({"DataSourceOnCDriveError.text=Warning: Path to multi-user data source is on \"C:\" drive", - "DataSourceOnCDriveError.noOpenCase.errMsg=Warning: Exception while getting open case." - }) - public boolean validatePanel() { - errorLabel.setVisible(false); - String path = getContentPaths(); - if (StringUtils.isBlank(path)) { - return false; - } - - // display warning if there is one (but don't disable "next" button) - try { - if (false == PathValidator.isValidForMultiUserCase(path, Case.getCurrentCaseThrows().getCaseType())) { - errorLabel.setVisible(true); - errorLabel.setText(Bundle.DataSourceOnCDriveError_text()); - } - } catch (NoCurrentCaseException ex) { - errorLabel.setVisible(true); - errorLabel.setText(Bundle.DataSourceOnCDriveError_noOpenCase_errMsg()); - } - - return new File(path).isFile() - || DriveUtils.isPhysicalDrive(path) - || DriveUtils.isPartition(path); - } - - public void storeSettings() { - String archivePathName = getContentPaths(); - if (null != archivePathName) { - String archivePath = archivePathName.substring(0, archivePathName.lastIndexOf(File.separator) + 1); - ModuleSettings.setConfigSetting(contextName, PROP_LAST_ARCHIVE_PATH, archivePath); - } - } - - public void readSettings() { - String lastArchivePath = ModuleSettings.getConfigSetting(contextName, PROP_LAST_ARCHIVE_PATH); - if (StringUtils.isNotBlank(lastArchivePath)) { - setContentPath(lastArchivePath); - } - } - - @Override - public void insertUpdate(DocumentEvent e) { - updateHelper(); - } - - @Override - public void removeUpdate(DocumentEvent e) { - updateHelper(); - } - - @Override - public void changedUpdate(DocumentEvent e) { - updateHelper(); - } - - /** - * Update functions are called by the pathTextField which has this set as - * it's DocumentEventListener. Each update function fires a property change - * to be caught by the parent panel. - * - */ - @NbBundle.Messages({"ArchiveFilePanel.moduleErr=Module Error", - "ArchiveFilePanel.moduleErr.msg=A module caused an error listening to ArchiveFilePanel updates." - + " See log to determine which module. Some data could be incomplete.\n"}) - private void updateHelper() { - try { - firePropertyChange(DataSourceProcessor.DSP_PANEL_EVENT.UPDATE_UI.toString(), false, true); - } catch (Exception e) { - logger.log(Level.SEVERE, "ArchiveFilePanel listener threw exception", e); //NON-NLS - MessageNotifyUtil.Notify.error(ArchiveFilePanel_moduleErr(), ArchiveFilePanel_moduleErr_msg()); - } - } - - /** - * Set the focus to the pathTextField. - */ - public void select() { - pathTextField.requestFocusInWindow(); - } -} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ArchiveUtil.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ArchiveUtil.java deleted file mode 100644 index c22dfbd4ee..0000000000 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/ArchiveUtil.java +++ /dev/null @@ -1,313 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2015 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.experimental.autoingest; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.io.RandomAccessFile; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import javax.swing.filechooser.FileFilter; -import net.sf.sevenzipjbinding.ISequentialOutStream; -import net.sf.sevenzipjbinding.ISevenZipInArchive; -import net.sf.sevenzipjbinding.SevenZip; -import net.sf.sevenzipjbinding.SevenZipException; -import net.sf.sevenzipjbinding.SevenZipNativeInitializationException; -import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream; -import net.sf.sevenzipjbinding.simple.ISimpleInArchive; -import net.sf.sevenzipjbinding.simple.ISimpleInArchiveItem; -import org.openide.util.NbBundle; -import org.sleuthkit.autopsy.casemodule.GeneralFilter; -import org.sleuthkit.autopsy.coreutils.FileUtil; - -/** - * Set of utilities that handles archive file extraction. Uses 7zip library. - */ -final class ArchiveUtil { - - private static final String[] SUPPORTED_EXTENSIONS = {"zip", "rar", "arj", "7z", "7zip", "gzip", "gz", "bzip2", "tar", "tgz",}; // NON-NLS - private static final List ARCHIVE_EXTS = Arrays.asList(".zip", ".rar", ".arj", ".7z", ".7zip", ".gzip", ".gz", ".bzip2", ".tar", ".tgz"); //NON-NLS - @NbBundle.Messages("GeneralFilter.archiveDesc.text=Archive Files (.zip, .rar, .arj, .7z, .7zip, .gzip, .gz, .bzip2, .tar, .tgz)") - private static final String ARCHIVE_DESC = Bundle.GeneralFilter_archiveDesc_text(); - private static final GeneralFilter SEVEN_ZIP_FILTER = new GeneralFilter(ARCHIVE_EXTS, ARCHIVE_DESC); - private static final List ARCHIVE_FILTERS = new ArrayList<>(); - static { - ARCHIVE_FILTERS.add(SEVEN_ZIP_FILTER); - } - - private ArchiveUtil() { - } - - static List getArchiveFilters() { - return ARCHIVE_FILTERS; - } - - static boolean isArchive(Path dataSourcePath) { - String fileName = dataSourcePath.getFileName().toString(); - // check whether it's a zip archive file that can be extracted - return isAcceptedByFiler(new File(fileName), ARCHIVE_FILTERS); - } - - private static boolean isAcceptedByFiler(File file, List filters) { - for (FileFilter filter : filters) { - if (filter.accept(file)) { - return true; - } - } - return false; - } - - /** - * Enum of mime types which support archive extraction - */ - private enum SupportedArchiveExtractionFormats { - - ZIP("application/zip"), //NON-NLS - SEVENZ("application/x-7z-compressed"), //NON-NLS - GZIP("application/gzip"), //NON-NLS - XGZIP("application/x-gzip"), //NON-NLS - XBZIP2("application/x-bzip2"), //NON-NLS - XTAR("application/x-tar"), //NON-NLS - XGTAR("application/x-gtar"), - XRAR("application/x-rar-compressed"); //NON-NLS - - private final String mimeType; - - SupportedArchiveExtractionFormats(final String mimeType) { - this.mimeType = mimeType; - } - - @Override - public String toString() { - return this.mimeType; - } - } - - /** - * Exception thrown when archive handling resulted in an error - */ - static class ArchiveExtractionException extends Exception { - - private static final long serialVersionUID = 1L; - - ArchiveExtractionException(String message) { - super(message); - } - - ArchiveExtractionException(String message, Throwable cause) { - super(message, cause); - } - } - - /** - * This method returns array of supported file extensions. - * - * @return String array of supported file extensions. - */ - static String[] getSupportedArchiveTypes(){ - return SUPPORTED_EXTENSIONS; - } - - /** - * This method returns true if the MIME type is currently supported. Else it - * returns false. - * - * @param mimeType File mime type - * - * @return This method returns true if the file format is currently - * supported. Else it returns false. - */ - static boolean isExtractionSupportedByMimeType(String mimeType) { - for (SupportedArchiveExtractionFormats s : SupportedArchiveExtractionFormats.values()) { - if (s.toString().equals(mimeType)) { - return true; - } - } - return false; - } - - /** - * This method returns true if the file extension is currently supported. - * Else it returns false. Attempt extension based detection in case Apache - * Tika based detection fails. - * - * @param extension File extension - * - * @return This method returns true if the file format is currently - * supported. Else it returns false. - */ - static boolean isExtractionSupportedByFileExtension(String extension) { - // attempt extension matching - for (String supportedExtension : SUPPORTED_EXTENSIONS) { - if (extension.equals(supportedExtension)) { - return true; - } - } - return false; - } - - /** - * Returns a list of file names contained within an archive. - * - * @param archiveFilePath Full path to the archive file - * - * @return List of file names contained within archive - * - * @throws - * ArchiveExtractionException - */ - static List getListOfFilesWithinArchive(String archiveFilePath) throws ArchiveExtractionException { - if (!SevenZip.isInitializedSuccessfully() && (SevenZip.getLastInitializationException() == null)) { - try { - SevenZip.initSevenZipFromPlatformJAR(); - } catch (SevenZipNativeInitializationException ex) { - throw new ArchiveExtractionException("AutoIngestDashboard_bnPause_paused", ex); - } - } - List files = new ArrayList<>(); - ISevenZipInArchive inArchive = null; - try { - RandomAccessFile randomAccessFile = new RandomAccessFile(new File(archiveFilePath), "r"); - inArchive = SevenZip.openInArchive(null, new RandomAccessFileInStream(randomAccessFile)); - final ISimpleInArchive simpleInArchive = inArchive.getSimpleInterface(); - for (ISimpleInArchiveItem item : simpleInArchive.getArchiveItems()) { - files.add(item.getPath()); - } - } catch (Exception ex) { - throw new ArchiveExtractionException("Exception while reading archive contents", ex); - } finally { - if (inArchive != null) { - try { - inArchive.close(); - } catch (SevenZipException ex) { - throw new ArchiveExtractionException("Exception while closing the archive", ex); - } - } - } - return files; - } - - /** - * Extracts contents of an archive file into a directory. - * - * @param archiveFilePath Full path to archive. - * @param destinationFolder Path to directory where results will be - * extracted to. - * - * @return List of file names contained within archive - * @throws - * ArchiveExtractionException - */ - static List unpackArchiveFile(String archiveFilePath, String destinationFolder) throws ArchiveExtractionException { - if (!SevenZip.isInitializedSuccessfully() && (SevenZip.getLastInitializationException() == null)) { - try { - SevenZip.initSevenZipFromPlatformJAR(); - } catch (SevenZipNativeInitializationException ex) { - throw new ArchiveExtractionException("Unable to initialize 7Zip libraries", ex); - } - } - List files = new ArrayList<>(); - ISevenZipInArchive inArchive = null; - try { - RandomAccessFile randomAccessFile = new RandomAccessFile(new File(archiveFilePath), "r"); - inArchive = SevenZip.openInArchive(null, new RandomAccessFileInStream(randomAccessFile)); - final ISimpleInArchive simpleInArchive = inArchive.getSimpleInterface(); - - for (ISimpleInArchiveItem entry : simpleInArchive.getArchiveItems()) { - String entryPathInArchive = entry.getPath(); - Path fullPath = Paths.get(destinationFolder, FileUtil.escapeFileName(entryPathInArchive)); // remove illegal characters from file name - File destFile = new File(fullPath.toString()); - File destinationParent = destFile.getParentFile(); - destinationParent.mkdirs(); - if (!entry.isFolder()) { - UnpackStream unpackStream = null; - try { - Long size = entry.getSize(); - unpackStream = new UnpackStream(destFile.toString(), size); - entry.extractSlow(unpackStream); - } catch (Exception ex) { - throw new ArchiveExtractionException("Exception while unpacking archive contents", ex); - } finally { - if (unpackStream != null) { - unpackStream.close(); - } - } - } - // keep track of extracted files - files.add(fullPath.toString()); - } - } catch (Exception ex) { - throw new ArchiveExtractionException("Exception while unpacking archive contents", ex); - } finally { - try { - if (inArchive != null) { - inArchive.close(); - } - } catch (SevenZipException ex) { - throw new ArchiveExtractionException("Exception while closing the archive", ex); - } - } - return files; - } - - /** - * Stream used to unpack an archive to local file - */ - private static class UnpackStream implements ISequentialOutStream { - - private OutputStream output; - private String destFilePath; - - UnpackStream(String destFilePath, long size) throws ArchiveExtractionException { - this.destFilePath = destFilePath; - try { - output = new FileOutputStream(destFilePath); - } catch (IOException ex) { - throw new ArchiveExtractionException("Exception while unpacking archive contents", ex); - } - - } - - @Override - public int write(byte[] bytes) throws SevenZipException { - try { - output.write(bytes); - } catch (IOException ex) { - throw new SevenZipException("Error writing unpacked file to " + destFilePath, ex); - } - return bytes.length; - } - - public void close() throws ArchiveExtractionException { - if (output != null) { - try { - output.flush(); - output.close(); - } catch (IOException ex) { - throw new ArchiveExtractionException("Exception while closing the archive", ex); - } - } - } - } -} diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java index 55332cc03c..cf0e5c615d 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java @@ -32,7 +32,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.BasicFileAttributes; -import java.sql.SQLException; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; @@ -70,7 +69,6 @@ import org.sleuthkit.autopsy.coordinationservice.CoordinationService.Lock; import org.sleuthkit.autopsy.core.RuntimeProperties; import org.sleuthkit.autopsy.core.ServicesMonitor; import org.sleuthkit.autopsy.core.ServicesMonitor.ServicesMonitorException; -import org.sleuthkit.autopsy.core.UserPreferencesException; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback; import org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback.DataSourceProcessorResult; import static org.sleuthkit.autopsy.corecomponentinterfaces.DataSourceProcessorCallback.DataSourceProcessorResult.CRITICAL_ERRORS; @@ -92,6 +90,9 @@ import org.sleuthkit.autopsy.experimental.configuration.SharedConfiguration; import org.sleuthkit.autopsy.experimental.configuration.SharedConfiguration.SharedConfigurationException; import org.sleuthkit.autopsy.datasourceprocessors.AutoIngestDataSourceProcessor; import org.sleuthkit.autopsy.datasourceprocessors.AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException; +import org.sleuthkit.autopsy.datasourceprocessors.AutoIngestDataSource; +import org.sleuthkit.autopsy.datasourceprocessors.AddDataSourceCallback; +import org.sleuthkit.autopsy.datasourceprocessors.DataSourceProcessorUtility; import org.sleuthkit.autopsy.experimental.autoingest.AutoIngestJob.AutoIngestJobException; import org.sleuthkit.autopsy.experimental.autoingest.AutoIngestNodeControlEvent.ControlEventType; import org.sleuthkit.autopsy.ingest.IngestJob; diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties index 4d2ade1b0a..0f074ca11c 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties @@ -219,10 +219,6 @@ AutoIngestMetricsDialog.reportTextArea.text= AutoIngestMetricsDialog.metricsButton.text=Generate Metrics Report AutoIngestMetricsDialog.closeButton.text=Close AutoIngestMetricsDialog.datePicker.toolTipText=Choose a date -ArchiveFilePanel.pathLabel.text=Browse for an archive file: -ArchiveFilePanel.browseButton.text=Browse -ArchiveFilePanel.pathTextField.text= -ArchiveFilePanel.errorLabel.text=Error Label AutoIngestMetricsDialog.startingDataLabel.text=Starting Date: AutoIngestControlPanel.bnDeprioritizeCase.text=Deprioritize Case AutoIngestControlPanel.bnDeprioritizeJob.text=Deprioritize Job diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties-MERGED b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties-MERGED index 0026d0c670..a7336e26a6 100755 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties-MERGED +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties-MERGED @@ -8,9 +8,6 @@ AinStatusNode.status.shuttingdown=Shutting Down AinStatusNode.status.startingup=Starting Up AinStatusNode.status.title=Status AinStatusNode.status.unknown=Unknown -ArchiveDSP.dsType.text=Archive file -ArchiveFilePanel.moduleErr=Module Error -ArchiveFilePanel.moduleErr.msg=A module caused an error listening to ArchiveFilePanel updates. See log to determine which module. Some data could be incomplete.\n AutoIngestAdminActions.cancelJobAction.title=Cancel Job AutoIngestAdminActions.cancelModuleAction.title=Cancel Module AutoIngestAdminActions.deleteCaseAction.error=Failed to delete case. @@ -170,12 +167,9 @@ CTL_AutoIngestDashboardOpenAction=Auto Ingest Dashboard CTL_AutoIngestDashboardTopComponent=Auto Ingest Jobs CTL_CasesDashboardAction=Multi-User Cases Dashboard CTL_CasesDashboardTopComponent=Cases -DataSourceOnCDriveError.noOpenCase.errMsg=Warning: Exception while getting open case. -DataSourceOnCDriveError.text=Warning: Path to multi-user data source is on "C:" drive DeleteCaseInputDirectoriesAction.menuItemText=Delete Input Directories DeleteCasesAction.menuItemText=Delete Case and Jobs DeleteCasesForReprocessingAction.menuItemText=Delete for Reprocessing -GeneralFilter.archiveDesc.text=Archive Files (.zip, .rar, .arj, .7z, .7zip, .gzip, .gz, .bzip2, .tar, .tgz) HINT_CasesDashboardTopComponent=This is an adminstrative dashboard for multi-user cases OpenAutoIngestLogAction.deletedLogErrorMsg=The case auto ingest log has been deleted. OpenAutoIngestLogAction.logOpenFailedErrorMsg=Failed to open case auto ingest log. See application log for details. @@ -183,7 +177,7 @@ OpenAutoIngestLogAction.menuItemText=Open Auto Ingest Log File # {0} - caseErrorMessage OpenCaseAction.errorMsg=Failed to open case: {0} OpenCaseAction.menuItemText=Open -OpenIDE-Module-Long-Description=This module contains features that are being developed by Basis Technology and are not part of the default Autopsy distribution. You can enable this module to use the new features. The features should be stable, but their exact behavior and API are subject to change. \n\nWe make no guarantee that the API of this module will not change, so developers should be careful when relying on it. +OpenIDE-Module-Long-Description=This module contains features that are being developed by Basis Technology and are not part of the default Autopsy distribution. You can enable this module to use the new features. The features should be stable, but their exact behavior and API are subject to change.\n\nWe make no guarantee that the API of this module will not change, so developers should be careful when relying on it. OpenIDE-Module-Name=Experimental OpenIDE-Module-Short-Description=This module contains features that are being developed by Basis Technology and are not part of the default Autopsy distribution. DisplayLogDialog.cannotOpenLog=Unable to open the selected case log file @@ -377,10 +371,6 @@ AutoIngestMetricsDialog.reportTextArea.text= AutoIngestMetricsDialog.metricsButton.text=Generate Metrics Report AutoIngestMetricsDialog.closeButton.text=Close AutoIngestMetricsDialog.datePicker.toolTipText=Choose a date -ArchiveFilePanel.pathLabel.text=Browse for an archive file: -ArchiveFilePanel.browseButton.text=Browse -ArchiveFilePanel.pathTextField.text= -ArchiveFilePanel.errorLabel.text=Error Label AutoIngestMetricsDialog.startingDataLabel.text=Starting Date: AutoIngestControlPanel.bnDeprioritizeCase.text=Deprioritize Case AutoIngestControlPanel.bnDeprioritizeJob.text=Deprioritize Job diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DataSourceProcessorUtility.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DataSourceProcessorUtility.java deleted file mode 100644 index 4878f7fa7d..0000000000 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/DataSourceProcessorUtility.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2011-2017 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.experimental.autoingest; - -import java.nio.file.Path; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import org.openide.util.Lookup; -import org.sleuthkit.autopsy.datasourceprocessors.AutoIngestDataSourceProcessor; -import org.sleuthkit.autopsy.datasourceprocessors.AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException; - -/** - * A utility class to find Data Source Processors - */ -class DataSourceProcessorUtility { - - private DataSourceProcessorUtility() { - } - - /** - * A utility method to find all Data Source Processors (DSP) that are able - * to process the input data source. Only the DSPs that implement - * AutoIngestDataSourceProcessor interface are used. - * - * @param dataSourcePath Full path to the data source - * @return Hash map of all DSPs that can process the data source along with - * their confidence score - * @throws - * org.sleuthkit.autopsy.datasourceprocessors.AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException - */ - static Map getDataSourceProcessorForFile(Path dataSourcePath, Collection processorCandidates) throws AutoIngestDataSourceProcessorException { - Map validDataSourceProcessorsMap = new HashMap<>(); - for (AutoIngestDataSourceProcessor processor : processorCandidates) { - int confidence = processor.canProcess(dataSourcePath); - if (confidence > 0) { - validDataSourceProcessorsMap.put(processor, confidence); - } - } - - return validDataSourceProcessorsMap; - } - - /** - * A utility method to find all Data Source Processors (DSP) that are able - * to process the input data source. Only the DSPs that implement - * AutoIngestDataSourceProcessor interface are used. Returns ordered list of - * data source processors. DSPs are ordered in descending order from highest - * confidence to lowest. - * - * @param dataSourcePath Full path to the data source - * - * @return Ordered list of data source processors. DSPs are ordered in - * descending order from highest confidence to lowest. - * - * @throws - * org.sleuthkit.autopsy.datasourceprocessors.AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException - */ - static List getOrderedListOfDataSourceProcessors(Path dataSourcePath) throws AutoIngestDataSourceProcessorException { - // lookup all AutomatedIngestDataSourceProcessors - Collection processorCandidates = Lookup.getDefault().lookupAll(AutoIngestDataSourceProcessor.class); - return getOrderedListOfDataSourceProcessors(dataSourcePath, processorCandidates); - } - - /** - * A utility method to find all Data Source Processors (DSP) that are able - * to process the input data source. Only the DSPs that implement - * AutoIngestDataSourceProcessor interface are used. Returns ordered list of - * data source processors. DSPs are ordered in descending order from highest - * confidence to lowest. - * - * @param dataSourcePath Full path to the data source - * @param processorCandidates Collection of AutoIngestDataSourceProcessor objects to use - * - * @return Ordered list of data source processors. DSPs are ordered in - * descending order from highest confidence to lowest. - * - * @throws - * org.sleuthkit.autopsy.datasourceprocessors.AutoIngestDataSourceProcessor.AutoIngestDataSourceProcessorException - */ - static List getOrderedListOfDataSourceProcessors(Path dataSourcePath, Collection processorCandidates) throws AutoIngestDataSourceProcessorException { - Map validDataSourceProcessorsMap = getDataSourceProcessorForFile(dataSourcePath, processorCandidates); - return orderDataSourceProcessorsByConfidence(validDataSourceProcessorsMap); - } - - - /** - * A utility method to get an ordered list of data source processors. DSPs - * are ordered in descending order from highest confidence to lowest. - * - * @param validDataSourceProcessorsMap Hash map of all DSPs that can process - * the data source along with their confidence score - * @return Ordered list of data source processors - */ - static List orderDataSourceProcessorsByConfidence(Map validDataSourceProcessorsMap) { - List validDataSourceProcessors = validDataSourceProcessorsMap.entrySet().stream() - .sorted(Map.Entry.comparingByValue().reversed()) - .map(Map.Entry::getKey) - .collect(Collectors.toList()); - - return validDataSourceProcessors; - } -} diff --git a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.java b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.java index 3d20fb7edb..701a2628aa 100644 --- a/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.java +++ b/ImageGallery/src/org/sleuthkit/autopsy/imagegallery/gui/drawableviews/GroupPane.java @@ -890,9 +890,6 @@ public class GroupPane extends BorderPane { t.consume(); break; case SECONDARY: - if (t.getClickCount() == 1) { - selectAllFiles(); - } if (isNotEmpty(selectionModel.getSelected())) { if (contextMenu == null) { contextMenu = buildContextMenu(); diff --git a/KeywordSearch/nbproject/project.xml b/KeywordSearch/nbproject/project.xml index 109ee0e66c..b87ba81e88 100644 --- a/KeywordSearch/nbproject/project.xml +++ b/KeywordSearch/nbproject/project.xml @@ -49,6 +49,14 @@ 1.31.1
+ + org.netbeans.swing.outline + + + + 1.34.1 + + org.openide.awt diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle.properties-MERGED b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle.properties-MERGED index 41d9e511b0..2247d7e198 100755 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle.properties-MERGED +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Bundle.properties-MERGED @@ -34,7 +34,8 @@ KeywordSearchIngestModule.startupMessage.failedToGetIndexSchema=Failed to get sc KeywordSearchResultFactory.createNodeForKey.noResultsFound.text=No results found. KeywordSearchResultFactory.query.exception.msg=Could not perform the query OpenIDE-Module-Display-Category=Ingest Module -OpenIDE-Module-Long-Description=Keyword Search ingest module.\n\nThe module indexes files found in the disk image at ingest time. \nIt then periodically runs the search on the indexed files using one or more keyword lists (containing pure words and/or regular expressions) and posts results.\n\nThe module also contains additional tools integrated in the main GUI, such as keyword list configuration, keyword seach bar in the top-right corner, extracted text viewer and search results viewer showing highlighted keywords found. + +OpenIDE-Module-Long-Description=Keyword Search ingest module.\n\nThe module indexes files found in the disk image at ingest time.\nIt then periodically runs the search on the indexed files using one or more keyword lists (containing pure words and/or regular expressions) and posts results.\n\nThe module also contains additional tools integrated in the main GUI, such as keyword list configuration, keyword search bar in the top-right corner, extracted text viewer and search results viewer showing highlighted keywords found. OpenIDE-Module-Name=KeywordSearch OptionsCategory_Name_KeywordSearchOptions=Keyword Search OptionsCategory_Keywords_KeywordSearchOptions=Keyword Search @@ -128,7 +129,7 @@ KeywordSearchFilterNode.getFileActions.viewInNewWinActionLbl=View in New Window KeywordSearchIngestModule.init.noKwInLstMsg=No keywords in keyword list. KeywordSearchIngestModule.init.onlyIdxKwSkipMsg=Only indexing will be done and keyword search will be skipped (you can still add keyword lists using the Keyword Lists - Add to Ingest). KeywordSearchIngestModule.doInBackGround.displayName=Periodic Keyword Search -KeywordSearchIngestModule.doInBackGround.finalizeMsg=- Finalizing +KeywordSearchIngestModule.doInBackGround.finalizeMsg=Finalizing KeywordSearchIngestModule.doInBackGround.pendingMsg=(Pending) RawText.FileText=File Text RawText.ResultText=Result Text @@ -224,7 +225,7 @@ Server.start.exception.cantStartSolr.msg=Could not start Solr server process Server.start.exception.cantStartSolr.msg2=Could not start Solr server process Server.isRunning.exception.errCheckSolrRunning.msg=Error checking if Solr server is running Server.isRunning.exception.errCheckSolrRunning.msg2=Error checking if Solr server is running -Server.openCore.exception.alreadyOpen.msg=Already an open Core! Explicitely close Core first. +Server.openCore.exception.alreadyOpen.msg=There is an already open Solr core. Explicitly close the core first. Server.queryNumIdxFiles.exception.msg=Error querying number of indexed files, Server.queryNumIdxChunks.exception.msg=Error querying number of indexed chunks, Server.queryNumIdxDocs.exception.msg=Error querying number of indexed documents, diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchIngestModule.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchIngestModule.java index ebebd6294f..beee700f85 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchIngestModule.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/KeywordSearchIngestModule.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2011-2018 Basis Technology Corp. + * Copyright 2011-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,7 +25,6 @@ import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; -import org.openide.util.Exceptions; import org.openide.util.Lookup; import org.openide.util.NbBundle; import org.openide.util.NbBundle.Messages; @@ -71,9 +70,11 @@ import org.sleuthkit.datamodel.TskData.FileKnown; "CannotRunFileTypeDetection=Unable to run file type detection." }) public final class KeywordSearchIngestModule implements FileIngestModule { - - /** generally text extractors should ignore archives and let unpacking - * modules take care of them */ + + /** + * generally text extractors should ignore archives and let unpacking + * modules take care of them + */ private static final List ARCHIVE_MIME_TYPES = ImmutableList.of( //ignore unstructured binary and compressed data, for which string extraction or unzipper works better @@ -108,7 +109,7 @@ public final class KeywordSearchIngestModule implements FileIngestModule { "application/x-lzop", //NON-NLS "application/x-z", //NON-NLS "application/x-compress"); //NON-NLS - + /** * Options for this extractor */ @@ -117,7 +118,6 @@ public final class KeywordSearchIngestModule implements FileIngestModule { EXTRACT_UTF8, ///< extract UTF8 text, true/false }; - enum UpdateFrequency { FAST(20), @@ -290,15 +290,15 @@ public final class KeywordSearchIngestModule implements FileIngestModule { } } } - + StringsConfig stringsConfig = new StringsConfig(); Map stringsOptions = KeywordSearchSettings.getStringExtractOptions(); stringsConfig.setExtractUTF8(Boolean.parseBoolean(stringsOptions.get(StringsExtractOptions.EXTRACT_UTF8.toString()))); stringsConfig.setExtractUTF16(Boolean.parseBoolean(stringsOptions.get(StringsExtractOptions.EXTRACT_UTF16.toString()))); stringsConfig.setLanguageScripts(KeywordSearchSettings.getStringExtractScripts()); - + stringsExtractionContext = Lookups.fixed(stringsConfig); - + indexer = new Indexer(); initialized = true; } @@ -482,12 +482,12 @@ public final class KeywordSearchIngestModule implements FileIngestModule { imageConfig.setOCREnabled(KeywordSearchSettings.getOcrOption()); ProcessTerminator terminator = () -> context.fileIngestIsCancelled(); Lookup extractionContext = Lookups.fixed(imageConfig, terminator); - + try { - TextExtractor extractor = TextExtractorFactory.getExtractor(aFile,extractionContext); + TextExtractor extractor = TextExtractorFactory.getExtractor(aFile, extractionContext); Reader extractedTextReader = extractor.getReader(); //divide into chunks and index - return Ingester.getDefault().indexText(extractedTextReader,aFile.getId(),aFile.getName(), aFile, context); + return Ingester.getDefault().indexText(extractedTextReader, aFile.getId(), aFile.getName(), aFile, context); } catch (TextExtractorFactory.NoTextExtractorFound | TextExtractor.InitReaderException ex) { //No text extractor found... run the default instead return false; @@ -509,7 +509,7 @@ public final class KeywordSearchIngestModule implements FileIngestModule { } TextExtractor stringsExtractor = TextExtractorFactory.getStringsExtractor(aFile, stringsExtractionContext); Reader extractedTextReader = stringsExtractor.getReader(); - if (Ingester.getDefault().indexText(extractedTextReader,aFile.getId(),aFile.getName(), aFile, KeywordSearchIngestModule.this.context)) { + if (Ingester.getDefault().indexText(extractedTextReader, aFile.getId(), aFile.getName(), aFile, KeywordSearchIngestModule.this.context)) { putIngestStatus(jobId, aFile.getId(), IngestStatus.STRINGS_INGESTED); return true; } else { @@ -619,12 +619,16 @@ public final class KeywordSearchIngestModule implements FileIngestModule { try { TextFileExtractor textFileExtractor = new TextFileExtractor(); Reader textReader = textFileExtractor.getReader(aFile); - if (Ingester.getDefault().indexText(textReader, aFile.getId(), aFile.getName(), aFile, context)) { + if (textReader == null) { + logger.log(Level.INFO, "Unable to extract with TextFileExtractor, Reader was null for file: {0}", aFile.getName()); + } else if (Ingester.getDefault().indexText(textReader, aFile.getId(), aFile.getName(), aFile, context)) { putIngestStatus(jobId, aFile.getId(), IngestStatus.TEXT_INGESTED); wasTextAdded = true; } - } catch (IngesterException | TextFileExtractorException ex) { + } catch (IngesterException ex) { logger.log(Level.WARNING, "Unable to index as unicode", ex); + } catch (TextFileExtractorException ex) { + logger.log(Level.INFO, "Could not extract text with TextFileExtractor", ex); } } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/TextFileExtractor.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/TextFileExtractor.java index 117c9ad6e9..66d26a95bf 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/TextFileExtractor.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/TextFileExtractor.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2018 Basis Technology Corp. + * Copyright 2018-2019 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -48,7 +48,9 @@ final class TextFileExtractor { throw new TextFileExtractorException("Unable to get string from detected text in TextFileExtractor", ex); } CharsetMatch match = detector.detect(); - if (match.getConfidence() < MIN_MATCH_CONFIDENCE) { + if (match == null) { + throw new TextFileExtractorException("Unable to detect any matches using TextFileExtractor"); + } else if (match.getConfidence() < MIN_MATCH_CONFIDENCE) { throw new TextFileExtractorException("Text does not match any character set with a high enough confidence for TextFileExtractor"); } diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/Bundle.properties b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/Bundle.properties new file mode 100755 index 0000000000..97b54b454f --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/Bundle.properties @@ -0,0 +1,21 @@ +SelectMultiUserCasesPanel.selectAllButton.text=Select All +SelectMultiUserCasesPanel.deselectAllButton.text=Deselect All +SelectMultiUserCasesPanel.jLabel1.text=Select case(s) for keyword search or start typing to search by case name +SelectMultiUserCasesPanel.confirmSelections.text=OK +SelectMultiUserCasesPanel.cancelButton.text=Cancel +MultiCaseKeywordSearchErrorDialog.closeButton.text=Close +MultiCaseKeywordSearchPanel.exactRadioButton.text_1=Exact Match +MultiCaseKeywordSearchPanel.substringRadioButton.text_1=Substring Match +MultiCaseKeywordSearchPanel.regexRadioButton.text_1=Regular Expression +MultiCaseKeywordSearchPanel.keywordTextField.text_1= +MultiCaseKeywordSearchPanel.toolDescriptionTextArea.text=Perform a keyword search on the selected cases. The case can be opened to examine the results more closely. +MultiCaseKeywordSearchPanel.casesLabel.text_1=Cases +MultiCaseKeywordSearchPanel.resultsLabel.text=Results +MultiCaseKeywordSearchPanel.searchButton.text=Search +MultiCaseKeywordSearchPanel.viewErrorsButton.text=View Errors +MultiCaseKeywordSearchPanel.warningLabel.text= +MultiCaseKeywordSearchPanel.exportButton.text=Export Results +MultiCaseKeywordSearchPanel.cancelButton.text=Cancel +MultiCaseKeywordSearchPanel.resultsCountLabel.text= +MultiCaseKeywordSearchPanel.pickCasesButton.text_1=Add Cases +SelectMultiUserCasesPanel.refreshButton.text=Refresh diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/Bundle.properties-MERGED b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/Bundle.properties-MERGED new file mode 100644 index 0000000000..a42701dd53 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/Bundle.properties-MERGED @@ -0,0 +1,102 @@ +CTL_MultiCaseKeywordSearchOpenAction=Multi-case Keyword Search +CTL_MultiCaseKeywordSearchTopComponent=Multi-case Keyword Search +CTL_MultiCaseKeywordSearchTopComponentAction=Multi-case Keyword Search +MultiCaseKeywordSearchErrorDialog.title.text=Error(s) While Searching +MultiCaseKeywordSearchNode.copyResultAction.text=Copy to clipboard +MultiCaseKeywordSearchNode.OpenCaseAction.text=Open Case +MultiCaseKeywordSearchNode.properties.case=Case +MultiCaseKeywordSearchNode.properties.caseDirectory=Case Directory +MultiCaseKeywordSearchNode.properties.dataSource=Data Source +MultiCaseKeywordSearchNode.properties.path=Keyword Hit Source Path +MultiCaseKeywordSearchNode.properties.source=Keyword Hit Source +MultiCaseKeywordSearchNode.properties.sourceType=Keyword Hit Source Type +MultiCaseKeywordSearchPanel.continueSearch.text=A search is currently being performed. Would you like the search to continue in the background while the search window is closed? +MultiCaseKeywordSearchPanel.continueSearch.title=Closing multi-case search +MultiCaseKeywordSearchPanel.countOfResults.label=Count: +MultiCaseKeywordSearchPanel.emptyNode.waitText=Please Wait... +# {0} - numberOfErrors +MultiCaseKeywordSearchPanel.errorsEncounter.text={0} Error(s) encountered while performing search +MultiCaseKeywordSearchPanel.searchResultsExport.csvExtensionFilterlbl=Comma Separated Values File (csv) +# {0} - file name +MultiCaseKeywordSearchPanel.searchResultsExport.exportMsg=Search results exported to {0} +MultiCaseKeywordSearchPanel.searchResultsExport.failedExportMsg=Export of search results failed +MultiCaseKeywordSearchPanel.searchResultsExport.featureName=Search Results Export +# {0} - file name +MultiCaseKeywordSearchPanel.searchResultsExport.fileExistPrompt=File {0} exists, overwrite? +MultiCaseKeywordSearchPanel.searchThread.cancellingText=Cancelling search +MultiCaseKeywordSearchPanel.warningText.emptySearch=You must enter something to search for in the text field. +MultiCaseKeywordSearchPanel.warningText.noCases=At least one case must be selected to perform a search. +MultiCaseKeywordSearchTopComponent.exceptionMessage.failedToCreatePanel=Failed to create Multi-case Keyword Search panel. +MultiCaseKeywordSearchTopComponent.name.text=Multi-case Keyword Search +MultiCaseSearcher.exceptionMessage.cancelledMessage=Search cancelled +# {0} - connection info +# {1} - case name +# {2} - case directory +MultiCaseSearcher.exceptionMessage.errorLoadingCore=Error connecting to Solr server and loading core (URL: {0}) for case {1} in {2} +# {0} - PostgreSQL server host +# {1} - PostgreSQL server port +# {2} - case database name +# {3} - case directory +MultiCaseSearcher.exceptionMessage.errorOpeningCaseDatabase=Error connecting to PostgreSQL server (Host/Port: [{0}:{1}] and opening case database {2} for case at {3} +# {0} - case directory +MultiCaseSearcher.exceptionMessage.failedToFindCaseMetadata=Failed to find case metadata file in {0} +# {0} - case_name +MultiCaseSearcher.exceptionMessage.failedToGetCaseDatabaseConnectionInfo=Failed to get case database connection info for case {0} +# {0} - case directory path +MultiCaseSearcher.exceptionMessage.failedToGetCaseDirReadlock=Failed to obtain read lock for case directory at {0} +# {0} - case directory +MultiCaseSearcher.exceptionMessage.failedToParseCaseMetadata=Failed to parse case file metadata in {0} +# {0} - Solr document id +# {1} - case database name +# {2} - case directory +MultiCaseSearcher.exceptionMessage.hitProcessingError=Failed to query case database for processing of Solr object id {0} of case {1} in {2} +# {0} - file name +# {1} - case directory +MultiCaseSearcher.exceptionMessage.missingSolrPropertiesFile=Missing {0} file in {1} +# {0} - file name +# {1} - case directory +MultiCaseSearcher.exceptionMessage.solrPropertiesFileParseError=Error parsing {0} file in {1} +# {0} - query +# {1} - case_name +MultiCaseSearcher.exceptionMessage.solrQueryError=Failed to execute query "{0}" on case {1} +# {0} - case name +# {1} - case counter +# {2} - total cases +MultiCaseSearcher.progressMessage.acquiringSharedLockForCase=Acquiring shared lock for "{0}" ({1} of {2} case(s)) +MultiCaseSearcher.progressMessage.creatingSolrQuery=Creating search query for Solr server +# {0} - case name +# {1} - case counter +# {2} - total cases +MultiCaseSearcher.progressMessage.executingSolrQueryForCase=Getting keyword hits for "{0}" ({1} of {2} case(s)) +MultiCaseSearcher.progressMessage.findingCases=Finding selected cases +# {0} - case name +# {1} - case counter +# {2} - total cases +MultiCaseSearcher.progressMessage.loadingSolrCoreForCase=Loading Solr core for "{0}" ({1} of {2} case(s)) +# {0} - case name +# {1} - case counter +# {2} - total cases +MultiCaseSearcher.progressMessage.openingCaseDbForCase=Opening case database for "{0}" ({1} of {2} case(s)) +# {0} - total cases +MultiCaseSearcher.progressMessage.startingCaseSearches=Searching {0} case(s) +SelectMultiUserCasesPanel.selectAllButton.text=Select All +SelectMultiUserCasesPanel.deselectAllButton.text=Deselect All +SelectMultiUserCasesPanel.jLabel1.text=Select case(s) for keyword search or start typing to search by case name +SelectMultiUserCasesPanel.confirmSelections.text=OK +SelectMultiUserCasesPanel.cancelButton.text=Cancel +MultiCaseKeywordSearchErrorDialog.closeButton.text=Close +MultiCaseKeywordSearchPanel.exactRadioButton.text_1=Exact Match +MultiCaseKeywordSearchPanel.substringRadioButton.text_1=Substring Match +MultiCaseKeywordSearchPanel.regexRadioButton.text_1=Regular Expression +MultiCaseKeywordSearchPanel.keywordTextField.text_1= +MultiCaseKeywordSearchPanel.toolDescriptionTextArea.text=Perform a keyword search on the selected cases. The case can be opened to examine the results more closely. +MultiCaseKeywordSearchPanel.casesLabel.text_1=Cases +MultiCaseKeywordSearchPanel.resultsLabel.text=Results +MultiCaseKeywordSearchPanel.searchButton.text=Search +MultiCaseKeywordSearchPanel.viewErrorsButton.text=View Errors +MultiCaseKeywordSearchPanel.warningLabel.text= +MultiCaseKeywordSearchPanel.exportButton.text=Export Results +MultiCaseKeywordSearchPanel.cancelButton.text=Cancel +MultiCaseKeywordSearchPanel.resultsCountLabel.text= +MultiCaseKeywordSearchPanel.pickCasesButton.text_1=Add Cases +SelectMultiUserCasesPanel.refreshButton.text=Refresh diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchErrorDialog.form b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchErrorDialog.form new file mode 100755 index 0000000000..c93c91817b --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchErrorDialog.form @@ -0,0 +1,82 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchErrorDialog.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchErrorDialog.java new file mode 100644 index 0000000000..69d5755d37 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchErrorDialog.java @@ -0,0 +1,113 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 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.keywordsearch.multicase; + +import javax.swing.JDialog; +import org.openide.util.NbBundle.Messages; +import org.openide.windows.WindowManager; + +/** + * Dialog to display the errors encounter while perfomring a multi-case keyword + * search. + */ +final class MultiCaseKeywordSearchErrorDialog extends JDialog { + + private static final long serialVersionUID = 1L; + + /** + * Creates new MultiCaseKeywordSearchErrorDialog + */ + @Messages({"MultiCaseKeywordSearchErrorDialog.title.text=Error(s) While Searching"}) + MultiCaseKeywordSearchErrorDialog(String contents) { + setTitle(Bundle.MultiCaseKeywordSearchErrorDialog_title_text()); + initComponents(); + errorsTextArea.setText(contents); + this.setLocationRelativeTo(WindowManager.getDefault().getMainWindow()); + pack(); + setModal(true); + setResizable(false); + setVisible(true); + } + + /** + * 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() { + + errorsScrollPane = new javax.swing.JScrollPane(); + errorsTextArea = new javax.swing.JTextArea(); + closeButton = new javax.swing.JButton(); + + errorsScrollPane.setPreferredSize(new java.awt.Dimension(470, 175)); + + errorsTextArea.setEditable(false); + errorsTextArea.setColumns(40); + errorsTextArea.setLineWrap(true); + errorsTextArea.setRows(5); + errorsTextArea.setWrapStyleWord(true); + errorsTextArea.setPreferredSize(new java.awt.Dimension(460, 160)); + errorsScrollPane.setViewportView(errorsTextArea); + + org.openide.awt.Mnemonics.setLocalizedText(closeButton, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchErrorDialog.class, "MultiCaseKeywordSearchErrorDialog.closeButton.text")); // NOI18N + closeButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + closeButtonActionPerformed(evt); + } + }); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); + getContentPane().setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(errorsScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 480, Short.MAX_VALUE)) + .addGroup(layout.createSequentialGroup() + .addGap(0, 0, Short.MAX_VALUE) + .addComponent(closeButton))) + .addContainerGap()) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(errorsScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 196, Short.MAX_VALUE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(closeButton) + .addGap(14, 14, 14)) + ); + }// //GEN-END:initComponents + + private void closeButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_closeButtonActionPerformed + dispose(); + }//GEN-LAST:event_closeButtonActionPerformed + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton closeButton; + private javax.swing.JScrollPane errorsScrollPane; + private javax.swing.JTextArea errorsTextArea; + // End of variables declaration//GEN-END:variables +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchNode.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchNode.java new file mode 100755 index 0000000000..04e66f1717 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchNode.java @@ -0,0 +1,292 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 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.keywordsearch.multicase; + +import java.awt.Toolkit; +import java.awt.datatransfer.StringSelection; +import java.awt.event.ActionEvent; +import java.io.File; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.logging.Level; +import javax.swing.AbstractAction; +import javax.swing.Action; +import org.openide.nodes.AbstractNode; +import org.openide.nodes.Children; +import org.openide.nodes.Node; +import org.openide.nodes.Sheet; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.casemodule.CaseActionCancelledException; +import org.sleuthkit.autopsy.casemodule.CaseActionException; +import static org.sleuthkit.autopsy.casemodule.CaseMetadata.getFileExtension; +import org.sleuthkit.autopsy.casemodule.StartupWindowProvider; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil; +import org.sleuthkit.autopsy.datamodel.NodeProperty; + +@NbBundle.Messages({ + "MultiCaseKeywordSearchNode.properties.case=Case", + "MultiCaseKeywordSearchNode.properties.caseDirectory=Case Directory", + "MultiCaseKeywordSearchNode.properties.dataSource=Data Source", + "MultiCaseKeywordSearchNode.properties.sourceType=Keyword Hit Source Type", + "MultiCaseKeywordSearchNode.properties.source=Keyword Hit Source", + "MultiCaseKeywordSearchNode.properties.path=Keyword Hit Source Path" +}) + +/** + * A root node containing child nodes of the results of a multi-case keyword + * Search. + */ +class MultiCaseKeywordSearchNode extends AbstractNode { + + private static final Logger LOGGER = Logger.getLogger(MultiCaseKeywordSearchNode.class.getName()); + + /** + * Construct a new MultiCaseKeywordSearchNode + * + * @param resultList the list of KeywordSearchHits which will be the + * children of this node. + */ + MultiCaseKeywordSearchNode(Collection resultList) { + super(new MultiCaseKeywordSearchChildren(resultList)); + } + + /** + * A factory for creating children of the MultiCaseKeywordSearchNode. + */ + static class MultiCaseKeywordSearchChildren extends Children.Keys { + + private final Collection resultList; + + /** + * Construct a new MultiCaseKeywordSearchChildren + * + * @param resultList the list of KeywordSearchHits which will be used to + * construct the children. + */ + MultiCaseKeywordSearchChildren(Collection resultList) { + this.resultList = resultList; + } + + @Override + protected void addNotify() { + super.addNotify(); + setKeys(resultList); + } + + @Override + protected void removeNotify() { + super.removeNotify(); + setKeys(Collections.emptyList()); + } + + @Override + protected Node[] createNodes(SearchHit t) { + return new Node[]{new SearchHitNode(t)}; + } + + @Override + public Object clone() { + return super.clone(); + } + + } + + /** + * A leaf node which represents a hit for the multi-case keyword search. + */ + static final class SearchHitNode extends AbstractNode { + + private final SearchHit searchHit; + + /** + * Construct a new SearchHitNode + * + * @param kwsHit the KeywordSearchHit which will be represented by this + * node. + */ + SearchHitNode(SearchHit kwsHit) { + super(Children.LEAF); + searchHit = kwsHit; + super.setName(searchHit.getCaseDisplayName()); + setDisplayName(searchHit.getCaseDisplayName()); + } + + @Override + public Action getPreferredAction() { + return new OpenCaseAction(getCasePath()); + } + + /** + * Get the path to the case directory + * + * @return the path to the case directory for the KeywordSearchHit + * represented by this node + */ + private String getCasePath() { + return searchHit.getCaseDirectoryPath(); + } + + @Override + protected Sheet createSheet() { + Sheet s = super.createSheet(); + Sheet.Set ss = s.get(Sheet.PROPERTIES); + if (ss == null) { + ss = Sheet.createPropertiesSet(); + s.put(ss); + } + ss.put(new NodeProperty<>(Bundle.MultiCaseKeywordSearchNode_properties_case(), Bundle.MultiCaseKeywordSearchNode_properties_case(), Bundle.MultiCaseKeywordSearchNode_properties_case(), + searchHit.getCaseDisplayName())); + ss.put(new NodeProperty<>(Bundle.MultiCaseKeywordSearchNode_properties_caseDirectory(), Bundle.MultiCaseKeywordSearchNode_properties_caseDirectory(), Bundle.MultiCaseKeywordSearchNode_properties_caseDirectory(), + searchHit.getCaseDirectoryPath())); + ss.put(new NodeProperty<>(Bundle.MultiCaseKeywordSearchNode_properties_dataSource(), Bundle.MultiCaseKeywordSearchNode_properties_dataSource(), Bundle.MultiCaseKeywordSearchNode_properties_dataSource(), + searchHit.getDataSourceName())); + ss.put(new NodeProperty<>(Bundle.MultiCaseKeywordSearchNode_properties_path(), Bundle.MultiCaseKeywordSearchNode_properties_path(), Bundle.MultiCaseKeywordSearchNode_properties_path(), + searchHit.getSourcePath())); + ss.put(new NodeProperty<>(Bundle.MultiCaseKeywordSearchNode_properties_sourceType(), Bundle.MultiCaseKeywordSearchNode_properties_sourceType(), Bundle.MultiCaseKeywordSearchNode_properties_sourceType(), + searchHit.getSourceType().getDisplayName())); + ss.put(new NodeProperty<>(Bundle.MultiCaseKeywordSearchNode_properties_source(), Bundle.MultiCaseKeywordSearchNode_properties_source(), Bundle.MultiCaseKeywordSearchNode_properties_source(), + searchHit.getSourceName())); + return s; + } + + @Override + public Action[] getActions(boolean context) { + List actions = new ArrayList<>(); + actions.add(new OpenCaseAction(getCasePath())); + actions.add(new CopyResultAction(searchHit)); + return actions.toArray(new Action[actions.size()]); + } + } + + @NbBundle.Messages({"MultiCaseKeywordSearchNode.copyResultAction.text=Copy to clipboard"}) + /** + * Put the contents of the selected row in the clipboard in the same tab + * seperated format as pressing ctrl+c. + */ + private static class CopyResultAction extends AbstractAction { + + private static final long serialVersionUID = 1L; + + SearchHit result; + + /** + * Construct a new CopyResultAction + */ + CopyResultAction(SearchHit selectedResult) { + super(Bundle.MultiCaseKeywordSearchNode_copyResultAction_text()); + result = selectedResult; + } + + @Override + public void actionPerformed(ActionEvent e) { + StringSelection resultSelection = new StringSelection(result.getCaseDisplayName() + "\t" + + result.getCaseDirectoryPath() + "\t" + + result.getDataSourceName() + "\t" + + result.getSourceType().getDisplayName() + "\t" + + result.getSourceName() + "\t" + + result.getSourcePath() + "\t"); + Toolkit.getDefaultToolkit().getSystemClipboard().setContents(resultSelection, resultSelection); + } + + @Override + public Object clone() throws CloneNotSupportedException { + return super.clone(); //To change body of generated methods, choose Tools | Templates. + } + + } + + @NbBundle.Messages({"MultiCaseKeywordSearchNode.OpenCaseAction.text=Open Case"}) + /** + * Action to open the case associated with the selected node. + */ + private static class OpenCaseAction extends AbstractAction { + + private static final long serialVersionUID = 1L; + private final String caseDirPath; + + /** + * Finds the path to the .aut file for the specified case directory. + * + * @param caseDirectory the directory to check for a .aut file + * + * @return the path to the first .aut file found in the directory + * + * @throws CaseActionException if there was an issue finding a .aut file + */ + private static String findAutFile(String caseDirectory) throws CaseActionException { + File caseFolder = Paths.get(caseDirectory).toFile(); + if (caseFolder.exists()) { + /* + * Search for '*.aut' files. + */ + File[] fileArray = caseFolder.listFiles(); + if (fileArray == null) { + throw new CaseActionException("No files found in case directory"); + } + String autFilePath = null; + for (File file : fileArray) { + String name = file.getName().toLowerCase(); + if (autFilePath == null && name.endsWith(getFileExtension())) { + return file.getAbsolutePath(); + } + } + throw new CaseActionException("No .aut files found in case directory"); + } + throw new CaseActionException("Case directory was not found"); + } + + /** + * Construct a new open case action + * + * @param path the path to the case directory for the case to open + */ + OpenCaseAction(String path) { + super(Bundle.MultiCaseKeywordSearchNode_OpenCaseAction_text()); + caseDirPath = path; + } + + @Override + public void actionPerformed(ActionEvent e) { + StartupWindowProvider.getInstance().close(); + new Thread( + () -> { + try { + Case.openAsCurrentCase(findAutFile(caseDirPath)); + } catch (CaseActionException ex) { + if (null != ex.getCause() && !(ex.getCause() instanceof CaseActionCancelledException)) { + LOGGER.log(Level.SEVERE, String.format("Error opening case with metadata file path %s", caseDirPath), ex); //NON-NLS + MessageNotifyUtil.Message.error(ex.getCause().getLocalizedMessage()); + } + } + } + ).start(); + } + + @Override + public Object clone() throws CloneNotSupportedException { + return super.clone(); //To change body of generated methods, choose Tools | Templates. + } + + } +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchOpenAction.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchOpenAction.java new file mode 100644 index 0000000000..2cc30ab381 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchOpenAction.java @@ -0,0 +1,66 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 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.keywordsearch.multicase; + +import org.openide.awt.ActionID; +import org.openide.awt.ActionReference; +import org.openide.awt.ActionRegistration; +import org.openide.util.HelpCtx; +import org.openide.util.NbBundle.Messages; +import org.openide.util.actions.CallableSystemAction; +import org.sleuthkit.autopsy.core.UserPreferences; + +@ActionID(category = "Tools", id = "org.sleuthkit.autopsy.experimental.autoingest.MultiCaseKeywordSearchOpenAction") +@ActionReference(path = "Menu/Tools", position = 202) +@ActionRegistration(displayName = "#CTL_MultiCaseKeywordSearchOpenAction", lazy = false) +@Messages({"CTL_MultiCaseKeywordSearchOpenAction=Multi-case Keyword Search"}) +/** + * Action to open the top level component for the multi-case keyword search. + */ +public final class MultiCaseKeywordSearchOpenAction extends CallableSystemAction { + + private static final String DISPLAY_NAME = Bundle.CTL_MultiCaseKeywordSearchOpenAction(); + private static final long serialVersionUID = 1L; + + @Override + public boolean isEnabled() { + return UserPreferences.getIsMultiUserModeEnabled(); + } + + @Override + public void performAction() { + MultiCaseKeywordSearchTopComponent.openTopComponent(); + } + + @Override + public String getName() { + return DISPLAY_NAME; + } + + @Override + public HelpCtx getHelpCtx() { + return HelpCtx.DEFAULT_HELP; + } + + @Override + public boolean asynchronous() { + return false; // run on edt + } + +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchPanel.form b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchPanel.form new file mode 100755 index 0000000000..ee2bd31d85 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchPanel.form @@ -0,0 +1,374 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchPanel.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchPanel.java new file mode 100644 index 0000000000..bc197c16a0 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchPanel.java @@ -0,0 +1,860 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 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.keywordsearch.multicase; + +import com.google.common.eventbus.Subscribe; +import com.google.common.eventbus.DeadEvent; +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.stream.Collectors; +import javax.swing.AbstractButton; +import javax.swing.DefaultListModel; +import javax.swing.DefaultListSelectionModel; +import javax.swing.JCheckBox; +import javax.swing.JFileChooser; +import javax.swing.table.TableColumn; +import javax.swing.JOptionPane; +import javax.swing.JTable; +import javax.swing.ListModel; +import javax.swing.ListSelectionModel; +import javax.swing.SwingUtilities; +import javax.swing.filechooser.FileNameExtensionFilter; +import javax.swing.table.TableCellRenderer; +import org.netbeans.swing.outline.DefaultOutlineModel; +import org.openide.explorer.ExplorerManager; +import org.netbeans.swing.outline.Outline; +import org.openide.nodes.Children; +import org.openide.nodes.Node; +import org.openide.util.NbBundle.Messages; +import org.openide.windows.WindowManager; +import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.datamodel.EmptyNode; +import org.sleuthkit.autopsy.keywordsearch.multicase.MultiCaseSearcher.MultiCaseSearcherException; +import org.sleuthkit.autopsy.keywordsearch.multicase.SearchQuery.QueryType; + +/** + * Panel to display the controls and results for the multi-case search. + */ +final class MultiCaseKeywordSearchPanel extends javax.swing.JPanel implements ExplorerManager.Provider { + + @Messages({ + "MultiCaseKeywordSearchPanel.emptyNode.waitText=Please Wait..." + }) + private static final long serialVersionUID = 1L; + private volatile SearchThread searchThread = null; + private final Outline outline; + private final ExplorerManager em; + private final org.openide.explorer.view.OutlineView outlineView; + private static final Logger LOGGER = Logger.getLogger(MultiCaseKeywordSearchPanel.class.getName()); + private static final EmptyNode PLEASE_WAIT_NODE = new EmptyNode(Bundle.MultiCaseKeywordSearchPanel_emptyNode_waitText()); + private static final MultiCaseKeywordSearchNode NO_RESULTS_NODE = new MultiCaseKeywordSearchNode(new ArrayList<>()); + private Collection allSearchHits = new ArrayList<>(); + private Collection searchExceptions = new ArrayList<>(); + private final SelectMultiUserCasesDialog caseSelectionDialog = SelectMultiUserCasesDialog.getInstance(); + private final Map caseNameToCaseDataMap; + private Node[] currentConfirmedSelections; + + /** + * Creates new form MultiCaseKeywordSearchPanel + */ + MultiCaseKeywordSearchPanel() { + em = new ExplorerManager(); + outlineView = new org.openide.explorer.view.OutlineView(); + outline = outlineView.getOutline(); + outlineView.setPropertyColumns( + Bundle.MultiCaseKeywordSearchNode_properties_caseDirectory(), Bundle.MultiCaseKeywordSearchNode_properties_caseDirectory(), + Bundle.MultiCaseKeywordSearchNode_properties_dataSource(), Bundle.MultiCaseKeywordSearchNode_properties_dataSource(), + Bundle.MultiCaseKeywordSearchNode_properties_path(), Bundle.MultiCaseKeywordSearchNode_properties_path(), + Bundle.MultiCaseKeywordSearchNode_properties_sourceType(), Bundle.MultiCaseKeywordSearchNode_properties_sourceType(), + Bundle.MultiCaseKeywordSearchNode_properties_source(), Bundle.MultiCaseKeywordSearchNode_properties_source()); + ((DefaultOutlineModel) outline.getOutlineModel()).setNodesColumnLabel(Bundle.MultiCaseKeywordSearchNode_properties_case()); + initComponents(); + outline.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + outline.setRootVisible(false); + outlineView.setPreferredSize(resultsScrollPane.getPreferredSize()); + resultsScrollPane.setViewportView(outlineView); + caseSelectionDialog.subscribeToNewCaseSelections(new ChangeListener() { + @Override + public void nodeSelectionChanged(Node[] selections, List selectionCaseData) { + populateCasesList(selectionCaseData); + currentConfirmedSelections = selections; + revalidate(); + repaint(); + } + }); + searchEnabled(true); + outline.setRowSelectionAllowed(false); + searchProgressBar.setVisible(false); + exportButton.setEnabled(false); + outline.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); + caseNameToCaseDataMap = new HashMap<>(); + setColumnWidths(); + + //Disable selection in JList + caseSelectionList.setSelectionModel(new DefaultListSelectionModel() { + @Override + public void setSelectionInterval(int index0, int index1) { + super.setSelectionInterval(-1, -1); + } + }); + } + + /** + * Listener for new selections + */ + public interface ChangeListener { + public void nodeSelectionChanged(Node[] selections, List selectionCaseData); + } + + /** + * If a collection of SearchHits is received update the results shown on the + * panel to include them. + * + * @param hits the collection of SearchHits which was received. + */ + @Messages({"MultiCaseKeywordSearchPanel.countOfResults.label=Count: "}) + @Subscribe + void subscribeToResults(Collection hits) { + allSearchHits.addAll(hits); + if (allSearchHits.size() > 0) { + MultiCaseKeywordSearchNode resultsNode = new MultiCaseKeywordSearchNode(allSearchHits); + SwingUtilities.invokeLater(() -> { + em.setRootContext(resultsNode); + outline.setRowSelectionAllowed(true); + resultsCountLabel.setText(Bundle.MultiCaseKeywordSearchPanel_countOfResults_label() + Integer.toString(outline.getRowCount())); + }); + } else { + em.setRootContext(NO_RESULTS_NODE); + resultsCountLabel.setText(Bundle.MultiCaseKeywordSearchPanel_countOfResults_label() + 0); + } + } + + /** + * If a string is received and it matches the + * MultiCaseSearcher.SEARCH_COMPLETE_STRING reset elements of this panel to + * reflect that the search is done. + * + * @param stringRecived the String which was received + */ + @Subscribe + void subscribeToStrings(String stringReceived) { + if (stringReceived.equals(MultiCaseSearcher.getSearchCompleteMessage())) { + searchThread.unregisterWithSearcher(MultiCaseKeywordSearchPanel.this); + searchThread = null; + searchEnabled(true); + if (!searchExceptions.isEmpty()) { + warningLabel.setText(Bundle.MultiCaseKeywordSearchPanel_errorsEncounter_text(searchExceptions.size())); + } + if (!em.getRootContext().equals(PLEASE_WAIT_NODE) && !em.getRootContext().equals(NO_RESULTS_NODE)) { + exportButton.setEnabled(true); + SwingUtilities.invokeLater(() -> { + exportButton.setEnabled(true); + setColumnWidths(); + }); + } + } else { + //If it is not the SEARCH_COMPLETE_STRING log it. + LOGGER.log(Level.INFO, "String posted to MultiCaseKeywordSearchPanel EventBus with value of " + stringReceived); + } + } + + /** + * If a InterruptedException is received over the EventBus update the + * warning label. + * + * @param exception the InterruptedException which was received. + */ + @Subscribe + void subscribeToInterruptionExceptions(InterruptedException exception) { + warningLabel.setText(exception.getMessage()); + //if we are still displaying please wait force it to update to no results + if (em.getRootContext().equals(PLEASE_WAIT_NODE)) { + em.setRootContext(NO_RESULTS_NODE); + resultsCountLabel.setText(Bundle.MultiCaseKeywordSearchPanel_countOfResults_label() + 0); + } + } + + /** + * If a MultiCaseSearcherException is received over the EventBus cancel the + * current search and update the warning label. + * + * @param exception the MultiCaseSearcherException which was received. + */ + @Messages({"# {0} - numberOfErrors", + "MultiCaseKeywordSearchPanel.errorsEncounter.text={0} Error(s) encountered while performing search" + }) + @Subscribe + void subscribeToMultiCaseSearcherExceptions(MultiCaseSearcherException exception) { + searchExceptions.add(exception); + } + + /** + * Log all other events received over the event bus which are not + * specifically covered by another @Subscribe method + * + * @param deadEvent Any object received over the event bus which was not of + * a type otherwise subscribed to + */ + @Subscribe + void subscribeToDeadEvents(DeadEvent deadEvent) { + LOGGER.log(Level.INFO, "Dead Event posted to MultiCaseKeywordSearchPanel EventBus " + deadEvent.toString()); + } + + private void displaySearchErrors() { + if (!searchExceptions.isEmpty()) { + StringBuilder strBuilder = new StringBuilder(""); + searchExceptions.forEach((exception) -> { + strBuilder.append("- ").append(exception.getMessage()).append(System.lineSeparator()); + }); + SwingUtilities.invokeLater(() -> { + new MultiCaseKeywordSearchErrorDialog(strBuilder.toString()); + }); + } + + } + + /** + * Get the list of cases from the Multi user case browser + */ + private void populateCasesList(List selectedNodes) { + caseSelectionList.removeAll(); + caseSelectionList.revalidate(); + caseSelectionList.repaint(); + caseNameToCaseDataMap.clear(); + DefaultListModel listModel = new DefaultListModel<>(); + Collections.sort(selectedNodes, (CaseNodeData o1, CaseNodeData o2) -> { + return o1.getName().toLowerCase() + .compareTo(o2.getName().toLowerCase()); + }); + + for (int i = 0; i < selectedNodes.size(); i++) { + CaseNodeData data = selectedNodes.get(i); + String multiUserCaseName = data.getName(); + listModel.addElement(multiUserCaseName); + /** + * Map out the name to CaseNodeData so we can retrieve it later for + * search. + */ + caseNameToCaseDataMap.put(multiUserCaseName, data); + } + caseSelectionList.setModel(listModel); + } + + @Override + public ExplorerManager getExplorerManager() { + return em; + } + + /** + * 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() { + + searchTypeGroup = new javax.swing.ButtonGroup(); + searchButton = new javax.swing.JButton(); + substringRadioButton = new javax.swing.JRadioButton(); + keywordTextField = new javax.swing.JTextField(); + exactRadioButton = new javax.swing.JRadioButton(); + regexRadioButton = new javax.swing.JRadioButton(); + casesLabel = new javax.swing.JLabel(); + resultsLabel = new javax.swing.JLabel(); + toolDescriptionScrollPane = new javax.swing.JScrollPane(); + toolDescriptionTextArea = new javax.swing.JTextArea(); + resultsScrollPane = new javax.swing.JScrollPane(); + cancelButton = new javax.swing.JButton(); + searchProgressBar = new javax.swing.JProgressBar(); + warningLabel = new javax.swing.JLabel(); + exportButton = new javax.swing.JButton(); + resultsCountLabel = new javax.swing.JLabel(); + viewErrorsButton = new javax.swing.JButton(); + pickCasesButton = new javax.swing.JButton(); + jScrollPane1 = new javax.swing.JScrollPane(); + caseSelectionList = new javax.swing.JList<>(); + + setName(""); // NOI18N + setOpaque(false); + setPreferredSize(new java.awt.Dimension(1000, 442)); + + org.openide.awt.Mnemonics.setLocalizedText(searchButton, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.searchButton.text")); // NOI18N + searchButton.setMaximumSize(new java.awt.Dimension(84, 23)); + searchButton.setMinimumSize(new java.awt.Dimension(84, 23)); + searchButton.setPreferredSize(new java.awt.Dimension(84, 23)); + searchButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + searchButtonActionPerformed(evt); + } + }); + + searchTypeGroup.add(substringRadioButton); + org.openide.awt.Mnemonics.setLocalizedText(substringRadioButton, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.substringRadioButton.text_1")); // NOI18N + substringRadioButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + substringRadioButtonActionPerformed(evt); + } + }); + + keywordTextField.setFont(new java.awt.Font("Monospaced", 0, 14)); // NOI18N + keywordTextField.setText(org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.keywordTextField.text_1")); // NOI18N + keywordTextField.setBorder(new javax.swing.border.LineBorder(new java.awt.Color(192, 192, 192), 1, true)); + keywordTextField.setMinimumSize(new java.awt.Dimension(2, 25)); + keywordTextField.setPreferredSize(new java.awt.Dimension(2, 25)); + + searchTypeGroup.add(exactRadioButton); + exactRadioButton.setSelected(true); + org.openide.awt.Mnemonics.setLocalizedText(exactRadioButton, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.exactRadioButton.text_1")); // NOI18N + + searchTypeGroup.add(regexRadioButton); + org.openide.awt.Mnemonics.setLocalizedText(regexRadioButton, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.regexRadioButton.text_1")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(casesLabel, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.casesLabel.text_1")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(resultsLabel, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.resultsLabel.text")); // NOI18N + + toolDescriptionTextArea.setEditable(false); + toolDescriptionTextArea.setBackground(new java.awt.Color(240, 240, 240)); + toolDescriptionTextArea.setColumns(20); + toolDescriptionTextArea.setFont(new java.awt.Font("Tahoma", 0, 11)); // NOI18N + toolDescriptionTextArea.setLineWrap(true); + toolDescriptionTextArea.setRows(3); + toolDescriptionTextArea.setText(org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.toolDescriptionTextArea.text")); // NOI18N + toolDescriptionTextArea.setWrapStyleWord(true); + toolDescriptionTextArea.setFocusable(false); + toolDescriptionScrollPane.setViewportView(toolDescriptionTextArea); + + resultsScrollPane.setMinimumSize(new java.awt.Dimension(100, 40)); + resultsScrollPane.setPreferredSize(new java.awt.Dimension(200, 100)); + resultsScrollPane.setRequestFocusEnabled(false); + + org.openide.awt.Mnemonics.setLocalizedText(cancelButton, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.cancelButton.text")); // NOI18N + cancelButton.setEnabled(false); + cancelButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + cancelButtonActionPerformed(evt); + } + }); + + warningLabel.setForeground(new java.awt.Color(200, 0, 0)); + org.openide.awt.Mnemonics.setLocalizedText(warningLabel, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.warningLabel.text")); // NOI18N + warningLabel.setFocusable(false); + + org.openide.awt.Mnemonics.setLocalizedText(exportButton, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.exportButton.text")); // NOI18N + exportButton.setMargin(new java.awt.Insets(2, 2, 2, 2)); + exportButton.setMaximumSize(new java.awt.Dimension(84, 23)); + exportButton.setMinimumSize(new java.awt.Dimension(84, 23)); + exportButton.setPreferredSize(new java.awt.Dimension(84, 23)); + exportButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + exportButtonActionPerformed(evt); + } + }); + + resultsCountLabel.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING); + org.openide.awt.Mnemonics.setLocalizedText(resultsCountLabel, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.resultsCountLabel.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(viewErrorsButton, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.viewErrorsButton.text")); // NOI18N + viewErrorsButton.setEnabled(false); + viewErrorsButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + viewErrorsButtonActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(pickCasesButton, org.openide.util.NbBundle.getMessage(MultiCaseKeywordSearchPanel.class, "MultiCaseKeywordSearchPanel.pickCasesButton.text_1")); // NOI18N + pickCasesButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + pickCasesButtonActionPerformed(evt); + } + }); + + jScrollPane1.setViewportView(caseSelectionList); + + 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) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(exactRadioButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(substringRadioButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(regexRadioButton)) + .addComponent(keywordTextField, javax.swing.GroupLayout.DEFAULT_SIZE, 591, Short.MAX_VALUE)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(toolDescriptionScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 295, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addComponent(casesLabel) + .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 174, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGroup(layout.createSequentialGroup() + .addComponent(pickCasesButton, javax.swing.GroupLayout.PREFERRED_SIZE, 84, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(searchButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(resultsLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 154, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(resultsCountLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 98, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addComponent(resultsScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(viewErrorsButton) + .addComponent(warningLabel, javax.swing.GroupLayout.DEFAULT_SIZE, 607, Short.MAX_VALUE)) + .addGap(14, 14, 14) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addComponent(exportButton, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addComponent(cancelButton, javax.swing.GroupLayout.DEFAULT_SIZE, 87, Short.MAX_VALUE)))))) + .addContainerGap()) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addGap(196, 196, 196) + .addComponent(searchProgressBar, javax.swing.GroupLayout.DEFAULT_SIZE, 608, Short.MAX_VALUE) + .addGap(108, 108, 108))) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false) + .addGroup(layout.createSequentialGroup() + .addComponent(keywordTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(regexRadioButton, javax.swing.GroupLayout.PREFERRED_SIZE, 23, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(exactRadioButton) + .addComponent(substringRadioButton)))) + .addComponent(toolDescriptionScrollPane)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(casesLabel) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(resultsLabel) + .addComponent(resultsCountLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 14, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(resultsScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 281, Short.MAX_VALUE) + .addComponent(jScrollPane1)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(warningLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 15, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(exportButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(pickCasesButton) + .addComponent(searchButton, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(viewErrorsButton) + .addComponent(cancelButton)) + .addContainerGap()) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addContainerGap(433, Short.MAX_VALUE) + .addComponent(searchProgressBar, javax.swing.GroupLayout.PREFERRED_SIZE, 22, javax.swing.GroupLayout.PREFERRED_SIZE) + .addContainerGap())) + ); + }// //GEN-END:initComponents + + @Messages({ + "MultiCaseKeywordSearchPanel.warningText.noCases=At least one case must be selected to perform a search.", + "MultiCaseKeywordSearchPanel.warningText.emptySearch=You must enter something to search for in the text field." + }) + /** + * perform a search if the previous search is done or no previous search has + * occured + */ + private void searchButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_searchButtonActionPerformed + if (null == searchThread) { + Collection cases = getCases(); + String searchString = keywordTextField.getText(); + if (cases.isEmpty()) { + warningLabel.setText(Bundle.MultiCaseKeywordSearchPanel_warningText_noCases()); + } else if (searchString.isEmpty()) { + warningLabel.setText(Bundle.MultiCaseKeywordSearchPanel_warningText_emptySearch()); + } else { + //Map case names to CaseNodeData objects + Collection caseNodeData = cases.stream() + .map(c -> caseNameToCaseDataMap.get(c)) + .collect(Collectors.toList()); + + //perform the search + warningLabel.setText(""); + allSearchHits = new ArrayList<>(); + searchExceptions = new ArrayList<>(); + searchEnabled(false); + exportButton.setEnabled(false); + outline.setRowSelectionAllowed(false); + SearchQuery kwsQuery = new SearchQuery(getQueryType(), searchString); + em.setRootContext(PLEASE_WAIT_NODE); + resultsCountLabel.setText(""); + searchThread = new SearchThread(caseNodeData, kwsQuery); + searchThread.registerWithSearcher(MultiCaseKeywordSearchPanel.this); + searchThread.start(); + } + } + }//GEN-LAST:event_searchButtonActionPerformed + + /** + * Get the case names from the Case List + * + * @return cases the cases that match the selected status of isSelected + */ + private Collection getCases() { + Collection cases = new HashSet<>(); + ListModel listModel = caseSelectionList.getModel(); + for(int i = 0; i < listModel.getSize(); i++) { + String caseName = listModel.getElementAt(i); + cases.add(caseName); + } + return cases; + } + + /** + * Get the type of Query which was selected by the user. + * + * @return one of the values of the QueryType enum + */ + private QueryType getQueryType() { + String queryTypeText = ""; + Enumeration buttonGroup = searchTypeGroup.getElements(); + while (buttonGroup.hasMoreElements()) { + AbstractButton dspButton = buttonGroup.nextElement(); + if (dspButton.isSelected()) { + queryTypeText = dspButton.getText(); + break; + } + } + if (queryTypeText.equals(substringRadioButton.getText())) { + return QueryType.SUBSTRING; + } else if (queryTypeText.equals(regexRadioButton.getText())) { + return QueryType.REGEX; + } else { + //default to Exact match + return QueryType.EXACT_MATCH; + } + } + + /** + * Set the column widths to have their width influenced by the width of the + * content in them for up to the first hundred rows. + */ + private void setColumnWidths() { + int widthLimit = 1000; + int margin = 4; + int padding = 8; + for (int col = 0; col < outline.getColumnCount(); col++) { + int width = 115; //min initial width for columns + int rowsToResize = Math.min(outline.getRowCount(), 100); + for (int row = 0; row < rowsToResize; row++) { + if (outline.getValueAt(row, col) != null) { + TableCellRenderer renderer = outline.getCellRenderer(row, col); + Component comp = outline.prepareRenderer(renderer, row, col); + width = Math.max(comp.getPreferredSize().width, width); + } + + } + width += 2 * margin + padding; + width = Math.min(width, widthLimit); + TableColumn column = outline.getColumnModel().getColumn(outline.convertColumnIndexToModel(col)); + column.setPreferredWidth(width); + } + resultsScrollPane.setPreferredSize(new Dimension(outline.getPreferredSize().width, resultsScrollPane.getPreferredSize().height)); + } + + /** + * Cancel the current multi-case search which is being performed. + * + * @param evt ignored + */ + private void cancelButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cancelButtonActionPerformed + cancelSearch(); + }//GEN-LAST:event_cancelButtonActionPerformed + + /** + * Cancel the current multi-case search which is being performed. + */ + @Messages({ + "MultiCaseKeywordSearchPanel.searchThread.cancellingText=Cancelling search"}) + private void cancelSearch() { + if (null != searchThread) { + warningLabel.setText(Bundle.MultiCaseKeywordSearchPanel_searchThread_cancellingText()); + searchThread.interrupt(); + } + } + + @Messages({"MultiCaseKeywordSearchPanel.searchResultsExport.csvExtensionFilterlbl=Comma Separated Values File (csv)", + "MultiCaseKeywordSearchPanel.searchResultsExport.featureName=Search Results Export", + "MultiCaseKeywordSearchPanel.searchResultsExport.failedExportMsg=Export of search results failed" + }) + /** + * Export the currently displayed search results to a file specified by the + * user with data saved in comma seperated format. + */ + private void exportButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exportButtonActionPerformed + JFileChooser chooser = new JFileChooser(); + final String EXTENSION = "csv"; //NON-NLS + FileNameExtensionFilter csvFilter = new FileNameExtensionFilter( + Bundle.MultiCaseKeywordSearchPanel_searchResultsExport_csvExtensionFilterlbl(), EXTENSION); + chooser.setFileFilter(csvFilter); + chooser.setFileSelectionMode(JFileChooser.FILES_ONLY); + chooser.setName("Choose file to export results to"); + chooser.setMultiSelectionEnabled(false); + int returnVal = chooser.showSaveDialog(this); + if (returnVal == JFileChooser.APPROVE_OPTION) { + File selFile = chooser.getSelectedFile(); + if (selFile == null) { + JOptionPane.showMessageDialog(this, + Bundle.MultiCaseKeywordSearchPanel_searchResultsExport_failedExportMsg(), + Bundle.MultiCaseKeywordSearchPanel_searchResultsExport_featureName(), + JOptionPane.WARNING_MESSAGE); + LOGGER.warning("Selected file was null, when trying to export search results"); + return; + } + String fileAbs = selFile.getAbsolutePath(); + if (!fileAbs.endsWith("." + EXTENSION)) { + fileAbs = fileAbs + "." + EXTENSION; + selFile = new File(fileAbs); + } + saveResultsAsTextFile(selFile); + } + }//GEN-LAST:event_exportButtonActionPerformed + + private void viewErrorsButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_viewErrorsButtonActionPerformed + displaySearchErrors(); + }//GEN-LAST:event_viewErrorsButtonActionPerformed + + private void pickCasesButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_pickCasesButtonActionPerformed + caseSelectionDialog.setVisible(true); + if (currentConfirmedSelections != null) { + caseSelectionDialog.setNodeSelections(currentConfirmedSelections); + } + + }//GEN-LAST:event_pickCasesButtonActionPerformed + + private void substringRadioButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_substringRadioButtonActionPerformed + // TODO add your handling code here: + }//GEN-LAST:event_substringRadioButtonActionPerformed + + /** + * Set the user interface elements to reflect whether the search feature is + * currently enabled or disabled. + * + * @param canSearch True if the search feature should be enabled, false if + * it should be disabled. + */ + private void searchEnabled(boolean canSearch) { + searchButton.setEnabled(canSearch); + cancelButton.setEnabled(!canSearch); + viewErrorsButton.setEnabled(canSearch); + viewErrorsButton.setVisible(!searchExceptions.isEmpty()); + } + + @Messages({"# {0} - file name", + "MultiCaseKeywordSearchPanel.searchResultsExport.fileExistPrompt=File {0} exists, overwrite?", + "# {0} - file name", + "MultiCaseKeywordSearchPanel.searchResultsExport.exportMsg=Search results exported to {0}" + }) + /** + * Saves the results to the file specified + */ + private void saveResultsAsTextFile(File resultsFile) { + if (resultsFile.exists()) { + //if the file already exists ask the user how to proceed + boolean shouldWrite = JOptionPane.showConfirmDialog(null, + Bundle.MultiCaseKeywordSearchPanel_searchResultsExport_fileExistPrompt(resultsFile.getName()), + Bundle.MultiCaseKeywordSearchPanel_searchResultsExport_featureName(), + JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE) + == JOptionPane.YES_OPTION; + if (!shouldWrite) { + return; + } + } + try { + BufferedWriter resultsWriter; + resultsWriter = new BufferedWriter(new FileWriter(resultsFile)); + int col = 0; + //write headers + while (col < outline.getColumnCount()) { + + resultsWriter.write(outline.getColumnName(col)); + col++; + if (col < outline.getColumnCount()) { + resultsWriter.write(","); + } + } + resultsWriter.write(System.lineSeparator()); + //write data + Children resultsChildren = em.getRootContext().getChildren(); + for (int row = 0; row < resultsChildren.getNodesCount(); row++) { + col = 0; + while (col < outline.getColumnCount()) { + if (outline.getValueAt(row, col) instanceof Node.Property) { + resultsWriter.write(((Node.Property) outline.getValueAt(row, col)).getValue().toString()); + } else { + resultsWriter.write(outline.getValueAt(row, col).toString()); + } + col++; + if (col < outline.getColumnCount()) { + resultsWriter.write(","); + } + } + resultsWriter.write(System.lineSeparator()); + } + resultsWriter.flush(); + resultsWriter.close(); + setColumnWidths(); + JOptionPane.showMessageDialog( + WindowManager.getDefault().getMainWindow(), + Bundle.MultiCaseKeywordSearchPanel_searchResultsExport_exportMsg(resultsFile.getName()), + Bundle.MultiCaseKeywordSearchPanel_searchResultsExport_featureName(), + JOptionPane.INFORMATION_MESSAGE); + } catch (IllegalAccessException | IOException | InvocationTargetException ex) { + JOptionPane.showMessageDialog(WindowManager.getDefault().getMainWindow(), + Bundle.MultiCaseKeywordSearchPanel_searchResultsExport_failedExportMsg(), + Bundle.MultiCaseKeywordSearchPanel_searchResultsExport_featureName(), + JOptionPane.WARNING_MESSAGE); + LOGGER.log(Level.WARNING, "Export of search results failed unable to write results csv file", ex); + } + } + + /** + * Ask the user if they want to continue their search while this window is + * closed. Cancels the current search if they select no. + */ + @Messages({ + "MultiCaseKeywordSearchPanel.continueSearch.text=A search is currently being performed. " + + "Would you like the search to continue in the background while the search window is closed?", + "MultiCaseKeywordSearchPanel.continueSearch.title=Closing multi-case search" + }) + void closeSearchPanel() { + if (cancelButton.isEnabled()) { + boolean shouldContinueSearch = JOptionPane.showConfirmDialog(null, + Bundle.MultiCaseKeywordSearchPanel_continueSearch_text(), + Bundle.MultiCaseKeywordSearchPanel_continueSearch_title(), + JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE) + == JOptionPane.YES_OPTION; + if (!shouldContinueSearch) { + cancelSearch(); + } + } + } + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton cancelButton; + private javax.swing.JList caseSelectionList; + private javax.swing.JLabel casesLabel; + private javax.swing.JRadioButton exactRadioButton; + private javax.swing.JButton exportButton; + private javax.swing.JScrollPane jScrollPane1; + private javax.swing.JTextField keywordTextField; + private javax.swing.JButton pickCasesButton; + private javax.swing.JRadioButton regexRadioButton; + private javax.swing.JLabel resultsCountLabel; + private javax.swing.JLabel resultsLabel; + private javax.swing.JScrollPane resultsScrollPane; + private javax.swing.JButton searchButton; + private javax.swing.JProgressBar searchProgressBar; + private javax.swing.ButtonGroup searchTypeGroup; + private javax.swing.JRadioButton substringRadioButton; + private javax.swing.JScrollPane toolDescriptionScrollPane; + private javax.swing.JTextArea toolDescriptionTextArea; + private javax.swing.JButton viewErrorsButton; + private javax.swing.JLabel warningLabel; + // End of variables declaration//GEN-END:variables + + /* + * A thread that performs a keyword search of cases + */ + private final class SearchThread extends Thread { + + private final Collection caseNodes; + private final SearchQuery searchQuery; + private final MultiCaseSearcher multiCaseSearcher = new MultiCaseSearcher(); + + /** + * Constructs a thread that performs a keyword search of cases + * + * @param caseNames The names of the cases to search. + * @param query The keyword search query to perform. + */ + private SearchThread(Collection caseNodes, SearchQuery searchQuery) { + this.caseNodes = caseNodes; + this.searchQuery = searchQuery; + } + + /** + * Register an object with the MultiCaseSearcher eventBus so that the + * object's subscribe methods can receive results. + * + * @param object the object to register with the MultiCaseSearcher + */ + private void registerWithSearcher(Object object) { + multiCaseSearcher.registerWithEventBus(object); + } + + /** + * Unregister an object with the MultiCaseSearcher so that the object's + * subscribe methods no longer receive results. + * + * @param object the object to unregister with the MultiCaseSearcher + */ + private void unregisterWithSearcher(Object object) { + multiCaseSearcher.unregisterWithEventBus(object); + } + + @Override + public void interrupt() { + super.interrupt(); + //in case it is running a method which causes InterruptedExceptions to be ignored + multiCaseSearcher.stopMultiCaseSearch(); + } + + @Override + public void run() { + multiCaseSearcher.performKeywordSearch(caseNodes, searchQuery, new MultiCaseKeywordSearchProgressIndicator(searchProgressBar)); + } + + } + +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchProgressIndicator.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchProgressIndicator.java new file mode 100755 index 0000000000..d63b2a1f8e --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchProgressIndicator.java @@ -0,0 +1,167 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 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.keywordsearch.multicase; + +import javax.swing.JProgressBar; +import javax.swing.SwingUtilities; +import org.sleuthkit.autopsy.progress.ProgressIndicator; + +/** + * A progress indicator that updates a JProgressBar. + */ +final class MultiCaseKeywordSearchProgressIndicator implements ProgressIndicator { + + private final JProgressBar progress; + + /** + * Construct a new JProgressIndicator + * + * @param progressBar the JProgressBar you want this indicator to update + */ + MultiCaseKeywordSearchProgressIndicator(JProgressBar progressBar) { + progress = progressBar; + progress.setStringPainted(true); + } + + /** + * Start showing progress in the progress bar. + * + * @param message the message to be displayed on the progress bar, null to + * display percent complete + * @param max The total number of work units to be completed. + */ + @Override + public void start(String message, int max) { + SwingUtilities.invokeLater(() -> { + progress.setIndeterminate(false); + progress.setMinimum(0); + progress.setString(message); //the message + progress.setValue(0); + progress.setMaximum(max); + progress.setVisible(true); + }); + } + + /** + * Start showing progress in the progress bar. + * + * @param message the message to be displayed on the progress bar, null to + * display percent complete + */ + @Override + public void start(String message) { + SwingUtilities.invokeLater(() -> { + progress.setIndeterminate(true); + progress.setMinimum(0); + progress.setString(message); + progress.setValue(0); + progress.setVisible(true); + }); + } + + /** + * Switches the progress indicator to indeterminate mode (the total number + * of work units to be completed is unknown). + * + * @param message the message to be displayed on the progress bar, null to + * display percent complete + */ + @Override + public void switchToIndeterminate(String message) { + SwingUtilities.invokeLater(() -> { + progress.setIndeterminate(true); + progress.setString(message); + }); + } + + /** + * Switches the progress indicator to determinate mode (the total number of + * work units to be completed is known). + * + * @param message the message to be displayed on the progress bar, null to + * display percent complete + * @param current The number of work units completed so far. + * @param max The total number of work units to be completed. + */ + @Override + public void switchToDeterminate(String message, int current, int max) { + SwingUtilities.invokeLater(() -> { + progress.setIndeterminate(false); + progress.setMinimum(0); + progress.setString(message); + progress.setValue(current); + progress.setMaximum(max); + }); + } + + /** + * Updates the progress indicator with a progress message. + * + * @param message the message to be displayed on the progress bar, null to + * display percent complete + */ + @Override + public void progress(String message) { + SwingUtilities.invokeLater(() -> { + progress.setString(message); + }); + } + + /** + * Updates the progress indicator with the number of work units completed so + * far when in determinate mode (the total number of work units to be + * completed is known). + * + * @param current Number of work units completed so far. + */ + @Override + public void progress(int current) { + SwingUtilities.invokeLater(() -> { + progress.setValue(current); + }); + } + + /** + * Updates the progress indicator with a progress message and the number of + * work units completed so far when in determinate mode (the total number of + * work units to be completed is known). + * + * @param message the message to be displayed on the progress bar, null to + * display percent complete + * @param current Number of work units completed so far. + */ + @Override + public void progress(String message, int current) { + SwingUtilities.invokeLater(() -> { + progress.setString(message); + progress.setValue(current); + }); + } + + /** + * Finishes the progress indicator when the task is completed. + */ + @Override + public void finish() { + SwingUtilities.invokeLater(() -> { + progress.setVisible(false); + }); + } + +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchTopComponent.form b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchTopComponent.form new file mode 100644 index 0000000000..cbd81a1c12 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchTopComponent.form @@ -0,0 +1,33 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchTopComponent.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchTopComponent.java new file mode 100755 index 0000000000..3ce6b28505 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseKeywordSearchTopComponent.java @@ -0,0 +1,140 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 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.keywordsearch.multicase; + +import java.awt.BorderLayout; +import java.awt.Component; +import org.openide.util.NbBundle.Messages; +import org.openide.windows.TopComponent; +import org.openide.windows.WindowManager; +import org.openide.windows.Mode; + +@TopComponent.Description( + preferredID = "MultiCaseKeywordSearchTopComponent", + persistenceType = TopComponent.PERSISTENCE_NEVER +) +@TopComponent.Registration(mode = "multiCaseKeywordSearch", openAtStartup = false) +@Messages({ + "CTL_MultiCaseKeywordSearchTopComponentAction=Multi-case Keyword Search", + "CTL_MultiCaseKeywordSearchTopComponent=Multi-case Keyword Search"}) +/** + * A top level component for the multi case keyword search feature. + */ +final class MultiCaseKeywordSearchTopComponent extends TopComponent { + + public final static String PREFERRED_ID = "MultiCaseKeywordSearchTopComponent"; // NON-NLS + private static final long serialVersionUID = 1L; + private static boolean topComponentInitialized = false; + + @Messages({ + "MultiCaseKeywordSearchTopComponent.exceptionMessage.failedToCreatePanel=Failed to create Multi-case Keyword Search panel.",}) + /** + * Open the top level component if it is not already open, if it is open + * bring it to the front and select it. + */ + static void openTopComponent() { + final MultiCaseKeywordSearchTopComponent tc = (MultiCaseKeywordSearchTopComponent) WindowManager.getDefault().findTopComponent(PREFERRED_ID); + if (tc != null) { + if (tc.isOpened() == false) { + topComponentInitialized = true; + Mode mode = WindowManager.getDefault().findMode("multiCaseKeywordSearch"); // NON-NLS + if (mode != null) { + mode.dockInto(tc); + } + tc.open(); + } + tc.toFront(); + tc.requestActive(); + } + } + + /** + * Close the top level componet. + */ + static void closeTopComponent() { + if (topComponentInitialized) { + final TopComponent tc = WindowManager.getDefault().findTopComponent(PREFERRED_ID); + if (tc != null) { + try { + tc.close(); + } catch (Exception e) { + + } + } + } + } + + @Messages({"MultiCaseKeywordSearchTopComponent.name.text=Multi-case Keyword Search"}) + /** + * Construct a new "MultiCaseKeywordSearchTopComponent. + */ + MultiCaseKeywordSearchTopComponent() { + initComponents(); + setName(Bundle.MultiCaseKeywordSearchTopComponent_name_text()); + setDisplayName(Bundle.MultiCaseKeywordSearchTopComponent_name_text()); + setToolTipText(Bundle.MultiCaseKeywordSearchTopComponent_name_text()); + setSize(this.getPreferredSize()); + setLayout(new BorderLayout()); + MultiCaseKeywordSearchPanel searchPanel = new MultiCaseKeywordSearchPanel(); + searchPanel.setSize(searchPanel.getPreferredSize()); + searchPanel.setVisible(true); + add(searchPanel); + } + + @Override + public void componentOpened() { + super.componentOpened(); + WindowManager.getDefault().setTopComponentFloating(this, true); + } + + @Override + public boolean canClose() { + for (Component component : getComponents()) { + if (component instanceof MultiCaseKeywordSearchPanel) { + ((MultiCaseKeywordSearchPanel) component).closeSearchPanel(); + } + } + return super.canClose(); + } + + /** + * 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. + */ + // //GEN-BEGIN:initComponents + private void initComponents() { + + setPreferredSize(new java.awt.Dimension(1002, 444)); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGap(0, 902, Short.MAX_VALUE) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGap(0, 444, Short.MAX_VALUE) + ); + }// //GEN-END:initComponents + + // Variables declaration - do not modify//GEN-BEGIN:variables + // End of variables declaration//GEN-END:variables +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseSearcher.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseSearcher.java new file mode 100644 index 0000000000..b26001a28e --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/MultiCaseSearcher.java @@ -0,0 +1,775 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 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.keywordsearch.multicase; + +import com.google.common.eventbus.EventBus; +import java.io.File; +import java.io.IOException; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.stream.Collectors; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; +import org.apache.commons.lang.StringUtils; +import org.apache.solr.client.solrj.SolrQuery; +import org.apache.solr.client.solrj.SolrRequest; +import org.apache.solr.client.solrj.SolrServerException; +import org.apache.solr.client.solrj.impl.HttpSolrServer; +import org.apache.solr.client.solrj.request.CoreAdminRequest; +import org.apache.solr.client.solrj.response.CoreAdminResponse; +import org.apache.solr.client.solrj.response.QueryResponse; +import org.apache.solr.common.SolrDocument; +import org.apache.solr.common.SolrDocumentList; +import org.apache.solr.common.params.CoreAdminParams; +import org.apache.solr.common.params.CursorMarkParams; +import org.openide.util.Exceptions; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.casemodule.CaseMetadata; +import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; +import org.sleuthkit.autopsy.coordinationservice.CoordinationService; +import org.sleuthkit.autopsy.core.UserPreferences; +import org.sleuthkit.autopsy.core.UserPreferencesException; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.UNCPathUtilities; +import org.sleuthkit.autopsy.keywordsearch.Server; +import org.sleuthkit.autopsy.progress.ProgressIndicator; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.CaseDbConnectionInfo; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.Report; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; +import org.w3c.dom.Document; +import org.xml.sax.SAXException; + +/** + * Performs keyword searches across multiple cases + */ +final class MultiCaseSearcher { + + private static final String CASE_AUTO_INGEST_LOG_NAME = "AUTO_INGEST_LOG.TXT"; //NON-NLS + private static final String SEARCH_COMPLETE_MESSAGE = "SEARCH_COMPLETE"; + private static final String RESOURCES_LOCK_SUFFIX = "_RESOURCES"; //NON-NLS + private static final int CASE_DIR_READ_LOCK_TIMEOUT_HOURS = 12; //NON-NLS + private static final String SOLR_SERVER_URL_FORMAT_STRING = "http://%s:%s/solr"; //NON-NLS + private static final String SOLR_CORE_URL_FORMAT_STRING = "http://%s:%s/solr/%s"; //NON-NLS + private final static String SOLR_METADATA_FILE_NAME = "SolrCore.properties"; //NON-NLS + private static final String SOLR_CORE_NAME_XPATH = "/SolrCores/Core/CoreName/text()"; //NON-NLS + private static final String TEXT_INDEX_NAME_XPATH = "/SolrCores/Core/TextIndexPath/text()"; //NON-NLS + private static final String SOLR_CORE_INSTANCE_PATH_PROPERTY = "instanceDir"; //NON-NLS + private static final String SOLR_CONFIG_SET_NAME = "AutopsyConfig"; //NON-NLS + private static final int MAX_RESULTS_PER_CURSOR_MARK = 512; + private static final String SOLR_DOC_ID_FIELD = Server.Schema.ID.toString(); //NON-NLS + private static final String SOLR_DOC_CONTENT_STR_FIELD = Server.Schema.CONTENT_STR.toString(); //NON-NLS + private static final String SOLR_DOC_CHUNK_SIZE_FIELD = Server.Schema.CHUNK_SIZE.toString(); //NON-NLS + private static final String SOLR_DOC_ID_PARTS_SEPARATOR = "_"; + private static final Logger logger = Logger.getLogger(MultiCaseSearcher.class.getName()); + private final EventBus eventBus = new EventBus("MultiCaseSearcherEventBus"); + private static final UNCPathUtilities pathUtils = new UNCPathUtilities(); + private volatile boolean searchStopped = true; + + MultiCaseSearcher() { + + } + + static String getSearchCompleteMessage() { + return SEARCH_COMPLETE_MESSAGE; + } + + /** + * + * Performs keyword searches across multiple cases + * + * @param caseNames The names of the cases to search. + * @param query The keyword search query to perform. + * @param progressIndicator A progrss indicator for the search. + * + * @return The search results. + * + * @throws MultiCaseSearcherException + * @throws InterruptedException + */ + @NbBundle.Messages({ + "MultiCaseSearcher.progressMessage.findingCases=Finding selected cases", + "MultiCaseSearcher.progressMessage.creatingSolrQuery=Creating search query for Solr server", + "# {0} - total cases", + "MultiCaseSearcher.progressMessage.startingCaseSearches=Searching {0} case(s)", + "# {0} - case name", + "# {1} - case counter", + "# {2} - total cases", + "MultiCaseSearcher.progressMessage.acquiringSharedLockForCase=Acquiring shared lock for \"{0}\" ({1} of {2} case(s))", + "# {0} - case name", + "# {1} - case counter", + "# {2} - total cases", + "MultiCaseSearcher.progressMessage.loadingSolrCoreForCase=Loading Solr core for \"{0}\" ({1} of {2} case(s))", + "# {0} - case name", + "# {1} - case counter", + "# {2} - total cases", + "MultiCaseSearcher.progressMessage.openingCaseDbForCase=Opening case database for \"{0}\" ({1} of {2} case(s))", + "# {0} - case name", + "# {1} - case counter", + "# {2} - total cases", + "MultiCaseSearcher.progressMessage.executingSolrQueryForCase=Getting keyword hits for \"{0}\" ({1} of {2} case(s))", + "# {0} - case directory path", + "MultiCaseSearcher.exceptionMessage.failedToGetCaseDirReadlock=Failed to obtain read lock for case directory at {0}", + "MultiCaseSearcher.exceptionMessage.cancelledMessage=Search cancelled" + }) + void performKeywordSearch(final Collection caseNodes, final SearchQuery query, final ProgressIndicator progressIndicator) { + progressIndicator.start(Bundle.MultiCaseSearcher_progressMessage_findingCases()); + try { + searchStopped = false; //mark the search as started + final List caseMetadata = getMultiCaseMetadata(caseNodes); + checkForCancellation(); + //eventBus.post("number of cases to search determined"); + progressIndicator.progress(Bundle.MultiCaseSearcher_progressMessage_creatingSolrQuery()); + final SolrQuery solrQuery = createSolrQuery(query); + checkForCancellation(); + final int totalCases = caseMetadata.size(); + int caseCounter = 1; + progressIndicator.progress(Bundle.MultiCaseSearcher_progressMessage_startingCaseSearches(totalCases)); + int totalSteps = 5; + progressIndicator.switchToDeterminate(Bundle.MultiCaseSearcher_progressMessage_startingCaseSearches(totalCases), 0, totalCases * totalSteps); + int caseNumber = 0; + for (MultiCaseMetadata aCase : caseMetadata) { + CaseMetadata metadata = aCase.getCaseMetadata(); + String caseName = metadata.getCaseDisplayName(); + SleuthkitCase caseDatabase = null; + + int stepsCompleted = 0; + progressIndicator.progress(Bundle.MultiCaseSearcher_progressMessage_acquiringSharedLockForCase(caseName, caseCounter, totalCases), stepsCompleted + caseNumber * totalSteps); + try (CoordinationService.Lock caseDirReadLock = CoordinationService.getInstance().tryGetSharedLock(CoordinationService.CategoryNode.CASES, aCase.getCaseMetadata().getCaseDirectory(), CASE_DIR_READ_LOCK_TIMEOUT_HOURS, TimeUnit.HOURS)) { + if (null == caseDirReadLock) { + throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_failedToGetCaseDirReadlock(aCase.getCaseMetadata().getCaseDirectory())); + } + checkForCancellation(); + ++stepsCompleted; + progressIndicator.progress(Bundle.MultiCaseSearcher_progressMessage_loadingSolrCoreForCase(caseName, caseCounter, totalCases), stepsCompleted + caseNumber * totalSteps); + final HttpSolrServer solrServer = loadSolrCoreForCase(aCase); + checkForCancellation(); + ++stepsCompleted; + progressIndicator.progress(Bundle.MultiCaseSearcher_progressMessage_openingCaseDbForCase(caseName, caseCounter, totalCases), stepsCompleted + caseNumber * totalSteps); + caseDatabase = openCase(aCase); + checkForCancellation(); + ++stepsCompleted; + progressIndicator.progress(Bundle.MultiCaseSearcher_progressMessage_executingSolrQueryForCase(caseName, caseCounter, totalCases), stepsCompleted + caseNumber * totalSteps); + eventBus.post(executeQuery(solrServer, solrQuery, caseDatabase, aCase)); + ++stepsCompleted; + + progressIndicator.progress(stepsCompleted + caseNumber * totalSteps); + ++caseCounter; + } catch (CoordinationService.CoordinationServiceException ex) { + throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_failedToGetCaseDirReadlock(aCase.getCaseMetadata().getCaseDirectory()), ex); + } catch (MultiCaseSearcherException exception) { + logger.log(Level.INFO, "Exception encountered while performing multi-case keyword search", exception); + eventBus.post(exception); + } finally { + if (null != caseDatabase) { + closeCase(caseDatabase); + } + } + caseNumber++; + } + } catch (InterruptedException exception) { + logger.log(Level.INFO, Bundle.MultiCaseSearcher_exceptionMessage_cancelledMessage(), exception); + eventBus.post(exception); + } catch (MultiCaseSearcherException exception) { + logger.log(Level.WARNING, "Exception encountered while performing multi-case keyword search", exception); + eventBus.post(new InterruptedException("Exception encountered while performing multi-case keyword search")); + eventBus.post(exception); + } finally { + progressIndicator.finish(); + eventBus.post(SEARCH_COMPLETE_MESSAGE); + } + } + + /** + * Gets metadata for the cases associated with one or more with the search + * + * @param caseNames The names of the cases to search. + * + * @return The metadata for the cases. + * + * @throws MultiCaseSearcherException + * @throws InterruptedException + */ + private List getMultiCaseMetadata(final Collection caseNodes) throws MultiCaseSearcherException, InterruptedException { + final Map casesToCasePaths = caseNodes.stream() + .collect(Collectors.toMap(CaseNodeData::getDirectory, CaseNodeData::getName)); + checkForCancellation(); + final List cases = new ArrayList<>(); + for (Map.Entry entry : casesToCasePaths.entrySet()) { + final Path caseDirectoryPath = entry.getKey(); + final CaseMetadata caseMetadata = getCaseMetadata(caseDirectoryPath); + checkForCancellation(); + final TextIndexMetadata textIndexMetadata = getTextIndexMetadata(caseDirectoryPath); + checkForCancellation(); + cases.add(new MultiCaseMetadata(caseMetadata, textIndexMetadata)); + } + return cases; + } + + /** + * Gets the metadata for a case from the case metadata file in a given case + * directory. + * + * @param caseDirectoryPath A case directory path. + * + * @return The case metadata. + * + * @throws MultiCaseSearcherException + */ + @NbBundle.Messages({ + "# {0} - case directory", "MultiCaseSearcher.exceptionMessage.failedToFindCaseMetadata=Failed to find case metadata file in {0}", + "# {0} - case directory", "MultiCaseSearcher.exceptionMessage.failedToParseCaseMetadata=Failed to parse case file metadata in {0}" + }) + + private static CaseMetadata getCaseMetadata(Path caseDirectoryPath) throws MultiCaseSearcherException { + Path metadataPath = CaseMetadata.getCaseMetadataFile(caseDirectoryPath); + if (metadataPath != null) { + try { + return new CaseMetadata(metadataPath); + } catch (CaseMetadata.CaseMetadataException ex) { + throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_failedToParseCaseMetadata(caseDirectoryPath), ex); + } + } + throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_failedToFindCaseMetadata(caseDirectoryPath)); + } + + /** + * Gets the text index metadata from the Solr.properties file in a given + * case directory. + * + * @param caseDirectoryPath A case directory path. + * + * @return The text index metadata. + * + * @throws MultiCaseSearcherException + */ + @NbBundle.Messages({ + "# {0} - file name", "# {1} - case directory", "MultiCaseSearcher.exceptionMessage.missingSolrPropertiesFile=Missing {0} file in {1}", + "# {0} - file name", "# {1} - case directory", "MultiCaseSearcher.exceptionMessage.solrPropertiesFileParseError=Error parsing {0} file in {1}",}) + private static TextIndexMetadata getTextIndexMetadata(Path caseDirectoryPath) throws MultiCaseSearcherException { + final Path solrMetaDataFilePath = Paths.get(caseDirectoryPath.toString(), SOLR_METADATA_FILE_NAME); + final File solrMetaDataFile = solrMetaDataFilePath.toFile(); + if (!solrMetaDataFile.exists() || !solrMetaDataFile.canRead()) { + throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_missingSolrPropertiesFile(SOLR_METADATA_FILE_NAME, caseDirectoryPath)); + } + try { + final DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + final Document doc = docBuilder.parse(solrMetaDataFile); + final XPath xPath = XPathFactory.newInstance().newXPath(); + XPathExpression xPathExpr = xPath.compile(SOLR_CORE_NAME_XPATH); + final String solrCoreName = (String) xPathExpr.evaluate(doc, XPathConstants.STRING); + xPathExpr = xPath.compile(TEXT_INDEX_NAME_XPATH); + final String relativeTextIndexPath = (String) xPathExpr.evaluate(doc, XPathConstants.STRING); + Path textIndexPath = caseDirectoryPath.resolve(relativeTextIndexPath); + textIndexPath = textIndexPath.getParent(); // Remove "index" path component + final String textIndexUNCPath = pathUtils.convertPathToUNC(textIndexPath.toString()); + return new TextIndexMetadata(caseDirectoryPath, solrCoreName, textIndexUNCPath); + } catch (ParserConfigurationException | SAXException | XPathExpressionException | IOException ex) { + throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_solrPropertiesFileParseError(SOLR_METADATA_FILE_NAME, caseDirectoryPath), ex); + } + } + + /** + * Converts a keyword search query into a Solr query. + * + * @param searchQuery A keyword search query. + * + * @return A Solr query. + */ + private static SolrQuery createSolrQuery(SearchQuery searchQuery) { + final SolrQuery solrQuery = new SolrQuery(); + solrQuery.setQuery(searchQuery.getSearchTerm()); + solrQuery.setRows(MAX_RESULTS_PER_CURSOR_MARK); + /* + * Note that setting the sort order is necessary for cursor based paging + * to work. + */ + solrQuery.setSort(SolrQuery.SortClause.asc(SOLR_DOC_ID_FIELD)); + solrQuery.setFields(SOLR_DOC_ID_FIELD, SOLR_DOC_CHUNK_SIZE_FIELD, SOLR_DOC_CONTENT_STR_FIELD); + return solrQuery; + } + + /** + * Connects to the Solr server and loads the Solr core for a given case. + * + * @param aCase + * + * @return A Solr server client object that can be used for executing + * queries of the specified text index. + * + * MultiCaseSearcherException + * + * @throws InterruptedException + */ + @NbBundle.Messages({ + "# {0} - connection info", + "# {1} - case name", + "# {2} - case directory", + "MultiCaseSearcher.exceptionMessage.errorLoadingCore=Error connecting to Solr server and loading core (URL: {0}) for case {1} in {2}" + }) + private HttpSolrServer loadSolrCoreForCase(MultiCaseMetadata aCase) throws MultiCaseSearcherException, InterruptedException { + TextIndexMetadata textIndexMetadata = aCase.getTextIndexMetadata(); + Server.IndexingServerProperties indexServer = Server.getMultiUserServerProperties(aCase.getCaseMetadata().getCaseDirectory()); + final String serverURL = String.format(SOLR_SERVER_URL_FORMAT_STRING, indexServer.getHost(), indexServer.getPort()); + try { + /* + * Connect to the Solr server. + */ + final HttpSolrServer solrServer = new HttpSolrServer(serverURL); + CoreAdminRequest statusRequest = new CoreAdminRequest(); + statusRequest.setCoreName(null); + statusRequest.setAction(CoreAdminParams.CoreAdminAction.STATUS); + statusRequest.setIndexInfoNeeded(false); + checkForCancellation(); + statusRequest.process(solrServer); + checkForCancellation(); + + /* + * Load the core for the text index if it is not already loaded. + */ + CoreAdminResponse response = CoreAdminRequest.getStatus(textIndexMetadata.getSolrCoreName(), solrServer); + if (null == response.getCoreStatus(textIndexMetadata.getSolrCoreName()).get(SOLR_CORE_INSTANCE_PATH_PROPERTY)) { + CoreAdminRequest.Create loadCoreRequest = new CoreAdminRequest.Create(); + loadCoreRequest.setDataDir(textIndexMetadata.getTextIndexPath()); + loadCoreRequest.setCoreName(textIndexMetadata.getSolrCoreName()); + loadCoreRequest.setConfigSet(SOLR_CONFIG_SET_NAME); + loadCoreRequest.setIsLoadOnStartup(false); + loadCoreRequest.setIsTransient(true); + solrServer.request(loadCoreRequest); + } + + /* + * Create a server client object that can be used for executing + * queries of the specified text index. + */ + final String coreURL = String.format(SOLR_CORE_URL_FORMAT_STRING, indexServer.getHost(), indexServer.getPort(), textIndexMetadata.getSolrCoreName()); + final HttpSolrServer coreServer = new HttpSolrServer(coreURL); + return coreServer; + + } catch (SolrServerException | IOException ex) { + throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_errorLoadingCore(serverURL, aCase.getCaseMetadata().getCaseName(), textIndexMetadata.getCaseDirectoryPath()), ex); + } + } + + /** + * Opens a case database. + * + * @param caseMetadata + * + * @return A case database. + * + * @throws MultiCaseSearcherException + * @throws InterruptedException + */ + @NbBundle.Messages({ + "# {0} - case_name", + "MultiCaseSearcher.exceptionMessage.failedToGetCaseDatabaseConnectionInfo=Failed to get case database connection info for case {0}", + "# {0} - PostgreSQL server host", + "# {1} - PostgreSQL server port", + "# {2} - case database name", + "# {3} - case directory", + "MultiCaseSearcher.exceptionMessage.errorOpeningCaseDatabase=Error connecting to PostgreSQL server (Host/Port: [{0}:{1}] and opening case database {2} for case at {3}" + }) + private SleuthkitCase openCase(MultiCaseMetadata aCase) throws MultiCaseSearcherException, InterruptedException { + CaseDbConnectionInfo dbConnectionInfo; + try { + dbConnectionInfo = UserPreferences.getDatabaseConnectionInfo(); + } catch (UserPreferencesException ex) { + throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_failedToGetCaseDatabaseConnectionInfo(aCase.getCaseMetadata().getCaseName()), ex); + } + checkForCancellation(); + final CaseMetadata caseMetadata = aCase.getCaseMetadata(); + try { + return SleuthkitCase.openCase(caseMetadata.getCaseDatabaseName(), UserPreferences.getDatabaseConnectionInfo(), caseMetadata.getCaseDirectory()); + } catch (UserPreferencesException | TskCoreException ex) { + throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_errorOpeningCaseDatabase(dbConnectionInfo.getHost(), dbConnectionInfo.getPort(), caseMetadata.getCaseDatabaseName(), caseMetadata.getCaseDirectory()), ex); + } + } + + /** + * Closes a case database. + * + * @param aCase a case database. + */ + private static void closeCase(SleuthkitCase aCase) { + aCase.close(); + } + + /** + * Executes a keyword search searchTerm in the text index of a case. + * + * @param solrServer The Solr server. + * @param solrQuery The Solr searchTerm. + * @param caseDatabase The case database. + * @param aCase The case metadata. + * + * @return A list of search results, possibly empty. + * + * @throws MultiCaseSearcherException + * @throws InterruptedException + */ + @NbBundle.Messages({ + "# {0} - query", + "# {1} - case_name", + "MultiCaseSearcher.exceptionMessage.solrQueryError=Failed to execute query \"{0}\" on case {1}" + }) + private Collection executeQuery(HttpSolrServer solrServer, SolrQuery solrQuery, SleuthkitCase caseDatabase, MultiCaseMetadata aCase) throws MultiCaseSearcherException, InterruptedException { + final List hits = new ArrayList<>(); + final Set uniqueObjectIds = new HashSet<>(); + String cursorMark = CursorMarkParams.CURSOR_MARK_START; + boolean allResultsProcessed = false; + while (!allResultsProcessed) { + checkForCancellation(); + solrQuery.set(CursorMarkParams.CURSOR_MARK_PARAM, cursorMark); + QueryResponse response; + try { + checkForCancellation(); + response = solrServer.query(solrQuery, SolrRequest.METHOD.POST); + } catch (SolrServerException ex) { + throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_solrQueryError(solrQuery.getQuery(), aCase.getCaseMetadata().getCaseName()), ex); + } + SolrDocumentList resultDocuments = response.getResults(); + for (SolrDocument resultDoc : resultDocuments) { + checkForCancellation(); + String solrDocumentId = resultDoc.getFieldValue(SOLR_DOC_ID_FIELD).toString(); + Long solrObjectId = parseSolrObjectId(solrDocumentId); + if (!uniqueObjectIds.contains(solrObjectId)) { + uniqueObjectIds.add(solrObjectId); + checkForCancellation(); + hits.add(processHit(solrObjectId, caseDatabase, aCase)); + } + } + checkForCancellation(); + String nextCursorMark = response.getNextCursorMark(); + if (cursorMark.equals(nextCursorMark)) { + allResultsProcessed = true; + } + cursorMark = nextCursorMark; + } + return hits; + } + + /** + * Parses a Solr document id to get the Solr object id. + * + * @param solrDocumentId A Solr document id. + * + * @return A Solr object id. + */ + private static Long parseSolrObjectId(String solrDocumentId) { + /** + * A Solr document id is of the form [solr_object_id] for Content object + * metadata documents and + * [solr_object_id][SOLR_DOC_ID_PARTS_SEPARATOR][chunk_id] for Content + * object text chunk documents. + */ + final String[] solrDocumentIdParts = solrDocumentId.split(SOLR_DOC_ID_PARTS_SEPARATOR); + if (1 == solrDocumentIdParts.length) { + return Long.parseLong(solrDocumentId); + } else { + return Long.parseLong(solrDocumentIdParts[0]); + } + } + + /** + * Creates a keyword search hit object for a Content object identified by + * its Solr object id. + * + * @param solrObjectId The Solr object id of a Content object. + * @param caseDatabase The case database of the case that has the Content. + * @param caseInfo Metadata about the case that has the content. + * + * @return + * + * @throws MultiCaseSearcherException + */ + @NbBundle.Messages({ + "# {0} - Solr document id", + "# {1} - case database name", + "# {2} - case directory", + "MultiCaseSearcher.exceptionMessage.hitProcessingError=Failed to query case database for processing of Solr object id {0} of case {1} in {2}" + }) + + private static SearchHit processHit(Long solrObjectId, SleuthkitCase caseDatabase, MultiCaseMetadata caseInfo) throws MultiCaseSearcherException { + try { + final long objectId = getObjectIdForSolrObjectId(solrObjectId, caseDatabase); + final CaseMetadata caseMetadata = caseInfo.getCaseMetadata(); + final String caseDisplayName = caseMetadata.getCaseDisplayName(); + final String caseDirectoryPath = caseMetadata.getCaseDirectory(); + final Content content = caseDatabase.getContentById(objectId); + final Content dataSource = content.getDataSource(); + final String dataSourceName = (dataSource == null) ? "" : dataSource.getName(); + SearchHit.SourceType sourceType = SearchHit.SourceType.FILE; + String sourceName = ""; + String sourcePath = ""; + if (content instanceof AbstractFile) { + AbstractFile sourceFile = (AbstractFile) content; + sourceName = sourceFile.getName(); + sourcePath = sourceFile.getLocalAbsPath(); + if (null == sourcePath) { + sourceType = SearchHit.SourceType.FILE; + sourcePath = sourceFile.getUniquePath(); + } else { + sourceType = SearchHit.SourceType.LOCAL_FILE; + sourceName = sourceFile.getName(); + } + } else if (content instanceof BlackboardArtifact) { + BlackboardArtifact sourceArtifact = (BlackboardArtifact) content; + sourceType = SearchHit.SourceType.ARTIFACT; + BlackboardArtifact.Type artifactType = caseDatabase.getArtifactType(sourceArtifact.getArtifactTypeName()); + sourceName = artifactType.getDisplayName(); + Content source = sourceArtifact.getParent(); + if (source instanceof AbstractFile) { + AbstractFile sourceFile = (AbstractFile) source; + sourcePath = sourceFile.getLocalAbsPath(); + if (null == sourcePath) { + sourcePath = sourceFile.getUniquePath(); + } + } else { + sourcePath = source.getUniquePath(); + } + } else if (content instanceof Report) { + Report report = (Report) content; + sourceType = SearchHit.SourceType.REPORT; + sourceName = report.getReportName(); + sourcePath = report.getUniquePath(); + } + + return new SearchHit(caseDisplayName, caseDirectoryPath, dataSourceName, sourceType, sourceName, sourcePath); + } catch (SQLException | TskCoreException ex) { + throw new MultiCaseSearcherException(Bundle.MultiCaseSearcher_exceptionMessage_hitProcessingError(solrObjectId, caseInfo.getCaseMetadata().getCaseName(), caseInfo.getCaseMetadata().getCaseDirectory()), ex); + } + } + + /** + * Gets the Sleuthkit object id that corresponds to the Solr object id of + * some content. + * + * @param solrObjectId A solr object id for some content. + * @param caseDatabase The case database for the case that includes the + * content. + * + * @return The Sleuthkit object id of the content. + * + * @throws MultiCaseSearcherException + * @throws TskCoreException + * @throws SQLException + */ + private static long getObjectIdForSolrObjectId(long solrObjectId, SleuthkitCase caseDatabase) throws MultiCaseSearcherException, TskCoreException, SQLException { + if (0 < solrObjectId) { + return solrObjectId; + } else { + try (SleuthkitCase.CaseDbQuery databaseQuery = caseDatabase.executeQuery("SELECT artifact_obj_id FROM blackboard_artifacts WHERE artifact_id = " + solrObjectId)) { + final ResultSet resultSet = databaseQuery.getResultSet(); + if (resultSet.next()) { + return resultSet.getLong("artifact_obj_id"); + } else { + throw new TskCoreException("Empty result set getting obj_id for artifact with artifact_id =" + solrObjectId); + } + } + } + } + + /** + * Checks to see if the current thread has been interrupted (i.e, the search + * has been cancelled) and throws an InterruptedException if it has been. + * + * @throws InterruptedException + */ + private void checkForCancellation() throws InterruptedException { + if (Thread.currentThread().isInterrupted() || searchStopped) { + throw new InterruptedException("Search Cancelled"); + } + } + + /** + * A bundle of metadata for a case. + */ + private final static class MultiCaseMetadata { + + private final CaseMetadata caseMetadata; + private final TextIndexMetadata textIndexMetadata; + + /** + * Contructs a bundle of metadata for a case + * + * @param caseMetadata The case metadata. + * @param textIndexMetaData The text index metadata for the case. + */ + private MultiCaseMetadata(CaseMetadata caseMetadata, TextIndexMetadata textIndexMetaData) { + this.caseMetadata = caseMetadata; + this.textIndexMetadata = textIndexMetaData; + } + + /** + * Gets the case metadata. + * + * @return The case metadata. + */ + private CaseMetadata getCaseMetadata() { + return this.caseMetadata; + } + + /** + * Gets the text index metadata for the case. + * + * @return The text index metadata. + */ + private TextIndexMetadata getTextIndexMetadata() { + return this.textIndexMetadata; + } + + } + + /** + * Bundles a case directory path, a Solr core fileName, and a text index UNC + * path. + */ + private final static class TextIndexMetadata { + + private final Path caseDirectoryPath; + private final String solrCoreName; + private final String textIndexUNCPath; + + /** + * Constructs an object that bundles a Solr core fileName and a text + * index UNC path. + * + * @param caseDirectoryPath The case directory path. + * @param solrCoreName The core fileName. + * @param textIndexUNCPath The text index path. + */ + private TextIndexMetadata(Path caseDirectoryPath, String solrCoreName, String textIndexUNCPath) { + this.caseDirectoryPath = caseDirectoryPath; + this.solrCoreName = solrCoreName; + this.textIndexUNCPath = textIndexUNCPath; + } + + /** + * Gets the case directory path. + * + * @return The path. + */ + private Path getCaseDirectoryPath() { + return this.caseDirectoryPath; + } + + /** + * Gets the Solr core fileName. + * + * @return The Solr core fileName. + */ + private String getSolrCoreName() { + return this.solrCoreName; + } + + /** + * + * Gets the UNC path of the text index. + * + * @return The path. + */ + private String getTextIndexPath() { + return this.textIndexUNCPath; + } + + } + + /** + * Exception thrown if there is an error executing a search. + */ + static final class MultiCaseSearcherException extends Exception { + + private static final long serialVersionUID = 1L; + + /** + * Constructs an instance of the exception thrown if there is an error + * executing a search. + * + * @param message The exception message. + */ + private MultiCaseSearcherException(String message) { + super(message); + } + + /** + * Constructs an instance of the exception thrown if there is an error + * executing a search. + * + * @param message The exception message. + * @param cause The Throwable that caused the error. + */ + private MultiCaseSearcherException(String message, Throwable cause) { + super(message, cause); + } + + } + + /** + * Tell the MultiCaseSearcher that it's current search can be stopped the + * next time it checks for cancellation. + */ + void stopMultiCaseSearch() { + //This is necessary because if the interrupt occurs during CoreAdminRequest.process, + //CoreAdminRequest.getStatus, or HttpSolrServer.query the interrupt gets ignored + searchStopped = true; + } + + /** + * Register an object with the MultiCaseSearcher eventBus so that it's + * subscribe methods can receive results. + * + * @param object the object to register with the eventBus + */ + void registerWithEventBus(Object object) { + eventBus.register(object); + } + + /** + * Unregister an object with the MultiCaseSearcher eventBus so that it's + * subscribe methods no longer receive results. + * + * @param object the object to unregister with the eventBus + */ + void unregisterWithEventBus(Object object) { + eventBus.unregister(object); + } + +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SearchHit.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SearchHit.java new file mode 100755 index 0000000000..9ed845f43c --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SearchHit.java @@ -0,0 +1,137 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 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.keywordsearch.multicase; + +import javax.annotation.concurrent.Immutable; + +/** + * A keyword search hit from a multi-case keyword search. + */ +@Immutable +final class SearchHit { + + private final String caseDisplayName; + private final String caseDirectoryPath; + private final String dataSourceName; + private final SourceType sourceType; + private final String sourceName; + private final String sourcePath; + + /** + * Constructs a keyword search hit from a multi-case search. + * + * @param caseDisplayName The display name of the case where the hit + * occurred. + * @param caseDirectoryPath The path of the directory of the case where the + * hit occurred. + * @param dataSourceName The name of the data source within the case + * where the hit occurred. + * @param sourceType The type of the source content object. + * @param sourceName The name of the source, e.g., a file name, an + * artifact type name, or a report module name. + * @param sourcePath The path of the source content, or the path of + * the parent source content object for an artifact + * source. + */ + SearchHit(String caseDisplayName, String caseDirectoryPath, String dataSourceName, SourceType sourceType, String sourceName, String sourcePath) { + this.caseDisplayName = caseDisplayName; + this.caseDirectoryPath = caseDirectoryPath; + this.dataSourceName = dataSourceName; + this.sourceType = sourceType; + this.sourceName = sourceName; + this.sourcePath = sourcePath; + } + + /** + * Gets the display name of the case where the hit occurred. + * + * @return The case display name. + */ + String getCaseDisplayName() { + return this.caseDisplayName; + } + + /** + * Gets the path of the directory of the case where the hit occurred. + * + * @return The case directory path. + */ + String getCaseDirectoryPath() { + return this.caseDirectoryPath; + } + + /** + * Gets the name of the data source within the case where the hit occurred. + * + * @return + */ + String getDataSourceName() { + return this.dataSourceName; + } + + /** + * Gets the type of the source content object. + * + * @return The source type. + */ + SourceType getSourceType() { + return this.sourceType; + } + + /** + * Gets the name of the source, e.g., a file name, an artifact type name, or + * a report module name. + * + * @return The source name. + */ + String getSourceName() { + return this.sourceName; + } + + /** + * Gets the path of the source content, or the path of the parent source + * content object for an artifact source. + * + * @return The source object path. + */ + String getSourcePath() { + return this.sourcePath; + } + + /** + * An enumeration of the source types for keyword search hits. + */ + enum SourceType { + FILE("File"), + LOCAL_FILE("Local File"), + ARTIFACT("Artifact"), + REPORT("Report"); + + private final String displayName; + + private SourceType(String displayName) { + this.displayName = displayName; + } + + String getDisplayName() { + return this.displayName; + } + } + +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SearchQuery.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SearchQuery.java new file mode 100644 index 0000000000..89b224e06c --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SearchQuery.java @@ -0,0 +1,156 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 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.keywordsearch.multicase; + +import javax.annotation.concurrent.Immutable; +import org.sleuthkit.autopsy.keywordsearch.Server; + +/** + * A keyword search query. + */ +@Immutable +final class SearchQuery { + + private static final String SEARCH_TERM_CHARS_TO_ESCAPE = "/+-&|!(){}[]^\"~*?:\\"; + private static final String SOLR_DOC_CONTENT_STR_FIELD = Server.Schema.CONTENT_STR.toString(); //NON-NLS + private final String searchTerm; + + /** + * Constructs a multicase keyword search query. + * + * @param queryType The query type. + * @param searchTerm The search term for the query. + */ + SearchQuery(QueryType queryType, String searchTerm) { + switch (queryType) { + case EXACT_MATCH: + this.searchTerm = prepareExactMatchSearchTerm(searchTerm); + break; + case SUBSTRING: + this.searchTerm = prepareSubstringSearchTerm(searchTerm); + break; + case REGEX: + this.searchTerm = prepareRegexSearchTerm(searchTerm); + break; + default: + this.searchTerm = searchTerm; + break; + } + } + + /** + * Gets the search term. + * + * @return The query. + */ + String getSearchTerm() { + return searchTerm; + } + + /** + * Escapes and quotes a given search term as required for an exact match + * search query. + * + * @param searchTerm A "raw" input search term. + * + * @return A search term suitable for an exact match query. + */ + private static String prepareExactMatchSearchTerm(String searchTerm) { + String escapedSearchTerm = escapeSearchTerm(searchTerm); + if (!searchTerm.startsWith("\"")) { + escapedSearchTerm = "\"" + escapedSearchTerm; + } + if (!searchTerm.endsWith("\"")) { + escapedSearchTerm += "\""; + } + return escapedSearchTerm; + } + + /** + * Adds delimiters and possibly wildcards to a given search terms as + * required for a regular expression search query. + * + * @param searchTerm A "raw" input search term. + * + * @return A search term suitable for a regex query. + */ + private static String prepareRegexSearchTerm(String searchTerm) { + /* + * Add slash delimiters and, if necessary, wildcards (.*) at the + * beginning and end of the search term. The wildcards are added because + * Lucerne automatically adds a '^' prefix and '$' suffix to the search + * terms for regex searches. Without the '.*' wildcards, the search term + * will have to match the entire content_str field, which is not + * generally the intent of the user. + */ + String regexSearchTerm = SOLR_DOC_CONTENT_STR_FIELD + + ":/" + + (searchTerm.startsWith(".*") ? "" : ".*") + + searchTerm.toLowerCase() + + (searchTerm.endsWith(".*") ? "" : ".*") + + "/"; + return regexSearchTerm; + } + + /** + * Escapes and adds delimiters and wpossibly wildcards to a given search + * term as required for a substring search. + * + * @param searchTerm A "raw" input search term. + * + * @return A search term suitable for a substring query. + */ + private static String prepareSubstringSearchTerm(String searchTerm) { + String escapedSearchTerm = escapeSearchTerm(searchTerm); + return prepareRegexSearchTerm(escapedSearchTerm); + } + + /** + * Escapes a search term as required for a Lucene query. + * + * @param searchTerm A "raw" input search term. + * + * @return An escaped version of the "raw" input search term. + */ + public static String escapeSearchTerm(String searchTerm) { + String rawSearchTerm = searchTerm.trim(); + if (0 == rawSearchTerm.length()) { + return rawSearchTerm; + } + StringBuilder escapedSearchTerm = new StringBuilder(rawSearchTerm.length()); + for (int i = 0; i < rawSearchTerm.length(); ++i) { + final char nextChar = rawSearchTerm.charAt(i); + if (SEARCH_TERM_CHARS_TO_ESCAPE.contains(Character.toString(nextChar))) { + escapedSearchTerm.append("\\"); + } + escapedSearchTerm.append(nextChar); + } + return escapedSearchTerm.toString(); + } + + /** + * An enumeration of the supported query types for keywod searches. + */ + enum QueryType { + EXACT_MATCH, + SUBSTRING, + REGEX; + } + +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCaseDialogCustomizer.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCaseDialogCustomizer.java new file mode 100755 index 0000000000..be5fee183f --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCaseDialogCustomizer.java @@ -0,0 +1,63 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 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.keywordsearch.multicase; + +import java.util.ArrayList; +import java.util.List; +import javax.swing.Action; +import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; +import org.sleuthkit.autopsy.casemodule.multiusercasesbrowser.MultiUserCaseBrowserCustomizer; + +/** + * Customizer for SelectMultiUserCasesPanel. Displays the 'Create date' and + * 'Directory' columns + */ +public class SelectMultiUserCaseDialogCustomizer implements MultiUserCaseBrowserCustomizer { + + @Override + public List getColumns() { + List properties = new ArrayList<>(); + properties.add(Column.CREATE_DATE); + properties.add(Column.DIRECTORY); + return properties; + } + + @Override + public List getSortColumns() { + List sortColumns = new ArrayList<>(); + sortColumns.add(new SortColumn(Column.CREATE_DATE, false, 1)); + return sortColumns; + } + + @Override + public boolean allowMultiSelect() { + return true; + } + + @Override + public List getActions(CaseNodeData nodeData) { + return new ArrayList<>(); + } + + @Override + public Action getPreferredAction(CaseNodeData nodeData) { + return null; + } + +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCasesDialog.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCasesDialog.java new file mode 100644 index 0000000000..75d449c932 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCasesDialog.java @@ -0,0 +1,90 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 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.keywordsearch.multicase; + +import java.awt.Dialog; +import java.beans.PropertyVetoException; +import org.openide.nodes.Node; +import org.openide.windows.WindowManager; +import org.sleuthkit.autopsy.keywordsearch.multicase.MultiCaseKeywordSearchPanel.ChangeListener; + +/** + * Dialog that will display the SelectMultiUserCasesPanel + */ +class SelectMultiUserCasesDialog extends javax.swing.JDialog { + + private static final long serialVersionUID = 1L; + private static SelectMultiUserCasesDialog instance; + private static SelectMultiUserCasesPanel multiUserCasesPanel; + + /** + * Gets the singleton JDialog that allows a user to open a multi-user case. + * + * @return The singleton JDialog instance. + */ + public synchronized static SelectMultiUserCasesDialog getInstance() { + if (instance == null) { + instance = new SelectMultiUserCasesDialog(); + instance.init(); + } + return instance; + } + + /** + * Listen for new case selections from the user. + * + * @param l Listener on new case selection events + */ + void subscribeToNewCaseSelections(ChangeListener l) { + multiUserCasesPanel.subscribeToNewCaseSelections(l); + } + + /** + * Set the node selections for the window + * + * @param selections Nodes to be automatically selected in the explorer view + */ + void setNodeSelections(Node[] selections) { + try { + multiUserCasesPanel.setSelections(selections); + } catch (PropertyVetoException ex) { + //Do-nothing + } + } + + /** + * Constructs a singleton JDialog that allows a user to open a multi-user + * case. + */ + private SelectMultiUserCasesDialog() { + super(WindowManager.getDefault().getMainWindow(), "Select Multi-User Cases", Dialog.ModalityType.APPLICATION_MODAL); + } + + /** + * Registers a keyboard action to hide the dialog when the escape key is + * pressed and adds a OpenMultiUserCasePanel child component. + */ + private void init() { + multiUserCasesPanel = new SelectMultiUserCasesPanel(this); + add(multiUserCasesPanel); + pack(); + setResizable(false); + multiUserCasesPanel.refreshDisplay(); + } +} diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCasesPanel.form b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCasesPanel.form new file mode 100755 index 0000000000..2751a26eae --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCasesPanel.form @@ -0,0 +1,103 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCasesPanel.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCasesPanel.java new file mode 100644 index 0000000000..decae45b49 --- /dev/null +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/multicase/SelectMultiUserCasesPanel.java @@ -0,0 +1,228 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2019 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.keywordsearch.multicase; + +import java.beans.PropertyVetoException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.swing.JDialog; +import org.openide.explorer.ExplorerManager; +import org.openide.nodes.Node; +import org.sleuthkit.autopsy.casemodule.multiusercases.CaseNodeData; +import org.sleuthkit.autopsy.casemodule.multiusercasesbrowser.MultiUserCasesBrowserPanel; +import org.sleuthkit.autopsy.keywordsearch.multicase.MultiCaseKeywordSearchPanel.ChangeListener; + +/** + * Panel for multi-user case selection + */ +class SelectMultiUserCasesPanel extends javax.swing.JPanel { + + private static final long serialVersionUID = 1L; + private final JDialog parentDialog; + private final MultiUserCasesBrowserPanel caseBrowserPanel; + private final List listeners; + + /** + * Constructs a JPanel that allows a user to open a multi-user case. + * + * @param parentDialog The parent dialog of the panel, may be null. If + * provided, the dialog is hidden when this poanel's + * cancel button is pressed. + */ + SelectMultiUserCasesPanel(JDialog parentDialog) { + initComponents(); + this.parentDialog = parentDialog; + initComponents(); // Machine generated code + caseBrowserPanel = new MultiUserCasesBrowserPanel(new ExplorerManager(), new SelectMultiUserCaseDialogCustomizer()); + multiUserCaseScrollPane.add(caseBrowserPanel); + multiUserCaseScrollPane.setViewportView(caseBrowserPanel); + listeners = new ArrayList<>(); + } + + /** + * Refreshes the child component that displays the multi-user cases known to + * the coordination service.. + */ + void refreshDisplay() { + caseBrowserPanel.displayCases(); + } + + /** + * Subscribes to the selections when the user presses the OK button. + * + * @param listener + */ + void subscribeToNewCaseSelections(ChangeListener listener) { + listeners.add(listener); + } + + /** + * Sets the selections in the panel + * + * @param selections + * + * @throws PropertyVetoException + */ + void setSelections(Node[] selections) throws PropertyVetoException { + caseBrowserPanel.getExplorerManager().setSelectedNodes(selections); + caseBrowserPanel.requestFocus(); + } + + /** + * 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() { + + multiUserCaseScrollPane = new javax.swing.JScrollPane(); + selectAllButton = new javax.swing.JButton(); + deselectAllButton = new javax.swing.JButton(); + jLabel1 = new javax.swing.JLabel(); + confirmSelections = new javax.swing.JButton(); + cancelButton = new javax.swing.JButton(); + refreshButton = new javax.swing.JButton(); + + org.openide.awt.Mnemonics.setLocalizedText(selectAllButton, org.openide.util.NbBundle.getMessage(SelectMultiUserCasesPanel.class, "SelectMultiUserCasesPanel.selectAllButton.text")); // NOI18N + selectAllButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + selectAllButtonActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(deselectAllButton, org.openide.util.NbBundle.getMessage(SelectMultiUserCasesPanel.class, "SelectMultiUserCasesPanel.deselectAllButton.text")); // NOI18N + deselectAllButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + deselectAllButtonActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(jLabel1, org.openide.util.NbBundle.getMessage(SelectMultiUserCasesPanel.class, "SelectMultiUserCasesPanel.jLabel1.text")); // NOI18N + + org.openide.awt.Mnemonics.setLocalizedText(confirmSelections, org.openide.util.NbBundle.getMessage(SelectMultiUserCasesPanel.class, "SelectMultiUserCasesPanel.confirmSelections.text")); // NOI18N + confirmSelections.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + confirmSelectionsActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(cancelButton, org.openide.util.NbBundle.getMessage(SelectMultiUserCasesPanel.class, "SelectMultiUserCasesPanel.cancelButton.text")); // NOI18N + cancelButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + cancelButtonActionPerformed(evt); + } + }); + + org.openide.awt.Mnemonics.setLocalizedText(refreshButton, org.openide.util.NbBundle.getMessage(SelectMultiUserCasesPanel.class, "SelectMultiUserCasesPanel.refreshButton.text")); // NOI18N + refreshButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + refreshButtonActionPerformed(evt); + } + }); + + 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(multiUserCaseScrollPane) + .addGroup(layout.createSequentialGroup() + .addComponent(selectAllButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(deselectAllButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(jLabel1, javax.swing.GroupLayout.PREFERRED_SIZE, 365, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 286, Short.MAX_VALUE) + .addComponent(refreshButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addComponent(confirmSelections, javax.swing.GroupLayout.PREFERRED_SIZE, 63, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(cancelButton))) + .addContainerGap()) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addComponent(multiUserCaseScrollPane, javax.swing.GroupLayout.PREFERRED_SIZE, 486, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(jLabel1) + .addComponent(selectAllButton) + .addComponent(deselectAllButton) + .addComponent(confirmSelections) + .addComponent(cancelButton) + .addComponent(refreshButton)) + .addContainerGap(15, Short.MAX_VALUE)) + ); + }// //GEN-END:initComponents + + private void selectAllButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_selectAllButtonActionPerformed + try { + caseBrowserPanel.getExplorerManager().setSelectedNodes(caseBrowserPanel.getExplorerManager().getRootContext().getChildren().getNodes()); + } catch (PropertyVetoException ex) { + //Ignore + } + }//GEN-LAST:event_selectAllButtonActionPerformed + + private void deselectAllButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_deselectAllButtonActionPerformed + try { + caseBrowserPanel.getExplorerManager().setSelectedNodes(new Node[0]); + } catch (PropertyVetoException ex) { + //Ignore + } + }//GEN-LAST:event_deselectAllButtonActionPerformed + + private void confirmSelectionsActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_confirmSelectionsActionPerformed + //Pull out the CaseNodeData objects from the selections + Node[] selections = caseBrowserPanel.getExplorerManager().getSelectedNodes(); + List caseNodeData = Stream.of(selections) + .map(n -> n.getLookup().lookup(CaseNodeData.class)) + .collect(Collectors.toList()); + listeners.forEach((l) -> { + l.nodeSelectionChanged(selections, caseNodeData); + }); + parentDialog.setVisible(false); + }//GEN-LAST:event_confirmSelectionsActionPerformed + + private void cancelButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cancelButtonActionPerformed + parentDialog.setVisible(false); + }//GEN-LAST:event_cancelButtonActionPerformed + + private void refreshButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_refreshButtonActionPerformed + caseBrowserPanel.displayCases(); + }//GEN-LAST:event_refreshButtonActionPerformed + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton cancelButton; + private javax.swing.JButton confirmSelections; + private javax.swing.JButton deselectAllButton; + private javax.swing.JLabel jLabel1; + private javax.swing.JScrollPane multiUserCaseScrollPane; + private javax.swing.JButton refreshButton; + private javax.swing.JButton selectAllButton; + // End of variables declaration//GEN-END:variables +} diff --git a/ManifestTool/ManifestGenerationAlgorithms.au3 b/ManifestTool/ManifestGenerationAlgorithms.au3 index 8cab9ec663..d846909e13 100644 --- a/ManifestTool/ManifestGenerationAlgorithms.au3 +++ b/ManifestTool/ManifestGenerationAlgorithms.au3 @@ -27,9 +27,14 @@ ;Get the list of names of algorithms Global $algorithms[3] ;increase size of array when adding new algorithms -$algorithms[0] = "Single Data Source" -$algorithms[1] = "Folder of Logical Files" -$algorithms[2] = "One Data Source Per Folder" +$algorithms[0] = "Single data source" +$algorithms[1] = "Folder of logical files" +$algorithms[2] = "One data source per folder" + +Global $algorithmDescriptions[3] ;increase size of array when adding new algorithms +$algorithmDescriptions[0] = "Create a single auto ingest manifest file for a single disk image or VM file." +$algorithmDescriptions[1] = "Create a single auto ingest manifest file for a single folder of logical files." +$algorithmDescriptions[2] = "Create a manifest file for the first supported image of each subfolder of a case folder. If no supported images exist in the folder a manifest will be generated for the folders contents as a logical file set. Supported disk image or VM files: .e01, .l01, .001, .ad1" ; $algorithms[2] = "All Files In One Folder" Global $progressArea = Null @@ -42,6 +47,11 @@ Func GetAlgorithmNames() Return $algorithms EndFunc +;Return the description for the specified algorithm index +Func GetAlgorithmDescription($index) + Return $algorithmDescriptions[$index] +EndFunc + ;Return the name of the first algorithm as a default algorithm Func GetDefaultAlgorithmName() Return $algorithms[0] diff --git a/ManifestTool/ManifestTool.au3 b/ManifestTool/ManifestTool.au3 index 015a2d8b58..c985820ed2 100644 --- a/ManifestTool/ManifestTool.au3 +++ b/ManifestTool/ManifestTool.au3 @@ -30,9 +30,9 @@ Opt("GUIOnEventMode", 1) ; Change to OnEvent mode ;Draw GUI and declare variables ; ;============================================== -local $windowHeight = 500 -local $windowWidth = 400 -local $windowTitle = "Autopsy AutoIngest Manifest File Generator" +local $windowHeight = 560 +local $windowWidth = 460 +local $windowTitle = "Autopsy Auto Ingest Manifest File Generator" Global $hMainGUI = GUICreate($windowTitle, $windowWidth, $windowHeight) ;To make GUI resize add following args -1, -1, $WS_OVERLAPPEDWINDOW) ;GUICtrlSetResizing ($hMainGUI, $GUI_DOCKBORDERS) GUISetOnEvent($GUI_EVENT_CLOSE, "CLOSEButton") @@ -48,16 +48,17 @@ local $progressAreaInset = 8 local $distanceFromTop = $topMargin local $distanceFromLeft = $leftMargin Global $defaultDirectory = @MyDocumentsDir & "\" -local $labelWidth = 58 -local $fieldWidth = 235 -local $buttonWidth = 60 +local $labelWidth = 63 +local $fieldWidth = 255 +local $buttonWidth = 95 local $fieldHeight = 20 +local $descriptionHeight = 50 local $progressAreaWidth = $windowWidth - 2*($progressAreaInset+$leftMargin) local $gapBetweenWidth = 10 local $gapBetweenHeight = 10 ;Draw the GUI Code -GUICtrlCreateLabel("Algorithm", $distanceFromLeft, $distanceFromTop+$labelOffset) +GUICtrlCreateLabel("Input", $distanceFromLeft, $distanceFromTop+$labelOffset) $distanceFromLeft = $distanceFromLeft+$labelWidth+$gapBetweenWidth Global $algorithmComboBox = GUICtrlCreateCombo(GetDefaultAlgorithmName(), $distanceFromLeft, $distanceFromTop, $fieldWidth, $fieldHeight, $CBS_DROPDOWNLIST) @@ -67,12 +68,19 @@ for $algorithmName IN $allAlgorithmNames ; Add additional items to the combobox. GUICtrlSetData($algorithmComboBox, $algorithmName) Next - $distanceFromLeft = $leftMargin $distanceFromTop = $distanceFromTop + $fieldHeight + $gapBetweenHeight -Global $rootFolderLabel = GUICtrlCreateLabel("Root Folder", $distanceFromLeft, $distanceFromTop+$labelOffset) +GUICtrlCreateLabel("Description", $distanceFromLeft, $distanceFromTop+$labelOffset) +$distanceFromLeft = $distanceFromLeft+$labelWidth+$gapBetweenWidth +;calculate height of progress area to use remaining space minus space for exit button +Global $descriptionArea = GUICtrlCreateEdit("", $distanceFromLeft, $distanceFromTop, $fieldWidth, $descriptionHeight, BitOr($ES_READONLY,$WS_VSCROLL, $ES_MULTILINE)) + +$distanceFromLeft = $leftMargin +$distanceFromTop = $distanceFromTop + $descriptionHeight + $gapBetweenHeight + +Global $caseDirectoryLabel = GUICtrlCreateLabel("Case Directory", $distanceFromLeft, $distanceFromTop+$labelOffset) $distanceFromLeft = $distanceFromLeft+$labelWidth+$gapBetweenWidth Global $rootFolderField = GUICtrlCreateInput("", $distanceFromLeft, $distanceFromTop, $fieldWidth, $fieldHeight) $distanceFromLeft = $distanceFromLeft +$fieldWidth+$gapBetweenWidth @@ -83,13 +91,14 @@ $distanceFromTop = $distanceFromTop + $fieldHeight + $gapBetweenHeight Global $caseNameLabel = GUICtrlCreateLabel("Case Name", $distanceFromLeft, $distanceFromTop+$labelOffset) $distanceFromLeft = $distanceFromLeft+$labelWidth+$gapBetweenWidth Global $caseNameField = GUICtrlCreateInput("", $distanceFromLeft, $distanceFromTop, $fieldWidth, $fieldHeight) -$distanceFromLeft = $leftMargin +$distanceFromLeft = $distanceFromLeft +$fieldWidth+$gapBetweenWidth $distanceFromTop = $distanceFromTop + $fieldHeight + $gapBetweenHeight -$distanceFromTop = $distanceFromTop + $gapBetweenHeight ;add an extra gap before run button -Global $runButton = GUICtrlCreateButton("Run", $distanceFromLeft, $distanceFromTop+$buttonOffset, $buttonWidth) -GUICtrlSetOnEvent($runButton, "AlgorithmRunAction") +$distanceFromTop = $distanceFromTop + $gapBetweenHeight ;add an extra gap before Generate Manifest button +Global $generateManifestButton = GUICtrlCreateButton("Generate Manifest", $distanceFromLeft, $distanceFromTop+$buttonOffset, $buttonWidth) +GUICtrlSetOnEvent($generateManifestButton, "AlgorithmGenerateManifestAction") $distanceFromTop = $distanceFromTop + $fieldHeight + $gapBetweenHeight +$distanceFromLeft = $leftMargin $distanceFromTop = $distanceFromTop + $fieldHeight + $gapBetweenHeight ;add extra gap before progress area local $ProgressLabel = GUICtrlCreateLabel("Progress", $distanceFromLeft, $distanceFromTop+$labelOffset) @@ -170,46 +179,50 @@ Func Redraw() ;Move controls based on what is hidden or shown using ControlGetPos() and GUICtrlSetPos() If $selectedAlgName == $allAlgorithmNames[2] Then ;"One Data Source Per Folder" ChangeToDefaultGUI() + GUICtrlSetData($descriptionArea, GetAlgorithmDescription(2)) ElseIf $selectedAlgName == $allAlgorithmNames[0] Then ;"Single Data Source" ChangeToSingleDataSourceGUI() + GUICtrlSetData($descriptionArea, GetAlgorithmDescription(0)) ElseIf $selectedAlgName == $allAlgorithmNames[1] Then ;"Folder of Logical Files" ChangeToFolderOfLogicalFilesGUI() + GUICtrlSetData($descriptionArea, GetAlgorithmDescription(1)) EndIf EndFunc ;==>AlgorithmComboBox ;Change the controls displayed in the GUI to the ones needed for the Single Data Source algorithm Func ChangeToSingleDataSourceGUI() ClearFields() - GUICtrlSetData($rootFolderLabel, "Data Source") + GUICtrlSetData($caseDirectoryLabel, "Data Source") GUICtrlSetState($caseNameField, $GUI_SHOW) GUICtrlSetState($caseNameLabel, $GUI_SHOW) GUICtrlSetOnEvent($browseButton, "BrowseForDataSourceFile") - GUICtrlSetState($runButton, $GUI_DISABLE) + GUICtrlSetState($generateManifestButton, $GUI_DISABLE) + EndFunc ;Change the controls displayed in the GUI to the ones needed for the Folder of Logical Files algorithm Func ChangeToFolderOfLogicalFilesGUI() ClearFields() - GUICtrlSetData($rootFolderLabel, "Data Source") - GUICtrlSetData($rootFolderLabel, "Data Source") + GUICtrlSetData($caseDirectoryLabel, "Data Source") + GUICtrlSetData($caseDirectoryLabel, "Data Source") GUICtrlSetState($caseNameField, $GUI_SHOW) GUICtrlSetState($caseNameLabel, $GUI_SHOW) GUICtrlSetOnEvent($browseButton, "Browse") - GUICtrlSetState($runButton, $GUI_DISABLE) + GUICtrlSetState($generateManifestButton, $GUI_DISABLE) EndFunc -;Change the controls displayed in the GUI to the ones needed for One +;Change the controls displayed in the GUI to the ones needed for One Data Source Per Folder Func ChangeToDefaultGUI() ClearFields() - GUICtrlSetData($rootFolderLabel, "Root Folder") + GUICtrlSetData($caseDirectoryLabel, "Case Directory") GUICtrlSetState($rootFolderField, $GUI_SHOW) - GUICtrlSetState($rootFolderLabel, $GUI_SHOW) + GUICtrlSetState($caseDirectoryLabel, $GUI_SHOW) GUICtrlSetState($caseNameField, $GUI_HIDE) GUICtrlSetState($caseNameLabel, $GUI_HIDE) GUICtrlSetOnEvent($browseButton, "Browse") ;rename to RootDirectory to root directory ;hide case name field - GUICtrlSetState($runButton, $GUI_DISABLE) + GUICtrlSetState($generateManifestButton, $GUI_DISABLE) EndFunc ;ensure that all fields for the selected algorithm are valid @@ -231,18 +244,18 @@ EndFunc ;ensure that the settings for the default algorithm are valid before enabling it Func ValidateDefaultFields($rootFolderPath) if ($rootFolderPath <> "" And FileExists($rootFolderPath)) Then - GUICtrlSetState($runButton, $GUI_ENABLE) + GUICtrlSetState($generateManifestButton, $GUI_ENABLE) Else - GUICtrlSetState($runButton, $GUI_DISABLE) + GUICtrlSetState($generateManifestButton, $GUI_DISABLE) EndIf EndFunc ;ensure that the settings for the Single Data Source and Folder of Logical Files algorithms are valid Func ValidateSingleDataSourceFields($dataSourcePath, $caseName) if ($dataSourcePath <> "" And FileExists($dataSourcePath) And $caseName <> "") Then - GUICtrlSetState($runButton, $GUI_ENABLE) + GUICtrlSetState($generateManifestButton, $GUI_ENABLE) Else - GUICtrlSetState($runButton, $GUI_DISABLE) + GUICtrlSetState($generateManifestButton, $GUI_DISABLE) EndIf EndFunc @@ -264,6 +277,12 @@ Func Browse() $defaultDirectory = $caseDrive & $caseDir GUICtrlSetData($rootFolderField, $selectedDirectory) EndIf + If GUICtrlRead($algorithmComboBox) == $allAlgorithmNames[2] Then ;"One Data Source Per Folder" + If ($selectedDirectory == $defaultDirectory) Then ;Don't allow root drives as selected directory for this algorithm + MsgBox(0, "Invalid Case Directory", "The directory is used to determine the case name and can not be the root directory of a disk.") + GUICtrlSetData($rootFolderField, "") + EndIf + EndIf GUICtrlSetState($caseNameField, $GUI_FOCUS) GUICtrlSetState($browseButton, $GUI_ENABLE) EndFunc ;==>BrowseButton @@ -284,13 +303,13 @@ Func BrowseForDataSourceFile() GUICtrlSetState($browseButton, $GUI_ENABLE) EndFunc -;Perform the action associated with the run button which should be defined in ManifestGenerationAlgorithms.au3 -Func AlgorithmRunAction() - ; Note: At this point @GUI_CtrlId would equal $runButton - GUICtrlSetState($runButton, $GUI_DISABLE) +;Perform the action associated with the generate manifest button which should be defined in ManifestGenerationAlgorithms.au3 +Func AlgorithmGenerateManifestAction() + ; Note: At this point @GUI_CtrlId would equal $generateManifestButton + GUICtrlSetState($generateManifestButton, $GUI_DISABLE) RunAlgorithm(GUICtrlRead($algorithmComboBox), GetSettings(), $progressField) - GUICtrlSetState($runButton, $GUI_ENABLE) -EndFunc ;==>RunButton + GUICtrlSetState($generateManifestButton, $GUI_ENABLE) +EndFunc ;==>GenerateManifestButton ;Get an array of settings as they are set on this panel Func GetSettings() @@ -306,11 +325,8 @@ Func CLOSEButton() ; @GUI_WinHandle will be either $hMainGUI or $hDummyGUI GUICtrlSetState($exitButton, $GUI_DISABLE) If @GUI_WinHandle = $hMainGUI Then - Local $msgBoxAnswer = MsgBox(1, "Close Tool Confirmation", "Press OK to confirm closing the tool") - if $msgBoxAnswer == 1 Then - WritePropertiesFile() - Exit - EndIf + WritePropertiesFile() + Exit EndIf GUICtrlSetState($exitButton, $GUI_ENABLE) EndFunc ;==>CLOSEButton \ No newline at end of file diff --git a/ManifestTool/ManifestTool.exe b/ManifestTool/ManifestTool.exe index c71401456b..2ec0dcba64 100644 Binary files a/ManifestTool/ManifestTool.exe and b/ManifestTool/ManifestTool.exe differ diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Bundle.properties-MERGED b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Bundle.properties-MERGED index 1cb8d79ecf..6d0cee851a 100755 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Bundle.properties-MERGED +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Bundle.properties-MERGED @@ -2,6 +2,10 @@ cannotBuildXmlParser=Unable to build XML parser: cannotLoadSEUQA=Unable to load Search Engine URL Query Analyzer settings file, SEUQAMappings.xml: cannotParseXml=Unable to parse XML file: ChromeCacheExtractor.moduleName=ChromeCacheExtractor +ChromeCacheExtractor.progressMsg={0}: Extracting cache entry {1} of {2} entries from {3} +DataSourceUsage_AndroidMedia=Android Media 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. @@ -14,11 +18,10 @@ ExtractEdge_process_errMsg_unableFindESEViewer=Unable to find ESEDatabaseViewer ExtractEdge_process_errMsg_webcacheFail=Failure processing Microsoft Edge WebCacheV01.dat file ExtractIE.getBookmark.errMsg.errGettingBookmarks=Error getting Internet Explorer Bookmarks. ExtractIE.getBookmark.errMsg.errPostingBookmarks=Error posting Internet Explorer Bookmark artifacts. -ExtractIE.getCookie.errMsg.errPostinCookiess=Error posting Internet Explorer Cookie artifacts. +ExtractIE.getCookie.errMsg.errPostingCookies=Error posting Internet Explorer Cookie artifacts. ExtractIE.getCookie.errMsg.errPostingCookiess=Error posting Internet Explorer Cookie artifacts. ExtractIE.getHistory.errMsg.errPostingHistory=Error posting Internet Explorer History artifacts. -ExtractIE.getHistory.errMsg.errPostinHistory=Error posting Internet Explorer History artifacts. -ExtractIE.parentModuleName.noSpace=RecentActivity +#{0} - the module name Extractor.errPostingArtifacts=Error posting {0} artifacts to the blackboard. ExtractOs.androidOs.label=Android ExtractOs.androidVolume.label=OS Drive (Android) @@ -138,6 +141,7 @@ 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_Cache=Chrome Cache Progress_Message_Chrome_Cookies=Chrome Cookies Progress_Message_Chrome_Downloads=Chrome Downloads Progress_Message_Chrome_FormHistory=Chrome Form History @@ -181,13 +185,13 @@ RAImageIngestModule.process.ingestMsg.results={0} - Browser Results RAImageIngestModule.complete.errMsg.failed={0} failed to complete - see log for details
RAImageIngestModule.getName=Recent Activity RAImageIngestModule.getDesc=Extracts recent user activity, such as Web browsing, recently used documents and installed programs. -RecentDocumentsByLnk.getRecDoc.errMsg.errCreatingArtifact={0}: Error creating Recent Document artifact. RecentDocumentsByLnk.getRecDoc.errMsg.errGetLnkFiles={0}: Error getting lnk Files. RecentDocumentsByLnk.getRecDoc.errParsingFile={0}: Error parsing Recent File {1} RecentDocumentsByLnk.parentModuleName.noSpace=RecentActivity RecentDocumentsByLnk.parentModuleName=Recent Activity 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/Chrome.java index ee71565534..8a948fb928 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Chrome.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/Chrome.java @@ -92,6 +92,7 @@ class Chrome extends Extract { "Progress_Message_Chrome_FormHistory=Chrome Form History", "Progress_Message_Chrome_AutoFill=Chrome Auto Fill", "Progress_Message_Chrome_Logins=Chrome Logins", + "Progress_Message_Chrome_Cache=Chrome Cache", }) @@ -125,7 +126,8 @@ class Chrome extends Extract { progressBar.progress(Bundle.Progress_Message_Chrome_Downloads()); this.getDownload(); - ChromeCacheExtractor chromeCacheExtractor = new ChromeCacheExtractor(dataSource, context); + progressBar.progress(Bundle.Progress_Message_Chrome_Cache()); + ChromeCacheExtractor chromeCacheExtractor = new ChromeCacheExtractor(dataSource, context, progressBar); chromeCacheExtractor.getCaches(); } diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ChromeCacheExtractor.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ChromeCacheExtractor.java index 936b0c0c4d..c9ae1b674c 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ChromeCacheExtractor.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/ChromeCacheExtractor.java @@ -44,6 +44,7 @@ import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; import org.sleuthkit.autopsy.casemodule.services.FileManager; import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.autopsy.datamodel.ContentUtils; +import org.sleuthkit.autopsy.ingest.DataSourceIngestModuleProgress; import org.sleuthkit.autopsy.ingest.IngestJobContext; import org.sleuthkit.autopsy.ingest.IngestModule.IngestModuleException; import org.sleuthkit.autopsy.ingest.IngestServices; @@ -92,12 +93,17 @@ final class ChromeCacheExtractor { private final Content dataSource; private final IngestJobContext context; + private final DataSourceIngestModuleProgress progressBar; private final IngestServices services = IngestServices.getInstance(); private Case currentCase; private FileManager fileManager; + // A file table to cache copies of index and data_n files. private final Map filesTable = new HashMap<>(); + // A file table to cache the f_* files. + private final Map externalFilesTable = new HashMap<>(); + /** * Encapsulates abstract file for a cache file as well as a temp file copy * that can be accessed as a random access file. @@ -126,12 +132,14 @@ final class ChromeCacheExtractor { } @NbBundle.Messages({ - "ChromeCacheExtractor.moduleName=ChromeCacheExtractor" + "ChromeCacheExtractor.moduleName=ChromeCacheExtractor", + "ChromeCacheExtractor.progressMsg={0}: Extracting cache entry {1} of {2} entries from {3}" }) - ChromeCacheExtractor(Content dataSource, IngestJobContext context ) { + ChromeCacheExtractor(Content dataSource, IngestJobContext context, DataSourceIngestModuleProgress progressBar ) { moduleName = Bundle.ChromeCacheExtractor_moduleName(); this.dataSource = dataSource; this.context = context; + this.progressBar = progressBar; } @@ -170,6 +178,7 @@ final class ChromeCacheExtractor { void subInit(String cachePath) throws IngestModuleException { filesTable.clear(); + externalFilesTable.clear(); String cacheAbsOutputFolderName = this.getAbsOutputFolderName() + cachePath; File outDir = new File(cacheAbsOutputFolderName); @@ -283,6 +292,9 @@ final class ChromeCacheExtractor { return; } } + + // find all f_* files in a single query. + findExternalFiles(cachePath); } catch (TskCoreException | IngestModuleException ex) { String msg = "Failed to find cache files in path " + cachePath; //NON-NLS @@ -305,8 +317,10 @@ final class ChromeCacheExtractor { // Process each address in the table for (int i = 0; i < indexHdr.getTableLen(); i++) { CacheAddress addr = new CacheAddress(indexFileROBuffer.getInt() & UINT32_MASK, cachePath); - if (addr.isInitialized()) { + progressBar.progress( NbBundle.getMessage(this.getClass(), + "ChromeCacheExtractor.progressMsg", + moduleName, i, indexHdr.getTableLen(), cachePath) ); try { List addedFiles = this.getCacheEntry(addr, sourceArtifacts, webCacheArtifacts); derivedFiles.addAll(addedFiles); @@ -411,13 +425,10 @@ final class ChromeCacheExtractor { moduleName, dataFile.get().getUniquePath())); - long pathID = Util.findID(dataSource, dataFile.get().getUniquePath()); - if (pathID != -1) { - webCacheArtifact.addAttribute(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH_ID, - moduleName, pathID)); - } + webCacheArtifact.addAttribute(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH_ID, + moduleName, dataFile.get().getId())); - webCacheArtifacts.add(webCacheArtifact); + webCacheArtifacts.add(webCacheArtifact); } if (isBrotliCompressed) { @@ -458,12 +469,10 @@ final class ChromeCacheExtractor { webCacheArtifact.addAttribute(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH, moduleName, derivedFile.getUniquePath())); - long pathID = Util.findID(dataSource, derivedFile.getUniquePath()); - if (pathID != -1) { - webCacheArtifact.addAttribute(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH_ID, - moduleName, pathID)); - } - + + webCacheArtifact.addAttribute(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH_ID, + moduleName, derivedFile.getId())); + webCacheArtifacts.add(webCacheArtifact); } @@ -484,13 +493,37 @@ final class ChromeCacheExtractor { } /** - * Finds abstract file for cache file with a specified name + * Finds all the f_* files in the specified path, and fills them in the + * effFilesTable, so that subsequent searches are fast. + * + * @param cachePath path under which to look for. + * + * @throws TskCoreException + */ + private void findExternalFiles(String cachePath) throws TskCoreException { + + List effFiles = fileManager.findFiles(dataSource, "f_%", cachePath); //NON-NLS + for (AbstractFile abstractFile : effFiles ) { + this.externalFilesTable.put(cachePath + abstractFile.getName(), abstractFile); + } + } + /** + * Finds abstract file for cache file with a specified name. + * First checks in the file tables. * * @param cacheFileName - * @return Opt + * @return Optional abstract file * @throws TskCoreException */ Optional findCacheFile(String cacheFileName, String cachePath) throws TskCoreException { + + String fileTableKey = cachePath + cacheFileName; + if (cacheFileName.startsWith("f_") && externalFilesTable.containsKey(fileTableKey)) { + return Optional.of(externalFilesTable.get(fileTableKey)); + } + if (filesTable.containsKey(fileTableKey)) { + return Optional.of(filesTable.get(fileTableKey).getAbstractFile()); + } List cacheFiles = fileManager.findFiles(dataSource, cacheFileName, cachePath); //NON-NLS if (!cacheFiles.isEmpty()) { @@ -913,9 +946,11 @@ final class ChromeCacheExtractor { return; } - cacheFileCopy = getCacheFileCopy(address.getFilename(), address.getCachePath()).get(); + // Don't extract data from external files. if (!address.isInExternalFile() ) { + cacheFileCopy = getCacheFileCopy(address.getFilename(), address.getCachePath()).get(); + this.data = new byte [length]; ByteBuffer buf = cacheFileCopy.getByteBuffer(); int dataOffset = DATAFILE_HDR_SIZE + address.getStartBlock() * address.getBlockSize(); @@ -951,8 +986,8 @@ final class ChromeCacheExtractor { i++; } - // hhtp headers are terminated by 0x00 0x00 - if (data[i+1] == 0) { + // http headers are terminated by 0x00 0x00 + if (i == data.length || data[i+1] == 0) { done = true; } @@ -964,10 +999,11 @@ final class ChromeCacheExtractor { httpResponse = headerLine; } else { int nPos = headerLine.indexOf(':'); - String key = headerLine.substring(0, nPos); - String val= headerLine.substring(nPos+1); - - httpHeaders.put(key.toLowerCase(), val); + if (nPos > 0 ) { + String key = headerLine.substring(0, nPos); + String val= headerLine.substring(nPos+1); + httpHeaders.put(key.toLowerCase(), val); + } } i++; diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/DataSourceUsageAnalyzer.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/DataSourceUsageAnalyzer.java index 8b40633d2b..e1ff144097 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/DataSourceUsageAnalyzer.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/DataSourceUsageAnalyzer.java @@ -32,7 +32,10 @@ import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardAttribute; import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.FileSystem; +import org.sleuthkit.datamodel.Image; import org.sleuthkit.datamodel.TskCoreException; +import org.sleuthkit.datamodel.TskData; /** * Analyzes data sources using heuristics to determine which types of operating @@ -43,6 +46,14 @@ import org.sleuthkit.datamodel.TskCoreException; class DataSourceUsageAnalyzer extends Extract { private static final Logger logger = Logger.getLogger(DataSourceUsageAnalyzer.class.getName()); + private static final int FAT_EXFAT_FLAGS = TskData.TSK_FS_TYPE_ENUM.TSK_FS_TYPE_FAT16.getValue() | + TskData.TSK_FS_TYPE_ENUM.TSK_FS_TYPE_FAT32.getValue() | + TskData.TSK_FS_TYPE_ENUM.TSK_FS_TYPE_EXFAT.getValue(); + private static final long HUNDRED_GB = 100*1024*1024*1024l; + + private static final String ANDROID_MEDIACARD_ROOT_FILENAMES[] = // files expected in root folder of an Android media card + {".android_secure", "android", "audio", + "photos", "dcim", "music", "pictures", "videos"}; //NON-NLS private Content dataSource; @Messages({ @@ -62,13 +73,18 @@ class DataSourceUsageAnalyzer extends Extract { } + private void createDataSourceUsageArtifacts() throws TskCoreException { + + createOSInfoDataSourceUsageArtifacts(); + createAndroidMediaCardArtifacts(); + } /** * Create TSK_DATA_SOURCE_USAGE artifacts based on OS_INFO artifacts * existing as well as other criteria such as specific paths existing. * * @throws TskCoreException */ - private void createDataSourceUsageArtifacts() throws TskCoreException { + private void createOSInfoDataSourceUsageArtifacts() throws TskCoreException { boolean windowsOsDetected = false; List osInfoArtifacts = tskCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_OS_INFO); for (BlackboardArtifact osInfoArt : osInfoArtifacts) { @@ -143,4 +159,54 @@ class DataSourceUsageAnalyzer extends Extract { } } } + + /** + * Checks to see if the data source might be an Android media card or a Flash drive. + * If so, creates TSK_DATA_SOURCE_USAGE artifact. + * + * @return true if any specified files exist false if none exist + * + * @throws TskCoreException + */ + @Messages({ + "DataSourceUsage_AndroidMedia=Android Media Card", + "DataSourceUsage_FlashDrive=Flash Drive" + }) + private void createAndroidMediaCardArtifacts() throws TskCoreException { + + if (dataSource instanceof Image) { + Image image = (Image) dataSource; + try { + if (image.getSize() > HUNDRED_GB) { + return; + } + + List fileSystems = image.getFileSystems(); + if (fileSystems.isEmpty() || fileSystems.size() > 1) { + return; + } + + FileSystem fileSystem = fileSystems.get(0); + if ( fileSystem == null || (fileSystem.getFsType().getValue() & FAT_EXFAT_FLAGS) == 0) { + return ; + } + + FileManager fileManager = currentCase.getServices().getFileManager(); + for (String fileName : ANDROID_MEDIACARD_ROOT_FILENAMES ) { + for (AbstractFile file : fileManager.findFiles(dataSource, fileName, "/")) { // NON-NLS + if (file.getParentPath().equals("/") && file.getName().equalsIgnoreCase(fileName)) { // NON-NLS + createDataSourceUsageArtifact(Bundle.DataSourceUsage_AndroidMedia()); + return; + } + } + } + + // If none of the Android paths is found but it meets other criteria, it might be just a flash drive + createDataSourceUsageArtifact(Bundle.DataSourceUsage_FlashDrive()); + + } catch (TskCoreException ex) { + logger.log(Level.SEVERE, "Exception while checking image: {0} for Andriod media card", image.getName() + ex.getMessage()); //NON-NLS + } + } + } } diff --git a/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties b/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties index 46410a1b4e..458b936acd 100644 --- a/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties +++ b/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties @@ -1,5 +1,5 @@ #Updated by build script -#Fri, 08 Mar 2019 12:24:19 +0100 +#Fri, 22 Mar 2019 09:08:00 +0100 LBL_splash_window_title=Starting Autopsy SPLASH_HEIGHT=314 SPLASH_WIDTH=538 diff --git a/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties b/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties index bbe8524619..944b2c8584 100644 --- a/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties +++ b/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties @@ -1,4 +1,4 @@ #Updated by build script -#Wed, 06 Mar 2019 16:04:26 +0100 +#Fri, 22 Mar 2019 09:08:00 +0100 CTL_MainWindow_Title=Autopsy 4.10.0 CTL_MainWindow_Title_No_Project=Autopsy 4.10.0 diff --git a/ruleset.xml b/ruleset.xml index 6913aa32ed..5e8d384232 100644 --- a/ruleset.xml +++ b/ruleset.xml @@ -94,7 +94,7 @@ --> - + - + @@ -258,8 +258,8 @@ - - + + @@ -289,7 +289,7 @@ - + diff --git a/setupSleuthkitBranch.py b/setupSleuthkitBranch.py index 881ce44570..0cdfab82f1 100644 --- a/setupSleuthkitBranch.py +++ b/setupSleuthkitBranch.py @@ -17,19 +17,6 @@ DEVELOP_BRANCH='develop' passed = 1 -def getSleuthkitBranchList(branchOwner): - # Returns the list of sleuthkit branches - cmd = ['git','branch','-a'] - retdir = os.getcwd() - os.chdir(TSK_HOME) - output = subprocess.check_output(cmd) - branches = [] - for branch in output.strip().split(): - if branch.startswith('remotes/'+branchOwner): - branches.append(branch.split('/')[2]) - os.chdir(retdir) - return branches - def gitSleuthkitCheckout(branch, branchOwner): ''' Checksout sleuthkit branch @@ -39,24 +26,23 @@ def gitSleuthkitCheckout(branch, branchOwner): # passed is a global variable that gets set to non-zero integer # When an error occurs global passed - if branch in getSleuthkitBranchList(branchOwner): - #add the remotes - #if the branch owner was origin substitute in the name of that owner - if (branchOwner==ORIGIN_OWNER): - gitHubUser="sleuthkit" - else: - gitHubUser=branchOwner - checkout=['git','checkout','-b',branchOwner+'-'+branch] - print("Command run:" + " ".join(checkout)) - passed = subprocess.call(checkout, stdout=sys.stdout,cwd=TSK_HOME) - cmd = ['git','pull', "/".join(["https://github.com", gitHubUser, "sleuthkit.git"]), branch] - if passed != 0: #0 would be success - #unable to create new branch return instead of pulling - return - print("Command run:" + " ".join(cmd)) - passed = subprocess.call(cmd,stdout=sys.stdout,cwd=TSK_HOME) - if (passed == 0): - sys.exit() #exit if successful + #add the remotes + #if the branch owner was origin substitute in the name of that owner + if (branchOwner==ORIGIN_OWNER): + gitHubUser="sleuthkit" + else: + gitHubUser=branchOwner + checkout=['git','checkout','-b',branchOwner+'-'+branch] + print("Command run:" + " ".join(checkout)) + passed = subprocess.call(checkout, stdout=sys.stdout,cwd=TSK_HOME) + cmd = ['git','pull', "/".join(["https://github.com", gitHubUser, "sleuthkit.git"]), branch] + if passed != 0: #0 would be success + #unable to create new branch return instead of pulling + return + print("Command run:" + " ".join(cmd)) + passed = subprocess.call(cmd,stdout=sys.stdout,cwd=TSK_HOME) + if (passed == 0): + sys.exit() #exit if successful else: print("Branch: " + branch + " does not exist for owner: " + branchOwner) diff --git a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/Bundle.properties-MERGED b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/Bundle.properties-MERGED index 2e238aaf3d..b39a22c484 100755 --- a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/Bundle.properties-MERGED +++ b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/Bundle.properties-MERGED @@ -1,6 +1,6 @@ MboxParser.handleAttch.noOpenCase.errMsg=Exception while getting open case. OpenIDE-Module-Display-Category=Ingest Module -OpenIDE-Module-Long-Description=Email Parser ingest module.\n\nThe module extracts MBOX and PST e-mail files and posts the results to the blackboard. \nIt knows about the Thunderbird folder structure for MBOX files. +OpenIDE-Module-Long-Description=Email Parser ingest module.\n\nThe module extracts MBOX and PST e-mail files and posts the results to the blackboard.\nIt knows about the Thunderbird folder structure for MBOX files. OpenIDE-Module-Name=Email Parser OpenIDE-Module-Short-Description=Parses MBOX and PST files MboxParser.parse.errMsg.failedToReadFile=Failed to read mbox file from disk. @@ -16,7 +16,6 @@ ThunderbirdMboxFileIngestModule.addArtifact.indexError.message=Failed to index e # {1} - file ID ThunderbirdMboxFileIngestModule.errorMessage.outOfDiskSpace=Out of disk space. Cannot copy '{0}' (id={1}) to parse. ThunderbirdMboxFileIngestModule.moduleName=Email Parser -ThunderbirdMboxFileIngestModule.noCommsManager.errMsg=Exception while getting communications manager. ThunderbirdMboxFileIngestModule.noOpenCase.errMsg=Exception while getting open case. ThunderbirdMboxFileIngestModule.processPst.errMsg.outOfDiskSpace=Out of disk space. Cannot copy {0} to parse. ThunderbirdMboxFileIngestModule.encryptionFileLevel=File-level Encryption diff --git a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/VcardParser.java b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/VcardParser.java index 09d148466b..36cd9a672f 100755 --- a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/VcardParser.java +++ b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/VcardParser.java @@ -164,8 +164,35 @@ final class VcardParser { List accountInstances = new ArrayList<>(); extractPhotos(vcard, abstractFile); - - ThunderbirdMboxFileIngestModule.addArtifactAttribute(vcard.getFormattedName().getValue(), BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME_PERSON, attributes); + + String name = ""; + if (vcard.getFormattedName() != null) { + name = vcard.getFormattedName().getValue(); + } else { + if (vcard.getStructuredName() != null) { + // Attempt to put the name together if there was no formatted version + for (String prefix:vcard.getStructuredName().getPrefixes()) { + name += prefix + " "; + } + if (vcard.getStructuredName().getGiven() != null) { + name += vcard.getStructuredName().getGiven() + " "; + } + if (vcard.getStructuredName().getFamily() != null) { + name += vcard.getStructuredName().getFamily() + " "; + } + for (String suffix:vcard.getStructuredName().getSuffixes()) { + name += suffix + " "; + } + if (! vcard.getStructuredName().getAdditionalNames().isEmpty()) { + name += "("; + for (String addName:vcard.getStructuredName().getAdditionalNames()) { + name += addName + " "; + } + name += ")"; + } + } + } + ThunderbirdMboxFileIngestModule.addArtifactAttribute(name, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME_PERSON, attributes); for (Telephone telephone : vcard.getTelephoneNumbers()) { addPhoneAttributes(telephone, abstractFile, attributes);