diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index 83345a859bf3f41171903a8dffa4563b4841db41..faa1b92a945d5aa98e91272211df022525c4ff68 --- a/.gitignore +++ b/.gitignore @@ -99,4 +99,5 @@ hs_err_pid*.log /thirdparty/yara/YaraJNIWrapper/dist/ /thirdparty/yara/YaraJNIWrapper/build/ /thirdparty/yara/YaraJNIWrapper/nbproject/private/ -thirdparty/yara/yarabridge/.vs/ +/thirdparty/yara/yarabridge/.vs/ + diff --git a/Core/build.xml b/Core/build.xml index 1ec2a3b481cd7a5dc6f84fba4861e77b9491ccc0..d2e9b00f169d395d418f76fd3b574fdbf0d2a68c 100644 --- a/Core/build.xml +++ b/Core/build.xml @@ -166,7 +166,7 @@ <get src="https://drive.google.com/uc?id=1ns2olaWsBu_c4EoE4Seh8t_B3U5RnLKd" dest="${test-input}/CommonFilesAttrs_img1_v1.vhd" skipexisting="true"/> </target> - + <target name="get-deps" depends="init-ivy,getTSKJars,get-thirdparty-dependencies,get-InternalPythonModules, download-binlist"> <mkdir dir="${ext.dir}"/> <copy file="${thirdparty.dir}/LICENSE-2.0.txt" todir="${ext.dir}" /> @@ -199,5 +199,110 @@ <globmapper from="*" to="*-MERGED"/> </copy> </target> + + <!--sets up integration test system properties, calls underlying test-init and then sets up the pathing jar--> + <target name="test-init" depends="projectized-common.test-init,getTestDataFiles,qa-functional-pathing-jar,unit-test-path-simplification" /> + + <!-- + The paths specified in 'module.run.classpath' are incorporated into the manifest of a jar and then the path to the + jar is used as part of the classpath for '-do-junit' instead of 'module.run.classpath'. This was done to prevent + classpath length issues on windows. More information on this technique can be found here: + https://stackoverflow.com/a/201969. + --> + <target name="qa-functional-pathing-jar" depends="projectized-common.test-init"> + <sequential> + <!--set up pathing jar based on module.run.classpath as classpath--> + <path id="test.qa-functional.pathing-jar.module-cp.classpath" path="${module.run.classpath}"/> + <pathconvert pathsep=" " refid="test.qa-functional.pathing-jar.module-cp.classpath" property="test.qa-functional.pathing-jar.module-cp.classpathstr"/> + <property name="test.qa-functional.pathing-jar.module-cp.loc" value="${cluster}/test.qa-functional.pathing.module-cp.jar"/> + <jar destfile="${test.qa-functional.pathing-jar.module-cp.loc}"> + <manifest> + <attribute name="Class-Path" value="${test.qa-functional.pathing-jar.module-cp.classpathstr}"/> + </manifest> + </jar> + + <!--grab properties from common.xml:test-init so that "test.qa-functional.run.cp" can be properly formed--> + <property name="build.test.qa-functional.dir" location="${build.dir}/test/qa-functional"/> + <property name="build.test.qa-functional.classes.dir" location="${build.test.qa-functional.dir}/classes"/> + <property name="test.qa-functional.cp.extra" value=""/> + + <!--set up "test.qa-functional.run.cp" to be used by common.xml:-do-junit--> + <path id="test.qa-functional.run.cp"> + <pathelement path="${build.test.qa-functional.classes.dir}"/> + <!-- Cannot use <path refid="cp"/> since that uses ${module.classpath} and we want ${module.run.classpath}: --> + <pathelement path="${test.qa-functional.runtime.cp}"/> + <pathelement path="${cp.extra}"/> + <pathelement location="${cluster}/${module.jar}"/> + <path refid="test.unit.lib.cp"/> + <!-- for compatibility with property based classpath--> + <pathelement path="${test.qa-functional.pathing-jar.module-cp.loc}"/> + <pathelement path="${test.qa-functional.run.cp.extra}"/> + <pathelement path="${test.qa-functional.cp.extra}"/> + <pathelement path="${test.extra.nb.javac.deps}"/> + </path> + </sequential> + </target> + + <!-- + This specifies the classpath for unit tests using * notation + (i.e. https://stackoverflow.com/questions/219585/including-all-the-jars-in-a-directory-within-the-java-classpath). + This solution involves taking the initial ‘module.run.classpath’ property and simplifying it to the directories containing jars + (i.e. instead of “/dir/lib1.jar:/dir/lib2.jar:/dir/lib3.jar” it becomes “/dir/*” ). + More information on ‘module.run.classpath’ can be found in “netbeans-plat\11.3\harness\README” and it appears that + “netbeans-plat\11.3\harness\build.xml:build-init target is in charge of setting the ‘module.run.classpath’ variable. + More information in Jira: 6970. + --> + <target name="unit-test-path-simplification" depends="projectized-common.test-init"> + <sequential> + <script language="javascript"> + <![CDATA[ + var moduleRunClasspath = project.getProperty("module.run.classpath"); + + var directories = []; + // searches for jar file parent directories with path separators of \ or / + var individualPathRegex = /^\s*(.+?[\\\/])[^\\\/]+?\.jar\s*$/i; + // split on ':' but not 'C:\' + var classPathRegex = /((C:\\)?.+?)(:|$)/gi; + var match; + while((match = classPathRegex.exec(moduleRunClasspath)) !== null) { + var thisPath = match[1]; + var pathMatch = thisPath.match(individualPathRegex); + // find unique matches + if (pathMatch && directories.indexOf(pathMatch[1]) < 0) { + directories.push(pathMatch[1]); + } + } + + // suffix with * + for (var i = 0; i < directories.length; i++) { + directories[i] = directories[i] + "*"; + } + + // set project property + project.setNewProperty("test.unit.abbreviatedModuleRunClassPath", directories.join(":")); + ]]> + </script> + + <!--grab properties from common.xml:test-init so that "test.unit.run.cp" can be properly formed--> + <property name="build.test.unit.dir" location="${build.dir}/test/unit"/> + <property name="build.test.unit.classes.dir" location="${build.test.unit.dir}/classes"/> + <property name="test.unit.cp.extra" value=""/> + + <!--set up "test.unit.run.cp" to be used by common.xml:-do-junit--> + <path id="test.unit.run.cp"> + <pathelement path="${build.test.unit.classes.dir}"/> + <!-- Cannot use <path refid="cp"/> since that uses ${module.classpath} and we want ${module.run.classpath}: --> + <pathelement path="${test.unit.runtime.cp}"/> + <pathelement path="${cp.extra}"/> + <pathelement location="${cluster}/${module.jar}"/> + <path refid="test.unit.lib.cp"/> + <!-- for compatibility with property based classpath--> + <pathelement path="${test.unit.abbreviatedModuleRunClassPath}"/> + <pathelement path="${test.unit.run.cp.extra}"/> + <pathelement path="${test.unit.cp.extra}"/> + <pathelement path="${test.extra.nb.javac.deps}"/> + </path> + </sequential> + </target> </project> diff --git a/Core/nbproject/project.xml b/Core/nbproject/project.xml index 1cddbaa638a64e9bb94c8511031dd1388524b363..96d3ea1db693d93c77a1e81530dd3796ca1ae886 100644 --- a/Core/nbproject/project.xml +++ b/Core/nbproject/project.xml @@ -232,7 +232,8 @@ <build-prerequisite/> <compile-dependency/> <run-dependency> - <specification-version>1.0</specification-version> + <release-version>1</release-version> + <specification-version>23</specification-version> </run-dependency> </dependency> <dependency> diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED index ec1b7be47ec91d00305af9859bf0cc68de6503a0..6df3399a0aaa9233bbaf44add103a5c7e5973599 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/Bundle.properties-MERGED @@ -96,7 +96,7 @@ Metadata.tableRowTitle.mimeType=MIME Type Metadata.tableRowTitle.name=Name Metadata.tableRowTitle.sectorSize=Sector Size Metadata.tableRowTitle.sha1=SHA1 -Metadata.tableRowTitle.sha256=SHA256 +Metadata.tableRowTitle.sha256=SHA-256 Metadata.tableRowTitle.size=Size Metadata.tableRowTitle.fileNameAlloc=File Name Allocation Metadata.tableRowTitle.metadataAlloc=Metadata Allocation diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/Metadata.java b/Core/src/org/sleuthkit/autopsy/contentviewers/Metadata.java index 796386a5b3e0acf466f4269ac0f93ce8f075c73b..45dae8e2e07eed6952e809c561b603524efe783d 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/Metadata.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/Metadata.java @@ -137,7 +137,7 @@ private void addRow(StringBuilder sb, String key, String value) { "Metadata.tableRowTitle.mimeType=MIME Type", "Metadata.nodeText.truncated=(results truncated)", "Metadata.tableRowTitle.sha1=SHA1", - "Metadata.tableRowTitle.sha256=SHA256", + "Metadata.tableRowTitle.sha256=SHA-256", "Metadata.tableRowTitle.imageType=Type", "Metadata.tableRowTitle.sectorSize=Sector Size", "Metadata.tableRowTitle.timezone=Time Zone", @@ -182,6 +182,11 @@ public void setNode(Node node) { md5 = NbBundle.getMessage(this.getClass(), "Metadata.tableRowContent.md5notCalc"); } addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.md5"), md5); + String sha256 = file.getSha256Hash(); + if (sha256 == null) { + sha256 = NbBundle.getMessage(this.getClass(), "Metadata.tableRowContent.md5notCalc"); + } + addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.sha256"), sha256); addRow(sb, NbBundle.getMessage(this.getClass(), "Metadata.tableRowTitle.hashLookupResults"), file.getKnown().toString()); addAcquisitionDetails(sb, dataSource); diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/PDFViewer.java b/Core/src/org/sleuthkit/autopsy/contentviewers/PDFViewer.java index b21a48a1937af96fc171f5b7cbbda0d9106c6f07..59e276ce435472c7e1c67bd4389c9ce65609b2c1 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/PDFViewer.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/PDFViewer.java @@ -20,6 +20,7 @@ import java.awt.BorderLayout; import java.awt.Component; +import java.awt.Container; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.util.Arrays; @@ -97,6 +98,9 @@ public void setFile(AbstractFile file) { // Add the IcePDF view to the center of our container. this.container.add(icePdfPanel, BorderLayout.CENTER); + + // Disable all components until the document is ready to view. + enableComponents(container, false); // Document is the 'M' in IcePDFs MVC set up. Read the data needed to // populate the model in the background. @@ -122,12 +126,13 @@ protected void done() { // will cause UI widgets to be updated. try { Document doc = get(); - controller.openDocument(doc, null); + controller.openDocument(doc, file.getName()); // This makes the PDF viewer appear as one continuous // document, which is the default for most popular PDF viewers. controller.setPageViewMode(DocumentViewControllerImpl.ONE_COLUMN_VIEW, true); // This makes it possible to select text by left clicking and dragging. controller.setDisplayTool(DocumentViewModelImpl.DISPLAY_TOOL_TEXT_SELECTION); + enableComponents(container, true); } catch (InterruptedException ex) { // Do nothing. } catch (ExecutionException ex) { @@ -140,10 +145,28 @@ protected void done() { file.getId(), file.getName()), ex); showErrorDialog(); } + } catch (Throwable ex) { + logger.log(Level.WARNING, String.format("PDF content viewer " + + "was unable to open document with id %d and name %s", + file.getId(), file.getName()), ex); } } }.execute(); } + + /** + * Recursively enable/disable all components in this content viewer. + * This will disable/enable all internal IcePDF Swing components too. + */ + private void enableComponents(Container container, boolean enabled) { + Component[] components = container.getComponents(); + for(Component component : components) { + component.setEnabled(enabled); + if (component instanceof Container) { + enableComponents((Container)component, enabled); + } + } + } @Override public Component getComponent() { diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/CommunicationArtifactViewerHelper.java b/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/CommunicationArtifactViewerHelper.java index a911ac20d74ab5fe0c6993d3b58ffc647af501f4..13e11d2a2dee19a98974158aabc3d41c38c51dad 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/CommunicationArtifactViewerHelper.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/CommunicationArtifactViewerHelper.java @@ -44,7 +44,7 @@ * A class to help display a communication artifact in a panel using a * gridbaglayout. */ -final class CommunicationArtifactViewerHelper { +public final class CommunicationArtifactViewerHelper { // Number of columns in the gridbag layout. private final static int MAX_COLS = 4; @@ -63,12 +63,12 @@ private CommunicationArtifactViewerHelper() { * * @param panel Panel to update. * @param gridbagLayout Layout to use. - * @param constraints Constrains to use. + * @param constraints Constraints to use. * @param headerString Heading string to display. * * @return JLabel Heading label added. */ - static JLabel addHeader(JPanel panel, GridBagLayout gridbagLayout, GridBagConstraints constraints, String headerString) { + public static JLabel addHeader(JPanel panel, GridBagLayout gridbagLayout, GridBagConstraints constraints, String headerString) { Insets savedInsets = constraints.insets; @@ -109,6 +109,23 @@ static JLabel addHeader(JPanel panel, GridBagLayout gridbagLayout, GridBagConstr return headingLabel; } + /** + * Add a key value row to the specified panel with the specified layout and + * constraints. + * + * + * @param panel Panel to update. + * @param gridbagLayout Layout to use. + * @param constraints Constraints to use. + * @param keyString Key name to display. + * @param valueString Value string to display. + * + */ + public static void addNameValueRow(JPanel panel, GridBagLayout gridbagLayout, GridBagConstraints constraints, String keyString, String valueString) { + addKey(panel, gridbagLayout, constraints, keyString); + addValue(panel, gridbagLayout, constraints, valueString); + } + /** * Adds the given component to the panel. * @@ -116,7 +133,7 @@ static JLabel addHeader(JPanel panel, GridBagLayout gridbagLayout, GridBagConstr * * @param panel Panel to update. * @param gridbagLayout Layout to use. - * @param constraints Constrains to use. + * @param constraints Constraints to use. * @param component Component to add. */ static void addComponent(JPanel panel, GridBagLayout gridbagLayout, GridBagConstraints constraints, JComponent component) { @@ -132,7 +149,7 @@ static void addComponent(JPanel panel, GridBagLayout gridbagLayout, GridBagConst * * @param panel Panel to update. * @param gridbagLayout Layout to use. - * @param constraints Constrains to use. + * @param constraints Constraints to use. */ static void addLineEndGlue(JPanel panel, GridBagLayout gridbagLayout, GridBagConstraints constraints) { // Place the filler just past the last column. @@ -159,9 +176,9 @@ static void addLineEndGlue(JPanel panel, GridBagLayout gridbagLayout, GridBagCon * * @param panel Panel to update. * @param gridbagLayout Layout to use. - * @param constraints Constrains to use. + * @param constraints Constraints to use. */ - static void addPageEndGlue(JPanel panel, GridBagLayout gridbagLayout, GridBagConstraints constraints) { + public static void addPageEndGlue(JPanel panel, GridBagLayout gridbagLayout, GridBagConstraints constraints) { constraints.gridx = 0; @@ -185,7 +202,7 @@ static void addPageEndGlue(JPanel panel, GridBagLayout gridbagLayout, GridBagCon * * @param panel Panel to update. * @param gridbagLayout Layout to use. - * @param constraints Constrains to use. + * @param constraints Constraints to use. */ static void addBlankLine(JPanel panel, GridBagLayout gridbagLayout, GridBagConstraints constraints) { constraints.gridy++; @@ -203,7 +220,7 @@ static void addBlankLine(JPanel panel, GridBagLayout gridbagLayout, GridBagConst * * @param panel Panel to update. * @param gridbagLayout Layout to use. - * @param constraints Constrains to use. + * @param constraints Constraints to use. * @param keyString Key name to display. * * @return Label added. @@ -217,7 +234,7 @@ static JLabel addKey(JPanel panel, GridBagLayout gridbagLayout, GridBagConstrain * * @param panel Panel to update. * @param gridbagLayout Layout to use. - * @param constraints Constrains to use. + * @param constraints Constraints to use. * @param keyString Key name to display. * @param gridx column index, must be less than MAX_COLS - 1. * @@ -246,8 +263,8 @@ static JLabel addKeyAtCol(JPanel panel, GridBagLayout gridbagLayout, GridBagCons * * @param panel Panel to update. * @param gridbagLayout Layout to use. - * @param constraints Constrains to use. - * @param keyString Value string to display. + * @param constraints Constraints to use. + * @param valueString Value string to display. * * @return Label added. */ @@ -260,7 +277,7 @@ static JTextPane addValue(JPanel panel, GridBagLayout gridbagLayout, GridBagCons * * @param panel Panel to update. * @param gridbagLayout Layout to use. - * @param constraints Constrains to use. + * @param constraints Constraints to use. * @param keyString Value string to display. * @param gridx Column index, must be less than MAX_COLS; * @@ -367,7 +384,7 @@ static JLabel addMessageRow(JPanel panel, GridBagLayout gridbagLayout, GridBagCo * * @param panel Panel to update. * @param gridbagLayout Layout to use. - * @param constraints Constrains to use. + * @param constraints Constraints to use. * @param accountIdentifier Account identifier to search the persona. * * @return List of AccountPersonaSearcherData objects. @@ -435,7 +452,7 @@ static List<AccountPersonaSearcherData> addPersonaRow(JPanel panel, GridBagLayou * * @param panel Panel to update. * @param gridbagLayout Layout to use. - * @param constraints Constrains to use. + * @param constraints Constraints to use. * @param contactId Contact name to display. * * @return A JLabel with the contact information. diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/DefaultArtifactContentViewer.java b/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/DefaultArtifactContentViewer.java index 8c0c89a865a00b79112e55b7cf625d6e885b29f6..0c9e41afd633340c90168c43bf2172030773a114 100644 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/DefaultArtifactContentViewer.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/artifactviewers/DefaultArtifactContentViewer.java @@ -54,15 +54,14 @@ import java.util.Locale; import java.util.Map; import javax.swing.SwingUtilities; +import org.sleuthkit.autopsy.discovery.ui.AbstractArtifactDetailsPanel; //import org.sleuthkit.autopsy.contentviewers.Bundle; /** - * This class displays a Blackboard artifact as a table listing all it's - * attributes names and values. + * This class displays a Blackboard artifact as a table of its attributes. */ - @SuppressWarnings("PMD.SingularField") // UI widgets cause lots of false positives -public class DefaultArtifactContentViewer extends javax.swing.JPanel implements ArtifactContentViewer { +public class DefaultArtifactContentViewer extends AbstractArtifactDetailsPanel implements ArtifactContentViewer { @NbBundle.Messages({ "DefaultArtifactContentViewer.attrsTableHeader.type=Type", @@ -71,11 +70,11 @@ public class DefaultArtifactContentViewer extends javax.swing.JPanel implements "DataContentViewerArtifact.failedToGetSourcePath.message=Failed to get source file path from case database", "DataContentViewerArtifact.failedToGetAttributes.message=Failed to get some or all attributes from case database" }) - + private final static Logger logger = Logger.getLogger(DefaultArtifactContentViewer.class.getName()); - + private static final long serialVersionUID = 1L; - + private static final String[] COLUMN_HEADERS = { Bundle.DefaultArtifactContentViewer_attrsTableHeader_type(), Bundle.DefaultArtifactContentViewer_attrsTableHeader_value(), @@ -124,7 +123,7 @@ public void columnMoved(TableColumnModelEvent e) { // do nothing } - @Override + @Override public void columnMarginChanged(ChangeEvent e) { updateRowHeights(); //When the user changes column width we may need to resize row height } @@ -153,12 +152,12 @@ private void updateRowHeights() { Component comp = resultsTable.prepareRenderer( resultsTable.getCellRenderer(row, valueColIndex), row, valueColIndex); final int rowHeight; - if (comp instanceof JTextArea) { + if (comp instanceof JTextArea) { final JTextArea tc = (JTextArea) comp; final View rootView = tc.getUI().getRootView(tc); java.awt.Insets i = tc.getInsets(); rootView.setSize(resultsTable.getColumnModel().getColumn(valueColIndex) - .getWidth() - (i.left + i.right +CELL_RIGHT_MARGIN), //current width minus borders + .getWidth() - (i.left + i.right + CELL_RIGHT_MARGIN), //current width minus borders Integer.MAX_VALUE); rowHeight = (int) rootView.getPreferredSpan(View.Y_AXIS); } else { @@ -267,7 +266,7 @@ public void actionPerformed(ActionEvent e) { * Resets the components to an empty view state. */ private void resetComponents() { - + ((DefaultTableModel) resultsTable.getModel()).setRowCount(0); } @@ -279,7 +278,7 @@ public Component getComponent() { @Override public void setArtifact(BlackboardArtifact artifact) { try { - ResultsTableArtifact resultsTableArtifact = new ResultsTableArtifact(artifact, artifact.getParent()); + ResultsTableArtifact resultsTableArtifact = artifact == null ? null : new ResultsTableArtifact(artifact, artifact.getParent()); SwingUtilities.invokeLater(new Runnable() { @Override @@ -289,7 +288,7 @@ public void run() { }); } catch (TskCoreException ex) { - logger.log(Level.SEVERE, String.format("Error getting parent content for artifact (artifact_id=%d, obj_id=%d)", artifact.getArtifactID(), artifact.getObjectID()), ex); + logger.log(Level.SEVERE, String.format("Error getting parent content for artifact (artifact_id=%d, obj_id=%d)", artifact.getArtifactID(), artifact.getObjectID()), ex); } } @@ -301,7 +300,7 @@ public boolean isSupported(BlackboardArtifact artifact) { } /** - * This class is a container to hold the data necessary for the artifact + * This class is a container to hold the data necessary for the artifact * being viewed. */ private class ResultsTableArtifact { @@ -340,20 +339,20 @@ private void addRows(BlackboardArtifact artifact) { */ String value; switch (attr.getAttributeType().getValueType()) { - + // Use Autopsy date formatting settings, not TSK defaults case DATETIME: value = epochTimeToString(attr.getValueLong()); break; - case JSON: + case JSON: // Get the attribute's JSON value and convert to indented multiline display string String jsonVal = attr.getValueString(); JsonParser parser = new JsonParser(); JsonObject json = parser.parse(jsonVal).getAsJsonObject(); - + value = toJsonDisplayString(json, ""); break; - + case STRING: case INTEGER: case LONG: @@ -398,43 +397,43 @@ private void addRows(BlackboardArtifact artifact) { String getArtifactDisplayName() { return artifactDisplayName; } - + private static final String INDENT_RIGHT = " "; private static final String NEW_LINE = "\n"; - + /** * Recursively converts a JSON element into an indented multi-line * display string. * - * @param element JSON element to convert + * @param element JSON element to convert * @param startIndent Starting indentation for the element. * * @return A multi-line display string. */ private String toJsonDisplayString(JsonElement element, String startIndent) { - + StringBuilder sb = new StringBuilder(""); JsonObject obj = element.getAsJsonObject(); for (Map.Entry<String, JsonElement> entry : obj.entrySet()) { - appendJsonElementToString(entry.getKey(), entry.getValue(), startIndent, sb ); + appendJsonElementToString(entry.getKey(), entry.getValue(), startIndent, sb); } String returnString = sb.toString(); - if (startIndent.length() == 0 && returnString.startsWith(NEW_LINE)) { + if (startIndent.length() == 0 && returnString.startsWith(NEW_LINE)) { returnString = returnString.substring(NEW_LINE.length()); } return returnString; } - - + /** - * Converts the given JSON element into string and appends to the given string builder. - * + * Converts the given JSON element into string and appends to the given + * string builder. + * * @param jsonKey * @param jsonElement * @param startIndent Starting indentation for the element. - * @param sb String builder to append to. + * @param sb String builder to append to. */ private void appendJsonElementToString(String jsonKey, JsonElement jsonElement, String startIndent, StringBuilder sb) { if (jsonElement.isJsonArray()) { @@ -463,11 +462,12 @@ private void appendJsonElementToString(String jsonKey, JsonElement jsonElement, sb.append(NEW_LINE).append(String.format("%s%s = null", startIndent, jsonKey)); } } - + /** * Converts epoch time to readable string. - * + * * @param epochTime epoch time value to be converted to string. + * * @return String with human readable time. */ private String epochTimeToString(long epochTime) { @@ -482,21 +482,20 @@ private String epochTimeToString(long epochTime) { } /** - * Updates the table view with the given artifact data. - * + * Updates the table view with the given artifact data. + * * It should be called on EDT. * * @param resultsTableArtifact Artifact data to display in the view. */ private void updateView(ResultsTableArtifact resultsTableArtifact) { this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); - DefaultTableModel tModel = ((DefaultTableModel) resultsTable.getModel()); - tModel.setDataVector(resultsTableArtifact.getRows(), COLUMN_HEADERS); + String[][] rows = resultsTableArtifact == null ? new String[0][0] : resultsTableArtifact.getRows(); + tModel.setDataVector(rows, COLUMN_HEADERS); updateColumnSizes(); updateRowHeights(); resultsTable.clearSelection(); - this.setCursor(null); } diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerArtifact.java b/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerArtifact.java index 6ccc70c92033fb09155daed9f161265dd7639c28..b015dbf3adfbaac246e7a27d590a932be5adee15 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerArtifact.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerArtifact.java @@ -357,7 +357,9 @@ public int isPreferred(Node node) { || (artifact.getArtifactTypeID() == ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT.getTypeID()) || (artifact.getArtifactTypeID() == ARTIFACT_TYPE.TSK_OBJECT_DETECTED.getTypeID()) || (artifact.getArtifactTypeID() == ARTIFACT_TYPE.TSK_METADATA_EXIF.getTypeID()) - || (artifact.getArtifactTypeID() == ARTIFACT_TYPE.TSK_EXT_MISMATCH_DETECTED.getTypeID())) { + || (artifact.getArtifactTypeID() == ARTIFACT_TYPE.TSK_EXT_MISMATCH_DETECTED.getTypeID()) + || (artifact.getArtifactTypeID() == ARTIFACT_TYPE.TSK_WEB_DOWNLOAD.getTypeID()) + || (artifact.getArtifactTypeID() == ARTIFACT_TYPE.TSK_WEB_CACHE.getTypeID())) { return 3; } else { return 6; diff --git a/Core/src/org/sleuthkit/autopsy/coreutils/XMLUtil.java b/Core/src/org/sleuthkit/autopsy/coreutils/XMLUtil.java index 966eb919e77086f900269704e0cf1bd9559c188b..4323753515a89d65e5704846d833befa0bad3b3e 100644 --- a/Core/src/org/sleuthkit/autopsy/coreutils/XMLUtil.java +++ b/Core/src/org/sleuthkit/autopsy/coreutils/XMLUtil.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2012-2014 Basis Technology Corp. + * Copyright 2012-2020 Basis Technology Corp. * Contact: carrier <at> sleuthkit <dot> org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -54,6 +54,40 @@ * -Loading documents from disk */ public class XMLUtil { + + private static DocumentBuilder getDocumentBuilder() throws ParserConfigurationException { + // See JIRA-6958 for details about class loading and jaxb. + ClassLoader original = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(XMLUtil.class.getClassLoader()); + DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); + return builderFactory.newDocumentBuilder(); + } finally { + Thread.currentThread().setContextClassLoader(original); + } + } + + private static SchemaFactory getSchemaFactory(String schemaLanguage) { + // See JIRA-6958 for details about class loading and jaxb. + ClassLoader original = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(XMLUtil.class.getClassLoader()); + return SchemaFactory.newInstance(schemaLanguage); + } finally { + Thread.currentThread().setContextClassLoader(original); + } + } + + private static TransformerFactory getTransformerFactory() { + // See JIRA-6958 for details about class loading and jaxb. + ClassLoader original = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(XMLUtil.class.getClassLoader()); + return TransformerFactory.newInstance(); + } finally { + Thread.currentThread().setContextClassLoader(original); + } + } /** * Creates a W3C DOM. @@ -63,9 +97,7 @@ public class XMLUtil { * @throws ParserConfigurationException */ public static Document createDocument() throws ParserConfigurationException { - DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); - DocumentBuilder builder = builderFactory.newDocumentBuilder(); - return builder.newDocument(); + return getDocumentBuilder().newDocument(); } /** @@ -100,8 +132,7 @@ public static <T> Document loadDocument(String docPath, Class<T> clazz, String s * @throws IOException */ public static Document loadDocument(String docPath) throws ParserConfigurationException, SAXException, IOException { - DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); - DocumentBuilder builder = builderFactory.newDocumentBuilder(); + DocumentBuilder builder = getDocumentBuilder(); Document doc = builder.parse(new FileInputStream(docPath)); return doc; } @@ -119,7 +150,7 @@ public static Document loadDocument(String docPath) throws ParserConfigurationEx public static <T> void validateDocument(final Document doc, Class<T> clazz, String schemaResourceName) throws SAXException, IOException { PlatformUtil.extractResourceToUserConfigDir(clazz, schemaResourceName, false); File schemaFile = new File(Paths.get(PlatformUtil.getUserConfigDirectory(), schemaResourceName).toAbsolutePath().toString()); - SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + SchemaFactory schemaFactory = getSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI); Schema schema = schemaFactory.newSchema(schemaFile); Validator validator = schema.newValidator(); validator.validate(new DOMSource(doc), new DOMResult()); @@ -140,7 +171,7 @@ public static <T> void validateDocument(final Document doc, Class<T> clazz, Stri * @throws IOException */ public static void saveDocument(final Document doc, String encoding, String docPath) throws TransformerConfigurationException, FileNotFoundException, UnsupportedEncodingException, TransformerException, IOException { - TransformerFactory xf = TransformerFactory.newInstance(); + TransformerFactory xf = getTransformerFactory(); xf.setAttribute("indent-number", 1); //NON-NLS Transformer xformer = xf.newTransformer(); xformer.setOutputProperty(OutputKeys.METHOD, "xml"); //NON-NLS @@ -178,7 +209,7 @@ public static <T> boolean xmlIsValid(DOMSource xmlfile, Class<T> clazz, String s try { PlatformUtil.extractResourceToUserConfigDir(clazz, schemaFile, false); File schemaLoc = new File(PlatformUtil.getUserConfigDirectory() + File.separator + schemaFile); - SchemaFactory schm = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + SchemaFactory schm = getSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI); try { Schema schema = schm.newSchema(schemaLoc); Validator validator = schema.newValidator(); @@ -226,10 +257,9 @@ public static <T> boolean xmlIsValid(Document doc, Class<T> clazz, String type) */ // TODO: Deprecate. public static <T> Document loadDoc(Class<T> clazz, String xmlPath) { - DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); Document ret = null; try { - DocumentBuilder builder = builderFactory.newDocumentBuilder(); + DocumentBuilder builder = getDocumentBuilder(); ret = builder.parse(new FileInputStream(xmlPath)); } catch (ParserConfigurationException e) { Logger.getLogger(clazz.getName()).log(Level.SEVERE, "Error loading XML file " + xmlPath + " : can't initialize parser.", e); //NON-NLS @@ -268,7 +298,7 @@ public static <T> Document loadDoc(Class<T> clazz, String xmlPath, String xsdPat */ // TODO: Deprecate. public static <T> boolean saveDoc(Class<T> clazz, String xmlPath, String encoding, final Document doc) { - TransformerFactory xf = TransformerFactory.newInstance(); + TransformerFactory xf = getTransformerFactory(); xf.setAttribute("indent-number", 1); //NON-NLS boolean success = false; try { diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java index 74ac603e0b66ad179dd65e3a0d29c9ae7817b08d..ee862eee96b6bbda28873e2b1898354ddae1cc4f 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/AbstractAbstractFileNode.java @@ -278,6 +278,7 @@ protected synchronized Sheet createSheet() { "AbstractAbstractFileNode.typeMetaColLbl=Type(Meta)", "AbstractAbstractFileNode.knownColLbl=Known", "AbstractAbstractFileNode.md5HashColLbl=MD5 Hash", + "AbstractAbstractFileNode.sha256HashColLbl=SHA-256 Hash", "AbstractAbstractFileNode.objectId=Object ID", "AbstractAbstractFileNode.mimeType=MIME Type", "AbstractAbstractFileNode.extensionColLbl=Extension"}) @@ -305,6 +306,7 @@ public enum AbstractFilePropertyType { TYPE_META(AbstractAbstractFileNode_typeMetaColLbl()), KNOWN(AbstractAbstractFileNode_knownColLbl()), MD5HASH(AbstractAbstractFileNode_md5HashColLbl()), + SHA256HASH(AbstractAbstractFileNode_sha256HashColLbl()), ObjectID(AbstractAbstractFileNode_objectId()), MIMETYPE(AbstractAbstractFileNode_mimeType()), EXTENSION(AbstractAbstractFileNode_extensionColLbl()); @@ -358,6 +360,7 @@ private List<NodeProperty<?>> getProperties() { properties.add(new NodeProperty<>(KNOWN.toString(), KNOWN.toString(), NO_DESCR, content.getKnown().getName())); properties.add(new NodeProperty<>(LOCATION.toString(), LOCATION.toString(), NO_DESCR, getContentPath(content))); properties.add(new NodeProperty<>(MD5HASH.toString(), MD5HASH.toString(), NO_DESCR, StringUtils.defaultString(content.getMd5Hash()))); + properties.add(new NodeProperty<>(SHA256HASH.toString(), SHA256HASH.toString(), NO_DESCR, StringUtils.defaultString(content.getSha256Hash()))); properties.add(new NodeProperty<>(MIMETYPE.toString(), MIMETYPE.toString(), NO_DESCR, StringUtils.defaultString(content.getMIMEType()))); properties.add(new NodeProperty<>(EXTENSION.toString(), EXTENSION.toString(), NO_DESCR, content.getNameExtension())); @@ -577,6 +580,7 @@ static public void fillPropertyMap(Map<String, Object> map, AbstractFile content map.put(FLAGS_META.toString(), content.getMetaFlagsAsString()); map.put(KNOWN.toString(), content.getKnown().getName()); map.put(MD5HASH.toString(), StringUtils.defaultString(content.getMd5Hash())); + map.put(SHA256HASH.toString(), StringUtils.defaultString(content.getSha256Hash())); map.put(MIMETYPE.toString(), StringUtils.defaultString(content.getMIMEType())); map.put(EXTENSION.toString(), content.getNameExtension()); } diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java b/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java index c7ccf32099778d26c471c575c7d12b2eb020c2b8..34a6859292b5fbd1e4f0ac6a95b89dab84a6b24e 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/BlackboardArtifactNode.java @@ -128,8 +128,8 @@ public class BlackboardArtifactNode extends AbstractContentNode<BlackboardArtifa }; private final BlackboardArtifact artifact; - private Content srcContent; - private volatile String translatedSourceName; + private Content srcContent; + private volatile String translatedSourceName; /* * A method has been provided to allow the injection of properties into this @@ -284,17 +284,49 @@ public BlackboardArtifactNode(BlackboardArtifact artifact) { */ private static Lookup createLookup(BlackboardArtifact artifact) { final long objectID = artifact.getObjectID(); + Content content = null; try { - Content content = contentCache.get(objectID, () -> artifact.getSleuthkitCase().getContentById(objectID)); + if (artifact.getArtifactTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_DOWNLOAD.getTypeID() || artifact.getArtifactTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_CACHE.getTypeID()) { + content = getPathIdFile(artifact); + } if (content == null) { - return Lookups.fixed(artifact); - } else { - return Lookups.fixed(artifact, content); + content = contentCache.get(objectID, () -> artifact.getSleuthkitCase().getContentById(objectID)); } } catch (ExecutionException ex) { logger.log(Level.SEVERE, MessageFormat.format("Error getting source content (artifact objID={0}", artifact.getId()), ex); //NON-NLS + content = null; + } + if (content == null) { return Lookups.fixed(artifact); + } else { + return Lookups.fixed(artifact, content); + } + + } + + /** + * Private helper method to allow content specified in a path id attribute + * to be retrieved. + * + * @param artifact The artifact for which content may be specified as a tsk + * path attribute. + * + * @return The Content specified by the artifact's path id attribute or null + * if there was no content available. + * + * @throws ExecutionException Error retrieving the file specified by the + * path id from the cache. + */ + private static Content getPathIdFile(BlackboardArtifact artifact) throws ExecutionException { + try { + BlackboardAttribute attribute = artifact.getAttribute(new BlackboardAttribute.Type(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH_ID)); + if (attribute != null) { + return contentCache.get(attribute.getValueLong(), () -> artifact.getSleuthkitCase().getContentById(attribute.getValueLong())); + } + } catch (TskCoreException ex) { + logger.log(Level.WARNING, MessageFormat.format("Error getting content for path id attrbiute for artifact: ", artifact.getId()), ex); //NON-NLS } + return null; } /** diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties-MERGED index e7310882a37daace90eed9e1148dcacee3579889..4e87eab218db8262498b8c07a956b27909ee939d 100755 --- a/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/datamodel/Bundle.properties-MERGED @@ -30,6 +30,7 @@ AbstractAbstractFileNode.modifiedTimeColLbl=Modified Time AbstractAbstractFileNode.nameColLbl=Name AbstractAbstractFileNode.objectId=Object ID AbstractAbstractFileNode.originalName=Original Name +AbstractAbstractFileNode.sha256HashColLbl=SHA-256 Hash AbstractAbstractFileNode.sizeColLbl=Size AbstractAbstractFileNode.tagsProperty.displayName=Tags AbstractAbstractFileNode.typeDirColLbl=Type(Dir) diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/accounts/Accounts.java b/Core/src/org/sleuthkit/autopsy/datamodel/accounts/Accounts.java index 8e9a17931e685fabbb3d4ea4feb1102837d9c8f6..3c5f94f34c0fac743e60e586ed06b496fc842057 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/accounts/Accounts.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/accounts/Accounts.java @@ -529,9 +529,11 @@ protected boolean createKeys(List<Long> list) { + getRejectedArtifactFilterClause(); //NON-NLS try (SleuthkitCase.CaseDbQuery results = skCase.executeQuery(query); ResultSet rs = results.getResultSet();) { + List<Long> tempList = new ArrayList<>(); while (rs.next()) { - list.add(rs.getLong("artifact_id")); //NON-NLS + tempList.add(rs.getLong("artifact_id")); // NON-NLS } + list.addAll(tempList); } catch (TskCoreException | SQLException ex) { LOGGER.log(Level.SEVERE, "Error querying for account artifacts.", ex); //NON-NLS } diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceInfoUtilities.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceInfoUtilities.java index 649c15940a0ab436c8a618657220c89b16afd677..c711f3c9a0459a4c754eb03ca8ac2137d0ddeba2 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceInfoUtilities.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceInfoUtilities.java @@ -398,6 +398,20 @@ static Long getLongOrNull(BlackboardArtifact artifact, Type attributeType) { BlackboardAttribute attr = getAttributeOrNull(artifact, attributeType); return (attr == null) ? null : attr.getValueLong(); } + + /** + * Retrieves the int value of a certain attribute type from an artifact. + * + * @param artifact The artifact. + * @param attributeType The attribute type. + * + * @return The 'getValueInt()' value or null if the attribute could not be + * retrieved. + */ + static Integer getIntOrNull(BlackboardArtifact artifact, Type attributeType) { + BlackboardAttribute attr = getAttributeOrNull(artifact, attributeType); + return (attr == null) ? null : attr.getValueInt(); + } /** * Retrieves the long value of a certain attribute type from an artifact and diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/RecentFilesSummary.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/RecentFilesSummary.java index 0a47e0ea6c84bb01cb01a51ac020feb731794ac5..6c4601af88bb0ba46ad2e904f0b6484da334679c 100755 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/RecentFilesSummary.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/RecentFilesSummary.java @@ -118,7 +118,7 @@ public List<RecentFileDetails> getRecentlyOpenedDocuments(DataSource dataSource, dataSource, DATETIME_ATT, DataSourceInfoUtilities.SortOrder.DESCENDING, - 10); + maxCount); List<RecentFileDetails> fileDetails = new ArrayList<>(); for (BlackboardArtifact artifact : artifactList) { @@ -134,12 +134,11 @@ public List<RecentFileDetails> getRecentlyOpenedDocuments(DataSource dataSource, } else if (attribute.getAttributeType().equals(PATH_ATT)) { path = attribute.getValueString(); } - - if (accessedTime != null) { - fileDetails.add(new RecentFileDetails(path, accessedTime)); - } } + if (accessedTime != null && accessedTime != 0) { + fileDetails.add(new RecentFileDetails(path, accessedTime)); + } } return fileDetails; @@ -190,7 +189,7 @@ public List<RecentDownloadDetails> getRecentDownloads(DataSource dataSource, int path = attribute.getValueString(); } } - if (accessedTime != null) { + if (accessedTime != null && accessedTime != 0L) { fileDetails.add(new RecentDownloadDetails(path, accessedTime, domain)); } } @@ -215,6 +214,10 @@ public List<RecentAttachmentDetails> getRecentAttachments(DataSource dataSource, return Collections.emptyList(); } + if (maxCount < 0) { + throw new IllegalArgumentException("Invalid maxCount passed to getRecentAttachments, value must be equal to or greater than 0"); + } + return createListFromMap(buildAttachmentMap(dataSource), maxCount); } @@ -241,7 +244,7 @@ private SortedMap<Long, List<RecentAttachmentDetails>> buildAttachmentMap(DataSo } BlackboardArtifact messageArtifact = skCase.getBlackboardArtifact(attribute.getValueLong()); - if (isMessageArtifact(messageArtifact)) { + if (messageArtifact != null && isMessageArtifact(messageArtifact)) { Content content = artifact.getParent(); if (content instanceof AbstractFile) { String sender; diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/TimelineSummary.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/TimelineSummary.java new file mode 100644 index 0000000000000000000000000000000000000000..1d634211beb9ea09bac7641a9eff37b66ddca700 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/TimelineSummary.java @@ -0,0 +1,318 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> 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.datasourcesummary.datamodel; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TimeZone; +import org.joda.time.Interval; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.SleuthkitCaseProvider.SleuthkitCaseProviderException; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DefaultUpdateGovernor; +import org.sleuthkit.autopsy.ingest.IngestManager; +import org.sleuthkit.autopsy.ingest.ModuleContentEvent; +import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.DataSource; +import org.sleuthkit.datamodel.TimelineEvent; +import org.sleuthkit.datamodel.TimelineEventType; +import org.sleuthkit.datamodel.TimelineFilter; +import org.sleuthkit.datamodel.TimelineFilter.DataSourcesFilter; +import org.sleuthkit.datamodel.TimelineFilter.RootFilter; +import org.sleuthkit.datamodel.TimelineManager; +import org.sleuthkit.datamodel.TskCoreException; +import java.util.function.Supplier; +import org.sleuthkit.autopsy.core.UserPreferences; + +/** + * Provides data source summary information pertaining to Timeline data. + */ +public class TimelineSummary implements DefaultUpdateGovernor { + + private static final long DAY_SECS = 24 * 60 * 60; + private static final Set<IngestManager.IngestJobEvent> INGEST_JOB_EVENTS = new HashSet<>( + Arrays.asList(IngestManager.IngestJobEvent.COMPLETED, IngestManager.IngestJobEvent.CANCELLED)); + + private static final Set<TimelineEventType> FILE_SYSTEM_EVENTS + = new HashSet<>(Arrays.asList( + TimelineEventType.FILE_MODIFIED, + TimelineEventType.FILE_ACCESSED, + TimelineEventType.FILE_CREATED, + TimelineEventType.FILE_CHANGED)); + + private final SleuthkitCaseProvider caseProvider; + private final Supplier<TimeZone> timeZoneProvider; + + /** + * Default constructor. + */ + public TimelineSummary() { + this(SleuthkitCaseProvider.DEFAULT, () -> TimeZone.getTimeZone(UserPreferences.getTimeZoneForDisplays())); + } + + /** + * Construct object with given SleuthkitCaseProvider + * + * @param caseProvider SleuthkitCaseProvider provider, cannot be null. + * @param timeZoneProvider The timezone provider, cannot be null. + */ + public TimelineSummary(SleuthkitCaseProvider caseProvider, Supplier<TimeZone> timeZoneProvider) { + this.caseProvider = caseProvider; + this.timeZoneProvider = timeZoneProvider; + } + + @Override + public boolean isRefreshRequired(ModuleContentEvent evt) { + return true; + } + + @Override + public boolean isRefreshRequired(AbstractFile file) { + return true; + } + + @Override + public boolean isRefreshRequired(IngestManager.IngestJobEvent evt) { + return (evt != null && INGEST_JOB_EVENTS.contains(evt)); + } + + @Override + public Set<IngestManager.IngestJobEvent> getIngestJobEventUpdates() { + return INGEST_JOB_EVENTS; + } + + /** + * Retrieves timeline summary data. + * + * @param dataSource The data source for which timeline data will be + * retrieved. + * @param recentDaysNum The maximum number of most recent days' activity to + * include. + * @return The retrieved data. + * @throws SleuthkitCaseProviderException + * @throws TskCoreException + */ + public TimelineSummaryData getData(DataSource dataSource, int recentDaysNum) throws SleuthkitCaseProviderException, TskCoreException { + TimeZone timeZone = this.timeZoneProvider.get(); + TimelineManager timelineManager = this.caseProvider.get().getTimelineManager(); + + // get a mapping of days from epoch to the activity for that day + Map<Long, DailyActivityAmount> dateCounts = getTimelineEventsByDay(dataSource, timelineManager, timeZone); + + // get minimum and maximum usage date by iterating through + Long minDay = null; + Long maxDay = null; + for (long daysFromEpoch : dateCounts.keySet()) { + minDay = (minDay == null) ? daysFromEpoch : Math.min(minDay, daysFromEpoch); + maxDay = (maxDay == null) ? daysFromEpoch : Math.max(maxDay, daysFromEpoch); + } + + // if no min date or max date, no usage; return null. + if (minDay == null || maxDay == null) { + return null; + } + + Date minDate = new Date(minDay * 1000 * DAY_SECS); + Date maxDate = new Date(maxDay * 1000 * DAY_SECS); + + // The minimum recent day will be within recentDaysNum from the maximum day + // (+1 since maxDay included) or the minimum day of activity + long minRecentDay = Math.max(maxDay - recentDaysNum + 1, minDay); + + // get most recent days activity + List<DailyActivityAmount> mostRecentActivityAmt = getMostRecentActivityAmounts(dateCounts, minRecentDay, maxDay); + + return new TimelineSummaryData(minDate, maxDate, mostRecentActivityAmt); + } + + /** + * Given activity by day, converts to most recent days' activity handling + * empty values. + * + * @param dateCounts The day from epoch mapped to activity amounts for that + * day. + * @param minRecentDay The minimum recent day in days from epoch. + * @param maxDay The maximum recent day in days from epoch; + * @return The most recent daily activity amounts. + */ + private List<DailyActivityAmount> getMostRecentActivityAmounts(Map<Long, DailyActivityAmount> dateCounts, long minRecentDay, long maxDay) { + List<DailyActivityAmount> mostRecentActivityAmt = new ArrayList<>(); + + for (long curRecentDay = minRecentDay; curRecentDay <= maxDay; curRecentDay++) { + DailyActivityAmount prevCounts = dateCounts.get(curRecentDay); + DailyActivityAmount countsHandleNotFound = prevCounts != null + ? prevCounts + : new DailyActivityAmount(new Date(curRecentDay * DAY_SECS * 1000), 0, 0); + + mostRecentActivityAmt.add(countsHandleNotFound); + } + return mostRecentActivityAmt; + } + + /** + * Fetches timeline events per day for a particular data source. + * + * @param dataSource The data source. + * @param timelineManager The timeline manager to use while fetching the + * data. + * @param timeZone The time zone to use to determine which day activity + * belongs. + * @return A Map mapping days from epoch to the activity for that day. + * @throws TskCoreException + */ + private Map<Long, DailyActivityAmount> getTimelineEventsByDay(DataSource dataSource, TimelineManager timelineManager, TimeZone timeZone) throws TskCoreException { + + DataSourcesFilter dataSourceFilter = new DataSourcesFilter(); + dataSourceFilter.addSubFilter(new TimelineFilter.DataSourceFilter(dataSource.getName(), dataSource.getId())); + + RootFilter dataSourceRootFilter = new RootFilter( + null, + null, + null, + null, + null, + dataSourceFilter, + null, + Collections.emptySet()); + + // get events for data source + long curRunTime = System.currentTimeMillis(); + List<TimelineEvent> events = timelineManager.getEvents(new Interval(1, curRunTime), dataSourceRootFilter); + + // get counts of events per day (left is file system events, right is everything else) + Map<Long, DailyActivityAmount> dateCounts = new HashMap<>(); + for (TimelineEvent evt : events) { + long curSecondsFromEpoch = evt.getTime(); + long curDaysFromEpoch = Instant.ofEpochMilli(curSecondsFromEpoch * 1000) + .atZone(timeZone.toZoneId()) + .toLocalDate() + .toEpochDay(); + + DailyActivityAmount prevAmt = dateCounts.get(curDaysFromEpoch); + long prevFileEvtCount = prevAmt == null ? 0 : prevAmt.getFileActivityCount(); + long prevArtifactEvtCount = prevAmt == null ? 0 : prevAmt.getArtifactActivityCount(); + Date thisDay = prevAmt == null ? new Date(curDaysFromEpoch * 1000 * DAY_SECS) : prevAmt.getDay(); + + boolean isFileEvt = FILE_SYSTEM_EVENTS.contains(evt.getEventType()); + long curFileEvtCount = prevFileEvtCount + (isFileEvt ? 1 : 0); + long curArtifactEvtCount = prevArtifactEvtCount + (isFileEvt ? 0 : 1); + + dateCounts.put(curDaysFromEpoch, new DailyActivityAmount(thisDay, curFileEvtCount, curArtifactEvtCount)); + } + + return dateCounts; + } + + /** + * All the data to be represented in the timeline summary tab. + */ + public static class TimelineSummaryData { + + private final Date minDate; + private final Date maxDate; + private final List<DailyActivityAmount> histogramActivity; + + /** + * Main constructor. + * + * @param minDate Earliest usage date recorded for the data source. + * @param maxDate Latest usage date recorded for the data source. + * @param recentDaysActivity A list of activity prior to and including + * the latest usage date by day. + */ + TimelineSummaryData(Date minDate, Date maxDate, List<DailyActivityAmount> recentDaysActivity) { + this.minDate = minDate; + this.maxDate = maxDate; + this.histogramActivity = (recentDaysActivity == null) ? Collections.emptyList() : Collections.unmodifiableList(recentDaysActivity); + } + + /** + * @return Earliest usage date recorded for the data source. + */ + public Date getMinDate() { + return minDate; + } + + /** + * @return Latest usage date recorded for the data source. + */ + public Date getMaxDate() { + return maxDate; + } + + /** + * @return A list of activity prior to and including the latest usage + * date by day. + */ + public List<DailyActivityAmount> getMostRecentDaysActivity() { + return histogramActivity; + } + } + + /** + * Represents the amount of usage based on timeline events for a day. + */ + public static class DailyActivityAmount { + + private final Date day; + private final long fileActivityCount; + private final long artifactActivityCount; + + /** + * Main constructor. + * + * @param day The day for which activity is being measured. + * @param fileActivityCount The amount of file activity timeline events. + * @param artifactActivityCount The amount of artifact timeline events. + */ + DailyActivityAmount(Date day, long fileActivityCount, long artifactActivityCount) { + this.day = day; + this.fileActivityCount = fileActivityCount; + this.artifactActivityCount = artifactActivityCount; + } + + /** + * @return The day for which activity is being measured. + */ + public Date getDay() { + return day; + } + + /** + * @return The amount of file activity timeline events. + */ + public long getFileActivityCount() { + return fileActivityCount; + } + + /** + * @return The amount of artifact timeline events. + */ + public long getArtifactActivityCount() { + return artifactActivityCount; + } + + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/TopProgramsSummary.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/TopProgramsSummary.java deleted file mode 100644 index e984b9efcad437686834abd7e2d04b937e5a7abd..0000000000000000000000000000000000000000 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/TopProgramsSummary.java +++ /dev/null @@ -1,370 +0,0 @@ -/* - * Autopsy Forensic Browser - * - * Copyright 2020 Basis Technology Corp. - * Contact: carrier <at> sleuthkit <dot> 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.datasourcesummary.datamodel; - -import org.sleuthkit.autopsy.datasourcesummary.uiutils.DefaultArtifactUpdateGovernor; -import java.io.File; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; -import org.apache.commons.lang.StringUtils; -import org.sleuthkit.autopsy.datasourcesummary.datamodel.SleuthkitCaseProvider.SleuthkitCaseProviderException; -import org.sleuthkit.datamodel.BlackboardArtifact; -import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE; -import org.sleuthkit.datamodel.BlackboardAttribute; -import org.sleuthkit.datamodel.DataSource; -import org.sleuthkit.datamodel.SleuthkitCase; -import org.sleuthkit.datamodel.TskCoreException; - -/** - * Provides information to populate Top Programs Summary queries. - */ -public class TopProgramsSummary implements DefaultArtifactUpdateGovernor { - - private static final Set<Integer> ARTIFACT_UPDATE_TYPE_IDS = new HashSet<>(Arrays.asList( - ARTIFACT_TYPE.TSK_PROG_RUN.getTypeID() - )); - - /** - * A SQL join type. - */ - private enum JoinType { - LEFT, - RIGHT, - INNER, - OUTER - } - - /** - * A blackboard attribute value column. - */ - private enum AttributeColumn { - value_text, - value_int32, - value_int64 - } - - /** - * The suffix joined to a key name for use as an identifier of a query. - */ - private static final String QUERY_SUFFIX = "_query"; - - /** - * Functions that determine the folder name of a list of path elements. If - * not matched, function returns null. - */ - private static final List<Function<List<String>, String>> SHORT_FOLDER_MATCHERS = Arrays.asList( - // handle Program Files and Program Files (x86) - if true, return the next folder - (pathList) -> { - if (pathList.size() < 2) { - return null; - } - - String rootParent = pathList.get(0).toUpperCase(); - if ("PROGRAM FILES".equals(rootParent) || "PROGRAM FILES (X86)".equals(rootParent)) { - return pathList.get(1); - } else { - return null; - } - }, - // if there is a folder named "APPLICATION DATA" or "APPDATA" - (pathList) -> { - for (String pathEl : pathList) { - String uppered = pathEl.toUpperCase(); - if ("APPLICATION DATA".equals(uppered) || "APPDATA".equals(uppered)) { - return "AppData"; - } - } - return null; - } - ); - - /** - * Creates a sql statement querying the blackboard attributes table for a - * particular attribute type and returning a specified value. That query - * also joins with the blackboard artifact table. - * - * @param joinType The type of join statement to create. - * @param attributeColumn The blackboard attribute column that should be - * returned. - * @param attrType The attribute type to query for. - * @param keyName The aliased name of the attribute to return. This - * is also used to calculate the alias of the query - * same as getFullKey. - * @param bbaName The blackboard artifact table alias. - * - * @return The generated sql statement. - */ - private static String getAttributeJoin(JoinType joinType, AttributeColumn attributeColumn, BlackboardAttribute.ATTRIBUTE_TYPE attrType, String keyName, String bbaName) { - String queryName = keyName + QUERY_SUFFIX; - String innerQueryName = "inner_attribute_" + queryName; - - return "\n" + joinType + " JOIN (\n" - + " SELECT \n" - + " " + innerQueryName + ".artifact_id,\n" - + " " + innerQueryName + "." + attributeColumn + " AS " + keyName + "\n" - + " FROM blackboard_attributes " + innerQueryName + "\n" - + " WHERE " + innerQueryName + ".attribute_type_id = " + attrType.getTypeID() + " -- " + attrType.name() + "\n" - + ") " + queryName + " ON " + queryName + ".artifact_id = " + bbaName + ".artifact_id\n"; - } - - /** - * Given a column key, creates the full name for the column key. - * - * @param key The column key. - * - * @return The full identifier for the column key. - */ - private static String getFullKey(String key) { - return key + QUERY_SUFFIX + "." + key; - } - - /** - * Constructs a SQL 'where' statement from a list of clauses and puts - * parenthesis around each clause. - * - * @param clauses The clauses - * - * @return The generated 'where' statement. - */ - private static String getWhereString(List<String> clauses) { - if (clauses.isEmpty()) { - return ""; - } - - List<String> parenthesized = clauses.stream() - .map(c -> "(" + c + ")") - .collect(Collectors.toList()); - - return "\nWHERE " + String.join("\n AND ", parenthesized) + "\n"; - } - - /** - * Generates a [column] LIKE sql clause. - * - * @param column The column identifier. - * @param likeString The string that will be used as column comparison. - * @param isLike if false, the statement becomes NOT LIKE. - * - * @return The generated statement. - */ - private static String getLikeClause(String column, String likeString, boolean isLike) { - return column + (isLike ? "" : " NOT") + " LIKE '" + likeString + "'"; - } - - private final SleuthkitCaseProvider provider; - - public TopProgramsSummary() { - this(SleuthkitCaseProvider.DEFAULT); - } - - public TopProgramsSummary(SleuthkitCaseProvider provider) { - this.provider = provider; - } - - @Override - public Set<Integer> getArtifactTypeIdsForRefresh() { - return ARTIFACT_UPDATE_TYPE_IDS; - } - - /** - * Retrieves a list of the top programs used on the data source. Currently - * determines this based off of which prefetch results return the highest - * count. - * - * @param dataSource The data source. - * @param count The number of programs to return. - * - * @return The top results objects found. - * - * @throws SleuthkitCaseProviderException - * @throws TskCoreException - * @throws SQLException - */ - public List<TopProgramsResult> getTopPrograms(DataSource dataSource, int count) - throws SleuthkitCaseProviderException, TskCoreException, SQLException { - if (dataSource == null || count <= 0) { - return Collections.emptyList(); - } - - // ntosboot should be ignored - final String ntosBootIdentifier = "NTOSBOOT"; - // programs in windows directory to be ignored - final String windowsDir = "/WINDOWS%"; - - final String nameParam = "name"; - final String pathParam = "path"; - final String runCountParam = "run_count"; - final String lastRunParam = "last_run"; - - String bbaQuery = "bba"; - - final String query = "SELECT\n" - + " " + getFullKey(nameParam) + " AS " + nameParam + ",\n" - + " " + getFullKey(pathParam) + " AS " + pathParam + ",\n" - + " MAX(" + getFullKey(runCountParam) + ") AS " + runCountParam + ",\n" - + " MAX(" + getFullKey(lastRunParam) + ") AS " + lastRunParam + "\n" - + "FROM blackboard_artifacts " + bbaQuery + "\n" - + getAttributeJoin(JoinType.INNER, AttributeColumn.value_text, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PROG_NAME, nameParam, bbaQuery) - + getAttributeJoin(JoinType.LEFT, AttributeColumn.value_text, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH, pathParam, bbaQuery) - + getAttributeJoin(JoinType.LEFT, AttributeColumn.value_int32, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_COUNT, runCountParam, bbaQuery) - + getAttributeJoin(JoinType.LEFT, AttributeColumn.value_int64, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME, lastRunParam, bbaQuery) - + getWhereString(Arrays.asList( - bbaQuery + ".artifact_type_id = " + BlackboardArtifact.ARTIFACT_TYPE.TSK_PROG_RUN.getTypeID(), - bbaQuery + ".data_source_obj_id = " + dataSource.getId(), - // exclude ntosBootIdentifier from results - getLikeClause(getFullKey(nameParam), ntosBootIdentifier, false), - // exclude windows directory items from results - getFullKey(pathParam) + " IS NULL OR " + getLikeClause(getFullKey(pathParam), windowsDir, false) - )) - + "GROUP BY " + getFullKey(nameParam) + ", " + getFullKey(pathParam) + "\n" - + "ORDER BY \n" - + " MAX(" + getFullKey(runCountParam) + ") DESC,\n" - + " MAX(" + getFullKey(lastRunParam) + ") DESC,\n" - + " " + getFullKey(nameParam) + " ASC"; - - DataSourceInfoUtilities.ResultSetHandler<List<TopProgramsResult>> handler = (resultSet) -> { - List<TopProgramsResult> progResults = new ArrayList<>(); - - boolean quitAtCount = false; - - while (resultSet.next() && (!quitAtCount || progResults.size() < count)) { - long lastRunEpoch = resultSet.getLong(lastRunParam); - Date lastRun = (resultSet.wasNull()) ? null : new Date(lastRunEpoch * 1000); - - Long runCount = resultSet.getLong(runCountParam); - if (resultSet.wasNull()) { - runCount = null; - } - - if (lastRun != null || runCount != null) { - quitAtCount = true; - } - - progResults.add(new TopProgramsResult( - resultSet.getString(nameParam), - resultSet.getString(pathParam), - runCount, - lastRun)); - } - - return progResults; - }; - - try (SleuthkitCase.CaseDbQuery dbQuery = provider.get().executeQuery(query); - ResultSet resultSet = dbQuery.getResultSet()) { - - return handler.process(resultSet); - } - } - - /** - * Determines a short folder name if any. Otherwise, returns empty string. - * - * @param strPath The string path. - * @param applicationName The application name. - * - * @return The short folder name or empty string if not found. - */ - public String getShortFolderName(String strPath, String applicationName) { - if (strPath == null) { - return ""; - } - - List<String> pathEls = new ArrayList<>(Arrays.asList(applicationName)); - - File file = new File(strPath); - while (file != null && StringUtils.isNotBlank(file.getName())) { - pathEls.add(file.getName()); - file = file.getParentFile(); - } - - Collections.reverse(pathEls); - - for (Function<List<String>, String> matchEntry : SHORT_FOLDER_MATCHERS) { - String result = matchEntry.apply(pathEls); - if (StringUtils.isNotBlank(result)) { - return result; - } - } - - return ""; - } - - /** - * Describes a result of a program run on a datasource. - */ - public static class TopProgramsResult { - - private final String programName; - private final String programPath; - private final Long runTimes; - private final Date lastRun; - - /** - * Main constructor. - * - * @param programName The name of the program. - * @param programPath The path of the program. - * @param runTimes The number of runs. - */ - TopProgramsResult(String programName, String programPath, Long runTimes, Date lastRun) { - this.programName = programName; - this.programPath = programPath; - this.runTimes = runTimes; - this.lastRun = lastRun; - } - - /** - * @return The name of the program - */ - public String getProgramName() { - return programName; - } - - /** - * @return The path of the program. - */ - public String getProgramPath() { - return programPath; - } - - /** - * @return The number of run times or null if not present. - */ - public Long getRunTimes() { - return runTimes; - } - - /** - * @return The last time the program was run or null if not present. - */ - public Date getLastRun() { - return lastRun; - } - } -} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/UserActivitySummary.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/UserActivitySummary.java index 481840776fb7f2540d8e55874ec414876edc4295..16da6f5c4bf1f41b01d2198c80f2b52f680e387e 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/UserActivitySummary.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/UserActivitySummary.java @@ -18,6 +18,7 @@ */ package org.sleuthkit.autopsy.datasourcesummary.datamodel; +import java.io.File; import org.sleuthkit.autopsy.datasourcesummary.uiutils.DefaultArtifactUpdateGovernor; import java.util.ArrayList; import java.util.Arrays; @@ -30,6 +31,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Function; import java.util.logging.Level; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -54,6 +56,36 @@ */ public class UserActivitySummary implements DefaultArtifactUpdateGovernor { + /** + * Functions that determine the folder name of a list of path elements. If + * not matched, function returns null. + */ + private static final List<Function<List<String>, String>> SHORT_FOLDER_MATCHERS = Arrays.asList( + // handle Program Files and Program Files (x86) - if true, return the next folder + (pathList) -> { + if (pathList.size() < 2) { + return null; + } + + String rootParent = pathList.get(0).toUpperCase(); + if ("PROGRAM FILES".equals(rootParent) || "PROGRAM FILES (X86)".equals(rootParent)) { + return pathList.get(1); + } else { + return null; + } + }, + // if there is a folder named "APPLICATION DATA" or "APPDATA" + (pathList) -> { + for (String pathEl : pathList) { + String uppered = pathEl.toUpperCase(); + if ("APPLICATION DATA".equals(uppered) || "APPDATA".equals(uppered)) { + return "AppData"; + } + } + return null; + } + ); + private static final BlackboardArtifact.Type TYPE_DEVICE_ATTACHED = new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_DEVICE_ATTACHED); private static final BlackboardArtifact.Type TYPE_WEB_HISTORY = new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_WEB_HISTORY); @@ -69,17 +101,51 @@ public class UserActivitySummary implements DefaultArtifactUpdateGovernor { private static final BlackboardAttribute.Type TYPE_DATETIME_START = new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DATETIME_START); private static final BlackboardAttribute.Type TYPE_DATETIME_END = new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DATETIME_END); private static final BlackboardAttribute.Type TYPE_DOMAIN = new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DOMAIN); + private static final BlackboardAttribute.Type TYPE_PROG_NAME = new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_PROG_NAME); + private static final BlackboardAttribute.Type TYPE_PATH = new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_PATH); + private static final BlackboardAttribute.Type TYPE_COUNT = new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_COUNT); + + private static final String NTOS_BOOT_IDENTIFIER = "NTOSBOOT"; + private static final String WINDOWS_PREFIX = "/WINDOWS"; private static final Comparator<TopAccountResult> TOP_ACCOUNT_RESULT_DATE_COMPARE = (a, b) -> a.getLastAccess().compareTo(b.getLastAccess()); private static final Comparator<TopWebSearchResult> TOP_WEBSEARCH_RESULT_DATE_COMPARE = (a, b) -> a.getDateAccessed().compareTo(b.getDateAccessed()); + /** + * Sorts TopProgramsResults pushing highest run time count then most recent + * run and then the program name that comes earliest in the alphabet. + */ + private static final Comparator<TopProgramsResult> TOP_PROGRAMS_RESULT_COMPARE = (a, b) -> { + // first priority for sorting is the run times + // if non-0, this is the return value for the comparator + int runTimesCompare = nullableCompare(a.getRunTimes(), b.getRunTimes()); + if (runTimesCompare != 0) { + return -runTimesCompare; + } + + // second priority for sorting is the last run date + // if non-0, this is the return value for the comparator + int lastRunCompare = nullableCompare( + a.getLastRun() == null ? null : a.getLastRun().getTime(), + b.getLastRun() == null ? null : b.getLastRun().getTime()); + + if (lastRunCompare != 0) { + return -lastRunCompare; + } + + // otherwise sort alphabetically + return (a.getProgramName() == null ? "" : a.getProgramName()) + .compareToIgnoreCase((b.getProgramName() == null ? "" : b.getProgramName())); + }; + private static final Set<Integer> ARTIFACT_UPDATE_TYPE_IDS = new HashSet<>(Arrays.asList( ARTIFACT_TYPE.TSK_WEB_SEARCH_QUERY.getTypeID(), ARTIFACT_TYPE.TSK_MESSAGE.getTypeID(), ARTIFACT_TYPE.TSK_EMAIL_MSG.getTypeID(), ARTIFACT_TYPE.TSK_CALLLOG.getTypeID(), ARTIFACT_TYPE.TSK_DEVICE_ATTACHED.getTypeID(), - ARTIFACT_TYPE.TSK_WEB_HISTORY.getTypeID() + ARTIFACT_TYPE.TSK_WEB_HISTORY.getTypeID(), + ARTIFACT_TYPE.TSK_PROG_RUN.getTypeID() )); private static final Set<String> DEVICE_EXCLUDE_LIST = new HashSet<>(Arrays.asList("ROOT_HUB", "ROOT_HUB20")); @@ -539,6 +605,189 @@ public List<TopAccountResult> getRecentAccounts(DataSource dataSource, int count .collect(Collectors.toList()); } + /** + * Determines a short folder name if any. Otherwise, returns empty string. + * + * @param strPath The string path. + * @param applicationName The application name. + * + * @return The short folder name or empty string if not found. + */ + public String getShortFolderName(String strPath, String applicationName) { + if (strPath == null) { + return ""; + } + + List<String> pathEls = new ArrayList<>(Arrays.asList(applicationName)); + + File file = new File(strPath); + while (file != null && org.apache.commons.lang.StringUtils.isNotBlank(file.getName())) { + pathEls.add(file.getName()); + file = file.getParentFile(); + } + + Collections.reverse(pathEls); + + for (Function<List<String>, String> matchEntry : SHORT_FOLDER_MATCHERS) { + String result = matchEntry.apply(pathEls); + if (org.apache.commons.lang.StringUtils.isNotBlank(result)) { + return result; + } + } + + return ""; + } + + /** + * Creates a TopProgramsResult from a TSK_PROG_RUN blackboard artifact. + * + * @param artifact The TSK_PROG_RUN blackboard artifact. + * + * @return The generated TopProgramsResult. + */ + private TopProgramsResult getTopProgramsResult(BlackboardArtifact artifact) { + String programName = DataSourceInfoUtilities.getStringOrNull(artifact, TYPE_PROG_NAME); + + // ignore items with no name or a ntos boot identifier + if (StringUtils.isBlank(programName) || NTOS_BOOT_IDENTIFIER.equalsIgnoreCase(programName)) { + return null; + } + + String path = DataSourceInfoUtilities.getStringOrNull(artifact, TYPE_PATH); + + // ignore windows directory + if (StringUtils.startsWithIgnoreCase(path, WINDOWS_PREFIX)) { + return null; + } + + Integer count = DataSourceInfoUtilities.getIntOrNull(artifact, TYPE_COUNT); + Long longCount = (count == null) ? null : (long) count; + + return new TopProgramsResult( + programName, + path, + longCount, + DataSourceInfoUtilities.getDateOrNull(artifact, TYPE_DATETIME) + ); + } + + /** + * Retrieves the maximum date given two (possibly null) dates. + * + * @param date1 First date. + * @param date2 Second date. + * + * @return The maximum non-null date or null if both items are null. + */ + private static Date getMax(Date date1, Date date2) { + if (date1 == null) { + return date2; + } else if (date2 == null) { + return date1; + } else { + return date1.compareTo(date2) > 0 ? date1 : date2; + } + } + + /** + * Returns the compare value favoring the higher non-null number. + * + * @param long1 First possibly null long. + * @param long2 Second possibly null long. + * + * @return Returns the compare value: 1,0,-1 favoring the higher non-null + * value. + */ + private static int nullableCompare(Long long1, Long long2) { + if (long1 == null && long2 == null) { + return 0; + } else if (long1 != null && long2 == null) { + return 1; + } else if (long1 == null && long2 != null) { + return -1; + } + + return Long.compare(long1, long2); + } + + /** + * Returns true if number is non-null and higher than 0. + * + * @param longNum The number. + * + * @return True if non-null and higher than 0. + */ + private static boolean isPositiveNum(Long longNum) { + return longNum != null && longNum > 0; + } + + + /** + * Retrieves the top programs results for the given data source limited to + * the count provided as a parameter. The highest run times are at the top + * of the list. If that information isn't available the last run date is + * used. If both, the last run date and the number of run times are + * unavailable, the programs will be sorted alphabetically, the count will + * be ignored and all items will be returned. + * + * @param dataSource The datasource. If the datasource is null, an empty + * list will be returned. + * @param count The number of results to return. This value must be > 0 + * or an IllegalArgumentException will be thrown. + * + * @return The sorted list and limited to the count if last run or run count + * information is available on any item. + * + * @throws SleuthkitCaseProviderException + * @throws TskCoreException + */ + public List<TopProgramsResult> getTopPrograms(DataSource dataSource, int count) throws SleuthkitCaseProviderException, TskCoreException { + assertValidCount(count); + + if (dataSource == null) { + return Collections.emptyList(); + } + + // Get TopProgramsResults for each TSK_PROG_RUN artifact + Collection<TopProgramsResult> results = caseProvider.get().getBlackboard().getArtifacts(ARTIFACT_TYPE.TSK_PROG_RUN.getTypeID(), dataSource.getId()) + .stream() + // convert to a TopProgramsResult object or null if missing critical information + .map((art) -> getTopProgramsResult(art)) + // remove any null items + .filter((res) -> res != null) + // group by the program name and program path + // The value will be a TopProgramsResult with the max run times + // and most recent last run date for each program name / program path pair. + .collect(Collectors.toMap( + res -> Pair.of(res.getProgramName(), res.getProgramPath()), + res -> res, + (res1, res2) -> { + return new TopProgramsResult( + res1.getProgramName(), + res1.getProgramPath(), + getMax(res1.getRunTimes(), res2.getRunTimes()), + getMax(res1.getLastRun(), res2.getLastRun())); + })).values(); + + List<TopProgramsResult> orderedResults = results.stream() + .sorted(TOP_PROGRAMS_RESULT_COMPARE) + .collect(Collectors.toList()); + + // only limit the list to count if there is no last run date and no run times. + if (!orderedResults.isEmpty()) { + TopProgramsResult topResult = orderedResults.get(0); + // if run times / last run information is available, the first item should have some value, + // and then the items should be limited accordingly. + if (isPositiveNum(topResult.getRunTimes()) + || (topResult.getLastRun() != null && isPositiveNum(topResult.getLastRun().getTime()))) { + return orderedResults.stream().limit(count).collect(Collectors.toList()); + } + } + + // otherwise return the alphabetized list with no limit applied. + return orderedResults; + } + /** * Object containing information about a web search artifact. */ @@ -722,4 +971,57 @@ public Date getLastVisit() { return lastVisit; } } + + /** + * Describes a result of a program run on a datasource. + */ + public static class TopProgramsResult { + + private final String programName; + private final String programPath; + private final Long runTimes; + private final Date lastRun; + + /** + * Main constructor. + * + * @param programName The name of the program. + * @param programPath The path of the program. + * @param runTimes The number of runs. + */ + TopProgramsResult(String programName, String programPath, Long runTimes, Date lastRun) { + this.programName = programName; + this.programPath = programPath; + this.runTimes = runTimes; + this.lastRun = lastRun; + } + + /** + * @return The name of the program + */ + public String getProgramName() { + return programName; + } + + /** + * @return The path of the program. + */ + public String getProgramPath() { + return programPath; + } + + /** + * @return The number of run times or null if not present. + */ + public Long getRunTimes() { + return runTimes; + } + + /** + * @return The last time the program was run or null if not present. + */ + public Date getLastRun() { + return lastRun; + } + } } diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties index 94b497d2ff4d19a102ec14c5afcf3718bb8bfce4..5d9e6adf0c6a5d4960a8f46fe6d96f3e62e706af 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties @@ -42,3 +42,4 @@ RecentFilesPanel.attachmentLabel.text=Recent Attachments PastCasesPanel.notableFileLabel.text=Cases with Common Items That Were Tagged as Notable PastCasesPanel.sameIdLabel.text=Past Cases with the Same Device IDs DataSourceSummaryTabbedPane.noDataSourceLabel.text=No data source has been selected. +TimelinePanel.activityRangeLabel.text=Activity Range diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties-MERGED index f03f55df3857f51c3e4c7a8c13f60d089ad2e4ac..4997d067f13569aec18210850a74fde783b108f1 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties-MERGED @@ -3,6 +3,7 @@ AnalysisPanel_keyColumn_title=Name AnalysisPanel_keywordSearchModuleName=Keyword Search # {0} - module name BaseDataSourceSummaryPanel_defaultNotIngestMessage=The {0} ingest module has not been run on this data source. +ContainerPanel_setFieldsForNonImageDataSource_na=N/A CTL_DataSourceSummaryAction=Data Source Summary DataSourceSummaryDialog.closeButton.text=Close ContainerPanel.displayNameLabel.text=Display Name: @@ -47,6 +48,7 @@ DataSourceSummaryTabbedPane_detailsTab_title=Container DataSourceSummaryTabbedPane_ingestHistoryTab_title=Ingest History DataSourceSummaryTabbedPane_pastCasesTab_title=Past Cases DataSourceSummaryTabbedPane_recentFileTab_title=Recent Files +DataSourceSummaryTabbedPane_timelineTab_title=Timeline DataSourceSummaryTabbedPane_typesTab_title=Types DataSourceSummaryTabbedPane_userActivityTab_title=User Activity PastCasesPanel_caseColumn_title=Case @@ -64,6 +66,11 @@ SizeRepresentationUtil_units_kilobytes=\ kB SizeRepresentationUtil_units_megabytes=\ MB SizeRepresentationUtil_units_petabytes=\ PB SizeRepresentationUtil_units_terabytes=\ TB +TimelinePanel_earliestLabel_title=Earliest +TimelinePanel_latestLabel_title=Latest +TimlinePanel_last30DaysChart_artifactEvts_title=Artifact Events +TimlinePanel_last30DaysChart_fileEvts_title=File Events +TimlinePanel_last30DaysChart_title=Last 30 Days TypesPanel_artifactsTypesPieChart_title=Artifact Types TypesPanel_fileMimeTypesChart_audio_title=Audio TypesPanel_fileMimeTypesChart_documents_title=Documents @@ -95,6 +102,7 @@ RecentFilesPanel.attachmentLabel.text=Recent Attachments PastCasesPanel.notableFileLabel.text=Cases with Common Items That Were Tagged as Notable PastCasesPanel.sameIdLabel.text=Past Cases with the Same Device IDs DataSourceSummaryTabbedPane.noDataSourceLabel.text=No data source has been selected. +TimelinePanel.activityRangeLabel.text=Activity Range UserActivityPanel_noDataExists=No communication data exists UserActivityPanel_tab_title=User Activity UserActivityPanel_TopAccountTableModel_accountType_header=Account Type diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/ContainerPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/ContainerPanel.java index 8f11e0dcd3cafa87900967ff4a706d7c447f9864..bc331d952b25d09da4c494fd7c9f90b8a9de28bb 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/ContainerPanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/ContainerPanel.java @@ -26,6 +26,7 @@ import java.util.logging.Level; import org.sleuthkit.autopsy.coreutils.Logger; import javax.swing.table.DefaultTableModel; +import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.datasourcesummary.datamodel.ContainerSummary; import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchResult.ResultType; @@ -52,7 +53,7 @@ private static class ContainerPanelData { /** * Main constructor. * - * @param dataSource The original datasource. + * @param dataSource The original datasource. * @param unallocatedFilesSize The unallocated file size. */ ContainerPanelData(DataSource dataSource, Long unallocatedFilesSize) { @@ -165,8 +166,6 @@ protected void fetchInformation(DataSource dataSource) { private void updateDetailsPanelData(DataSource selectedDataSource, Long unallocatedFilesSize) { clearTableValues(); if (selectedDataSource != null) { - unallocatedSizeValue.setText(SizeRepresentationUtil.getSizeString(unallocatedFilesSize)); - timeZoneValue.setText(selectedDataSource.getTimeZone()); displayNameValue.setText(selectedDataSource.getName()); originalNameValue.setText(selectedDataSource.getName()); deviceIdValue.setText(selectedDataSource.getDeviceId()); @@ -178,24 +177,48 @@ private void updateDetailsPanelData(DataSource selectedDataSource, Long unalloca } if (selectedDataSource instanceof Image) { - setFieldsForImage((Image) selectedDataSource); + setFieldsForImage((Image) selectedDataSource, unallocatedFilesSize); + } else { + setFieldsForNonImageDataSource(); } } - updateFieldVisibility(); + this.repaint(); } + @Messages({ + "ContainerPanel_setFieldsForNonImageDataSource_na=N/A" + }) + private void setFieldsForNonImageDataSource() { + String NA = Bundle.ContainerPanel_setFieldsForNonImageDataSource_na(); + + unallocatedSizeValue.setText(NA); + imageTypeValue.setText(NA); + sizeValue.setText(NA); + sectorSizeValue.setText(NA); + timeZoneValue.setText(NA); + + ((DefaultTableModel) filePathsTable.getModel()).addRow(new Object[]{NA}); + + md5HashValue.setText(NA); + sha1HashValue.setText(NA); + sha256HashValue.setText(NA); + } + /** * Sets text fields for an image. This should be called after * clearTableValues and before updateFieldVisibility to ensure the proper * rendering. * * @param selectedImage The selected image. + * @param unallocatedFilesSize Unallocated file size in bytes. */ - private void setFieldsForImage(Image selectedImage) { + private void setFieldsForImage(Image selectedImage, Long unallocatedFilesSize) { + unallocatedSizeValue.setText(SizeRepresentationUtil.getSizeString(unallocatedFilesSize)); imageTypeValue.setText(selectedImage.getType().getName()); sizeValue.setText(SizeRepresentationUtil.getSizeString(selectedImage.getSize())); sectorSizeValue.setText(SizeRepresentationUtil.getSizeString(selectedImage.getSsize())); + timeZoneValue.setText(selectedImage.getTimeZone()); for (String path : selectedImage.getPaths()) { ((DefaultTableModel) filePathsTable.getModel()).addRow(new Object[]{path}); @@ -233,41 +256,6 @@ private void setFieldsForImage(Image selectedImage) { } } - /** - * Update the visibility of all fields and their labels based on whether - * they have contents. Empty fields have them and their contents hidden. - */ - private void updateFieldVisibility() { - displayNameValue.setVisible(!displayNameValue.getText().isEmpty()); - displayNameLabel.setVisible(!displayNameValue.getText().isEmpty()); - originalNameValue.setVisible(!originalNameValue.getText().isEmpty()); - originalNameLabel.setVisible(!originalNameValue.getText().isEmpty()); - deviceIdValue.setVisible(!deviceIdValue.getText().isEmpty()); - deviceIdLabel.setVisible(!deviceIdValue.getText().isEmpty()); - timeZoneValue.setVisible(!timeZoneValue.getText().isEmpty()); - timeZoneLabel.setVisible(!timeZoneValue.getText().isEmpty()); - acquisitionDetailsTextArea.setVisible(!acquisitionDetailsTextArea.getText().isEmpty()); - acquisitionDetailsLabel.setVisible(!acquisitionDetailsTextArea.getText().isEmpty()); - acquisitionDetailsScrollPane.setVisible(!acquisitionDetailsTextArea.getText().isEmpty()); - imageTypeValue.setVisible(!imageTypeValue.getText().isEmpty()); - imageTypeLabel.setVisible(!imageTypeValue.getText().isEmpty()); - sizeValue.setVisible(!sizeValue.getText().isEmpty()); - sizeLabel.setVisible(!sizeValue.getText().isEmpty()); - sectorSizeValue.setVisible(!sectorSizeValue.getText().isEmpty()); - sectorSizeLabel.setVisible(!sectorSizeValue.getText().isEmpty()); - md5HashValue.setVisible(!md5HashValue.getText().isEmpty()); - md5HashLabel.setVisible(!md5HashValue.getText().isEmpty()); - sha1HashValue.setVisible(!sha1HashValue.getText().isEmpty()); - sha1HashLabel.setVisible(!sha1HashValue.getText().isEmpty()); - sha256HashValue.setVisible(!sha256HashValue.getText().isEmpty()); - sha256HashLabel.setVisible(!sha256HashValue.getText().isEmpty()); - unallocatedSizeValue.setVisible(!unallocatedSizeValue.getText().isEmpty()); - unallocatedSizeLabel.setVisible(!unallocatedSizeValue.getText().isEmpty()); - filePathsTable.setVisible(filePathsTable.getRowCount() > 0); - filePathsLabel.setVisible(filePathsTable.getRowCount() > 0); - filePathsScrollPane.setVisible(filePathsTable.getRowCount() > 0); - } - /** * Set the contents of all fields to be empty. */ diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryTabbedPane.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryTabbedPane.java index 55320f7dbb6308f16ab9704af97bc07af38019ba..3670124fa24349bab9f6e19d01c1c1081899c47b 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryTabbedPane.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/DataSourceSummaryTabbedPane.java @@ -38,7 +38,8 @@ "DataSourceSummaryTabbedPane_ingestHistoryTab_title=Ingest History", "DataSourceSummaryTabbedPane_recentFileTab_title=Recent Files", "DataSourceSummaryTabbedPane_pastCasesTab_title=Past Cases", - "DataSourceSummaryTabbedPane_analysisTab_title=Analysis" + "DataSourceSummaryTabbedPane_analysisTab_title=Analysis", + "DataSourceSummaryTabbedPane_timelineTab_title=Timeline" }) public class DataSourceSummaryTabbedPane extends javax.swing.JPanel { @@ -56,10 +57,10 @@ private static class DataSourceTab { /** * Main constructor. * - * @param tabTitle The title of the tab. - * @param component The component to be displayed. + * @param tabTitle The title of the tab. + * @param component The component to be displayed. * @param onDataSource The function to be called on a new data source. - * @param onClose Called to cleanup resources when closing tabs. + * @param onClose Called to cleanup resources when closing tabs. */ DataSourceTab(String tabTitle, Component component, Consumer<DataSource> onDataSource, Runnable onClose) { this.tabTitle = tabTitle; @@ -72,7 +73,7 @@ private static class DataSourceTab { * Main constructor. * * @param tabTitle The title of the tab. - * @param panel The component to be displayed in the tab. + * @param panel The component to be displayed in the tab. */ DataSourceTab(String tabTitle, BaseDataSourceSummaryPanel panel) { this.tabTitle = tabTitle; @@ -123,6 +124,7 @@ public Runnable getOnClose() { new DataSourceTab(Bundle.DataSourceSummaryTabbedPane_analysisTab_title(), new AnalysisPanel()), new DataSourceTab(Bundle.DataSourceSummaryTabbedPane_recentFileTab_title(), new RecentFilesPanel()), new DataSourceTab(Bundle.DataSourceSummaryTabbedPane_pastCasesTab_title(), new PastCasesPanel()), + new DataSourceTab(Bundle.DataSourceSummaryTabbedPane_timelineTab_title(), new TimelinePanel()), // do nothing on closing new DataSourceTab(Bundle.DataSourceSummaryTabbedPane_ingestHistoryTab_title(), ingestHistoryPanel, ingestHistoryPanel::setDataSource, () -> { }), diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/PastCasesPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/PastCasesPanel.java index 76d36f4785cd7983297a40fcbae9e3aac56ed417..2400334ee3d3e59f856e643c384c415da9f6426e 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/PastCasesPanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/PastCasesPanel.java @@ -20,7 +20,6 @@ import java.util.Arrays; import java.util.List; -import java.util.function.Function; import org.apache.commons.lang3.tuple.Pair; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.centralrepository.ingestmodule.CentralRepoIngestModuleFactory; @@ -28,7 +27,6 @@ import org.sleuthkit.autopsy.datasourcesummary.datamodel.PastCasesSummary.PastCasesResult; import org.sleuthkit.autopsy.datasourcesummary.uiutils.CellModelTableCellRenderer.DefaultCellModel; import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchResult; -import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchResult.ResultType; import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchWorker; import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchWorker.DataFetchComponents; import org.sleuthkit.autopsy.datasourcesummary.uiutils.IngestRunningLabel; @@ -103,31 +101,8 @@ public PastCasesPanel(PastCasesSummary pastCaseData) { * @param result The result. */ private void handleResult(DataFetchResult<PastCasesResult> result) { - showResultWithModuleCheck(notableFileTable, getSubResult(result, (res) -> res.getTaggedNotable()), CR_FACTORY, CR_NAME); - showResultWithModuleCheck(sameIdTable, getSubResult(result, (res) -> res.getSameIdsResults()), CR_FACTORY, CR_NAME); - } - - /** - * Given an input data fetch result, creates an error result if the original - * is an error. Otherwise, uses the getSubResult function on the underlying - * data to create a new DataFetchResult. - * - * @param inputResult The input result. - * @param getSubComponent The means of getting the data given the original - * data. - * - * @return The new result with the error of the original or the processed - * data. - */ - private <O> DataFetchResult<O> getSubResult(DataFetchResult<PastCasesResult> inputResult, Function<PastCasesResult, O> getSubResult) { - if (inputResult == null) { - return null; - } else if (inputResult.getResultType() == ResultType.SUCCESS) { - O innerData = (inputResult.getData() == null) ? null : getSubResult.apply(inputResult.getData()); - return DataFetchResult.getSuccessResult(innerData); - } else { - return DataFetchResult.getErrorResult(inputResult.getException()); - } + showResultWithModuleCheck(notableFileTable, DataFetchResult.getSubResult(result, (res) -> res.getTaggedNotable()), CR_FACTORY, CR_NAME); + showResultWithModuleCheck(sameIdTable, DataFetchResult.getSubResult(result, (res) -> res.getSameIdsResults()), CR_FACTORY, CR_NAME); } @Override diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TimelinePanel.form b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TimelinePanel.form new file mode 100644 index 0000000000000000000000000000000000000000..e3493d7a0d44177ae0541da539221ff964eb638d --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TimelinePanel.form @@ -0,0 +1,214 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.4" maxVersion="1.9" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="1"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> + </AuxValues> + + <Layout> + <DimensionLayout dim="0"> + <Group type="103" groupAlignment="0" attributes="0"> + <Component id="mainScrollPane" alignment="0" pref="400" max="32767" attributes="0"/> + </Group> + </DimensionLayout> + <DimensionLayout dim="1"> + <Group type="103" groupAlignment="0" attributes="0"> + <Component id="mainScrollPane" alignment="0" pref="300" max="32767" attributes="0"/> + </Group> + </DimensionLayout> + </Layout> + <SubComponents> + <Container class="javax.swing.JScrollPane" name="mainScrollPane"> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/> + <SubComponents> + <Container class="javax.swing.JPanel" name="mainContentPanel"> + <Properties> + <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor"> + <Border info="org.netbeans.modules.form.compat2.border.EmptyBorderInfo"> + <EmptyBorder bottom="10" left="10" right="10" top="10"/> + </Border> + </Property> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignBoxLayout"> + <Property name="axis" type="int" value="3"/> + </Layout> + <SubComponents> + <Container class="javax.swing.JPanel" name="ingestRunningPanel"> + <Properties> + <Property name="alignmentX" type="float" value="0.0"/> + <Property name="maximumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[32767, 25]"/> + </Property> + <Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[10, 25]"/> + </Property> + <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[10, 25]"/> + </Property> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_CreateCodeCustom" type="java.lang.String" value="ingestRunningLabel"/> + <AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignFlowLayout"/> + </Container> + <Component class="javax.swing.JLabel" name="activityRangeLabel"> + <Properties> + <Property name="font" type="java.awt.Font" editor="org.netbeans.beaninfo.editors.FontEditor"> + <Font name="Segoe UI" size="12" style="1"/> + </Property> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties" key="TimelinePanel.activityRangeLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> + </Property> + </Properties> + <AccessibilityProperties> + <Property name="AccessibleContext.accessibleName" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="org/sleuthkit/autopsy/corecomponents/Bundle.properties" key="PastCasesPanel.notableFileLabel.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> + </Property> + </AccessibilityProperties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/> + </AuxValues> + </Component> + <Component class="javax.swing.Box$Filler" name="filler1"> + <Properties> + <Property name="maximumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[0, 2]"/> + </Property> + <Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[0, 2]"/> + </Property> + <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[0, 2]"/> + </Property> + <Property name="alignmentX" type="float" value="0.0"/> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/> + <AuxValue name="classDetails" type="java.lang.String" value="Box.Filler.VerticalStrut"/> + </AuxValues> + </Component> + <Container class="javax.swing.JPanel" name="earliestLabelPanel"> + <Properties> + <Property name="alignmentX" type="float" value="0.0"/> + <Property name="maximumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[32767, 20]"/> + </Property> + <Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[100, 20]"/> + </Property> + <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[100, 20]"/> + </Property> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_CreateCodeCustom" type="java.lang.String" value="earliestLabel"/> + <AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignFlowLayout"/> + </Container> + <Container class="javax.swing.JPanel" name="latestLabelPanel"> + <Properties> + <Property name="alignmentX" type="float" value="0.0"/> + <Property name="maximumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[32767, 20]"/> + </Property> + <Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[100, 20]"/> + </Property> + <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[100, 20]"/> + </Property> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_CreateCodeCustom" type="java.lang.String" value="latestLabel"/> + <AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignFlowLayout"/> + </Container> + <Component class="javax.swing.Box$Filler" name="filler2"> + <Properties> + <Property name="maximumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[0, 20]"/> + </Property> + <Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[0, 20]"/> + </Property> + <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[0, 20]"/> + </Property> + <Property name="alignmentX" type="float" value="0.0"/> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/> + <AuxValue name="classDetails" type="java.lang.String" value="Box.Filler.VerticalStrut"/> + </AuxValues> + </Component> + <Container class="javax.swing.JPanel" name="sameIdPanel"> + <Properties> + <Property name="alignmentX" type="float" value="0.0"/> + <Property name="maximumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[600, 300]"/> + </Property> + <Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[600, 300]"/> + </Property> + <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[600, 300]"/> + </Property> + <Property name="verifyInputWhenFocusTarget" type="boolean" value="false"/> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_CreateCodeCustom" type="java.lang.String" value="last30DaysChart"/> + <AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignFlowLayout"/> + </Container> + <Component class="javax.swing.Box$Filler" name="filler5"> + <Properties> + <Property name="maximumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> + <Dimension value="[0, 32767]"/> + </Property> + <Property name="alignmentX" type="float" value="0.0"/> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/> + <AuxValue name="classDetails" type="java.lang.String" value="Box.Filler.VerticalGlue"/> + </AuxValues> + </Component> + </SubComponents> + </Container> + </SubComponents> + </Container> + </SubComponents> +</Form> diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TimelinePanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TimelinePanel.java new file mode 100644 index 0000000000000000000000000000000000000000..87f170ccabfe5329d5c9dedb634fa3d368da24c0 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TimelinePanel.java @@ -0,0 +1,263 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> 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.datasourcesummary.ui; + +import java.awt.Color; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import org.apache.commons.collections.CollectionUtils; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.TimelineSummary; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.TimelineSummary.DailyActivityAmount; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.TimelineSummary.TimelineSummaryData; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.BarChartPanel; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.BarChartPanel.BarChartItem; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.BarChartPanel.BarChartSeries; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.BarChartPanel.OrderedKey; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchResult; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchWorker; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchWorker.DataFetchComponents; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.IngestRunningLabel; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.LoadableComponent; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.LoadableLabel; +import org.sleuthkit.datamodel.DataSource; + +/** + * A tab shown in data source summary displaying information about a data + * source's timeline events. + */ +@Messages({ + "TimelinePanel_earliestLabel_title=Earliest", + "TimelinePanel_latestLabel_title=Latest", + "TimlinePanel_last30DaysChart_title=Last 30 Days", + "TimlinePanel_last30DaysChart_fileEvts_title=File Events", + "TimlinePanel_last30DaysChart_artifactEvts_title=Artifact Events",}) +public class TimelinePanel extends BaseDataSourceSummaryPanel { + + private static final long serialVersionUID = 1L; + private static final DateFormat EARLIEST_LATEST_FORMAT = getUtcFormat("MMM d, yyyy"); + private static final DateFormat CHART_FORMAT = getUtcFormat("MMM d"); + private static final int MOST_RECENT_DAYS_COUNT = 30; + + /** + * Creates a DateFormat formatter that uses UTC for time zone. + * + * @param formatString The date format string. + * @return The data format. + */ + private static DateFormat getUtcFormat(String formatString) { + return new SimpleDateFormat(formatString, Locale.getDefault()); + } + + // components displayed in the tab + private final IngestRunningLabel ingestRunningLabel = new IngestRunningLabel(); + private final LoadableLabel earliestLabel = new LoadableLabel(Bundle.TimelinePanel_earliestLabel_title()); + private final LoadableLabel latestLabel = new LoadableLabel(Bundle.TimelinePanel_latestLabel_title()); + private final BarChartPanel last30DaysChart = new BarChartPanel(Bundle.TimlinePanel_last30DaysChart_title(), "", ""); + + // all loadable components on this tab + private final List<LoadableComponent<?>> loadableComponents = Arrays.asList(earliestLabel, latestLabel, last30DaysChart); + + // actions to load data for this tab + private final List<DataFetchComponents<DataSource, ?>> dataFetchComponents; + + public TimelinePanel() { + this(new TimelineSummary()); + } + + /** + * Creates new form PastCasesPanel + */ + public TimelinePanel(TimelineSummary timelineData) { + // set up data acquisition methods + dataFetchComponents = Arrays.asList( + new DataFetchWorker.DataFetchComponents<>( + (dataSource) -> timelineData.getData(dataSource, MOST_RECENT_DAYS_COUNT), + (result) -> handleResult(result)) + ); + + initComponents(); + } + + /** + * Formats a date using a DateFormat. In the event that the date is null, + * returns a null string. + * + * @param date The date to format. + * @param formatter The DateFormat to use to format the date. + * @return The formatted string generated from the formatter or null if the + * date is null. + */ + private static String formatDate(Date date, DateFormat formatter) { + return date == null ? null : formatter.format(date); + } + + private static final Color FILE_EVT_COLOR = new Color(228, 22, 28); + private static final Color ARTIFACT_EVT_COLOR = new Color(21, 227, 100); + + /** + * Converts DailyActivityAmount data retrieved from TimelineSummary into + * data to be displayed as a bar chart. + * + * @param recentDaysActivity The data retrieved from TimelineSummary. + * @return The data to be displayed in the BarChart. + */ + private List<BarChartSeries> parseChartData(List<DailyActivityAmount> recentDaysActivity) { + // if no data, return null indicating no result. + if (CollectionUtils.isEmpty(recentDaysActivity)) { + return null; + } + + // Create a bar chart item for each recent days activity item + List<BarChartItem> fileEvtCounts = new ArrayList<>(); + List<BarChartItem> artifactEvtCounts = new ArrayList<>(); + + for (int i = 0; i < recentDaysActivity.size(); i++) { + DailyActivityAmount curItem = recentDaysActivity.get(i); + + long fileAmt = curItem.getFileActivityCount(); + long artifactAmt = curItem.getArtifactActivityCount() * 100; + String formattedDate = (i == 0 || i == recentDaysActivity.size() - 1) + ? formatDate(curItem.getDay(), CHART_FORMAT) : ""; + + OrderedKey thisKey = new OrderedKey(formattedDate, i); + fileEvtCounts.add(new BarChartItem(thisKey, fileAmt)); + artifactEvtCounts.add(new BarChartItem(thisKey, artifactAmt)); + } + + return Arrays.asList( + new BarChartSeries(Bundle.TimlinePanel_last30DaysChart_fileEvts_title(), FILE_EVT_COLOR, fileEvtCounts), + new BarChartSeries(Bundle.TimlinePanel_last30DaysChart_artifactEvts_title(), ARTIFACT_EVT_COLOR, artifactEvtCounts)); + } + + /** + * Handles displaying the result for each displayable item in the + * TimelinePanel by breaking the TimelineSummaryData result into its + * constituent parts and then sending each data item to the pertinent + * component. + * + * @param result The result to be displayed on this tab. + */ + private void handleResult(DataFetchResult<TimelineSummaryData> result) { + earliestLabel.showDataFetchResult(DataFetchResult.getSubResult(result, r -> formatDate(r.getMinDate(), EARLIEST_LATEST_FORMAT))); + latestLabel.showDataFetchResult(DataFetchResult.getSubResult(result, r -> formatDate(r.getMaxDate(), EARLIEST_LATEST_FORMAT))); + last30DaysChart.showDataFetchResult(DataFetchResult.getSubResult(result, r -> parseChartData(r.getMostRecentDaysActivity()))); + } + + @Override + protected void fetchInformation(DataSource dataSource) { + fetchInformation(dataFetchComponents, dataSource); + } + + @Override + protected void onNewDataSource(DataSource dataSource) { + onNewDataSource(dataFetchComponents, loadableComponents, dataSource); + } + + @Override + public void close() { + ingestRunningLabel.unregister(); + super.close(); + } + + /** + * 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") + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + + javax.swing.JScrollPane mainScrollPane = new javax.swing.JScrollPane(); + javax.swing.JPanel mainContentPanel = new javax.swing.JPanel(); + javax.swing.JPanel ingestRunningPanel = ingestRunningLabel; + javax.swing.JLabel activityRangeLabel = new javax.swing.JLabel(); + javax.swing.Box.Filler filler1 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 2), new java.awt.Dimension(0, 2), new java.awt.Dimension(0, 2)); + javax.swing.JPanel earliestLabelPanel = earliestLabel; + javax.swing.JPanel latestLabelPanel = latestLabel; + javax.swing.Box.Filler filler2 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 20), new java.awt.Dimension(0, 20), new java.awt.Dimension(0, 20)); + javax.swing.JPanel sameIdPanel = last30DaysChart; + javax.swing.Box.Filler filler5 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 32767)); + + mainContentPanel.setBorder(javax.swing.BorderFactory.createEmptyBorder(10, 10, 10, 10)); + mainContentPanel.setLayout(new javax.swing.BoxLayout(mainContentPanel, javax.swing.BoxLayout.PAGE_AXIS)); + + ingestRunningPanel.setAlignmentX(0.0F); + ingestRunningPanel.setMaximumSize(new java.awt.Dimension(32767, 25)); + ingestRunningPanel.setMinimumSize(new java.awt.Dimension(10, 25)); + ingestRunningPanel.setPreferredSize(new java.awt.Dimension(10, 25)); + mainContentPanel.add(ingestRunningPanel); + + activityRangeLabel.setFont(new java.awt.Font("Segoe UI", 1, 12)); // NOI18N + org.openide.awt.Mnemonics.setLocalizedText(activityRangeLabel, org.openide.util.NbBundle.getMessage(TimelinePanel.class, "TimelinePanel.activityRangeLabel.text")); // NOI18N + mainContentPanel.add(activityRangeLabel); + activityRangeLabel.getAccessibleContext().setAccessibleName(org.openide.util.NbBundle.getMessage(TimelinePanel.class, "PastCasesPanel.notableFileLabel.text")); // NOI18N + + filler1.setAlignmentX(0.0F); + mainContentPanel.add(filler1); + + earliestLabelPanel.setAlignmentX(0.0F); + earliestLabelPanel.setMaximumSize(new java.awt.Dimension(32767, 20)); + earliestLabelPanel.setMinimumSize(new java.awt.Dimension(100, 20)); + earliestLabelPanel.setPreferredSize(new java.awt.Dimension(100, 20)); + mainContentPanel.add(earliestLabelPanel); + + latestLabelPanel.setAlignmentX(0.0F); + latestLabelPanel.setMaximumSize(new java.awt.Dimension(32767, 20)); + latestLabelPanel.setMinimumSize(new java.awt.Dimension(100, 20)); + latestLabelPanel.setPreferredSize(new java.awt.Dimension(100, 20)); + mainContentPanel.add(latestLabelPanel); + + filler2.setAlignmentX(0.0F); + mainContentPanel.add(filler2); + + sameIdPanel.setAlignmentX(0.0F); + sameIdPanel.setMaximumSize(new java.awt.Dimension(600, 300)); + sameIdPanel.setMinimumSize(new java.awt.Dimension(600, 300)); + sameIdPanel.setPreferredSize(new java.awt.Dimension(600, 300)); + sameIdPanel.setVerifyInputWhenFocusTarget(false); + mainContentPanel.add(sameIdPanel); + + filler5.setAlignmentX(0.0F); + mainContentPanel.add(filler5); + + mainScrollPane.setViewportView(mainContentPanel); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(mainScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 400, Short.MAX_VALUE) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(mainScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 300, Short.MAX_VALUE) + ); + }// </editor-fold>//GEN-END:initComponents + + + // Variables declaration - do not modify//GEN-BEGIN:variables + // End of variables declaration//GEN-END:variables +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TypesPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TypesPanel.java index 448bf6bbea54de84cb18cf9d05a82a4f69794fcc..247aa0c304ee94ae720d6dbf79f14ae0ca9e9f50 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TypesPanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TypesPanel.java @@ -18,7 +18,6 @@ */ package org.sleuthkit.autopsy.datasourcesummary.ui; -import java.awt.BorderLayout; import java.awt.Color; import java.sql.SQLException; import java.text.DecimalFormat; @@ -30,7 +29,6 @@ import java.util.logging.Level; import java.util.stream.Collectors; import java.util.stream.Stream; -import javax.swing.JLabel; import org.apache.commons.lang3.StringUtils; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.coreutils.FileTypeUtils.FileTypeCategory; @@ -40,13 +38,13 @@ import org.sleuthkit.autopsy.datasourcesummary.datamodel.IngestModuleCheckUtil; import org.sleuthkit.autopsy.datasourcesummary.datamodel.MimeTypeSummary; import org.sleuthkit.autopsy.datasourcesummary.datamodel.SleuthkitCaseProvider.SleuthkitCaseProviderException; -import org.sleuthkit.autopsy.datasourcesummary.uiutils.AbstractLoadableComponent; import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchResult; import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchResult.ResultType; import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchWorker; import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchWorker.DataFetchComponents; import org.sleuthkit.autopsy.datasourcesummary.uiutils.IngestRunningLabel; import org.sleuthkit.autopsy.datasourcesummary.uiutils.LoadableComponent; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.LoadableLabel; import org.sleuthkit.autopsy.datasourcesummary.uiutils.PieChartPanel; import org.sleuthkit.autopsy.datasourcesummary.uiutils.PieChartPanel.PieChartItem; import org.sleuthkit.autopsy.modules.filetypeid.FileTypeIdModuleFactory; @@ -78,46 +76,6 @@ "TypesPanel_sizeLabel_title=Size"}) class TypesPanel extends BaseDataSourceSummaryPanel { - /** - * A label that allows for displaying loading messages and can be used with - * a DataFetchResult. Text displays as "<key>:<value | message>". - */ - private static class LoadableLabel extends AbstractLoadableComponent<String> { - - private static final long serialVersionUID = 1L; - - private final JLabel label = new JLabel(); - private final String key; - - /** - * Main constructor for the label. - * - * @param key The key to be displayed. - */ - LoadableLabel(String key) { - this.key = key; - setLayout(new BorderLayout()); - add(label, BorderLayout.CENTER); - this.showResults(null); - } - - private void setValue(String value) { - String formattedKey = StringUtils.isBlank(key) ? "" : key; - String formattedValue = StringUtils.isBlank(value) ? "" : value; - label.setText(String.format("%s: %s", formattedKey, formattedValue)); - } - - @Override - protected void setMessage(boolean visible, String message) { - setValue(message); - } - - @Override - protected void setResults(String data) { - setValue(data); - } - } - /** * Data for types pie chart. */ @@ -129,9 +87,9 @@ private static class TypesPieChartData { /** * Main constructor. * - * @param pieSlices The pie slices. + * @param pieSlices The pie slices. * @param usefulContent True if this is useful content; false if there - * is 0 mime type information. + * is 0 mime type information. */ public TypesPieChartData(List<PieChartItem> pieSlices, boolean usefulContent) { this.pieSlices = pieSlices; @@ -165,9 +123,9 @@ private static class TypesPieCategory { /** * Main constructor. * - * @param label The label for this slice. + * @param label The label for this slice. * @param mimeTypes The mime types associated with this slice. - * @param color The color associated with this slice. + * @param color The color associated with this slice. */ TypesPieCategory(String label, Set<String> mimeTypes, Color color) { this.label = label; @@ -178,9 +136,9 @@ private static class TypesPieCategory { /** * Constructor that accepts FileTypeCategory. * - * @param label The label for this slice. + * @param label The label for this slice. * @param mimeTypes The mime types associated with this slice. - * @param color The color associated with this slice. + * @param color The color associated with this slice. */ TypesPieCategory(String label, FileTypeCategory fileCategory, Color color) { this(label, fileCategory.getMediaTypes(), color); @@ -278,8 +236,8 @@ public void close() { /** * Creates a new TypesPanel. * - * @param mimeTypeData The service for mime types. - * @param typeData The service for file types data. + * @param mimeTypeData The service for mime types. + * @param typeData The service for file types data. * @param containerData The service for container information. */ public TypesPanel( @@ -358,7 +316,7 @@ protected void onNewDataSource(DataSource dataSource) { * Gets all the data for the file type pie chart. * * @param mimeTypeData The means of acquiring data. - * @param dataSource The datasource. + * @param dataSource The datasource. * * @return The pie chart items. */ diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/UserActivityPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/UserActivityPanel.java index 476d8c5e6fa20c56b72bcc1a52b4b855e4b3a381..baab54217ccd6f46d6fd7217a14ab22b6de5b18d 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/UserActivityPanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/UserActivityPanel.java @@ -29,12 +29,11 @@ import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.datasourcesummary.datamodel.IngestModuleCheckUtil; import org.sleuthkit.autopsy.datasourcesummary.datamodel.UserActivitySummary; -import org.sleuthkit.autopsy.datasourcesummary.datamodel.TopProgramsSummary; import org.sleuthkit.autopsy.datasourcesummary.datamodel.UserActivitySummary.TopAccountResult; import org.sleuthkit.autopsy.datasourcesummary.datamodel.UserActivitySummary.TopDeviceAttachedResult; import org.sleuthkit.autopsy.datasourcesummary.datamodel.UserActivitySummary.TopWebSearchResult; -import org.sleuthkit.autopsy.datasourcesummary.datamodel.TopProgramsSummary.TopProgramsResult; import org.sleuthkit.autopsy.datasourcesummary.datamodel.UserActivitySummary.TopDomainsResult; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.UserActivitySummary.TopProgramsResult; import org.sleuthkit.autopsy.datasourcesummary.uiutils.CellModelTableCellRenderer.DefaultCellModel; import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchWorker.DataFetchComponents; import org.sleuthkit.autopsy.datasourcesummary.uiutils.IngestRunningLabel; @@ -227,35 +226,30 @@ private static String getFormatted(Date date) { private final IngestRunningLabel ingestRunningLabel = new IngestRunningLabel(); private final List<DataFetchComponents<DataSource, ?>> dataFetchComponents; - private final TopProgramsSummary topProgramsData; - + private final UserActivitySummary userActivityData; + /** * Creates a new UserActivityPanel. */ public UserActivityPanel() { - this(new TopProgramsSummary(), new UserActivitySummary()); + this(new UserActivitySummary()); } /** * Creates a new UserActivityPanel. * - * @param topProgramsData Class from which to obtain top programs data. * @param userActivityData Class from which to obtain remaining user * activity data. */ - public UserActivityPanel( - TopProgramsSummary topProgramsData, - UserActivitySummary userActivityData) { - - super(topProgramsData, userActivityData); - - this.topProgramsData = topProgramsData; + public UserActivityPanel(UserActivitySummary userActivityData) { + super(userActivityData); + this.userActivityData = userActivityData; // set up data acquisition methods this.dataFetchComponents = Arrays.asList( // top programs query new DataFetchComponents<DataSource, List<TopProgramsResult>>( - (dataSource) -> topProgramsData.getTopPrograms(dataSource, TOP_PROGS_COUNT), + (dataSource) -> userActivityData.getTopPrograms(dataSource, TOP_PROGS_COUNT), (result) -> { showResultWithModuleCheck(topProgramsTable, result, IngestModuleCheckUtil.RECENT_ACTIVITY_FACTORY, @@ -307,7 +301,7 @@ public UserActivityPanel( * @return The underlying short folder name if one exists. */ private String getShortFolderName(String path, String appName) { - return this.topProgramsData.getShortFolderName(path, appName); + return this.userActivityData.getShortFolderName(path, appName); } @Override diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/BarChartPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/BarChartPanel.java new file mode 100644 index 0000000000000000000000000000000000000000..3f3822f7c2c597fbccf6e740682ded3cef0b2afd --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/BarChartPanel.java @@ -0,0 +1,307 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> 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.datasourcesummary.uiutils; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Font; +import java.util.Collections; +import java.util.List; +import javax.swing.JLabel; +import org.apache.commons.collections4.CollectionUtils; +import org.jfree.chart.ChartFactory; +import org.jfree.chart.ChartPanel; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.axis.ValueAxis; +import org.jfree.chart.plot.CategoryPlot; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.chart.renderer.category.BarRenderer; +import org.jfree.chart.renderer.category.StandardBarPainter; +import org.jfree.data.category.DefaultCategoryDataset; + +/** + * A bar chart panel. + */ +public class BarChartPanel extends AbstractLoadableComponent<List<BarChartPanel.BarChartSeries>> { + + /** + * Represents a series in a bar chart where all items pertain to one + * category. + */ + public static class BarChartSeries { + + private final Comparable<?> key; + private final Color color; + private final List<BarChartItem> items; + + /** + * Main constructor. + * + * @param color The color for this series. + * @param items The bars to be displayed for this series. + */ + public BarChartSeries(Comparable<?> key, Color color, List<BarChartItem> items) { + this.key = key; + this.color = color; + this.items = (items == null) ? Collections.emptyList() : Collections.unmodifiableList(items); + } + + /** + * @return The color for this series. + */ + public Color getColor() { + return color; + } + + /** + * @return The bars to be displayed for this series. + */ + public List<BarChartItem> getItems() { + return items; + } + + /** + * @return The key for this item. + */ + public Comparable<?> getKey() { + return key; + } + } + + /** + * An individual bar to be displayed in the bar chart. + */ + public static class BarChartItem { + + private final Comparable<?> key; + private final double value; + + /** + * Main constructor. + * + * @param label The key for this bar. Also serves as the label using + * toString(). + * @param value The value for this item. + */ + public BarChartItem(Comparable<?> key, double value) { + this.key = key; + this.value = value; + } + + /** + * @return The key for this item. + */ + public Comparable<?> getKey() { + return key; + } + + /** + * @return The value for this item. + */ + public double getValue() { + return value; + } + } + + /** + * JFreeChart bar charts don't preserve the order of bars provided to the + * chart, but instead uses the comparable nature to order items. This + * provides order using a provided index as well as the value for the axis. + */ + public static class OrderedKey implements Comparable<OrderedKey> { + + private final Object keyValue; + private final int keyIndex; + + /** + * Main constructor. + * + * @param keyValue The value for the key to be displayed in the domain + * axis. + * @param keyIndex The index at which it will be displayed. + */ + public OrderedKey(Object keyValue, int keyIndex) { + this.keyValue = keyValue; + this.keyIndex = keyIndex; + } + + /** + * @return The value for the key to be displayed in the domain axis. + */ + Object getKeyValue() { + return keyValue; + } + + /** + * @return The index at which it will be displayed. + */ + int getKeyIndex() { + return keyIndex; + } + + @Override + public int compareTo(OrderedKey o) { + // this will have a higher value than null. + if (o == null) { + return 1; + } + + // compare by index + return Integer.compare(this.getKeyIndex(), o.getKeyIndex()); + } + + @Override + public int hashCode() { + int hash = 3; + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final OrderedKey other = (OrderedKey) obj; + if (this.keyIndex != other.keyIndex) { + return false; + } + return true; + } + + @Override + public String toString() { + // use toString on the key. + return this.getKeyValue() == null ? null : this.getKeyValue().toString(); + } + } + + private static final long serialVersionUID = 1L; + + private static final Font DEFAULT_FONT = new JLabel().getFont(); + private static final Font DEFAULT_HEADER_FONT = new Font(DEFAULT_FONT.getName(), DEFAULT_FONT.getStyle(), (int) (DEFAULT_FONT.getSize() * 1.5)); + + private final ChartMessageOverlay overlay = new ChartMessageOverlay(); + private final DefaultCategoryDataset dataset = new DefaultCategoryDataset(); + private final JFreeChart chart; + private final CategoryPlot plot; + + /** + * Main constructor assuming null values for all items. + */ + public BarChartPanel() { + this(null, null, null); + } + + /** + * Main constructor for the pie chart. + * + * @param title The title for this pie chart. + * @param categoryLabel The x-axis label. + * @param valueLabel The y-axis label. + */ + public BarChartPanel(String title, String categoryLabel, String valueLabel) { + this.chart = ChartFactory.createStackedBarChart( + title, + categoryLabel, + valueLabel, + dataset, + PlotOrientation.VERTICAL, + true, false, false); + + // set style to match autopsy components + chart.setBackgroundPaint(null); + chart.getTitle().setFont(DEFAULT_HEADER_FONT); + + this.plot = ((CategoryPlot) chart.getPlot()); + this.plot.getRenderer().setBaseItemLabelFont(DEFAULT_FONT); + plot.setBackgroundPaint(null); + plot.setOutlinePaint(null); + + // hide y axis labels + ValueAxis range = plot.getRangeAxis(); + range.setVisible(false); + + // make sure x axis labels don't get cut off + plot.getDomainAxis().setMaximumCategoryLabelWidthRatio(10); + + ((BarRenderer) plot.getRenderer()).setBarPainter(new StandardBarPainter()); + + // Create Panel + ChartPanel panel = new ChartPanel(chart); + panel.addOverlay(overlay); + panel.setPopupMenu(null); + + this.setLayout(new BorderLayout()); + this.add(panel, BorderLayout.CENTER); + } + + /** + * @return The title for this chart if one exists. + */ + public String getTitle() { + return (this.chart == null || this.chart.getTitle() == null) + ? null + : this.chart.getTitle().getText(); + } + + /** + * Sets the title for this pie chart. + * + * @param title The title. + * + * @return As a utility, returns this. + */ + public BarChartPanel setTitle(String title) { + this.chart.getTitle().setText(title); + return this; + } + + @Override + protected void setMessage(boolean visible, String message) { + this.overlay.setVisible(visible); + this.overlay.setMessage(message); + } + + @Override + protected void setResults(List<BarChartPanel.BarChartSeries> data) { + this.dataset.clear(); + + if (CollectionUtils.isNotEmpty(data)) { + for (int s = 0; s < data.size(); s++) { + BarChartPanel.BarChartSeries series = data.get(s); + if (series != null && CollectionUtils.isNotEmpty(series.getItems())) { + if (series.getColor() != null) { + this.plot.getRenderer().setSeriesPaint(s, series.getColor()); + } + + for (int i = 0; i < series.getItems().size(); i++) { + BarChartItem bar = series.getItems().get(i); + this.dataset.setValue(bar.getValue(), series.getKey(), bar.getKey()); + } + } + } + } + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ChartMessageOverlay.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ChartMessageOverlay.java new file mode 100644 index 0000000000000000000000000000000000000000..2e21dfb796067b9f8d1c68acc23cc982186d104b --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ChartMessageOverlay.java @@ -0,0 +1,63 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> 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.datasourcesummary.uiutils; + +import java.awt.Graphics2D; +import org.jfree.chart.ChartPanel; +import org.jfree.chart.panel.AbstractOverlay; +import org.jfree.chart.panel.Overlay; + +/** + * A JFreeChart message overlay that can show a message for the purposes of the + * LoadableComponent. + */ +class ChartMessageOverlay extends AbstractOverlay implements Overlay { + + private static final long serialVersionUID = 1L; + private final BaseMessageOverlay overlay = new BaseMessageOverlay(); + + // multiply this value by the smaller dimension (height or width) of the component + // to determine width of text to be displayed. + private static final double MESSAGE_WIDTH_FACTOR = .6; + + /** + * Sets this layer visible when painted. In order to be shown in UI, this + * component needs to be repainted. + * + * @param visible Whether or not it is visible. + */ + void setVisible(boolean visible) { + overlay.setVisible(visible); + } + + /** + * Sets the message to be displayed in the child jlabel. + * + * @param message The message to be displayed. + */ + void setMessage(String message) { + overlay.setMessage(message); + } + + @Override + public void paintOverlay(Graphics2D gd, ChartPanel cp) { + int labelWidth = (int) (Math.min(cp.getWidth(), cp.getHeight()) * MESSAGE_WIDTH_FACTOR); + overlay.paintOverlay(gd, cp.getWidth(), cp.getHeight(), labelWidth); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DataFetchResult.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DataFetchResult.java index 93cc24f5feb55b93f1d65d7141644d08c7dae33a..6e2d8f49910aea5caf40660fc0f23aedafbf0b06 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DataFetchResult.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/DataFetchResult.java @@ -18,6 +18,8 @@ */ package org.sleuthkit.autopsy.datasourcesummary.uiutils; +import java.util.function.Function; + /** * The result of a loading process. */ @@ -30,6 +32,29 @@ public enum ResultType { SUCCESS, ERROR } + /** + * A utility method that, given an input data fetch result, creates an error + * result if the original is an error. Otherwise, uses the getSubResult + * function on the underlying data to create a new DataFetchResult. + * + * @param inputResult The input result. + * @param getSubComponent The means of getting the data given the original + * data. + * + * @return The new result with the error of the original or the processed + * data. + */ + public static <I, O> DataFetchResult<O> getSubResult(DataFetchResult<I> inputResult, Function<I, O> getSubResult) { + if (inputResult == null) { + return null; + } else if (inputResult.getResultType() == ResultType.SUCCESS) { + O innerData = (inputResult.getData() == null) ? null : getSubResult.apply(inputResult.getData()); + return DataFetchResult.getSuccessResult(innerData); + } else { + return DataFetchResult.getErrorResult(inputResult.getException()); + } + } + /** * Creates a DataFetchResult of loaded data including the data. * @@ -59,9 +84,8 @@ public static <R> DataFetchResult<R> getErrorResult(Throwable e) { /** * Main constructor for the DataLoadingResult. * - * @param state The state of the result. - * @param data If the result is SUCCESS, the data related to this - * result. + * @param state The state of the result. + * @param data If the result is SUCCESS, the data related to this result. * @param exception If the result is ERROR, the related exception. */ private DataFetchResult(ResultType state, R data, Throwable exception) { diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/LoadableLabel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/LoadableLabel.java new file mode 100644 index 0000000000000000000000000000000000000000..3fdf81ad1848ef5a2ec1eeee4d308236dec6ba89 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/LoadableLabel.java @@ -0,0 +1,63 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> 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.datasourcesummary.uiutils; + +import java.awt.BorderLayout; +import javax.swing.JLabel; +import org.apache.commons.lang3.StringUtils; + +/** + * A label that allows for displaying loading messages and can be used with a + * DataFetchResult. Text displays as "<key>:<value | message>". + */ +public class LoadableLabel extends AbstractLoadableComponent<String> { + + private static final long serialVersionUID = 1L; + + private final JLabel label = new JLabel(); + private final String key; + + /** + * Main constructor for the label. + * + * @param key The key to be displayed. + */ + public LoadableLabel(String key) { + this.key = key; + setLayout(new BorderLayout()); + add(label, BorderLayout.CENTER); + this.showResults(null); + } + + private void setValue(String value) { + String formattedKey = StringUtils.isBlank(key) ? "" : key; + String formattedValue = StringUtils.isBlank(value) ? "" : value; + label.setText(String.format("%s: %s", formattedKey, formattedValue)); + } + + @Override + protected void setMessage(boolean visible, String message) { + setValue(message); + } + + @Override + protected void setResults(String data) { + setValue(data); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/PieChartPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/PieChartPanel.java index 971cb83367200f2c22812c17d4769544e5d7d537..fa0d00dab66e1e629a31762479c303a11e8d556f 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/PieChartPanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/PieChartPanel.java @@ -21,7 +21,6 @@ import java.awt.BorderLayout; import java.awt.Color; import java.awt.Font; -import java.awt.Graphics2D; import java.text.DecimalFormat; import java.util.List; import javax.swing.JLabel; @@ -30,8 +29,6 @@ import org.jfree.chart.JFreeChart; import org.jfree.chart.labels.PieSectionLabelGenerator; import org.jfree.chart.labels.StandardPieSectionLabelGenerator; -import org.jfree.chart.panel.AbstractOverlay; -import org.jfree.chart.panel.Overlay; import org.jfree.chart.plot.PiePlot; import org.jfree.data.general.DefaultPieDataset; import org.openide.util.NbBundle.Messages; @@ -59,7 +56,7 @@ public static class PieChartItem { * @param label The label for this pie slice. * @param value The value for this item. * @param color The color for the pie slice. Can be null for - * auto-determined. + * auto-determined. */ public PieChartItem(String label, double value, Color color) { this.label = label; @@ -89,46 +86,6 @@ public Color getColor() { } } - /** - * A JFreeChart message overlay that can show a message for the purposes of - * the LoadableComponent. - */ - private static class MessageOverlay extends AbstractOverlay implements Overlay { - - private static final long serialVersionUID = 1L; - private final BaseMessageOverlay overlay = new BaseMessageOverlay(); - - // multiply this value by the smaller dimension (height or width) of the component - // to determine width of text to be displayed. - private static final double MESSAGE_WIDTH_FACTOR = .6; - - /** - * Sets this layer visible when painted. In order to be shown in UI, - * this component needs to be repainted. - * - * @param visible Whether or not it is visible. - */ - void setVisible(boolean visible) { - overlay.setVisible(visible); - } - - /** - * Sets the message to be displayed in the child jlabel. - * - * @param message The message to be displayed. - */ - void setMessage(String message) { - overlay.setMessage(message); - } - - @Override - public void paintOverlay(Graphics2D gd, ChartPanel cp) { - int labelWidth = (int) (Math.min(cp.getWidth(), cp.getHeight()) * MESSAGE_WIDTH_FACTOR); - overlay.paintOverlay(gd, cp.getWidth(), cp.getHeight(), labelWidth); - } - - } - private static final long serialVersionUID = 1L; private static final Font DEFAULT_FONT = new JLabel().getFont(); @@ -146,7 +103,7 @@ public void paintOverlay(Graphics2D gd, ChartPanel cp) { = new StandardPieSectionLabelGenerator( "{0}: {1} ({2})", new DecimalFormat("#,###"), new DecimalFormat("0.0%")); - private final MessageOverlay overlay = new MessageOverlay(); + private final ChartMessageOverlay overlay = new ChartMessageOverlay(); private final DefaultPieDataset dataset = new DefaultPieDataset(); private final JFreeChart chart; private final PiePlot plot; @@ -242,7 +199,7 @@ protected void setResults(List<PieChartPanel.PieChartItem> data) { /** * Shows a message on top of data. * - * @param data The data. + * @param data The data. * @param message The message. */ public synchronized void showDataWithMessage(List<PieChartPanel.PieChartItem> data, String message) { diff --git a/Core/src/org/sleuthkit/autopsy/discovery/search/DiscoveryEventUtils.java b/Core/src/org/sleuthkit/autopsy/discovery/search/DiscoveryEventUtils.java index e224c02d516bf74a82ce3a13262858a07e7d9605..0b9a0a98c1b893af6ad5bdde76f32e3365e6e9a6 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/search/DiscoveryEventUtils.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/DiscoveryEventUtils.java @@ -19,12 +19,14 @@ package org.sleuthkit.autopsy.discovery.search; import com.google.common.eventbus.EventBus; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import org.sleuthkit.autopsy.discovery.search.DiscoveryKeyUtils.GroupKey; import org.sleuthkit.autopsy.discovery.search.SearchData.Type; import org.sleuthkit.datamodel.AbstractFile; +import org.sleuthkit.datamodel.BlackboardArtifact; /** * Class to handle event bus and events for discovery tool. @@ -88,13 +90,13 @@ public ClearInstanceSelectionEvent() { //no arg constructor } } - + /** - * Event to signal that any background tasks currently running should - * be cancelled. + * Event to signal that any background tasks currently running should be + * cancelled. */ public static final class CancelBackgroundTasksEvent { - + public CancelBackgroundTasksEvent() { //no-arg constructor } @@ -124,6 +126,30 @@ public List<AbstractFile> getInstances() { } } + /** + * Event to signal that the list should be populated. + */ + public static final class PopulateDomainTabsEvent { + + private final String domain; + + /** + * Construct a new PopulateDomainTabsEvent. + */ + public PopulateDomainTabsEvent(String domain) { + this.domain = domain; + } + + /** + * Get the domain for the details area. + * + * @return The the domain for the details area. + */ + public String getDomain() { + return domain; + } + } + /** * Event to signal the completion of a search being performed. */ @@ -203,6 +229,47 @@ public ResultsSorter.SortingMethod getResultSort() { } + /** + * Event to signal the completion of a search being performed. + */ + public static final class ArtifactSearchResultEvent { + + private final List<BlackboardArtifact> listOfArtifacts = new ArrayList<>(); + private final BlackboardArtifact.ARTIFACT_TYPE artifactType; + + /** + * Construct a new ArtifactSearchResultEvent with a list of specified + * artifacts and an artifact type. + * + * @param artifactType The type of artifacts in the list. + * @param listOfArtifacts The list of artifacts retrieved. + */ + public ArtifactSearchResultEvent(BlackboardArtifact.ARTIFACT_TYPE artifactType, List<BlackboardArtifact> listOfArtifacts) { + if (listOfArtifacts != null) { + this.listOfArtifacts.addAll(listOfArtifacts); + } + this.artifactType = artifactType; + } + + /** + * Get the list of artifacts included in the event. + * + * @return The list of artifacts retrieved. + */ + public List<BlackboardArtifact> getListOfArtifacts() { + return Collections.unmodifiableList(listOfArtifacts); + } + + /** + * Get the type of BlackboardArtifact type of which exist in the list. + * + * @return The BlackboardArtifact type of which exist in the list. + */ + public BlackboardArtifact.ARTIFACT_TYPE getArtifactType() { + return artifactType; + } + } + /** * Event to signal the completion of page retrieval and include the page * contents. diff --git a/Core/src/org/sleuthkit/autopsy/discovery/search/SearchFiltering.java b/Core/src/org/sleuthkit/autopsy/discovery/search/SearchFiltering.java index 9505cfa5e285ee1c4a71a39485d5b05886e9d577..f39bb56634c43253db457ba9b11f5c329d64ef40 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/search/SearchFiltering.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/search/SearchFiltering.java @@ -33,6 +33,7 @@ import org.sleuthkit.datamodel.TskCoreException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; @@ -217,6 +218,15 @@ public ArtifactTypeFilter(List<ARTIFACT_TYPE> types) { this.types = types; } + /** + * Get the list of artifact types specified by the filter. + * + * @return The list of artifact types specified by the filter. + */ + public List<ARTIFACT_TYPE> getTypes() { + return Collections.unmodifiableList(types); + } + @Override public String getWhereClause() { StringJoiner joiner = new StringJoiner(","); diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/AbstractArtifactDetailsPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/AbstractArtifactDetailsPanel.java new file mode 100644 index 0000000000000000000000000000000000000000..b36e93728a93e4ae57136532b4f2eee9c9a11300 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/AbstractArtifactDetailsPanel.java @@ -0,0 +1,41 @@ +/* + * Autopsy + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> 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.discovery.ui; + +import javax.swing.JPanel; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; +import org.sleuthkit.datamodel.BlackboardArtifact; + +/** + * Class for ensuring all ArtifactDetailsPanels have a setArtifact method. + * + */ +public abstract class AbstractArtifactDetailsPanel extends JPanel { + + private static final long serialVersionUID = 1L; + + /** + * Called to display the contents of the given artifact. + * + * @param artifact the artifact to display. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + abstract public void setArtifact(BlackboardArtifact artifact); + +} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/AbstractDiscoveryFilterPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/AbstractDiscoveryFilterPanel.java index 3e4ab4559261f45bd37a0c69711629aba68a2154..7fc964ad52c8f9cda3d6ddcc9d2c639126f6ae4e 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/AbstractDiscoveryFilterPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/AbstractDiscoveryFilterPanel.java @@ -24,6 +24,7 @@ import javax.swing.JLabel; import javax.swing.JList; import javax.swing.event.ListSelectionListener; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; /** * Abstract class extending JPanel for filter controls. @@ -41,6 +42,7 @@ abstract class AbstractDiscoveryFilterPanel extends javax.swing.JPanel { * selected, null to indicate leaving selected items * unchanged or that there are no items to select. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) abstract void configurePanel(boolean selected, int[] indicesSelected); /** @@ -48,6 +50,7 @@ abstract class AbstractDiscoveryFilterPanel extends javax.swing.JPanel { * * @return The JCheckBox which enables and disables this filter. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) abstract JCheckBox getCheckbox(); /** @@ -57,6 +60,7 @@ abstract class AbstractDiscoveryFilterPanel extends javax.swing.JPanel { * @return The JList which contains the values available for selection for * this filter. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) abstract JList<?> getList(); /** @@ -65,6 +69,7 @@ abstract class AbstractDiscoveryFilterPanel extends javax.swing.JPanel { * * @return The JLabel to display under the JCheckBox. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) abstract JLabel getAdditionalLabel(); /** @@ -73,6 +78,7 @@ abstract class AbstractDiscoveryFilterPanel extends javax.swing.JPanel { * @return If the settings are invalid returns the error that has occurred, * otherwise returns empty string. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) abstract String checkForError(); /** @@ -82,6 +88,7 @@ abstract class AbstractDiscoveryFilterPanel extends javax.swing.JPanel { * @param actionlistener The listener for the checkbox selection events. * @param listListener The listener for the list selection events. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) void addListeners(ActionListener actionListener, ListSelectionListener listListener) { if (getCheckbox() != null) { getCheckbox().addActionListener(actionListener); @@ -97,11 +104,13 @@ void addListeners(ActionListener actionListener, ListSelectionListener listListe * @return The AbstractFilter for the selected settings, null if the * settings are not in use. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) abstract AbstractFilter getFilter(); /** * Remove listeners from the checkbox and the list if they exist. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) void removeListeners() { if (getCheckbox() != null) { for (ActionListener listener : getCheckbox().getActionListeners()) { diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/AbstractFiltersPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/AbstractFiltersPanel.java index 6e8137a6ed3c9bae05c28b9aeb4bdc9c4824fbed..c8f71a5d99f4224ba12aa7a2a66fe2a17224a1b6 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/AbstractFiltersPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/AbstractFiltersPanel.java @@ -32,6 +32,7 @@ import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import org.apache.commons.lang3.StringUtils; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.discovery.search.DiscoveryAttributes.GroupingAttributeType; import org.sleuthkit.autopsy.discovery.search.Group; import org.sleuthkit.autopsy.discovery.search.ResultsSorter.SortingMethod; @@ -65,6 +66,7 @@ abstract class AbstractFiltersPanel extends JPanel implements ActionListener, Li /** * Setup necessary for implementations of this abstract class. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) AbstractFiltersPanel() { firstColumnPanel.setLayout(new GridBagLayout()); secondColumnPanel.setLayout(new GridBagLayout()); @@ -75,6 +77,7 @@ abstract class AbstractFiltersPanel extends JPanel implements ActionListener, Li * * @return The type of results this panel filters. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) abstract SearchData.Type getType(); /** @@ -88,7 +91,8 @@ abstract class AbstractFiltersPanel extends JPanel implements ActionListener, Li * list, null if none are selected. * @param column The column to add the DiscoveryFilterPanel to. */ - final synchronized void addFilter(AbstractDiscoveryFilterPanel filterPanel, boolean isSelected, int[] indicesSelected, int column) { + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + final void addFilter(AbstractDiscoveryFilterPanel filterPanel, boolean isSelected, int[] indicesSelected, int column) { if (!isInitialized) { constraints.gridy = 0; constraints.anchor = GridBagConstraints.FIRST_LINE_START; @@ -132,6 +136,7 @@ final synchronized void addFilter(AbstractDiscoveryFilterPanel filterPanel, bool * * @param splitPane The JSplitPane which the columns are added to. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) final void addPanelsToScrollPane(JSplitPane splitPane) { splitPane.setLeftComponent(firstColumnPanel); splitPane.setRightComponent(secondColumnPanel); @@ -142,7 +147,8 @@ final void addPanelsToScrollPane(JSplitPane splitPane) { /** * Clear the filters from the panel */ - final synchronized void clearFilters() { + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + final void clearFilters() { for (AbstractDiscoveryFilterPanel filterPanel : filters) { filterPanel.removeListeners(); } @@ -159,6 +165,7 @@ final synchronized void clearFilters() { * column. * @param columnIndex The column to add the Component to. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) private void addToGridBagLayout(Component componentToAdd, Component additionalComponentToAdd, int columnIndex) { addToColumn(componentToAdd, columnIndex); if (additionalComponentToAdd != null) { @@ -174,6 +181,7 @@ private void addToGridBagLayout(Component componentToAdd, Component additionalCo * @param component The Component to add. * @param columnNumber The column to add the Component to. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) private void addToColumn(Component component, int columnNumber) { if (columnNumber == 0) { firstColumnPanel.add(component, constraints); @@ -186,7 +194,8 @@ private void addToColumn(Component component, int columnNumber) { * Check if the fields are valid, and fire a property change event to * indicate any errors. */ - synchronized void validateFields() { + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + void validateFields() { String errorString = null; for (AbstractDiscoveryFilterPanel filterPanel : filters) { errorString = filterPanel.checkForError(); @@ -197,6 +206,7 @@ synchronized void validateFields() { firePropertyChange("FilterError", null, errorString); } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override public void actionPerformed(ActionEvent e) { validateFields(); @@ -209,6 +219,7 @@ public void actionPerformed(ActionEvent e) { * * @return True if the ObjectsDetectedFilter is supported, false otherwise. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) boolean isObjectsFilterSupported() { for (AbstractDiscoveryFilterPanel filter : filters) { if (filter instanceof ObjectDetectedFilterPanel) { @@ -223,6 +234,7 @@ boolean isObjectsFilterSupported() { * * @return True if the HashSetFilter is supported, false otherwise. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) boolean isHashSetFilterSupported() { for (AbstractDiscoveryFilterPanel filter : filters) { if (filter instanceof HashSetFilterPanel) { @@ -237,6 +249,7 @@ boolean isHashSetFilterSupported() { * * @return True if the InterestingItemsFilter is supported, false otherwise. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) boolean isInterestingItemsFilterSupported() { for (AbstractDiscoveryFilterPanel filter : filters) { if (filter instanceof InterestingItemsFilterPanel) { @@ -251,8 +264,8 @@ boolean isInterestingItemsFilterSupported() { * * @return The list of filters selected by the user. */ - synchronized List<AbstractFilter> getFilters() { - + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + List<AbstractFilter> getFilters() { List<AbstractFilter> filtersToUse = new ArrayList<>(); if (getType() != SearchData.Type.DOMAIN) { //Domain type does not have a file type filtersToUse.add(new SearchFiltering.FileTypeFilter(getType())); @@ -268,6 +281,7 @@ synchronized List<AbstractFilter> getFilters() { return filtersToUse; } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override public void valueChanged(ListSelectionEvent evt) { if (!evt.getValueIsAdjusting()) { @@ -282,6 +296,7 @@ public void valueChanged(ListSelectionEvent evt) { * * @return The most recently used sorting method. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) SortingMethod getLastSortingMethod() { return lastSortingMethod; } @@ -291,6 +306,7 @@ SortingMethod getLastSortingMethod() { * * @param lastSortingMethod The most recently used sorting method. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) final void setLastSortingMethod(SortingMethod lastSortingMethod) { this.lastSortingMethod = lastSortingMethod; } @@ -300,6 +316,7 @@ final void setLastSortingMethod(SortingMethod lastSortingMethod) { * * @return The most recently used grouping attribute. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) GroupingAttributeType getLastGroupingAttributeType() { return lastGroupingAttributeType; } @@ -310,6 +327,7 @@ GroupingAttributeType getLastGroupingAttributeType() { * @param lastGroupingAttributeType The most recently used grouping * attribute. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) final void setLastGroupingAttributeType(GroupingAttributeType lastGroupingAttributeType) { this.lastGroupingAttributeType = lastGroupingAttributeType; } @@ -319,6 +337,7 @@ final void setLastGroupingAttributeType(GroupingAttributeType lastGroupingAttrib * * @return The most recently used group sorting algorithm. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) Group.GroupSortingAlgorithm getLastGroupSortingAlg() { return lastGroupSortingAlg; } @@ -329,6 +348,7 @@ Group.GroupSortingAlgorithm getLastGroupSortingAlg() { * @param lastGroupSortingAlg The most recently used group sorting * algorithm. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) final void setLastGroupSortingAlg(Group.GroupSortingAlgorithm lastGroupSortingAlg) { this.lastGroupSortingAlg = lastGroupSortingAlg; } diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/ArtifactTypeFilterPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/ArtifactTypeFilterPanel.java index 089c18bf13ca6d6571acc2a12116f3b72d5d435c..e6a1ccaaed101cc879220c1bb4cb76d3120e3201 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/ArtifactTypeFilterPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/ArtifactTypeFilterPanel.java @@ -26,6 +26,7 @@ import javax.swing.JLabel; import javax.swing.JList; import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.discovery.search.SearchData; import org.sleuthkit.autopsy.discovery.search.SearchFiltering.ArtifactTypeFilter; import org.sleuthkit.datamodel.BlackboardArtifact; @@ -40,6 +41,7 @@ class ArtifactTypeFilterPanel extends AbstractDiscoveryFilterPanel { /** * Creates new form ArtifactTypeFilterPanel */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) ArtifactTypeFilterPanel() { initComponents(); setUpArtifactTypeFilter(); @@ -49,6 +51,7 @@ class ArtifactTypeFilterPanel extends AbstractDiscoveryFilterPanel { /** * Initialize the data source filter. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) private void setUpArtifactTypeFilter() { int count = 0; DefaultListModel<ArtifactTypeItem> artifactTypeModel = (DefaultListModel<ArtifactTypeItem>) artifactList.getModel(); @@ -104,6 +107,7 @@ private void artifactTypeCheckboxActionPerformed(java.awt.event.ActionEvent evt) artifactList.setEnabled(artifactTypeCheckbox.isSelected()); }//GEN-LAST:event_artifactTypeCheckboxActionPerformed + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override void configurePanel(boolean selected, int[] indicesSelected) { artifactTypeCheckbox.setSelected(selected); @@ -119,11 +123,13 @@ void configurePanel(boolean selected, int[] indicesSelected) { } } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override JCheckBox getCheckbox() { return artifactTypeCheckbox; } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override JList<?> getList() { return artifactList; @@ -134,6 +140,7 @@ JLabel getAdditionalLabel() { return null; } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @NbBundle.Messages({"ArtifactTypeFilterPanel.selectionNeeded.text=At least one Result type must be selected."}) @Override String checkForError() { @@ -143,6 +150,7 @@ String checkForError() { return ""; } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override AbstractFilter getFilter() { if (artifactTypeCheckbox.isSelected() && !artifactList.getSelectedValuesList().isEmpty()) { diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/ArtifactsListPanel.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/ArtifactsListPanel.form new file mode 100644 index 0000000000000000000000000000000000000000..d5c94ead08fd41b2662ee0220975eb6f354c6455 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/ArtifactsListPanel.form @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.6" maxVersion="1.9" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> + <Properties> + <Property name="opaque" type="boolean" value="false"/> + </Properties> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="1"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> + </AuxValues> + + <Layout> + <DimensionLayout dim="0"> + <Group type="103" groupAlignment="0" attributes="0"> + <Component id="jScrollPane1" pref="400" max="32767" attributes="0"/> + </Group> + </DimensionLayout> + <DimensionLayout dim="1"> + <Group type="103" groupAlignment="0" attributes="0"> + <Component id="jScrollPane1" alignment="0" pref="607" max="32767" attributes="0"/> + </Group> + </DimensionLayout> + </Layout> + <SubComponents> + <Container class="javax.swing.JScrollPane" name="jScrollPane1"> + <Properties> + <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor"> + <Border info="null"/> + </Property> + </Properties> + <AuxValues> + <AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/> + <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.support.JScrollPaneSupportLayout"/> + <SubComponents> + <Component class="javax.swing.JTable" name="jTable1"> + <Properties> + <Property name="autoCreateRowSorter" type="boolean" value="true"/> + <Property name="model" type="javax.swing.table.TableModel" editor="org.netbeans.modules.form.RADConnectionPropertyEditor"> + <Connection code="tableModel" type="code"/> + </Property> + <Property name="selectionModel" type="javax.swing.ListSelectionModel" editor="org.netbeans.modules.form.editors2.JTableSelectionModelEditor"> + <JTableSelectionModel selectionMode="0"/> + </Property> + </Properties> + </Component> + </SubComponents> + </Container> + </SubComponents> +</Form> diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/ArtifactsListPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/ArtifactsListPanel.java new file mode 100644 index 0000000000000000000000000000000000000000..6235761d6e09276e74d447c6ef7df1d78f5b1ef2 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/ArtifactsListPanel.java @@ -0,0 +1,346 @@ +/* + * Autopsy + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> 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.discovery.ui; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import javax.swing.JPanel; +import javax.swing.event.ListSelectionListener; +import javax.swing.table.AbstractTableModel; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang.StringUtils; +import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Panel to display list of artifacts for selected domain. + * + */ +class ArtifactsListPanel extends JPanel { + + private static final long serialVersionUID = 1L; + private final DomainArtifactTableModel tableModel; + private static final Logger logger = Logger.getLogger(ArtifactsListPanel.class.getName()); + + /** + * Creates new form ArtifactsListPanel. + * + * @param artifactType The type of artifact displayed in this table. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + ArtifactsListPanel(BlackboardArtifact.ARTIFACT_TYPE artifactType) { + tableModel = new DomainArtifactTableModel(artifactType); + initComponents(); + jTable1.getRowSorter().toggleSortOrder(0); + jTable1.getRowSorter().toggleSortOrder(0); + } + + /** + * Add a listener to the table of artifacts to perform actions when an + * artifact is selected. + * + * @param listener The listener to add to the table of artifacts. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + void addSelectionListener(ListSelectionListener listener) { + jTable1.getSelectionModel().addListSelectionListener(listener); + } + + /** + * Remove a listener from the table of artifacts. + * + * @param listener The listener to remove from the table of artifacts. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + void removeListSelectionListener(ListSelectionListener listener) { + jTable1.getSelectionModel().removeListSelectionListener(listener); + } + + /** + * The artifact which is currently selected, null if no artifact is + * selected. + * + * @return The currently selected BlackboardArtifact or null if none is + * selected. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + BlackboardArtifact getSelectedArtifact() { + int selectedIndex = jTable1.getSelectionModel().getLeadSelectionIndex(); + if (selectedIndex < jTable1.getSelectionModel().getMinSelectionIndex() || jTable1.getSelectionModel().getMaxSelectionIndex() < 0 || selectedIndex > jTable1.getSelectionModel().getMaxSelectionIndex()) { + return null; + } + return tableModel.getArtifactByRow(jTable1.convertRowIndexToModel(selectedIndex)); + } + + /** + * Whether the list of artifacts is empty. + * + * @return true if the list of artifacts is empty, false if there are + * artifacts. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + boolean isEmpty() { + return tableModel.getRowCount() <= 0; + } + + /** + * Select the first available artifact in the list if it is not empty to + * populate the panel to the right. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + void selectFirst() { + if (!isEmpty()) { + jTable1.setRowSelectionInterval(0, 0); + } else { + jTable1.clearSelection(); + } + } + + /** + * Add the specified list of artifacts to the list of artifacts which should + * be displayed. + * + * @param artifactList The list of artifacts to display. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + void addArtifacts(List<BlackboardArtifact> artifactList) { + tableModel.setContents(artifactList); + jTable1.validate(); + jTable1.repaint(); + tableModel.fireTableDataChanged(); + } + + /** + * Remove all artifacts from the list of artifacts displayed. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + void clearArtifacts() { + tableModel.setContents(new ArrayList<>()); + tableModel.fireTableDataChanged(); + } + + /** + * 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. + */ + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + + javax.swing.JScrollPane jScrollPane1 = new javax.swing.JScrollPane(); + jTable1 = new javax.swing.JTable(); + + setOpaque(false); + + jScrollPane1.setBorder(null); + + jTable1.setAutoCreateRowSorter(true); + jTable1.setModel(tableModel); + jTable1.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); + jScrollPane1.setViewportView(jTable1); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 400, Short.MAX_VALUE) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 607, Short.MAX_VALUE) + ); + }// </editor-fold>//GEN-END:initComponents + + /** + * Table model which allows the artifact table in this panel to mimic a list + * of artifacts. + */ + private class DomainArtifactTableModel extends AbstractTableModel { + + private static final long serialVersionUID = 1L; + private final List<BlackboardArtifact> artifactList = new ArrayList<>(); + private final BlackboardArtifact.ARTIFACT_TYPE artifactType; + + /** + * Construct a new DomainArtifactTableModel. + * + * @param artifactType The type of artifact displayed in this table. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + DomainArtifactTableModel(BlackboardArtifact.ARTIFACT_TYPE artifactType) { + this.artifactType = artifactType; + } + + /** + * Set the list of artifacts which should be represented by this table + * model. + * + * @param artifacts The list of BlackboardArtifacts to represent. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + void setContents(List<BlackboardArtifact> artifacts) { + jTable1.clearSelection(); + artifactList.clear(); + artifactList.addAll(artifacts); + } + + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + @Override + public int getRowCount() { + return artifactList.size(); + } + + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + @Override + public int getColumnCount() { + if (artifactType == BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_CACHE) { + return 3; + } else { + return 2; + } + } + + /** + * Get the BlackboardArtifact at the specified row. + * + * @param rowIndex The row the artifact to return is at. + * + * @return The BlackboardArtifact at the specified row. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + BlackboardArtifact getArtifactByRow(int rowIndex) { + return artifactList.get(rowIndex); + } + + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + @NbBundle.Messages({"ArtifactsListPanel.value.noValue=No value available."}) + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + if (columnIndex < 2 || artifactType == BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_CACHE) { + try { + for (BlackboardAttribute bba : getArtifactByRow(rowIndex).getAttributes()) { + if (!StringUtils.isBlank(bba.getDisplayString())) { + String stringFromAttribute = getStringForColumn(bba, columnIndex); + if (!StringUtils.isBlank(stringFromAttribute)) { + return stringFromAttribute; + } + } + } + return getFallbackValue(rowIndex, columnIndex); + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "Error getting attributes for artifact " + getArtifactByRow(rowIndex).getArtifactID(), ex); + } + } + return Bundle.ArtifactsListPanel_value_noValue(); + } + + /** + * Get the appropriate String for the specified column from the + * BlackboardAttribute. + * + * @param bba The BlackboardAttribute which may contain a value. + * @param columnIndex The column the value will be displayed in. + * + * @return The value from the specified attribute which should be + * displayed in the specified column, null if the specified + * attribute does not contain a value for that column. + * + * @throws TskCoreException When unable to get abstract files based on + * the TSK_PATH_ID. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + private String getStringForColumn(BlackboardAttribute bba, int columnIndex) throws TskCoreException { + if (columnIndex == 0 && bba.getAttributeType().getTypeID() == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_ACCESSED.getTypeID()) { + return bba.getDisplayString(); + } else if (columnIndex == 1) { + if (artifactType == BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_DOWNLOAD || artifactType == BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_CACHE) { + if (bba.getAttributeType().getTypeID() == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH_ID.getTypeID()) { + return Case.getCurrentCase().getSleuthkitCase().getAbstractFileById(bba.getValueLong()).getName(); + } else if (bba.getAttributeType().getTypeID() == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH.getTypeID()) { + return FilenameUtils.getName(bba.getDisplayString()); + } + } else if (bba.getAttributeType().getTypeID() == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_TITLE.getTypeID()) { + return bba.getDisplayString(); + } + } else if (columnIndex == 2 && bba.getAttributeType().getTypeID() == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH_ID.getTypeID()) { + return Case.getCurrentCase().getSleuthkitCase().getAbstractFileById(bba.getValueLong()).getMIMEType(); + } + return null; + } + + /** + * Private helper method to use when the value we want for either date + * or title is not available. + * + * + * @param rowIndex The row the artifact to return is at. + * @param columnIndex The column index the attribute will be displayed + * at. + * + * @return A string that can be used in place of the accessed date time + * attribute title when they are not available. + * + * @throws TskCoreException + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + private String getFallbackValue(int rowIndex, int columnIndex) throws TskCoreException { + for (BlackboardAttribute bba : getArtifactByRow(rowIndex).getAttributes()) { + if (columnIndex == 0 && bba.getAttributeType().getTypeName().startsWith("TSK_DATETIME") && !StringUtils.isBlank(bba.getDisplayString())) { + return bba.getDisplayString(); + } else if (columnIndex == 1 && bba.getAttributeType().getTypeID() == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL.getTypeID() && !StringUtils.isBlank(bba.getDisplayString())) { + return bba.getDisplayString(); + } + } + return Bundle.ArtifactsListPanel_value_noValue(); + } + + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + @NbBundle.Messages({"ArtifactsListPanel.titleColumn.name=Title", + "ArtifactsListPanel.fileNameColumn.name=Name", + "ArtifactsListPanel.dateColumn.name=Date/Time", + "ArtifactsListPanel.mimeTypeColumn.name=MIME Type"}) + @Override + public String getColumnName(int column) { + switch (column) { + case 0: + return Bundle.ArtifactsListPanel_dateColumn_name(); + case 1: + if (artifactType == BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_CACHE || artifactType == BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_DOWNLOAD) { + return Bundle.ArtifactsListPanel_fileNameColumn_name(); + } else { + return Bundle.ArtifactsListPanel_titleColumn_name(); + } + case 2: + return Bundle.ArtifactsListPanel_mimeTypeColumn_name(); + default: + return ""; + } + } + } + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JTable jTable1; + // End of variables declaration//GEN-END:variables +} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/ArtifactsWorker.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/ArtifactsWorker.java new file mode 100644 index 0000000000000000000000000000000000000000..9a47e164cfa368473902c527c8e4355b1e89d0d5 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/ArtifactsWorker.java @@ -0,0 +1,80 @@ +/* + * Autopsy + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> 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.discovery.ui; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import javax.swing.SwingWorker; +import org.apache.commons.lang3.StringUtils; +import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.discovery.search.DiscoveryEventUtils; +import org.sleuthkit.autopsy.discovery.search.DomainSearch; +import org.sleuthkit.autopsy.discovery.search.DomainSearchArtifactsRequest; +import org.sleuthkit.datamodel.BlackboardArtifact; + +/** + * SwingWorker to retrieve a list of artifacts for a specified type and domain. + */ +class ArtifactsWorker extends SwingWorker<List<BlackboardArtifact>, Void> { + + private final BlackboardArtifact.ARTIFACT_TYPE artifactType; + private final static Logger logger = Logger.getLogger(ArtifactsWorker.class.getName()); + private final String domain; + + /** + * Construct a new ArtifactsWorker. + * + * @param artifactType The type of artifact being retrieved. + * @param domain The domain the artifacts should have as an attribute. + */ + ArtifactsWorker(BlackboardArtifact.ARTIFACT_TYPE artifactType, String domain) { + this.artifactType = artifactType; + this.domain = domain; + } + + @Override + protected List<BlackboardArtifact> doInBackground() throws Exception { + if (artifactType != null && !StringUtils.isBlank(domain)) { + DomainSearch domainSearch = new DomainSearch(); + return domainSearch.getArtifacts(new DomainSearchArtifactsRequest(Case.getCurrentCase().getSleuthkitCase(), domain, artifactType)); + } + return new ArrayList<>(); + } + + @Override + protected void done() { + List<BlackboardArtifact> listOfArtifacts = new ArrayList<>(); + if (!isCancelled()) { + try { + listOfArtifacts.addAll(get()); + } catch (InterruptedException | ExecutionException ex) { + logger.log(Level.SEVERE, "Exception while trying to get list of artifacts for Domain details for artifact type: " + + artifactType.getDisplayName() + " and domain: " + domain, ex); + } catch (CancellationException ignored) { + //Worker was cancelled after previously finishing its background work, exception ignored to cut down on non-helpful logging + } + } + DiscoveryEventUtils.getDiscoveryEventBus().post(new DiscoveryEventUtils.ArtifactSearchResultEvent(artifactType, listOfArtifacts)); + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/Bundle.properties b/Core/src/org/sleuthkit/autopsy/discovery/ui/Bundle.properties index 5fc4ff56e6fcaa8f3a314bd72972e0e084e66c84..851045c71aa72ae32db8e550329d9628e15379dc 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/Bundle.properties @@ -51,10 +51,12 @@ HashSetFilterPanel.hashSetCheckbox.text=Hash Set: PastOccurrencesFilterPanel.pastOccurrencesCheckbox.text=Past Occurrences: DocumentFilterPanel.documentsFiltersSplitPane.border.title=Step 2: Filter which documents to show ObjectDetectedFilterPanel.text=Object Detected: -DetailsPanel.instancesList.border.title=Instances DateFilterPanel.mostRecentRadioButton.text=Only last: DateFilterPanel.dateFilterCheckBox.text=Date Filter: DomainSummaryPanel.activityLabel.text= DomainSummaryPanel.pagesLabel.text= DomainSummaryPanel.filesDownloadedLabel.text= DomainSummaryPanel.totalVisitsLabel.text= +FileDetailsPanel.instancesList.border.title=Instances +CookieDetailsPanel.jLabel1.text=Artifact: +CookieDetailsPanel.jLabel2.text= diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/discovery/ui/Bundle.properties-MERGED index 7c2c5e6757e9470c0399e5a0585d7728a5547097..a5305503b5a04d6e376cd5f9a8e799d702e34b2e 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/Bundle.properties-MERGED @@ -1,3 +1,8 @@ +ArtifactsListPanel.dateColumn.name=Date/Time +ArtifactsListPanel.fileNameColumn.name=Name +ArtifactsListPanel.mimeTypeColumn.name=MIME Type +ArtifactsListPanel.titleColumn.name=Title +ArtifactsListPanel.value.noValue=No value available. ArtifactTypeFilterPanel.selectionNeeded.text=At least one Result type must be selected. CTL_OpenDiscoveryAction=Discovery DataSourceFilterPanel.error.text=At least one data source must be selected. @@ -126,13 +131,15 @@ HashSetFilterPanel.hashSetCheckbox.text=Hash Set: PastOccurrencesFilterPanel.pastOccurrencesCheckbox.text=Past Occurrences: DocumentFilterPanel.documentsFiltersSplitPane.border.title=Step 2: Filter which documents to show ObjectDetectedFilterPanel.text=Object Detected: -DetailsPanel.instancesList.border.title=Instances DateFilterPanel.mostRecentRadioButton.text=Only last: DateFilterPanel.dateFilterCheckBox.text=Date Filter: DomainSummaryPanel.activityLabel.text= DomainSummaryPanel.pagesLabel.text= DomainSummaryPanel.filesDownloadedLabel.text= DomainSummaryPanel.totalVisitsLabel.text= +FileDetailsPanel.instancesList.border.title=Instances +CookieDetailsPanel.jLabel1.text=Artifact: +CookieDetailsPanel.jLabel2.text= VideoThumbnailPanel.bytes.text=bytes VideoThumbnailPanel.deleted.text=All instances of file are deleted. VideoThumbnailPanel.gigaBytes.text=GB @@ -144,3 +151,7 @@ VideoThumbnailPanel.nameLabel.more.text=\ and {0} more # {1} - units VideoThumbnailPanel.sizeLabel.text=Size: {0} {1} VideoThumbnailPanel.terraBytes.text=TB +WebHistoryDetailsPanel.details.attrHeader=Attributes +WebHistoryDetailsPanel.details.dataSource=Data Source +WebHistoryDetailsPanel.details.file=File +WebHistoryDetailsPanel.details.sourceHeader=Source diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/ContentViewerDetailsPanel.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/ContentViewerDetailsPanel.form new file mode 100644 index 0000000000000000000000000000000000000000..2c7924e2a4178535a69140e08538958b1ff20ca0 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/ContentViewerDetailsPanel.form @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.4" maxVersion="1.9" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="1"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> + <AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,1,44,0,0,1,-112"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout"/> +</Form> diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/ContentViewerDetailsPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/ContentViewerDetailsPanel.java new file mode 100644 index 0000000000000000000000000000000000000000..81e4fcaddc68e17d246045389c606fae6721ebae --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/ContentViewerDetailsPanel.java @@ -0,0 +1,69 @@ +/* + * Autopsy + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> 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.discovery.ui; + +import org.openide.nodes.Node; +import org.sleuthkit.autopsy.corecomponents.DataContentPanel; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; +import org.sleuthkit.autopsy.datamodel.BlackboardArtifactNode; +import org.sleuthkit.datamodel.BlackboardArtifact; + +/** + * Details panel for displaying the collection of content viewers. + */ +final class ContentViewerDetailsPanel extends AbstractArtifactDetailsPanel { + + private static final long serialVersionUID = 1L; + private final DataContentPanel contentViewer = DataContentPanel.createInstance(); + + /** + * Creates new form ContentViewerDetailsPanel + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + ContentViewerDetailsPanel() { + initComponents(); + add(contentViewer); + } + + /** + * 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") + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + + setLayout(new java.awt.BorderLayout()); + }// </editor-fold>//GEN-END:initComponents + + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + @Override + public void setArtifact(BlackboardArtifact artifact) { + Node node = Node.EMPTY; + if (artifact != null) { + node = new BlackboardArtifactNode(artifact); + } + contentViewer.setNode(node); + } + + + // Variables declaration - do not modify//GEN-BEGIN:variables + // End of variables declaration//GEN-END:variables +} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/DataSourceFilterPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/DataSourceFilterPanel.java index ab54df4341216ba3d99e985bc093274824efd9f0..065c989ddbb6643a30315f7035f13b5253146409 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/DataSourceFilterPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DataSourceFilterPanel.java @@ -30,6 +30,7 @@ import org.openide.util.NbBundle; import org.sleuthkit.autopsy.casemodule.Case; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.discovery.search.SearchFiltering; import org.sleuthkit.datamodel.DataSource; import org.sleuthkit.datamodel.TskCoreException; @@ -45,6 +46,7 @@ final class DataSourceFilterPanel extends AbstractDiscoveryFilterPanel { /** * Creates new form DataSourceFilterPanel. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) DataSourceFilterPanel() { initComponents(); setUpDataSourceFilter(); @@ -109,6 +111,7 @@ private void dataSourceCheckboxActionPerformed(java.awt.event.ActionEvent evt) { private javax.swing.JScrollPane dataSourceScrollPane; // End of variables declaration//GEN-END:variables + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override void configurePanel(boolean selected, int[] indicesSelected) { dataSourceCheckbox.setSelected(selected); @@ -124,6 +127,7 @@ void configurePanel(boolean selected, int[] indicesSelected) { } } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override JCheckBox getCheckbox() { return dataSourceCheckbox; @@ -137,6 +141,7 @@ JLabel getAdditionalLabel() { /** * Initialize the data source filter. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) private void setUpDataSourceFilter() { int count = 0; try { @@ -156,6 +161,7 @@ private void setUpDataSourceFilter() { } } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override JList<?> getList() { return dataSourceList; @@ -193,6 +199,7 @@ public String toString() { } } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @NbBundle.Messages({"DataSourceFilterPanel.error.text=At least one data source must be selected."}) @Override String checkForError() { @@ -202,6 +209,7 @@ String checkForError() { return ""; } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override AbstractFilter getFilter() { if (dataSourceCheckbox.isSelected()) { diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/DateFilterPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/DateFilterPanel.java index 528a5d1bc290980661f6e6fcb38004ade4899900..2fbe6610ff02ad2673126eec7042c56e77515386 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/DateFilterPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DateFilterPanel.java @@ -33,6 +33,7 @@ import javax.swing.event.ListSelectionListener; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.communications.Utils; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.discovery.search.SearchFiltering; /** @@ -48,6 +49,7 @@ class DateFilterPanel extends AbstractDiscoveryFilterPanel { */ @NbBundle.Messages({"# {0} - timeZone", "DateFilterPanel.dateRange.text=Date Range ({0}):"}) + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) DateFilterPanel() { initComponents(); rangeRadioButton.setText(Bundle.DateFilterPanel_dateRange_text(Utils.getUserPreferredZoneId().toString())); @@ -225,6 +227,7 @@ private void rangeRadioButtonStateChanged(javax.swing.event.ChangeEvent evt) {// endCheckBox.firePropertyChange("EndButtonChange", true, false); }//GEN-LAST:event_rangeRadioButtonStateChanged + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override void configurePanel(boolean selected, int[] indicesSelected) { dateFilterCheckBox.setSelected(selected); @@ -238,6 +241,7 @@ void configurePanel(boolean selected, int[] indicesSelected) { } } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override JCheckBox getCheckbox() { return dateFilterCheckBox; @@ -253,6 +257,7 @@ JLabel getAdditionalLabel() { return null; } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override void addListeners(ActionListener actionListener, ListSelectionListener listListener) { dateFilterCheckBox.addActionListener(actionListener); @@ -274,6 +279,7 @@ public void dateChanged(DateChangeEvent event) { }); } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override void removeListeners() { for (ActionListener listener : dateFilterCheckBox.getActionListeners()) { @@ -302,6 +308,7 @@ void removeListeners() { } } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @NbBundle.Messages({"DateFilterPanel.invalidRange.text=Range or Only Last must be selected.", "DateFilterPanel.startOrEndNeeded.text=A start or end date must be specified to use the range filter.", "DateFilterPanel.startAfterEnd.text=Start date should be before the end date when both are enabled."}) @@ -320,6 +327,7 @@ String checkForError() { return ""; } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override AbstractFilter getFilter() { if (dateFilterCheckBox.isSelected()) { diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryDialog.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryDialog.java index f2f42bd1134576618ace5f9d15be54fc7d7e195d..565dad8a88184fb13c44035fca4bcfc9a229d1d7 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryDialog.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryDialog.java @@ -40,6 +40,7 @@ import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoException; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.discovery.search.DiscoveryAttributes; import org.sleuthkit.autopsy.discovery.search.DiscoveryEventUtils; import org.sleuthkit.autopsy.discovery.search.Group; @@ -99,6 +100,7 @@ static synchronized DiscoveryDialog getDiscoveryDialogInstance() { /** * Private constructor to construct a new DiscoveryDialog */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Messages("DiscoveryDialog.name.text=Discovery") private DiscoveryDialog() { super(WindowManager.getDefault().getMainWindow(), Bundle.DiscoveryDialog_name_text(), true); @@ -151,6 +153,7 @@ public void itemStateChanged(ItemEvent event) { /** * Update the search settings to a default state. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) void updateSearchSettings() { removeAllPanels(); imageFilterPanel = null; @@ -176,6 +179,7 @@ void updateSearchSettings() { /** * Set the type buttons to a default state where none are selected. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) private void unselectAllButtons() { imagesButton.setSelected(false); imagesButton.setEnabled(true); @@ -194,6 +198,7 @@ private void unselectAllButtons() { /** * Private helper method to perform update of comboboxes update. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) private void updateComboBoxes() { // Set up the grouping attributes List<GroupingAttributeType> groupingAttrs = new ArrayList<>(); @@ -230,6 +235,7 @@ private void updateComboBoxes() { * * @return The panel that corresponds to the currently selected type. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) private AbstractFiltersPanel getSelectedFilterPanel() { switch (type) { case IMAGE: @@ -251,6 +257,7 @@ private AbstractFiltersPanel getSelectedFilterPanel() { * * @param type The Type of GroupingAttribute to add. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) private void addTypeToGroupByComboBox(GroupingAttributeType type) { switch (type) { case FREQUENCY: @@ -282,7 +289,8 @@ private void addTypeToGroupByComboBox(GroupingAttributeType type) { /** * Validate the filter settings for File type filters. */ - synchronized void validateDialog() { + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + void validateDialog() { AbstractFiltersPanel panel = getSelectedFilterPanel(); if (panel != null) { panel.validateFields(); @@ -551,6 +559,7 @@ private void documentsButtonActionPerformed(java.awt.event.ActionEvent evt) {//G /** * Helper method to remove all filter panels and their listeners */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) private void removeAllPanels() { if (imageFilterPanel != null) { remove(imageFilterPanel); @@ -635,6 +644,7 @@ private void domainsButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN repaint(); }//GEN-LAST:event_domainsButtonActionPerformed + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override public void dispose() { setVisible(false); @@ -643,6 +653,7 @@ public void dispose() { /** * Cancel the searchWorker if it exists. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) void cancelSearch() { if (searchWorker != null) { searchWorker.cancel(true); @@ -656,6 +667,7 @@ void cancelSearch() { * @param error The error message to display, empty string if there is no * error. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) private void setValid(String error) { if (StringUtils.isBlank(error)) { errorLabel.setText(""); diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryThumbnailChildren.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryThumbnailChildren.java index dce68e3a41b6a1bd520b385445d90f9ba3e72dda..229c7510e370831b2949a2e984b6057520c3e78d 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryThumbnailChildren.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryThumbnailChildren.java @@ -27,6 +27,7 @@ import org.openide.nodes.Node; import org.openide.nodes.Sheet; import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.datamodel.AbstractAbstractFileNode; import org.sleuthkit.autopsy.datamodel.FileNode; import org.sleuthkit.datamodel.AbstractFile; @@ -42,28 +43,27 @@ class DiscoveryThumbnailChildren extends Children.Keys<AbstractFile> { /* * Creates the list of thumbnails from the given list of AbstractFiles. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) DiscoveryThumbnailChildren(List<AbstractFile> files) { super(false); - this.files = files; - } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override protected Node[] createNodes(AbstractFile t) { return new Node[]{new ThumbnailNode(t)}; } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override protected void addNotify() { super.addNotify(); - Set<AbstractFile> thumbnails = new TreeSet<>((AbstractFile file1, AbstractFile file2) -> { int result = Long.compare(file1.getSize(), file2.getSize()); if (result == 0) { result = file1.getName().compareTo(file2.getName()); } - return result; }); thumbnails.addAll(files); @@ -75,10 +75,12 @@ protected void addNotify() { */ static class ThumbnailNode extends FileNode { + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) ThumbnailNode(AbstractFile file) { super(file, false); } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override protected Sheet createSheet() { Sheet sheet = super.createSheet(); diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryTopComponent.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryTopComponent.form index 54630599ec188533000e2ceebf07085dfe1f8b1c..c84525148c2710bfe1725d99fd6768beece3c73c 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryTopComponent.form +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryTopComponent.form @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8" ?> -<Form version="1.4" maxVersion="1.9" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> +<Form version="1.5" maxVersion="1.9" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> <Properties> <Property name="minimumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> <Dimension value="[199, 200]"/> diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryTopComponent.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryTopComponent.java index 03a271dcdd00878b9cc69c417a0a19f4cfc39a4d..371f329f0047bd6a0532285e604d44b7b76ddc29 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryTopComponent.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryTopComponent.java @@ -28,6 +28,7 @@ import java.util.List; import java.util.stream.Collectors; import javax.swing.JSplitPane; +import javax.swing.SwingUtilities; import javax.swing.plaf.basic.BasicSplitPaneDivider; import javax.swing.plaf.basic.BasicSplitPaneUI; import org.openide.util.NbBundle; @@ -40,6 +41,8 @@ import org.sleuthkit.autopsy.discovery.search.DiscoveryEventUtils; import org.sleuthkit.autopsy.discovery.search.SearchData.Type; import static org.sleuthkit.autopsy.discovery.search.SearchData.Type.DOMAIN; +import org.sleuthkit.autopsy.discovery.search.SearchFiltering.ArtifactTypeFilter; +import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE; /** * Create a dialog for displaying the Discovery results. @@ -55,8 +58,8 @@ public final class DiscoveryTopComponent extends TopComponent { private static final int ANIMATION_INCREMENT = 30; private volatile static int resultsAreaSize = 250; private final GroupListPanel groupListPanel; - private final DetailsPanel detailsPanel; private final ResultsPanel resultsPanel; + private String selectedDomainTabName; private Type searchType; private int dividerLocation = -1; private SwingAnimator animator = null; @@ -70,10 +73,7 @@ public DiscoveryTopComponent() { setName(Bundle.DiscoveryTopComponent_name()); groupListPanel = new GroupListPanel(); resultsPanel = new ResultsPanel(); - detailsPanel = new DetailsPanel(); mainSplitPane.setLeftComponent(groupListPanel); - rightSplitPane.setTopComponent(resultsPanel); - rightSplitPane.setBottomComponent(detailsPanel); //set color of divider rightSplitPane.setUI(new BasicSplitPaneUI() { @Override @@ -95,6 +95,7 @@ public void propertyChange(PropertyChangeEvent evt) { } } }); + rightSplitPane.setTopComponent(resultsPanel); } /** @@ -108,6 +109,7 @@ private final class BasicSplitPaneDividerImpl extends BasicSplitPaneDivider { * @param ui The component which contains the split pane this divider is * in. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) BasicSplitPaneDividerImpl(BasicSplitPaneUI ui) { super(ui); this.setLayout(new BorderLayout()); @@ -129,11 +131,13 @@ public static DiscoveryTopComponent getTopComponent() { /** * Reset the top component so it isn't displaying any results. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) public void resetTopComponent() { resultsPanel.resetResultViewer(); groupListPanel.resetGroupList(); } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override public void componentOpened() { super.componentOpened(); @@ -141,9 +145,9 @@ public void componentOpened() { DiscoveryEventUtils.getDiscoveryEventBus().register(this); DiscoveryEventUtils.getDiscoveryEventBus().register(resultsPanel); DiscoveryEventUtils.getDiscoveryEventBus().register(groupListPanel); - DiscoveryEventUtils.getDiscoveryEventBus().register(detailsPanel); } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override protected void componentClosed() { DiscoveryDialog.getDiscoveryDialogInstance().cancelSearch(); @@ -152,7 +156,10 @@ protected void componentClosed() { DiscoveryEventUtils.getDiscoveryEventBus().unregister(this); DiscoveryEventUtils.getDiscoveryEventBus().unregister(groupListPanel); DiscoveryEventUtils.getDiscoveryEventBus().unregister(resultsPanel); - DiscoveryEventUtils.getDiscoveryEventBus().unregister(detailsPanel); + DiscoveryEventUtils.getDiscoveryEventBus().unregister(rightSplitPane.getBottomComponent()); + if (rightSplitPane.getBottomComponent() instanceof DomainDetailsPanel) { + selectedDomainTabName = ((DomainDetailsPanel) rightSplitPane.getBottomComponent()).getSelectedTabName(); + } super.componentClosed(); } @@ -262,7 +269,7 @@ public List<Mode> availableModes(List<Mode> modes) { */ @Subscribe void handleDetailsVisibleEvent(DiscoveryEventUtils.DetailsVisibleEvent detailsVisibleEvent) { - if (resultsPanel.getActiveType() != DOMAIN) { + SwingUtilities.invokeLater(() -> { if (animator != null && animator.isRunning()) { animator.stop(); animator = null; @@ -274,7 +281,7 @@ void handleDetailsVisibleEvent(DiscoveryEventUtils.DetailsVisibleEvent detailsVi animator = new SwingAnimator(new HideDetailsAreaCallback()); } animator.start(); - } + }); } /** @@ -289,12 +296,12 @@ void handleDetailsVisibleEvent(DiscoveryEventUtils.DetailsVisibleEvent detailsVi "DiscoveryTopComponent.searchError.text=Error no type specified for search."}) @Subscribe void handleSearchStartedEvent(DiscoveryEventUtils.SearchStartedEvent searchStartedEvent) { - newSearchButton.setText(Bundle.DiscoveryTopComponent_cancelButton_text()); - progressMessageTextArea.setForeground(Color.red); - searchType = searchStartedEvent.getType(); - progressMessageTextArea.setText(Bundle.DiscoveryTopComponent_searchInProgress_text(searchType.name())); - rightSplitPane.getComponent(1).setVisible(searchStartedEvent.getType() != DOMAIN); - rightSplitPane.getComponent(2).setVisible(searchStartedEvent.getType() != DOMAIN); + SwingUtilities.invokeLater(() -> { + newSearchButton.setText(Bundle.DiscoveryTopComponent_cancelButton_text()); + progressMessageTextArea.setForeground(Color.red); + searchType = searchStartedEvent.getType(); + progressMessageTextArea.setText(Bundle.DiscoveryTopComponent_searchInProgress_text(searchType.name())); + }); } /** @@ -310,19 +317,51 @@ void handleSearchStartedEvent(DiscoveryEventUtils.SearchStartedEvent searchStart "DiscoveryTopComponent.domainSearch.text=Type: Domain", "DiscoveryTopComponent.additionalFilters.text=; "}) void handleSearchCompleteEvent(DiscoveryEventUtils.SearchCompleteEvent searchCompleteEvent) { - newSearchButton.setText(Bundle.DiscoveryTopComponent_newSearch_text()); - progressMessageTextArea.setForeground(Color.black); - String descriptionText = ""; - if (searchType == DOMAIN) { - //domain does not have a file type filter to add the type information so it is manually added - descriptionText = Bundle.DiscoveryTopComponent_domainSearch_text(); - if (!searchCompleteEvent.getFilters().isEmpty()) { - descriptionText += Bundle.DiscoveryTopComponent_additionalFilters_text(); + SwingUtilities.invokeLater(() -> { + newSearchButton.setText(Bundle.DiscoveryTopComponent_newSearch_text()); + progressMessageTextArea.setForeground(Color.black); + String descriptionText = ""; + if (searchType == DOMAIN) { + //domain does not have a file type filter to add the type information so it is manually added + descriptionText = Bundle.DiscoveryTopComponent_domainSearch_text(); + if (!searchCompleteEvent.getFilters().isEmpty()) { + descriptionText += Bundle.DiscoveryTopComponent_additionalFilters_text(); + } + selectedDomainTabName = validateLastSelectedType(searchCompleteEvent); + rightSplitPane.setBottomComponent(new DomainDetailsPanel(selectedDomainTabName)); + } else { + rightSplitPane.setBottomComponent(new FileDetailsPanel()); + } + DiscoveryEventUtils.getDiscoveryEventBus().register(rightSplitPane.getBottomComponent()); + descriptionText += searchCompleteEvent.getFilters().stream().map(AbstractFilter::getDesc).collect(Collectors.joining("; ")); + progressMessageTextArea.setText(Bundle.DiscoveryTopComponent_searchComplete_text(descriptionText)); + progressMessageTextArea.setCaretPosition(0); + }); + } + + /** + * Get the name of the tab which was last selected unless the tab last + * selected would not be included in the types currently being displayed or + * was not previously set. + * + * @return The name of the tab which should be selected in the new + * DomainDetailsPanel. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + private String validateLastSelectedType(DiscoveryEventUtils.SearchCompleteEvent searchCompleteEvent) { + String typeFilteredOn = selectedDomainTabName; + for (AbstractFilter filter : searchCompleteEvent.getFilters()) { + if (filter instanceof ArtifactTypeFilter) { + for (ARTIFACT_TYPE type : ((ArtifactTypeFilter) filter).getTypes()) { + typeFilteredOn = type.getDisplayName(); + if (selectedDomainTabName == null || typeFilteredOn.equalsIgnoreCase(selectedDomainTabName)) { + break; + } + } + break; } } - descriptionText += searchCompleteEvent.getFilters().stream().map(AbstractFilter::getDesc).collect(Collectors.joining("; ")); - progressMessageTextArea.setText(Bundle.DiscoveryTopComponent_searchComplete_text(descriptionText)); - progressMessageTextArea.setCaretPosition(0); + return typeFilteredOn; } /** @@ -334,9 +373,11 @@ void handleSearchCompleteEvent(DiscoveryEventUtils.SearchCompleteEvent searchCom @Messages({"DiscoveryTopComponent.searchCancelled.text=Search has been cancelled."}) @Subscribe void handleSearchCancelledEvent(DiscoveryEventUtils.SearchCancelledEvent searchCancelledEvent) { - newSearchButton.setText(Bundle.DiscoveryTopComponent_newSearch_text()); - progressMessageTextArea.setForeground(Color.red); - progressMessageTextArea.setText(Bundle.DiscoveryTopComponent_searchCancelled_text()); + SwingUtilities.invokeLater(() -> { + newSearchButton.setText(Bundle.DiscoveryTopComponent_newSearch_text()); + progressMessageTextArea.setForeground(Color.red); + progressMessageTextArea.setText(Bundle.DiscoveryTopComponent_searchCancelled_text()); + }); } @@ -345,12 +386,14 @@ void handleSearchCancelledEvent(DiscoveryEventUtils.SearchCancelledEvent searchC */ private final class ShowDetailsAreaCallback implements SwingAnimatorCallback { + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override public void callback(Object caller) { dividerLocation -= ANIMATION_INCREMENT; repaint(); } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override public boolean hasTerminated() { if (dividerLocation != JSplitPane.UNDEFINED_CONDITION && dividerLocation < resultsAreaSize) { @@ -368,12 +411,14 @@ public boolean hasTerminated() { */ private final class HideDetailsAreaCallback implements SwingAnimatorCallback { + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override public void callback(Object caller) { dividerLocation += ANIMATION_INCREMENT; repaint(); } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override public boolean hasTerminated() { if (dividerLocation > rightSplitPane.getHeight() || dividerLocation == JSplitPane.UNDEFINED_CONDITION) { @@ -399,6 +444,7 @@ private final class AnimatedSplitPane extends JSplitPane { private static final long serialVersionUID = 1L; + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override public void paintComponent(Graphics g) { if (animator != null && animator.isRunning() && (dividerLocation == JSplitPane.UNDEFINED_CONDITION diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryUiUtils.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryUiUtils.java index a1a15616e735ec668d087bd50fc0b2ae55f3fb0a..7b4287a0b6a9f5fbcaa63c16485ba7ba999fa5ae 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryUiUtils.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DiscoveryUiUtils.java @@ -51,6 +51,7 @@ import org.sleuthkit.autopsy.corelibs.ScalrWrapper; import org.sleuthkit.autopsy.coreutils.ImageUtils; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; import static org.sleuthkit.autopsy.coreutils.VideoUtils.getVideoFileInTempDir; import org.sleuthkit.autopsy.datamodel.ContentUtils; import org.sleuthkit.autopsy.discovery.search.ResultFile; @@ -173,6 +174,7 @@ static List<String> getSetNames(BlackboardArtifact.ARTIFACT_TYPE artifactType, B * * @return True if the point is over the icon, false otherwise. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) static boolean isPointOnIcon(Component comp, Point point) { return comp instanceof JComponent && point.x >= comp.getX() && point.x <= comp.getX() + ICON_SIZE && point.y >= comp.getY() && point.y <= comp.getY() + ICON_SIZE; } @@ -186,6 +188,7 @@ static boolean isPointOnIcon(Component comp, Point point) { * @param isDeletedLabel The label to set the icon and tooltip for. */ @NbBundle.Messages({"DiscoveryUiUtils.isDeleted.text=All instances of file are deleted."}) + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) static void setDeletedIcon(boolean isDeleted, javax.swing.JLabel isDeletedLabel) { if (isDeleted) { isDeletedLabel.setIcon(DELETED_ICON); @@ -203,6 +206,7 @@ static void setDeletedIcon(boolean isDeleted, javax.swing.JLabel isDeletedLabel) * score of. * @param scoreLabel The label to set the icon and tooltip for. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) static void setScoreIcon(ResultFile resultFile, javax.swing.JLabel scoreLabel) { switch (resultFile.getScore()) { case NOTABLE_SCORE: @@ -232,6 +236,7 @@ static int getIconSize() { * Helper method to display an error message when the results of the * Discovery Top component may be incomplete. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @NbBundle.Messages({"DiscoveryUiUtils.resultsIncomplete.text=Discovery results may be incomplete"}) static void displayErrorMessage(DiscoveryDialog dialog) { //check if modules run and assemble message diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentFilterPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentFilterPanel.java index 44e303ccc1356329bb0fbec84b01c2191c4a2e5a..fe2dfe4b1f8e898842d86a0bf2aab05e760b744b 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentFilterPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentFilterPanel.java @@ -19,6 +19,7 @@ package org.sleuthkit.autopsy.discovery.ui; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.discovery.search.SearchData; /** @@ -32,6 +33,7 @@ final class DocumentFilterPanel extends AbstractFiltersPanel { /** * Constructs a new DocumentFilterPanel. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) DocumentFilterPanel() { super(); initComponents(); diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentPanel.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentPanel.form index 9329d6f9762b706f34b182012fe52a0afbfba34f..7f61fbd6fb93767da141fa5e7d71ae3d6464ee30 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentPanel.form +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentPanel.form @@ -79,9 +79,6 @@ <Property name="icon" type="javax.swing.Icon" editor="org.netbeans.modules.form.editors2.IconEditor"> <Image iconType="3" name="/org/sleuthkit/autopsy/images/file-icon-deleted.png"/> </Property> - <Property name="toolTipText" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> - <ResourceString bundle="org/sleuthkit/autopsy/discovery/ui/Bundle.properties" key="DocumentPanel.isDeletedLabel.toolTipText" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> - </Property> <Property name="maximumSize" type="java.awt.Dimension" editor="org.netbeans.modules.form.RADConnectionPropertyEditor"> <Connection code="new Dimension(org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize(),org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize())" type="code"/> </Property> @@ -91,6 +88,9 @@ <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.modules.form.RADConnectionPropertyEditor"> <Connection code="new Dimension(org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize(),org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize())" type="code"/> </Property> + <Property name="toolTipText" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="org/sleuthkit/autopsy/discovery/ui/Bundle.properties" key="DocumentPanel.isDeletedLabel.toolTipText" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> + </Property> </Properties> </Component> <Component class="javax.swing.JLabel" name="scoreLabel"> @@ -98,7 +98,6 @@ <Property name="icon" type="javax.swing.Icon" editor="org.netbeans.modules.form.editors2.IconEditor"> <Image iconType="3" name="/org/sleuthkit/autopsy/images/red-circle-exclamation.png"/> </Property> - <Property name="toolTipText" type="java.lang.String" value=""/> <Property name="maximumSize" type="java.awt.Dimension" editor="org.netbeans.modules.form.RADConnectionPropertyEditor"> <Connection code="new Dimension(org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize(),org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize())" type="code"/> </Property> @@ -108,6 +107,7 @@ <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.modules.form.RADConnectionPropertyEditor"> <Connection code="new Dimension(org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize(),org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize())" type="code"/> </Property> + <Property name="toolTipText" type="java.lang.String" value=""/> </Properties> </Component> <Component class="javax.swing.JLabel" name="fileSizeLabel"> diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentPanel.java index 08bdd70791817c6aa0a13e2fc9ae0bb811973b63..a0c37e8639564b699dbe07b7abeeeec046e04dd2 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentPanel.java @@ -29,6 +29,7 @@ import javax.swing.ListCellRenderer; import org.openide.util.NbBundle.Messages; import org.sleuthkit.autopsy.corecomponents.AutoWrappingJTextPane; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.discovery.search.SearchData; /** @@ -43,6 +44,7 @@ class DocumentPanel extends javax.swing.JPanel implements ListCellRenderer<Docum /** * Creates new form DocumentPanel. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) DocumentPanel() { initComponents(); } @@ -150,7 +152,7 @@ private void initComponents() { "DocumentPanel.numberOfImages.text=1 of {0} images", "DocumentPanel.numberOfImages.noImages=No images", "DocumentPanel.noImageExtraction.text=0 of ? images"}) - + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override public Component getListCellRendererComponent(JList<? extends DocumentWrapper> list, DocumentWrapper value, int index, boolean isSelected, boolean cellHasFocus) { fileSizeLabel.setText(DiscoveryUiUtils.getFileSizeString(value.getResultFile().getFirstInstance().getSize())); @@ -180,6 +182,7 @@ public Component getListCellRendererComponent(JList<? extends DocumentWrapper> l return this; } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override public String getToolTipText(MouseEvent event) { if (event != null) { diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentPreviewViewer.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentPreviewViewer.java index 0ef1173c0e63450c1a72e933195f64f57707e2c4..505fddf437f4965764489eea264ae05760a6b28b 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentPreviewViewer.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DocumentPreviewViewer.java @@ -22,6 +22,7 @@ import java.util.List; import javax.swing.DefaultListModel; import javax.swing.event.ListSelectionListener; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.datamodel.AbstractFile; /** @@ -35,6 +36,7 @@ final class DocumentPreviewViewer extends javax.swing.JPanel { /** * Creates new form DocumentViewer. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) DocumentPreviewViewer() { initComponents(); } @@ -75,11 +77,10 @@ private void initComponents() { /** * Clear the list of documents being displayed. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) void clearViewer() { - synchronized (this) { - documentListModel.removeAllElements(); - documentScrollPane.getVerticalScrollBar().setValue(0); - } + documentListModel.removeAllElements(); + documentScrollPane.getVerticalScrollBar().setValue(0); } /** @@ -88,6 +89,7 @@ void clearViewer() { * * @param listener The ListSelectionListener to add to the selection model. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) void addListSelectionListener(ListSelectionListener listener) { documentList.getSelectionModel().addListSelectionListener(listener); } @@ -99,13 +101,12 @@ void addListSelectionListener(ListSelectionListener listener) { * @return The list of AbstractFiles which are represented by the selected * document preview. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) List<AbstractFile> getInstancesForSelected() { - synchronized (this) { - if (documentList.getSelectedIndex() == -1) { - return new ArrayList<>(); - } else { - return documentListModel.getElementAt(documentList.getSelectedIndex()).getResultFile().getAllInstances(); - } + if (documentList.getSelectedIndex() == -1) { + return new ArrayList<>(); + } else { + return documentListModel.getElementAt(documentList.getSelectedIndex()).getResultFile().getAllInstances(); } } @@ -120,9 +121,8 @@ List<AbstractFile> getInstancesForSelected() { * @param documentWrapper The object which contains the document preview * which will be displayed. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) void addDocument(DocumentWrapper documentWrapper) { - synchronized (this) { - documentListModel.addElement(documentWrapper); - } + documentListModel.addElement(documentWrapper); } } diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainArtifactsTabPanel.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainArtifactsTabPanel.form new file mode 100644 index 0000000000000000000000000000000000000000..1e93e06f8624a869ed8e04944d5a0ee76d5bdc25 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainArtifactsTabPanel.form @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.4" maxVersion="1.9" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="1"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> + <AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,1,44,0,0,1,-112"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout"/> + <SubComponents> + <Container class="javax.swing.JSplitPane" name="jSplitPane1"> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout" value="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout$BorderConstraintsDescription"> + <BorderConstraints direction="Center"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.support.JSplitPaneSupportLayout"/> + </Container> + </SubComponents> +</Form> diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainArtifactsTabPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainArtifactsTabPanel.java new file mode 100644 index 0000000000000000000000000000000000000000..295107ef7853223f55eb9ca0216717fc8391255e --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainArtifactsTabPanel.java @@ -0,0 +1,188 @@ +/* + * Autopsy + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> 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.discovery.ui; + +import com.google.common.eventbus.Subscribe; +import java.util.logging.Level; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.SwingUtilities; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import org.sleuthkit.autopsy.contentviewers.artifactviewers.DefaultArtifactContentViewer; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; +import org.sleuthkit.autopsy.discovery.search.DiscoveryEventUtils; +import org.sleuthkit.datamodel.BlackboardArtifact; + +/** + * JPanel which should be used as a tab in the domain artifacts details area. + */ +final class DomainArtifactsTabPanel extends JPanel { + + private static final long serialVersionUID = 1L; + private final static Logger logger = Logger.getLogger(DomainArtifactsTabPanel.class.getName()); + private final ArtifactsListPanel listPanel; + private final BlackboardArtifact.ARTIFACT_TYPE artifactType; + private AbstractArtifactDetailsPanel rightPanel = null; + + private volatile ArtifactRetrievalStatus status = ArtifactRetrievalStatus.UNPOPULATED; + private final ListSelectionListener listener = new ListSelectionListener() { + @Override + public void valueChanged(ListSelectionEvent event) { + if (!event.getValueIsAdjusting()) { + rightPanel.setArtifact(listPanel.getSelectedArtifact()); + } + } + }; + + /** + * Creates new form CookiesPanel + * + * @param type The type of Artifact this tab is displaying information for. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + DomainArtifactsTabPanel(BlackboardArtifact.ARTIFACT_TYPE type) { + initComponents(); + this.artifactType = type; + listPanel = new ArtifactsListPanel(artifactType); + jSplitPane1.setLeftComponent(listPanel); + setRightComponent(); + listPanel.addSelectionListener(listener); + } + + /** + * Set the right component of the tab panel, which will display the details + * for the artifact. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + private void setRightComponent() { + switch (artifactType) { + case TSK_WEB_HISTORY: + rightPanel = new WebHistoryDetailsPanel(); + break; + case TSK_WEB_COOKIE: + case TSK_WEB_SEARCH_QUERY: + case TSK_WEB_BOOKMARK: + rightPanel = new DefaultArtifactContentViewer(); + break; + case TSK_WEB_DOWNLOAD: + case TSK_WEB_CACHE: + rightPanel = new ContentViewerDetailsPanel(); + break; + default: + rightPanel = new DefaultArtifactContentViewer(); + break; + } + if (rightPanel != null) { + jSplitPane1.setRightComponent(new JScrollPane(rightPanel)); + } + } + + /** + * Get the status of the panel which indicates if it is populated. + * + * @return The ArtifactRetrievalStatuss of the panel. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + ArtifactRetrievalStatus getStatus() { + return status; + } + + /** + * Manually set the status of the panel. + * + * @param status The ArtifactRetrievalStatus of the panel. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + void setStatus(ArtifactRetrievalStatus status) { + this.status = status; + if (status == ArtifactRetrievalStatus.UNPOPULATED && rightPanel != null) { + rightPanel.setArtifact(null); + } + } + + /** + * Handle the event which indicates the artifacts have been retrieved. + * + * @param artifactresultEvent The event which indicates the artifacts have + * been retrieved. + */ + @Subscribe + void handleArtifactSearchResultEvent(DiscoveryEventUtils.ArtifactSearchResultEvent artifactresultEvent) { + SwingUtilities.invokeLater(() -> { + if (artifactType == artifactresultEvent.getArtifactType()) { + listPanel.removeListSelectionListener(listener); + listPanel.addArtifacts(artifactresultEvent.getListOfArtifacts()); + listPanel.addSelectionListener(listener); + listPanel.selectFirst(); + try { + DiscoveryEventUtils.getDiscoveryEventBus().unregister(this); + } catch (IllegalArgumentException notRegistered) { + logger.log(Level.INFO, "Attempting to unregister tab which was not registered"); + // attempting to remove a tab that was never registered + } + status = ArtifactRetrievalStatus.POPULATED; + setEnabled(!listPanel.isEmpty()); + validate(); + repaint(); + } + }); + } + + /** + * Get the type of Artifact the panel exists for. + * + * @return The ARTIFACT_TYPE of the BlackboardArtifact being displayed. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + BlackboardArtifact.ARTIFACT_TYPE getArtifactType() { + return artifactType; + } + + /** + * 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") + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + + jSplitPane1 = new javax.swing.JSplitPane(); + + setLayout(new java.awt.BorderLayout()); + add(jSplitPane1, java.awt.BorderLayout.CENTER); + }// </editor-fold>//GEN-END:initComponents + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JSplitPane jSplitPane1; + // End of variables declaration//GEN-END:variables + + /** + * Enum to keep track of the populated state of this panel. + */ + enum ArtifactRetrievalStatus { + UNPOPULATED(), + POPULATING(), + POPULATED(); + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainDetailsPanel.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainDetailsPanel.form new file mode 100644 index 0000000000000000000000000000000000000000..162d346225bd910a8e575ecefd7cfc2ca76a97f5 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainDetailsPanel.form @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.5" maxVersion="1.9" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> + <Properties> + <Property name="enabled" type="boolean" value="false"/> + </Properties> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="1"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> + <AuxValue name="designerSize" type="java.awt.Dimension" value="-84,-19,0,5,115,114,0,18,106,97,118,97,46,97,119,116,46,68,105,109,101,110,115,105,111,110,65,-114,-39,-41,-84,95,68,20,2,0,2,73,0,6,104,101,105,103,104,116,73,0,5,119,105,100,116,104,120,112,0,0,1,44,0,0,1,-112"/> + </AuxValues> + + <Layout class="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout"/> + <SubComponents> + <Container class="javax.swing.JTabbedPane" name="jTabbedPane1"> + <Constraints> + <Constraint layoutClass="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout" value="org.netbeans.modules.form.compat2.layouts.DesignBorderLayout$BorderConstraintsDescription"> + <BorderConstraints direction="Center"/> + </Constraint> + </Constraints> + + <Layout class="org.netbeans.modules.form.compat2.layouts.support.JTabbedPaneSupportLayout"/> + </Container> + </SubComponents> +</Form> diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainDetailsPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainDetailsPanel.java new file mode 100644 index 0000000000000000000000000000000000000000..6fd407cbaf65d6b085531266171f3cbf2feae060 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainDetailsPanel.java @@ -0,0 +1,182 @@ +/* + * Autopsy + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> 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.discovery.ui; + +import com.google.common.eventbus.Subscribe; +import java.awt.Component; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import org.apache.commons.lang.StringUtils; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; +import org.sleuthkit.autopsy.discovery.search.DiscoveryEventUtils; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.autopsy.discovery.search.SearchData; + +/** + * Panel to display details area for domain discovery results. + * + */ +final class DomainDetailsPanel extends JPanel { + + private static final long serialVersionUID = 1L; + private ArtifactsWorker detailsWorker; + private String domain; + private String selectedTabName; + + /** + * Creates new form ArtifactDetailsPanel. + * + * @param selectedTabName The name of the tab to select initially. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + DomainDetailsPanel(String selectedTabName) { + initComponents(); + addArtifactTabs(selectedTabName); + } + + /** + * Add the tabs for each of the artifact types which we will be displaying. + * + * @param tabName The name of the tab to select initially. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + private void addArtifactTabs(String tabName) { + for (BlackboardArtifact.ARTIFACT_TYPE type : SearchData.Type.DOMAIN.getArtifactTypes()) { + jTabbedPane1.add(type.getDisplayName(), new DomainArtifactsTabPanel(type)); + } + selectedTabName = tabName; + selectTab(); + jTabbedPane1.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + if (jTabbedPane1.getSelectedIndex() >= 0) { + String newTabTitle = jTabbedPane1.getTitleAt(jTabbedPane1.getSelectedIndex()); + if (selectedTabName == null || !selectedTabName.equals(newTabTitle)) { + selectedTabName = newTabTitle; + runDomainWorker(); + } + } + } + }); + } + + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + /** + * Set the selected tab index to be the previously selected tab if a + * previously selected tab exists. + */ + private void selectTab() { + for (int i = 0; i < jTabbedPane1.getTabCount(); i++) { + if (!StringUtils.isBlank(selectedTabName) && selectedTabName.equals(jTabbedPane1.getTitleAt(i))) { + jTabbedPane1.setSelectedIndex(i); + return; + } + } + } + + /** + * Run the worker which retrieves the list of artifacts for the domain to + * populate the details area. + */ + private void runDomainWorker() { + SwingUtilities.invokeLater(() -> { + Component selectedComponent = jTabbedPane1.getSelectedComponent(); + if (selectedComponent instanceof DomainArtifactsTabPanel) { + if (detailsWorker != null && !detailsWorker.isDone()) { + detailsWorker.cancel(true); + } + DomainArtifactsTabPanel selectedTab = (DomainArtifactsTabPanel) selectedComponent; + if (selectedTab.getStatus() == DomainArtifactsTabPanel.ArtifactRetrievalStatus.UNPOPULATED) { + selectedTab.setStatus(DomainArtifactsTabPanel.ArtifactRetrievalStatus.POPULATING); + DiscoveryEventUtils.getDiscoveryEventBus().register(selectedTab); + detailsWorker = new ArtifactsWorker(selectedTab.getArtifactType(), domain); + detailsWorker.execute(); + } + } + }); + } + + /** + * Populate the the details tabs. + * + * @param populateEvent The PopulateDomainTabsEvent which indicates which + * domain the details tabs should be populated for. + */ + @Subscribe + void handlePopulateDomainTabsEvent(DiscoveryEventUtils.PopulateDomainTabsEvent populateEvent) { + SwingUtilities.invokeLater(() -> { + domain = populateEvent.getDomain(); + resetTabsStatus(); + selectTab(); + runDomainWorker(); + if (StringUtils.isBlank(domain)) { + //send fade out event + DiscoveryEventUtils.getDiscoveryEventBus().post(new DiscoveryEventUtils.DetailsVisibleEvent(false)); + } else { + //send fade in event + DiscoveryEventUtils.getDiscoveryEventBus().post(new DiscoveryEventUtils.DetailsVisibleEvent(true)); + } + }); + } + + /** + * Private helper method to ensure tabs will re-populate after a new domain + * is selected. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + private void resetTabsStatus() { + for (Component comp : jTabbedPane1.getComponents()) { + if (comp instanceof DomainArtifactsTabPanel) { + ((DomainArtifactsTabPanel) comp).setStatus(DomainArtifactsTabPanel.ArtifactRetrievalStatus.UNPOPULATED); + } + } + } + + /** + * Get the name of the tab that was most recently selected. + * + * @return The name of the tab that was most recently selected. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + String getSelectedTabName() { + return selectedTabName; + } + + /** + * 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") + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + + jTabbedPane1 = new javax.swing.JTabbedPane(); + + setEnabled(false); + setLayout(new java.awt.BorderLayout()); + add(jTabbedPane1, java.awt.BorderLayout.CENTER); + }// </editor-fold>//GEN-END:initComponents + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JTabbedPane jTabbedPane1; + // End of variables declaration//GEN-END:variables +} diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainFilterPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainFilterPanel.java index bce5577054ddb795906de0dc051a9a38b01ab71c..3216110694c088e5d2b60ba3ff8518e0e11df24b 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainFilterPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainFilterPanel.java @@ -19,6 +19,7 @@ package org.sleuthkit.autopsy.discovery.ui; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.discovery.search.DiscoveryAttributes; import org.sleuthkit.autopsy.discovery.search.ResultsSorter; import org.sleuthkit.autopsy.discovery.search.SearchData; @@ -34,6 +35,7 @@ public class DomainFilterPanel extends AbstractFiltersPanel { /** * Creates new form DomainFilterPanel. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) public DomainFilterPanel() { super(); initComponents(); diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainSummaryPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainSummaryPanel.java index c6979663a707db42b823c15d304ac594ec343773..74e0d7d0bac0ec093ce97efa87c877fd7e8047d4 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainSummaryPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainSummaryPanel.java @@ -30,6 +30,7 @@ import javax.swing.JList; import javax.swing.ListCellRenderer; import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; /** * Class which displays a preview and details about a domain. @@ -43,6 +44,7 @@ class DomainSummaryPanel extends javax.swing.JPanel implements ListCellRenderer< /** * Creates new form DomainPanel. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) DomainSummaryPanel() { initComponents(); domainNameLabel.setFont(domainNameLabel.getFont().deriveFont(domainNameLabel.getFont().getStyle(), domainNameLabel.getFont().getSize() + 6)); @@ -136,6 +138,7 @@ private void initComponents() { private javax.swing.JLabel totalVisitsLabel; // End of variables declaration//GEN-END:variables + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @NbBundle.Messages({"# {0} - startDate", "# {1} - endDate", "DomainSummaryPanel.activity.text=Activity: {0} to {1}", @@ -163,6 +166,7 @@ public Component getListCellRendererComponent(JList<? extends DomainWrapper> lis return this; } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override public String getToolTipText(MouseEvent event) { if (event != null) { diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainSummaryViewer.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainSummaryViewer.form index 22296c01783ac99feef9d303de01a33346996470..9f1e3516b9bacd597c575bbf3f8ce86bc47044ed 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainSummaryViewer.form +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainSummaryViewer.form @@ -39,8 +39,6 @@ </Properties> <AuxValues> <AuxValue name="JavaCodeGenerator_TypeParameters" type="java.lang.String" value="<DomainWrapper>"/> - <AuxValue name="JavaCodeGenerator_VariableLocal" type="java.lang.Boolean" value="true"/> - <AuxValue name="JavaCodeGenerator_VariableModifier" type="java.lang.Integer" value="0"/> </AuxValues> </Component> </SubComponents> diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainSummaryViewer.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainSummaryViewer.java index 593ae49949dfb38289196431adef6c097dc75494..66125e36d0f72a663d411bc3ab63035677586d11 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainSummaryViewer.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/DomainSummaryViewer.java @@ -19,6 +19,9 @@ package org.sleuthkit.autopsy.discovery.ui; import javax.swing.DefaultListModel; +import javax.swing.event.ListSelectionListener; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; +import org.sleuthkit.autopsy.discovery.search.DiscoveryEventUtils; /** * A JPanel to display domain summaries. @@ -30,20 +33,20 @@ public class DomainSummaryViewer extends javax.swing.JPanel { private final DefaultListModel<DomainWrapper> domainListModel = new DefaultListModel<>(); /** - * Clear the list of documents being displayed. + * Creates new form DomainSummaryPanel */ - void clearViewer() { - synchronized (this) { - domainListModel.removeAllElements(); - domainScrollPane.getVerticalScrollBar().setValue(0); - } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + public DomainSummaryViewer() { + initComponents(); } /** - * Creates new form DomainSummaryPanel + * Clear the list of documents being displayed. */ - public DomainSummaryViewer() { - initComponents(); + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + void clearViewer() { + domainListModel.removeAllElements(); + domainScrollPane.getVerticalScrollBar().setValue(0); } /** @@ -56,7 +59,7 @@ public DomainSummaryViewer() { private void initComponents() { domainScrollPane = new javax.swing.JScrollPane(); - javax.swing.JList<DomainWrapper> domainList = new javax.swing.JList<>(); + domainList = new javax.swing.JList<>(); setLayout(new java.awt.BorderLayout()); @@ -69,6 +72,7 @@ private void initComponents() { // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JList<DomainWrapper> domainList; private javax.swing.JScrollPane domainScrollPane; // End of variables declaration//GEN-END:variables @@ -78,9 +82,39 @@ private void initComponents() { * @param domainWrapper The object which contains the domain summary which * will be displayed. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) void addDomain(DomainWrapper domainWrapper) { - synchronized (this) { - domainListModel.addElement(domainWrapper); + domainListModel.addElement(domainWrapper); + } + + /** + * Send an event to perform the population of the domain details tabs to + * reflect the currently selected domain. Will populate the list with + * nothing when a domain is not used. + * + * @param useDomain If the currently selected domain should be used to + * retrieve a list. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + void sendPopulateEvent(boolean useDomain) { + String domain = ""; + if (useDomain) { + if (domainList.getSelectedIndex() != -1) { + domain = domainListModel.getElementAt(domainList.getSelectedIndex()).getResultDomain().getDomain(); + } } + //send populateMesage + DiscoveryEventUtils.getDiscoveryEventBus().post(new DiscoveryEventUtils.PopulateDomainTabsEvent(domain)); + } + + /** + * Add a selection listener to the list of document previews being + * displayed. + * + * @param listener The ListSelectionListener to add to the selection model. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + void addListSelectionListener(ListSelectionListener listener) { + domainList.getSelectionModel().addListSelectionListener(listener); } } diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/DetailsPanel.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/FileDetailsPanel.form similarity index 96% rename from Core/src/org/sleuthkit/autopsy/discovery/ui/DetailsPanel.form rename to Core/src/org/sleuthkit/autopsy/discovery/ui/FileDetailsPanel.form index bd3d8c5af9b2af6708be5ccdf2dab8a7529b9676..1cac14678036bece07b3c830cda7571a44a7aff9 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/DetailsPanel.form +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/FileDetailsPanel.form @@ -105,8 +105,8 @@ <Properties> <Property name="border" type="javax.swing.border.Border" editor="org.netbeans.modules.form.editors2.BorderEditor"> <Border info="org.netbeans.modules.form.compat2.border.TitledBorderInfo"> - <TitledBorder title="Instances"> - <ResourceString PropertyName="titleX" bundle="org/sleuthkit/autopsy/discovery/ui/Bundle.properties" key="DetailsPanel.instancesList.border.title" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> + <TitledBorder title="<FileDetailsPanel.instancesList.border.title>"> + <ResourceString PropertyName="titleX" bundle="org/sleuthkit/autopsy/discovery/ui/Bundle.properties" key="FileDetailsPanel.instancesList.border.title" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> </TitledBorder> </Border> </Property> diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/DetailsPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/FileDetailsPanel.java similarity index 94% rename from Core/src/org/sleuthkit/autopsy/discovery/ui/DetailsPanel.java rename to Core/src/org/sleuthkit/autopsy/discovery/ui/FileDetailsPanel.java index e302669278bbc3a73d5d7f3fdb00be80aa36f7b6..9230459d6db63ac7c45f9921579b3965684057e6 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/DetailsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/FileDetailsPanel.java @@ -36,6 +36,7 @@ import org.sleuthkit.autopsy.actions.DeleteFileContentTagAction; import org.sleuthkit.autopsy.corecomponents.DataContentPanel; import org.sleuthkit.autopsy.corecomponents.TableFilterNode; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.datamodel.FileNode; import org.sleuthkit.autopsy.directorytree.ExternalViewerAction; import org.sleuthkit.autopsy.directorytree.ViewContextAction; @@ -48,7 +49,7 @@ /** * Panel to display the details of the selected result. */ -final class DetailsPanel extends javax.swing.JPanel { +final class FileDetailsPanel extends javax.swing.JPanel { private static final long serialVersionUID = 1L; @@ -59,7 +60,8 @@ final class DetailsPanel extends javax.swing.JPanel { /** * Creates new form DetailsPanel. */ - DetailsPanel() { + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + FileDetailsPanel() { initComponents(); dataContentPanel = DataContentPanel.createInstance(); detailsSplitPane.setBottomComponent(dataContentPanel); @@ -112,7 +114,9 @@ public void valueChanged(ListSelectionEvent e) { */ @Subscribe void handleClearSelectionListener(DiscoveryEventUtils.ClearInstanceSelectionEvent clearEvent) { - instancesList.clearSelection(); + SwingUtilities.invokeLater(() -> { + instancesList.clearSelection(); + }); } /** @@ -122,7 +126,7 @@ void handleClearSelectionListener(DiscoveryEventUtils.ClearInstanceSelectionEven * instances list should be populated */ @Subscribe - synchronized void handlePopulateInstancesListEvent(DiscoveryEventUtils.PopulateInstancesListEvent populateEvent) { + void handlePopulateInstancesListEvent(DiscoveryEventUtils.PopulateInstancesListEvent populateEvent) { SwingUtilities.invokeLater(() -> { List<AbstractFile> files = populateEvent.getInstances(); if (files.isEmpty()) { @@ -154,7 +158,8 @@ synchronized void handlePopulateInstancesListEvent(DiscoveryEventUtils.PopulateI * * @return The AbstractFile which is currently selected. */ - synchronized AbstractFile getSelectedFile() { + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + AbstractFile getSelectedFile() { if (instancesList.getSelectedIndex() == -1) { return null; } else { @@ -186,7 +191,7 @@ private void initComponents() { instancesScrollPane.setPreferredSize(new java.awt.Dimension(775, 60)); - instancesList.setBorder(javax.swing.BorderFactory.createTitledBorder(org.openide.util.NbBundle.getMessage(DetailsPanel.class, "DetailsPanel.instancesList.border.title"))); // NOI18N + instancesList.setBorder(javax.swing.BorderFactory.createTitledBorder(org.openide.util.NbBundle.getMessage(FileDetailsPanel.class, "FileDetailsPanel.instancesList.border.title"))); // NOI18N instancesList.setModel(instancesListModel); instancesList.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION); instancesList.setCellRenderer(new InstancesCellRenderer()); @@ -241,6 +246,7 @@ private class InstancesCellRenderer extends DefaultListCellRenderer { private static final long serialVersionUID = 1L; + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) { super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/GroupListPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/GroupListPanel.java index 714d5202f06750c18ef2dd70b76947550bc1627a..5a3b5393af7507b0af6862f864fc110c82466f2c 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/GroupListPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/GroupListPanel.java @@ -31,6 +31,7 @@ import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.discovery.search.DiscoveryAttributes; import org.sleuthkit.autopsy.discovery.search.DiscoveryEventUtils; import org.sleuthkit.autopsy.discovery.search.DiscoveryKeyUtils.GroupKey; @@ -56,6 +57,7 @@ final class GroupListPanel extends javax.swing.JPanel { /** * Creates new form GroupListPanel. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) GroupListPanel() { initComponents(); } @@ -67,8 +69,10 @@ final class GroupListPanel extends javax.swing.JPanel { */ @Subscribe void handleSearchStartedEvent(DiscoveryEventUtils.SearchStartedEvent searchStartedEvent) { - type = searchStartedEvent.getType(); - groupKeyList.setListData(new GroupKey[0]); + SwingUtilities.invokeLater(() -> { + type = searchStartedEvent.getType(); + groupKeyList.setListData(new GroupKey[0]); + }); } @Messages({"GroupsListPanel.noFileResults.message.text=No files were found for the selected filters.\n\n" @@ -90,27 +94,29 @@ void handleSearchStartedEvent(DiscoveryEventUtils.SearchStartedEvent searchStart */ @Subscribe void handleSearchCompleteEvent(DiscoveryEventUtils.SearchCompleteEvent searchCompleteEvent) { - groupMap = searchCompleteEvent.getGroupMap(); - searchfilters = searchCompleteEvent.getFilters(); - groupingAttribute = searchCompleteEvent.getGroupingAttr(); - groupSort = searchCompleteEvent.getGroupSort(); - resultSortMethod = searchCompleteEvent.getResultSort(); - groupKeyList.setListData(groupMap.keySet().toArray(new GroupKey[groupMap.keySet().size()])); SwingUtilities.invokeLater(() -> { - if (groupKeyList.getModel().getSize() > 0) { - groupKeyList.setSelectedIndex(0); - } else if (type == DOMAIN) { - JOptionPane.showMessageDialog(DiscoveryTopComponent.getTopComponent(), - Bundle.GroupsListPanel_noDomainResults_message_text(), - Bundle.GroupsListPanel_noResults_title_text(), - JOptionPane.PLAIN_MESSAGE); - } else { - JOptionPane.showMessageDialog(DiscoveryTopComponent.getTopComponent(), - Bundle.GroupsListPanel_noFileResults_message_text(), - Bundle.GroupsListPanel_noResults_title_text(), - JOptionPane.PLAIN_MESSAGE); - } - setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + groupMap = searchCompleteEvent.getGroupMap(); + searchfilters = searchCompleteEvent.getFilters(); + groupingAttribute = searchCompleteEvent.getGroupingAttr(); + groupSort = searchCompleteEvent.getGroupSort(); + resultSortMethod = searchCompleteEvent.getResultSort(); + groupKeyList.setListData(groupMap.keySet().toArray(new GroupKey[groupMap.keySet().size()])); + SwingUtilities.invokeLater(() -> { + if (groupKeyList.getModel().getSize() > 0) { + groupKeyList.setSelectedIndex(0); + } else if (type == DOMAIN) { + JOptionPane.showMessageDialog(DiscoveryTopComponent.getTopComponent(), + Bundle.GroupsListPanel_noDomainResults_message_text(), + Bundle.GroupsListPanel_noResults_title_text(), + JOptionPane.PLAIN_MESSAGE); + } else { + JOptionPane.showMessageDialog(DiscoveryTopComponent.getTopComponent(), + Bundle.GroupsListPanel_noFileResults_message_text(), + Bundle.GroupsListPanel_noResults_title_text(), + JOptionPane.PLAIN_MESSAGE); + } + setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); + }); }); } @@ -168,10 +174,9 @@ public void valueChanged(javax.swing.event.ListSelectionEvent evt) { /** * Reset the group list to be empty. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) void resetGroupList() { - SwingUtilities.invokeLater(() -> { - setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); - }); + setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); groupKeyList.setListData(new GroupKey[0]); } @@ -211,6 +216,7 @@ private class GroupListRenderer extends DefaultListCellRenderer { private static final long serialVersionUID = 1L; + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override public java.awt.Component getListCellRendererComponent( JList<?> list, diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/HashSetFilterPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/HashSetFilterPanel.java index 4698c43dc3784727a2ca951ce84fe0808c596840..0257352a167280430691c80149da819f421516b8 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/HashSetFilterPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/HashSetFilterPanel.java @@ -27,6 +27,7 @@ import javax.swing.JList; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.discovery.search.SearchFiltering; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardAttribute; @@ -43,6 +44,7 @@ final class HashSetFilterPanel extends AbstractDiscoveryFilterPanel { /** * Creates new form HashSetFilterPaenl. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) HashSetFilterPanel() { initComponents(); setUpHashFilter(); @@ -51,6 +53,7 @@ final class HashSetFilterPanel extends AbstractDiscoveryFilterPanel { /** * Initialize the hash filter. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) private void setUpHashFilter() { int count = 0; try { @@ -123,6 +126,7 @@ private void hashSetCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//G private javax.swing.JScrollPane hashSetScrollPane; // End of variables declaration//GEN-END:variables + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override void configurePanel(boolean selected, int[] indicesSelected) { boolean hasHashSets = hashSetList.getModel().getSize() > 0; @@ -140,6 +144,7 @@ void configurePanel(boolean selected, int[] indicesSelected) { } } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override JCheckBox getCheckbox() { return hashSetCheckbox; @@ -150,6 +155,7 @@ JLabel getAdditionalLabel() { return null; } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @NbBundle.Messages({"HashSetFilterPanel.error.text=At least one hash set name must be selected."}) @Override String checkForError() { @@ -159,11 +165,13 @@ String checkForError() { return ""; } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override JList<?> getList() { return hashSetList; } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override AbstractFilter getFilter() { if (hashSetCheckbox.isSelected()) { diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/ImageFilterPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/ImageFilterPanel.java index 308cdd569b351e4b8cd057a7820d494ec68a02fe..4557b02d7423a7ac342e7d13c884c9a23e416a5d 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/ImageFilterPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/ImageFilterPanel.java @@ -19,6 +19,7 @@ package org.sleuthkit.autopsy.discovery.ui; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.discovery.search.SearchData; /** @@ -32,6 +33,7 @@ final class ImageFilterPanel extends AbstractFiltersPanel { /** * Creates new form ImageFilterPanel. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) ImageFilterPanel() { super(); initComponents(); diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/ImageThumbnailPanel.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/ImageThumbnailPanel.form index 46c5d3601fb82e0fc724e64a5f02c29410f430bd..2a52cb88d357be1726718819cd6814e343e1a5bc 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/ImageThumbnailPanel.form +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/ImageThumbnailPanel.form @@ -84,7 +84,6 @@ </Component> <Component class="javax.swing.JLabel" name="nameLabel"> <Properties> - <Property name="toolTipText" type="java.lang.String" value=""/> <Property name="maximumSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> <Dimension value="[159, 12]"/> </Property> @@ -94,6 +93,7 @@ <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.beaninfo.editors.DimensionEditor"> <Dimension value="[159, 12]"/> </Property> + <Property name="toolTipText" type="java.lang.String" value=""/> </Properties> </Component> <Component class="javax.swing.JLabel" name="isDeletedLabel"> @@ -101,9 +101,6 @@ <Property name="icon" type="javax.swing.Icon" editor="org.netbeans.modules.form.editors2.IconEditor"> <Image iconType="3" name="/org/sleuthkit/autopsy/images/file-icon-deleted.png"/> </Property> - <Property name="toolTipText" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> - <ResourceString bundle="org/sleuthkit/autopsy/discovery/ui/Bundle.properties" key="ImageThumbnailPanel.isDeletedLabel.toolTipText" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> - </Property> <Property name="maximumSize" type="java.awt.Dimension" editor="org.netbeans.modules.form.RADConnectionPropertyEditor"> <Connection code="new Dimension(org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize(),org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize())" type="code"/> </Property> @@ -113,6 +110,9 @@ <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.modules.form.RADConnectionPropertyEditor"> <Connection code="new Dimension(org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize(),org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize())" type="code"/> </Property> + <Property name="toolTipText" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="org/sleuthkit/autopsy/discovery/ui/Bundle.properties" key="ImageThumbnailPanel.isDeletedLabel.toolTipText" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> + </Property> </Properties> </Component> <Component class="javax.swing.JLabel" name="scoreLabel"> @@ -120,7 +120,6 @@ <Property name="icon" type="javax.swing.Icon" editor="org.netbeans.modules.form.editors2.IconEditor"> <Image iconType="3" name="/org/sleuthkit/autopsy/images/red-circle-exclamation.png"/> </Property> - <Property name="toolTipText" type="java.lang.String" value=""/> <Property name="maximumSize" type="java.awt.Dimension" editor="org.netbeans.modules.form.RADConnectionPropertyEditor"> <Connection code="new Dimension(org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize(),org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize())" type="code"/> </Property> @@ -130,6 +129,7 @@ <Property name="preferredSize" type="java.awt.Dimension" editor="org.netbeans.modules.form.RADConnectionPropertyEditor"> <Connection code="new Dimension(org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize(),org.sleuthkit.autopsy.discovery.ui.DiscoveryUiUtils.getIconSize())" type="code"/> </Property> + <Property name="toolTipText" type="java.lang.String" value=""/> </Properties> </Component> </SubComponents> diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/ImageThumbnailPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/ImageThumbnailPanel.java index d294fa866d3bf0401a4d30bc8547ff2bee1e6293..22ce4a0809b57eeace9307d1e5173538381514a0 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/ImageThumbnailPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/ImageThumbnailPanel.java @@ -28,6 +28,7 @@ import javax.swing.JList; import javax.swing.ListCellRenderer; import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; /** * Class which displays a thumbnail and information for an image file. @@ -41,6 +42,7 @@ final class ImageThumbnailPanel extends javax.swing.JPanel implements ListCellRe /** * Creates new form ImageThumbnailPanel */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) ImageThumbnailPanel() { initComponents(); } @@ -129,6 +131,7 @@ private void initComponents() { private javax.swing.JLabel thumbnailLabel; // End of variables declaration//GEN-END:variables + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @NbBundle.Messages({ "# {0} - otherInstanceCount", "ImageThumbnailPanel.nameLabel.more.text= and {0} more", @@ -152,6 +155,7 @@ public Component getListCellRendererComponent(JList<? extends ImageThumbnailWrap return this; } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override public String getToolTipText(MouseEvent event) { if (event != null) { diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/ImageThumbnailViewer.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/ImageThumbnailViewer.java index 273cca8023ce1f7878c24e1bd9a2256953f56edf..cd9fd4909a6e696d8d36128f6106587ee266583c 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/ImageThumbnailViewer.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/ImageThumbnailViewer.java @@ -22,6 +22,7 @@ import java.util.List; import javax.swing.DefaultListModel; import javax.swing.event.ListSelectionListener; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.datamodel.AbstractFile; /** @@ -37,6 +38,7 @@ final class ImageThumbnailViewer extends javax.swing.JPanel { /** * Creates new form ImageThumbnailViewer. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) ImageThumbnailViewer() { initComponents(); @@ -77,6 +79,7 @@ private void initComponents() { * * @param listener The ListSelectionListener to add to the selection model. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) void addListSelectionListener(ListSelectionListener listener) { thumbnailList.getSelectionModel().addListSelectionListener(listener); } @@ -88,24 +91,23 @@ void addListSelectionListener(ListSelectionListener listener) { * @return The list of AbstractFiles which are represented by the selected * image thumbnail. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) List<AbstractFile> getInstancesForSelected() { - synchronized (this) { - if (thumbnailList.getSelectedIndex() == -1) { - return new ArrayList<>(); - } else { - return thumbnailListModel.getElementAt(thumbnailList.getSelectedIndex()).getResultFile().getAllInstances(); - } + if (thumbnailList.getSelectedIndex() == -1) { + return new ArrayList<>(); + } else { + return thumbnailListModel.getElementAt(thumbnailList.getSelectedIndex()).getResultFile().getAllInstances(); } } /** * Clear the list of thumbnails being displayed. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) void clearViewer() { - synchronized (this) { - thumbnailListModel.removeAllElements(); - thumbnailListScrollPane.getVerticalScrollBar().setValue(0); - } + thumbnailListModel.removeAllElements(); + thumbnailListScrollPane.getVerticalScrollBar().setValue(0); + } /** @@ -114,9 +116,8 @@ void clearViewer() { * @param thumbnailWrapper The object which contains the thumbnail which * will be displayed. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) void addImage(ImageThumbnailWrapper thumbnailWrapper) { - synchronized (this) { - thumbnailListModel.addElement(thumbnailWrapper); - } + thumbnailListModel.addElement(thumbnailWrapper); } } diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/InterestingItemsFilterPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/InterestingItemsFilterPanel.java index b77956937d1852b5c0b7e76ad56c6e5a0219fc11..184ddb1d29d7f21ab8a7869ac4c385f0ed9668f4 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/InterestingItemsFilterPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/InterestingItemsFilterPanel.java @@ -27,6 +27,7 @@ import javax.swing.JList; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.discovery.search.SearchFiltering; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardAttribute; @@ -43,6 +44,7 @@ final class InterestingItemsFilterPanel extends AbstractDiscoveryFilterPanel { /** * Creates new form InterestingItemsFilterPanel. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) InterestingItemsFilterPanel() { initComponents(); setUpInterestingItemsFilter(); @@ -51,6 +53,7 @@ final class InterestingItemsFilterPanel extends AbstractDiscoveryFilterPanel { /** * Initialize the interesting items filter. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) private void setUpInterestingItemsFilter() { int count = 0; try { @@ -118,6 +121,7 @@ private void interestingItemsCheckboxActionPerformed(java.awt.event.ActionEvent interestingItemsList.setEnabled(interestingItemsCheckbox.isSelected()); }//GEN-LAST:event_interestingItemsCheckboxActionPerformed + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override void configurePanel(boolean selected, int[] indicesSelected) { boolean hasInterestingItems = interestingItemsList.getModel().getSize() > 0; @@ -135,6 +139,7 @@ void configurePanel(boolean selected, int[] indicesSelected) { } } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override JCheckBox getCheckbox() { return interestingItemsCheckbox; @@ -145,6 +150,7 @@ JLabel getAdditionalLabel() { return null; } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @NbBundle.Messages({"InterestingItemsFilterPanel.error.text=At least one interesting file set name must be selected."}) @Override String checkForError() { @@ -161,11 +167,13 @@ String checkForError() { private javax.swing.JScrollPane interestingItemsScrollPane; // End of variables declaration//GEN-END:variables + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override JList<?> getList() { return interestingItemsList; } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override AbstractFilter getFilter() { if (interestingItemsCheckbox.isSelected()) { diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/ObjectDetectedFilterPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/ObjectDetectedFilterPanel.java index eb1c1525c59d8b003bf668cda6c6f81263718e04..a6eb3de1c52fc6fd0ab322673f5144f964210f45 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/ObjectDetectedFilterPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/ObjectDetectedFilterPanel.java @@ -27,6 +27,7 @@ import javax.swing.JList; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.discovery.search.SearchFiltering; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardAttribute; @@ -43,6 +44,7 @@ final class ObjectDetectedFilterPanel extends AbstractDiscoveryFilterPanel { /** * Creates new form ObjectDetectedFilter. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) ObjectDetectedFilterPanel() { initComponents(); setUpObjectFilter(); @@ -51,6 +53,7 @@ final class ObjectDetectedFilterPanel extends AbstractDiscoveryFilterPanel { /** * Initialize the object filter. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) private void setUpObjectFilter() { int count = 0; try { @@ -129,6 +132,7 @@ private void objectsCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//G private javax.swing.JScrollPane objectsScrollPane; // End of variables declaration//GEN-END:variables + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override void configurePanel(boolean selected, int[] indicesSelected) { boolean hasObjects = objectsList.getModel().getSize() > 0; @@ -146,6 +150,7 @@ void configurePanel(boolean selected, int[] indicesSelected) { } } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override JCheckBox getCheckbox() { return objectsCheckbox; @@ -155,6 +160,8 @@ JCheckBox getCheckbox() { JLabel getAdditionalLabel() { return null; } + + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @NbBundle.Messages({"ObjectDetectedFilterPanel.error.text=At least one object type name must be selected."}) @Override String checkForError() { @@ -164,11 +171,13 @@ String checkForError() { return ""; } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override JList<?> getList() { return objectsList; } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override AbstractFilter getFilter() { if (objectsCheckbox.isSelected()) { diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/OpenDiscoveryAction.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/OpenDiscoveryAction.java index 33228a6a9b36a61cc3523017e9cb33292a68dea7..ec767c4c8387edf265adfd9bb239b54498756842 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/OpenDiscoveryAction.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/OpenDiscoveryAction.java @@ -31,6 +31,7 @@ import org.openide.util.actions.CallableSystemAction; import org.openide.util.actions.Presenter; import org.sleuthkit.autopsy.casemodule.Case; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; /** * Class to open the Discovery dialog. Allows the user to run searches and see @@ -76,6 +77,7 @@ public void performAction() { * * @return The toolbar button */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override public Component getToolbarPresenter() { ImageIcon icon = new ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/discovery-icon-24.png")); //NON-NLS diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/ParentFolderFilterPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/ParentFolderFilterPanel.java index a2a0d9615f2844e7ec28540a37d00257d6032841..2a77c28873d0e0ba95d1e4b7bf4f01ff64c3959d 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/ParentFolderFilterPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/ParentFolderFilterPanel.java @@ -26,6 +26,7 @@ import javax.swing.JLabel; import javax.swing.JList; import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.discovery.search.SearchFiltering; import org.sleuthkit.autopsy.discovery.search.SearchFiltering.ParentSearchTerm; @@ -41,6 +42,7 @@ final class ParentFolderFilterPanel extends AbstractDiscoveryFilterPanel { /** * Creates new form ParentFolderFilterPanel. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) ParentFolderFilterPanel() { initComponents(); setUpParentPathFilter(); @@ -49,6 +51,7 @@ final class ParentFolderFilterPanel extends AbstractDiscoveryFilterPanel { /** * Initialize the parent path filter. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) private void setUpParentPathFilter() { fullRadioButton.setSelected(true); includeRadioButton.setSelected(true); @@ -239,6 +242,7 @@ private void addButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIR private javax.swing.JRadioButton substringRadioButton; // End of variables declaration//GEN-END:variables + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override void configurePanel(boolean selected, int[] indicesSelected) { parentCheckbox.setSelected(selected); @@ -270,16 +274,19 @@ void configurePanel(boolean selected, int[] indicesSelected) { } } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override JCheckBox getCheckbox() { return parentCheckbox; } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override JLabel getAdditionalLabel() { return parentLabel; } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @NbBundle.Messages({"ParentFolderFilterPanel.error.text=At least one parent path must be entered."}) @Override String checkForError() { @@ -290,6 +297,7 @@ String checkForError() { return ""; } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) /** * Utility method to get the parent path objects out of the JList. * @@ -303,11 +311,13 @@ private List<SearchFiltering.ParentSearchTerm> getParentPaths() { return results; } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override JList<?> getList() { return parentList; } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override AbstractFilter getFilter() { if (parentCheckbox.isSelected()) { diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/PastOccurrencesFilterPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/PastOccurrencesFilterPanel.java index b19c79355613f3cd34be007d465360c502f441ac..bedd0a24c865db332e521f289822d9c63431226a 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/PastOccurrencesFilterPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/PastOccurrencesFilterPanel.java @@ -24,6 +24,7 @@ import javax.swing.JList; import org.openide.util.NbBundle; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.discovery.search.AbstractFilter; import org.sleuthkit.autopsy.discovery.search.SearchData; import org.sleuthkit.autopsy.discovery.search.SearchData.Frequency; @@ -41,6 +42,7 @@ final class PastOccurrencesFilterPanel extends AbstractDiscoveryFilterPanel { /** * Creates new form PastOccurrencesFilterPanel. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) PastOccurrencesFilterPanel(Type type) { initComponents(); this.type = type; @@ -101,6 +103,7 @@ private void pastOccurrencesCheckboxActionPerformed(java.awt.event.ActionEvent e /** * Initialize the frequency filter. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) private void setUpFrequencyFilter() { int count = 0; DefaultListModel<SearchData.Frequency> frequencyListModel = (DefaultListModel<SearchData.Frequency>) crFrequencyList.getModel(); @@ -126,6 +129,7 @@ private void setUpFrequencyFilter() { private javax.swing.JCheckBox pastOccurrencesCheckbox; // End of variables declaration//GEN-END:variables + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override void configurePanel(boolean selected, int[] indicesSelected) { boolean canBeFilteredOn = type != Type.DOMAIN || CentralRepository.isEnabled(); @@ -144,6 +148,7 @@ void configurePanel(boolean selected, int[] indicesSelected) { } } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override JCheckBox getCheckbox() { return pastOccurrencesCheckbox; @@ -154,6 +159,7 @@ JLabel getAdditionalLabel() { return null; } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @NbBundle.Messages({"PastOccurrencesFilterPanel.error.text=At least one value in the past occurrence filter must be selected."}) @Override String checkForError() { @@ -163,11 +169,13 @@ String checkForError() { return ""; } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override JList<?> getList() { return crFrequencyList; } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override AbstractFilter getFilter() { if (pastOccurrencesCheckbox.isSelected()) { diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/ResultsPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/ResultsPanel.java index c077f65f223c585975fe8f83b993964106cd82ac..b6aad696ce134334cbd9e6d9a099bde609739442 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/ResultsPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/ResultsPanel.java @@ -40,6 +40,7 @@ import org.sleuthkit.autopsy.coreutils.Logger; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.discovery.search.DiscoveryAttributes; import org.sleuthkit.autopsy.discovery.search.DiscoveryEventUtils; import org.sleuthkit.autopsy.discovery.search.DiscoveryKeyUtils.GroupKey; @@ -85,6 +86,7 @@ final class ResultsPanel extends javax.swing.JPanel { */ @Messages({"ResultsPanel.viewFileInDir.name=View File in Directory", "ResultsPanel.openInExternalViewer.name=Open in External Viewer"}) + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) ResultsPanel() { initComponents(); imageThumbnailViewer = new ImageThumbnailViewer(); @@ -125,9 +127,14 @@ final class ResultsPanel extends javax.swing.JPanel { } } }); - //JIRA-TODO 6307 Add listener for domainSummaryViewer when 6782, 6773, and the other details area related stories are done + domainSummaryViewer.addListSelectionListener((e) -> { + if (resultType == SearchData.Type.DOMAIN) { + domainSummaryViewer.sendPopulateEvent(!e.getValueIsAdjusting()); + } + }); } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) SearchData.Type getActiveType() { return resultType; } @@ -139,6 +146,7 @@ SearchData.Type getActiveType() { * @return The list of AbstractFiles which are represented by the item * selected in the results viewer area. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) private List<AbstractFile> getInstancesForSelected() { if (null != resultType) { switch (resultType) { @@ -209,23 +217,25 @@ void handlePageRetrievedEvent(DiscoveryEventUtils.PageRetrievedEvent pageRetriev } ); } - + @Subscribe void handleCancelBackgroundTasksEvent(DiscoveryEventUtils.CancelBackgroundTasksEvent cancelEvent) { - for (SwingWorker<Void, Void> thumbWorker : resultContentWorkers) { - if (!thumbWorker.isDone()) { - thumbWorker.cancel(true); + SwingUtilities.invokeLater(() -> { + for (SwingWorker<Void, Void> thumbWorker : resultContentWorkers) { + if (!thumbWorker.isDone()) { + thumbWorker.cancel(true); + } } - } - - resultContentWorkers.clear(); + resultContentWorkers.clear(); + }); } /** * Reset the result viewer and any associate workers to a default empty * state. */ - synchronized void resetResultViewer() { + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + void resetResultViewer() { resultsViewerPanel.remove(imageThumbnailViewer); resultsViewerPanel.remove(videoThumbnailViewer); resultsViewerPanel.remove(documentPreviewViewer); @@ -250,7 +260,8 @@ synchronized void resetResultViewer() { * * @param results The list of ResultFiles to populate the video viewer with. */ - synchronized void populateVideoViewer(List<Result> results) { + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + void populateVideoViewer(List<Result> results) { for (Result result : results) { VideoThumbnailWorker thumbWorker = new VideoThumbnailWorker((ResultFile) result); thumbWorker.execute(); @@ -265,7 +276,8 @@ synchronized void populateVideoViewer(List<Result> results) { * * @param results The list of ResultFiles to populate the image viewer with. */ - synchronized void populateImageViewer(List<Result> results) { + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + void populateImageViewer(List<Result> results) { for (Result result : results) { ImageThumbnailWorker thumbWorker = new ImageThumbnailWorker((ResultFile) result); thumbWorker.execute(); @@ -281,7 +293,8 @@ synchronized void populateImageViewer(List<Result> results) { * @param results The list of ResultFiles to populate the document viewer * with. */ - synchronized void populateDocumentViewer(List<Result> results) { + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + void populateDocumentViewer(List<Result> results) { for (Result result : results) { DocumentPreviewWorker documentWorker = new DocumentPreviewWorker((ResultFile) result); documentWorker.execute(); @@ -297,7 +310,8 @@ synchronized void populateDocumentViewer(List<Result> results) { * @param results The list of ResultDomains to populate the domain summary * viewer with. */ - synchronized void populateDomainViewer(List<Result> results) { + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + void populateDomainViewer(List<Result> results) { SleuthkitCase currentCase; try { currentCase = Case.getCurrentCaseThrows().getSleuthkitCase(); @@ -374,37 +388,33 @@ void handleSearchCompleteEvent(DiscoveryEventUtils.SearchCompleteEvent searchCom * @param startingEntry The index of the first file in the group to include * in this page. */ - @Subscribe - private synchronized void setPage(int startingEntry - ) { + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + private void setPage(int startingEntry) { int pageSize = pageSizeComboBox.getItemAt(pageSizeComboBox.getSelectedIndex()); - synchronized (this) { - if (pageWorker != null && !pageWorker.isDone()) { - pageWorker.cancel(true); - } - CentralRepository centralRepo = null; - if (CentralRepository.isEnabled()) { - try { - centralRepo = CentralRepository.getInstance(); - } catch (CentralRepoException ex) { - centralRepo = null; - logger.log(Level.SEVERE, "Error loading central repository database, no central repository options will be available for Discovery", ex); - } - } - if (groupSize != 0) { - pageWorker = new PageWorker(searchFilters, groupingAttribute, groupSort, fileSortMethod, selectedGroupKey, startingEntry, pageSize, resultType, centralRepo); - pageWorker.execute(); - } else { - SwingUtilities.invokeLater(() -> { - pageSizeComboBox.setEnabled(true); - }); + if (pageWorker != null && !pageWorker.isDone()) { + pageWorker.cancel(true); + } + CentralRepository centralRepo = null; + if (CentralRepository.isEnabled()) { + try { + centralRepo = CentralRepository.getInstance(); + } catch (CentralRepoException ex) { + centralRepo = null; + logger.log(Level.SEVERE, "Error loading central repository database, no central repository options will be available for Discovery", ex); } } + if (groupSize != 0) { + pageWorker = new PageWorker(searchFilters, groupingAttribute, groupSort, fileSortMethod, selectedGroupKey, startingEntry, pageSize, resultType, centralRepo); + pageWorker.execute(); + } else { + pageSizeComboBox.setEnabled(true); + } } /** * Enable the paging controls based on what exists in the page. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Messages({"# {0} - currentPage", "# {1} - totalPages", "ResultsPanel.currentPage.displayValue=Page: {0} of {1}"}) @@ -678,6 +688,7 @@ private void pageSizeChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_pa /** * Disable all the paging controls. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) private void disablePagingControls() { nextPageButton.setEnabled(false); previousPageButton.setEnabled(false); @@ -708,6 +719,7 @@ private class VideoThumbnailWorker extends SwingWorker<Void, Void> { * @param file The ResultFile which represents the video file thumbnails * are being retrieved for. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) VideoThumbnailWorker(ResultFile file) { thumbnailWrapper = new VideoThumbnailsWrapper(file); videoThumbnailViewer.addVideo(thumbnailWrapper); @@ -746,6 +758,7 @@ private class ImageThumbnailWorker extends SwingWorker<Void, Void> { * @param file The ResultFile which represents the image file thumbnails * are being retrieved for. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) ImageThumbnailWorker(ResultFile file) { thumbnailWrapper = new ImageThumbnailWrapper(file); imageThumbnailViewer.addImage(thumbnailWrapper); @@ -788,6 +801,7 @@ private class DocumentPreviewWorker extends SwingWorker<Void, Void> { * @param file The ResultFile which represents the document file a * preview is being retrieved for. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) DocumentPreviewWorker(ResultFile file) { documentWrapper = new DocumentWrapper(file); documentPreviewViewer.addDocument(documentWrapper); @@ -836,6 +850,7 @@ private class DomainThumbnailWorker extends SwingWorker<Void, Void> { * @param file The ResultFile which represents the domain attribute the * preview is being retrieved for. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) DomainThumbnailWorker(SleuthkitCase caseDb, ResultDomain domain) { this.caseDb = caseDb; domainWrapper = new DomainWrapper(domain); diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/ResultsSplitPaneDivider.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/ResultsSplitPaneDivider.java index cffb2dac7ffc2c8b06f6a49911409dec5f3af6dd..87c42fbc39d1238d336d552b57e90a596bdfb220 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/ResultsSplitPaneDivider.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/ResultsSplitPaneDivider.java @@ -19,6 +19,7 @@ package org.sleuthkit.autopsy.discovery.ui; import java.awt.Cursor; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.discovery.search.DiscoveryEventUtils; @@ -32,6 +33,7 @@ final class ResultsSplitPaneDivider extends javax.swing.JPanel { /** * Creates new form LabeledSplitPaneDivider. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) ResultsSplitPaneDivider() { initComponents(); } diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/SizeFilterPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/SizeFilterPanel.java index 0585750aa4132869e1d38e09f84539b6ca8a6fe4..e037f1f8eedae039a446063e738b19870770a359 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/SizeFilterPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/SizeFilterPanel.java @@ -26,6 +26,7 @@ import javax.swing.JLabel; import javax.swing.JList; import org.openide.util.NbBundle; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.discovery.search.SearchData; import org.sleuthkit.autopsy.discovery.search.SearchData.FileSize; import org.sleuthkit.autopsy.discovery.search.SearchFiltering; @@ -42,6 +43,7 @@ final class SizeFilterPanel extends AbstractDiscoveryFilterPanel { * * @param type The type of result being searched for. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) SizeFilterPanel(SearchData.Type type) { initComponents(); setUpSizeFilter(type); @@ -109,6 +111,7 @@ private void sizeCheckboxActionPerformed(java.awt.event.ActionEvent evt) {//GEN- private javax.swing.JScrollPane sizeScrollPane; // End of variables declaration//GEN-END:variables + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override void configurePanel(boolean selected, int[] indicesSelected) { sizeCheckbox.setSelected(selected); @@ -124,6 +127,7 @@ void configurePanel(boolean selected, int[] indicesSelected) { } } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override JCheckBox getCheckbox() { return sizeCheckbox; @@ -137,6 +141,7 @@ JLabel getAdditionalLabel() { /** * Initialize the file size filter. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) private void setUpSizeFilter(SearchData.Type fileType) { int count = 0; DefaultListModel<FileSize> sizeListModel = (DefaultListModel<FileSize>) sizeList.getModel(); @@ -169,6 +174,7 @@ private void setUpSizeFilter(SearchData.Type fileType) { @NbBundle.Messages({"SizeFilterPanel.error.text=At least one size must be selected."}) @Override + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) String checkForError() { if (sizeCheckbox.isSelected() && sizeList.getSelectedValuesList().isEmpty()) { return Bundle.SizeFilterPanel_error_text(); @@ -177,11 +183,13 @@ String checkForError() { } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override JList<?> getList() { return sizeList; } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override AbstractFilter getFilter() { if (sizeCheckbox.isSelected()) { diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/UserCreatedFilterPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/UserCreatedFilterPanel.java index d65596633aa2be578957a3b74033baa8a0258f82..10d976f7fe60b67e5cf4bf22f85e0b7e4cfe5052 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/UserCreatedFilterPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/UserCreatedFilterPanel.java @@ -22,6 +22,7 @@ import javax.swing.JCheckBox; import javax.swing.JLabel; import javax.swing.JList; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.discovery.search.SearchFiltering; /** @@ -34,6 +35,7 @@ final class UserCreatedFilterPanel extends AbstractDiscoveryFilterPanel { /** * Creates new form UserCreatedFilterPanel. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) UserCreatedFilterPanel() { initComponents(); } @@ -69,11 +71,13 @@ private void initComponents() { ); }// </editor-fold>//GEN-END:initComponents + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override void configurePanel(boolean selected, int[] indicesSelected) { userCreatedCheckbox.setSelected(selected); } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override JCheckBox getCheckbox() { return userCreatedCheckbox; @@ -99,6 +103,7 @@ JList<?> getList() { return null; } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override AbstractFilter getFilter() { if (userCreatedCheckbox.isSelected()) { diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/VideoFilterPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/VideoFilterPanel.java index 82b0d030bce8d1397aef5099a466cdc67420a7b2..ca96c217c4f02325056cc4da2cf0114dc4c46230 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/VideoFilterPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/VideoFilterPanel.java @@ -19,6 +19,7 @@ package org.sleuthkit.autopsy.discovery.ui; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.autopsy.discovery.search.SearchData; /** @@ -32,6 +33,7 @@ final class VideoFilterPanel extends AbstractFiltersPanel { /** * Creates new form VideoFilterPanel. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) VideoFilterPanel() { super(); initComponents(); diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/VideoThumbnailPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/VideoThumbnailPanel.java index 7a62937582fd7b6fdc7580585b5e65900ced3ef2..161e6a0770411a067a75e076f2e135e11c12da4d 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/VideoThumbnailPanel.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/VideoThumbnailPanel.java @@ -32,6 +32,7 @@ import javax.swing.JList; import javax.swing.ListCellRenderer; import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; /** * Class which displays thumbnails and information for a video file. @@ -47,6 +48,7 @@ final class VideoThumbnailPanel extends javax.swing.JPanel implements ListCellRe /** * Creates new form VideoThumbnailPanel. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) VideoThumbnailPanel() { initComponents(); this.setFocusable(true); @@ -58,6 +60,7 @@ final class VideoThumbnailPanel extends javax.swing.JPanel implements ListCellRe * @param thumbnailWrapper The object which contains the video thumbnails to * add. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) private void addThumbnails(VideoThumbnailsWrapper thumbnailWrapper) { imagePanel.removeAll(); GridBagConstraints gridBagConstraints = new GridBagConstraints(); @@ -164,6 +167,7 @@ private void initComponents() { private javax.swing.JLabel scoreLabel; // End of variables declaration//GEN-END:variables + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Messages({ "# {0} - otherInstanceCount", "VideoThumbnailPanel.nameLabel.more.text= and {0} more", @@ -231,6 +235,7 @@ private String getFileSizeString(long bytes) { return Bundle.VideoThumbnailPanel_sizeLabel_text(size, units); } + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) @Override public String getToolTipText(MouseEvent event) { if (event != null) { diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/VideoThumbnailViewer.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/VideoThumbnailViewer.java index 8824e6f5d891a25f607f5452111e171be3a95771..f2c250344d5d378d7487e9875224736179e520ef 100644 --- a/Core/src/org/sleuthkit/autopsy/discovery/ui/VideoThumbnailViewer.java +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/VideoThumbnailViewer.java @@ -22,6 +22,7 @@ import java.util.List; import javax.swing.DefaultListModel; import javax.swing.event.ListSelectionListener; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; import org.sleuthkit.datamodel.AbstractFile; /** @@ -36,6 +37,7 @@ final class VideoThumbnailViewer extends javax.swing.JPanel { /** * Creates new form VideoThumbnailViewer. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) VideoThumbnailViewer() { initComponents(); } @@ -45,6 +47,7 @@ final class VideoThumbnailViewer extends javax.swing.JPanel { * * @param listener The ListSelectionListener to add to the selection model. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) void addListSelectionListener(ListSelectionListener listener) { thumbnailList.getSelectionModel().addListSelectionListener(listener); } @@ -56,24 +59,22 @@ void addListSelectionListener(ListSelectionListener listener) { * @return The list of AbstractFiles which are represented by the selected * Video thumbnails. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) List<AbstractFile> getInstancesForSelected() { - synchronized (this) { - if (thumbnailList.getSelectedIndex() == -1) { - return new ArrayList<>(); - } else { - return thumbnailListModel.getElementAt(thumbnailList.getSelectedIndex()).getResultFile().getAllInstances(); - } + if (thumbnailList.getSelectedIndex() == -1) { + return new ArrayList<>(); + } else { + return thumbnailListModel.getElementAt(thumbnailList.getSelectedIndex()).getResultFile().getAllInstances(); } } /** * Clear the list of thumbnails being displayed. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) void clearViewer() { - synchronized (this) { - thumbnailListModel.removeAllElements(); - thumbnailListScrollPane.getVerticalScrollBar().setValue(0); - } + thumbnailListModel.removeAllElements(); + thumbnailListScrollPane.getVerticalScrollBar().setValue(0); } /** @@ -82,10 +83,9 @@ void clearViewer() { * @param thumbnailWrapper The object which contains the thumbnails which * will be displayed. */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) void addVideo(VideoThumbnailsWrapper thumbnailWrapper) { - synchronized (this) { - thumbnailListModel.addElement(thumbnailWrapper); - } + thumbnailListModel.addElement(thumbnailWrapper); } /** diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/WebHistoryDetailsPanel.form b/Core/src/org/sleuthkit/autopsy/discovery/ui/WebHistoryDetailsPanel.form new file mode 100644 index 0000000000000000000000000000000000000000..5f3eab1a5f91bb18ae28ed94f4701042de7d9b17 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/WebHistoryDetailsPanel.form @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8" ?> + +<Form version="1.4" maxVersion="1.9" type="org.netbeans.modules.form.forminfo.JPanelFormInfo"> + <AuxValues> + <AuxValue name="FormSettings_autoResourcing" type="java.lang.Integer" value="1"/> + <AuxValue name="FormSettings_autoSetComponentName" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_generateFQN" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_generateMnemonicsCode" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_i18nAutoMode" type="java.lang.Boolean" value="true"/> + <AuxValue name="FormSettings_layoutCodeTarget" type="java.lang.Integer" value="1"/> + <AuxValue name="FormSettings_listenerGenerationStyle" type="java.lang.Integer" value="0"/> + <AuxValue name="FormSettings_variablesLocal" type="java.lang.Boolean" value="false"/> + <AuxValue name="FormSettings_variablesModifier" type="java.lang.Integer" value="2"/> + </AuxValues> + + <Layout> + <DimensionLayout dim="0"> + <Group type="103" groupAlignment="0" attributes="0"> + <EmptySpace min="0" pref="400" max="32767" attributes="0"/> + </Group> + </DimensionLayout> + <DimensionLayout dim="1"> + <Group type="103" groupAlignment="0" attributes="0"> + <EmptySpace min="0" pref="300" max="32767" attributes="0"/> + </Group> + </DimensionLayout> + </Layout> +</Form> diff --git a/Core/src/org/sleuthkit/autopsy/discovery/ui/WebHistoryDetailsPanel.java b/Core/src/org/sleuthkit/autopsy/discovery/ui/WebHistoryDetailsPanel.java new file mode 100644 index 0000000000000000000000000000000000000000..deaef104f5e61c8b4162b422a9d69961ffdcb17a --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/discovery/ui/WebHistoryDetailsPanel.java @@ -0,0 +1,222 @@ +/* + * Autopsy + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> 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.discovery.ui; + +import java.awt.Component; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import javax.swing.JScrollPane; +import org.openide.util.NbBundle; +import org.openide.util.lookup.ServiceProvider; +import org.sleuthkit.autopsy.contentviewers.artifactviewers.ArtifactContentViewer; +import org.sleuthkit.autopsy.contentviewers.artifactviewers.CommunicationArtifactViewerHelper; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.coreutils.ThreadConfined; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Panel to display the details for a Web History Artifact. + */ +@ServiceProvider(service = ArtifactContentViewer.class) +public class WebHistoryDetailsPanel extends AbstractArtifactDetailsPanel implements ArtifactContentViewer { + + private static final long serialVersionUID = 1L; + private static final Logger logger = Logger.getLogger(WebHistoryDetailsPanel.class.getName()); + private BlackboardArtifact webHistoryArtifact; + private final GridBagLayout gridBagLayout = new GridBagLayout(); + private final List<BlackboardAttribute> urlList = new ArrayList<>(); + private final List<BlackboardAttribute> dateAccessedList = new ArrayList<>(); + private final List<BlackboardAttribute> referrerUrlList = new ArrayList<>(); + private final List<BlackboardAttribute> titleList = new ArrayList<>(); + private final List<BlackboardAttribute> programNameList = new ArrayList<>(); + private final List<BlackboardAttribute> domainList = new ArrayList<>(); + private final List<BlackboardAttribute> otherList = new ArrayList<>(); + private final GridBagConstraints gridBagConstraints = new GridBagConstraints(); + private String dataSourceName; + private String sourceFileName; + + /** + * Creates new form WebHistoryDetailsPanel. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + public WebHistoryDetailsPanel() { + initComponents(); + } + + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + @Override + public void setArtifact(BlackboardArtifact artifact) { + resetComponent(); + if (artifact != null) { + try { + extractArtifactData(artifact); + } catch (TskCoreException ex) { + logger.log(Level.WARNING, "Unable to get attributes for artifact " + artifact.getArtifactID(), ex); + } + updateView(); + } + this.setLayout(this.gridBagLayout); + this.revalidate(); + this.repaint(); + } + + /** + * Extracts data from the artifact to be displayed in the panel. + * + * @param artifact Artifact to show. + * + * @throws TskCoreException + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + private void extractArtifactData(BlackboardArtifact artifact) throws TskCoreException { + + webHistoryArtifact = artifact; + // Get all the attributes and group them by the section panels they go in + for (BlackboardAttribute bba : webHistoryArtifact.getAttributes()) { + if (bba.getAttributeType().getTypeName().startsWith("TSK_URL")) { + urlList.add(bba); + } else if (bba.getAttributeType().getTypeName().startsWith("TSK_PROG_NAME")) { + programNameList.add(bba); + } else if (bba.getAttributeType().getTypeName().startsWith("TSK_DOMAIN")) { + domainList.add(bba); + } else if (bba.getAttributeType().getTypeName().startsWith("TSK_REFERRER")) { + referrerUrlList.add(bba); + } else if (bba.getAttributeType().getTypeName().startsWith("TSK_DATETIME_ACCESSED")) { + dateAccessedList.add(bba); + } else if (bba.getAttributeType().getTypeName().startsWith("TSK_TITLE")) { + titleList.add(bba); + } else { + otherList.add(bba); + } + + } + + dataSourceName = webHistoryArtifact.getDataSource().getName(); + sourceFileName = webHistoryArtifact.getParent().getName(); + } + + /** + * Reset the panel so that it is empty. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + private void resetComponent() { + // clear the panel + this.removeAll(); + gridBagConstraints.anchor = GridBagConstraints.FIRST_LINE_START; + gridBagConstraints.gridy = 0; + gridBagConstraints.gridx = 0; + gridBagConstraints.weighty = 0.0; + gridBagConstraints.weightx = 0.0; // keep components fixed horizontally. + gridBagConstraints.insets = new java.awt.Insets(0, 12, 0, 0); + gridBagConstraints.fill = GridBagConstraints.NONE; + webHistoryArtifact = null; + dataSourceName = null; + sourceFileName = null; + urlList.clear(); + dateAccessedList.clear(); + referrerUrlList.clear(); + titleList.clear(); + programNameList.clear(); + domainList.clear(); + otherList.clear(); + } + + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + @Override + public Component getComponent() { + // Slap a vertical scrollbar on the panel. + return new JScrollPane(this, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + } + + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + @Override + public boolean isSupported(BlackboardArtifact artifact) { + return (artifact != null) + && (artifact.getArtifactTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_HISTORY.getTypeID()); + } + + @NbBundle.Messages({"WebHistoryDetailsPanel.details.attrHeader=Attributes", + "WebHistoryDetailsPanel.details.sourceHeader=Source", + "WebHistoryDetailsPanel.details.dataSource=Data Source", + "WebHistoryDetailsPanel.details.file=File"}) + /** + * 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") + // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents + private void initComponents() { + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); + this.setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGap(0, 400, Short.MAX_VALUE) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGap(0, 300, Short.MAX_VALUE) + ); + }// </editor-fold>//GEN-END:initComponents + + /** + * Update the view to reflect the current artifact's details. + */ + @ThreadConfined(type = ThreadConfined.ThreadType.AWT) + private void updateView() { + CommunicationArtifactViewerHelper.addHeader(this, gridBagLayout, gridBagConstraints, Bundle.WebHistoryDetailsPanel_details_attrHeader()); + + for (BlackboardAttribute bba : this.titleList) { + CommunicationArtifactViewerHelper.addNameValueRow(this, gridBagLayout, gridBagConstraints, bba.getAttributeType().getDisplayName(), bba.getDisplayString()); + } + for (BlackboardAttribute bba : dateAccessedList) { + CommunicationArtifactViewerHelper.addNameValueRow(this, gridBagLayout, gridBagConstraints, bba.getAttributeType().getDisplayName(), bba.getDisplayString()); + } + for (BlackboardAttribute bba : domainList) { + CommunicationArtifactViewerHelper.addNameValueRow(this, gridBagLayout, gridBagConstraints, bba.getAttributeType().getDisplayName(), bba.getDisplayString()); + } + for (BlackboardAttribute bba : urlList) { + CommunicationArtifactViewerHelper.addNameValueRow(this, gridBagLayout, gridBagConstraints, bba.getAttributeType().getDisplayName(), bba.getDisplayString()); + } + for (BlackboardAttribute bba : referrerUrlList) { + CommunicationArtifactViewerHelper.addNameValueRow(this, gridBagLayout, gridBagConstraints, bba.getAttributeType().getDisplayName(), bba.getDisplayString()); + } + for (BlackboardAttribute bba : programNameList) { + CommunicationArtifactViewerHelper.addNameValueRow(this, gridBagLayout, gridBagConstraints, bba.getAttributeType().getDisplayName(), bba.getDisplayString()); + } + for (BlackboardAttribute bba : otherList) { + CommunicationArtifactViewerHelper.addNameValueRow(this, gridBagLayout, gridBagConstraints, bba.getAttributeType().getDisplayName(), bba.getDisplayString()); + } + CommunicationArtifactViewerHelper.addHeader(this, gridBagLayout, gridBagConstraints, Bundle.WebHistoryDetailsPanel_details_sourceHeader()); + CommunicationArtifactViewerHelper.addNameValueRow(this, gridBagLayout, gridBagConstraints, Bundle.WebHistoryDetailsPanel_details_dataSource(), dataSourceName); + CommunicationArtifactViewerHelper.addNameValueRow(this, gridBagLayout, gridBagConstraints, Bundle.WebHistoryDetailsPanel_details_file(), sourceFileName); + // add veritcal glue at the end + CommunicationArtifactViewerHelper.addPageEndGlue(this, gridBagLayout, this.gridBagConstraints); + } + + + // Variables declaration - do not modify//GEN-BEGIN:variables + // End of variables declaration//GEN-END:variables +} diff --git a/Core/src/org/sleuthkit/autopsy/filesearch/Bundle.properties b/Core/src/org/sleuthkit/autopsy/filesearch/Bundle.properties index 8960448da43aa80dc24939cefff229c89146d53b..034149f5ec80150ee4adfcf29204969b852b1fef 100644 --- a/Core/src/org/sleuthkit/autopsy/filesearch/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/filesearch/Bundle.properties @@ -49,6 +49,7 @@ SizeSearchPanel.sizeCompareComboBox.lessThan=less than FileSearchPanel.searchButton.text=Search MimeTypePanel.mimeTypeCheckBox.text=MIME Type: HashSearchPanel.md5CheckBox.text=MD5: +Sha256HashSearchPanel.sha256CheckBox.text=SHA-256: HashSearchPanel.emptyHashMsg.text=Must enter something for hash search. FileSearchPanel.errorLabel.text=\ DataSourcePanel.dataSourceCheckBox.label=Data Source: @@ -58,3 +59,5 @@ DataSourcePanel.dataSourceNoteLabel.text=*Note: Multiple data sources can be sel DateSearchPanel.noLimitLabel.text=*Empty fields mean "No Limit" DateSearchPanel.dateFormatLabel.text=*The date format is mm/dd/yyyy MimeTypePanel.noteLabel.text=*Note: Multiple MIME types can be selected +HashSearchPanel.sha256CheckBox.text=SHA-256: +HashSearchPanel.sha256TextField.text= diff --git a/Core/src/org/sleuthkit/autopsy/filesearch/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/filesearch/Bundle.properties-MERGED index b304bc1342d314038aa13fe9bdb0cbe6fdb14530..93c5ed79876d005affedc1750760334bccda8189 100755 --- a/Core/src/org/sleuthkit/autopsy/filesearch/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/filesearch/Bundle.properties-MERGED @@ -5,7 +5,9 @@ FileSearchPanel.emptyNode.display.text=No results found. HashSearchFilter.errorMessage.emptyHash=Hash data is empty. HashSearchFilter.errorMessage.wrongCharacter=MD5 contains invalid hex characters. # {0} - hash data length -HashSearchFilter.errorMessage.wrongLength=Input length({0}), doesn''t match the MD5 length(32). +HashSearchFilter.errorMessage.wrongLengthMd5=Input length({0}), doesn''t match the MD5 length(32). +# {0} - hash data length +HashSearchFilter.errorMessage.wrongLengthSha256=Input length({0}), doesn''t match the SHA-256 length(64). KnownStatusSearchFilter.errorMessage.noKnownStatusCheckboxSelected=At least one known status checkbox must be selected. MimeTypeFilter.errorMessage.emptyMimeType=At least one MIME type must be selected. NameSearchFilter.errorMessage.emtpyName=Please input a name to search. @@ -63,6 +65,7 @@ SizeSearchPanel.sizeCompareComboBox.lessThan=less than FileSearchPanel.searchButton.text=Search MimeTypePanel.mimeTypeCheckBox.text=MIME Type: HashSearchPanel.md5CheckBox.text=MD5: +Sha256HashSearchPanel.sha256CheckBox.text=SHA-256: HashSearchPanel.emptyHashMsg.text=Must enter something for hash search. FileSearchPanel.errorLabel.text=\ DataSourcePanel.dataSourceCheckBox.label=Data Source: @@ -72,3 +75,5 @@ DataSourcePanel.dataSourceNoteLabel.text=*Note: Multiple data sources can be sel DateSearchPanel.noLimitLabel.text=*Empty fields mean "No Limit" DateSearchPanel.dateFormatLabel.text=*The date format is mm/dd/yyyy MimeTypePanel.noteLabel.text=*Note: Multiple MIME types can be selected +HashSearchPanel.sha256CheckBox.text=SHA-256: +HashSearchPanel.sha256TextField.text= diff --git a/Core/src/org/sleuthkit/autopsy/filesearch/HashSearchFilter.java b/Core/src/org/sleuthkit/autopsy/filesearch/HashSearchFilter.java index d77fe5826e45ba99e947367241a099e1ead74ac8..1585f1051e143c4d39dd28afa6f7f38470219879 100644 --- a/Core/src/org/sleuthkit/autopsy/filesearch/HashSearchFilter.java +++ b/Core/src/org/sleuthkit/autopsy/filesearch/HashSearchFilter.java @@ -41,18 +41,36 @@ public HashSearchFilter(HashSearchPanel component) { @Override public boolean isEnabled() { - return this.getComponent().getHashCheckBox().isSelected(); + return (this.getComponent().getMd5HashCheckBox().isSelected() + || this.getComponent().getSha256HashCheckBox().isSelected()); } @Override public String getPredicate() throws FilterValidationException { - String md5Hash = this.getComponent().getSearchTextField().getText(); + String predicate = ""; + if (this.getComponent().getMd5HashCheckBox().isSelected()) { + String md5Hash = this.getComponent().getMd5TextField().getText(); - if (md5Hash.isEmpty()) { - throw new FilterValidationException(EMPTY_HASH_MESSAGE); + if (md5Hash.isEmpty()) { + throw new FilterValidationException(EMPTY_HASH_MESSAGE); + } + predicate = "md5 = '" + md5Hash.toLowerCase() + "'"; //NON-NLS } + + if (this.getComponent().getSha256HashCheckBox().isSelected()) { + String sha256Hash = this.getComponent().getSha256TextField().getText(); - return "md5 = '" + md5Hash.toLowerCase() + "'"; //NON-NLS + if (sha256Hash.isEmpty()) { + throw new FilterValidationException(EMPTY_HASH_MESSAGE); + } + if (predicate.isEmpty()) { + predicate = "sha256 = '" + sha256Hash.toLowerCase() + "'"; //NON-NLS + } else { + predicate = "( " + predicate + " AND sha256 = '" + sha256Hash.toLowerCase() + "')"; //NON-NLS + } + } + + return predicate; } @Override @@ -63,23 +81,45 @@ public void addActionListener(ActionListener l) { @Override @Messages({ "HashSearchFilter.errorMessage.emptyHash=Hash data is empty.", - "# {0} - hash data length", "HashSearchFilter.errorMessage.wrongLength=Input length({0}), doesn''t match the MD5 length(32).", + "# {0} - hash data length", + "HashSearchFilter.errorMessage.wrongLengthMd5=Input length({0}), doesn''t match the MD5 length(32).", + "# {0} - hash data length", + "HashSearchFilter.errorMessage.wrongLengthSha256=Input length({0}), doesn''t match the SHA-256 length(64).", "HashSearchFilter.errorMessage.wrongCharacter=MD5 contains invalid hex characters." }) public boolean isValid() { - String inputHashData = this.getComponent().getSearchTextField().getText(); - if (inputHashData.isEmpty()) { - setLastError(Bundle.HashSearchFilter_errorMessage_emptyHash()); - return false; - } - if (inputHashData.length() != 32) { - setLastError(Bundle.HashSearchFilter_errorMessage_wrongLength(inputHashData.length())); - return false; + if (this.getComponent().getMd5HashCheckBox().isSelected()) { + String inputHashData = this.getComponent().getMd5TextField().getText(); + if (inputHashData.isEmpty()) { + setLastError(Bundle.HashSearchFilter_errorMessage_emptyHash()); + return false; + } + if (inputHashData.length() != 32) { + setLastError(Bundle.HashSearchFilter_errorMessage_wrongLengthMd5(inputHashData.length())); + return false; + } + if (!inputHashData.matches("[0-9a-fA-F]+")) { + setLastError(Bundle.HashSearchFilter_errorMessage_wrongCharacter()); + return false; + } } - if (!inputHashData.matches("[0-9a-fA-F]+")) { - setLastError(Bundle.HashSearchFilter_errorMessage_wrongCharacter()); - return false; + + if (this.getComponent().getSha256HashCheckBox().isSelected()) { + String inputHashData = this.getComponent().getSha256TextField().getText(); + if (inputHashData.isEmpty()) { + setLastError(Bundle.HashSearchFilter_errorMessage_emptyHash()); + return false; + } + if (inputHashData.length() != 64) { + setLastError(Bundle.HashSearchFilter_errorMessage_wrongLengthSha256(inputHashData.length())); + return false; + } + if (!inputHashData.matches("[0-9a-fA-F]+")) { + setLastError(Bundle.HashSearchFilter_errorMessage_wrongCharacter()); + return false; + } } + return true; } } diff --git a/Core/src/org/sleuthkit/autopsy/filesearch/HashSearchPanel.form b/Core/src/org/sleuthkit/autopsy/filesearch/HashSearchPanel.form index 0ad53c965327195c2f10687163adf323e9b37c70..88fd9cabfcdfc7df6c46728be2b42733c00d3b81 100644 --- a/Core/src/org/sleuthkit/autopsy/filesearch/HashSearchPanel.form +++ b/Core/src/org/sleuthkit/autopsy/filesearch/HashSearchPanel.form @@ -56,34 +56,67 @@ <Group type="103" groupAlignment="0" attributes="0"> <Group type="102" alignment="0" attributes="0"> <EmptySpace min="0" pref="0" max="-2" attributes="0"/> - <Component id="hashCheckBox" min="-2" max="-2" attributes="0"/> + <Group type="103" groupAlignment="0" attributes="0"> + <Group type="102" attributes="0"> + <Component id="sha256CheckBox" min="-2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="sha256TextField" pref="254" max="32767" attributes="0"/> + </Group> + <Group type="102" attributes="0"> + <Component id="md5CheckBox" min="-2" max="-2" attributes="0"/> + <EmptySpace max="-2" attributes="0"/> + <Component id="md5TextField" max="32767" attributes="0"/> + </Group> + </Group> <EmptySpace max="-2" attributes="0"/> - <Component id="searchTextField" min="-2" pref="247" max="-2" attributes="0"/> - <EmptySpace min="0" pref="0" max="-2" attributes="0"/> </Group> </Group> </DimensionLayout> <DimensionLayout dim="1"> <Group type="103" groupAlignment="0" attributes="0"> - <Group type="103" groupAlignment="3" attributes="0"> - <Component id="hashCheckBox" alignment="3" min="-2" max="-2" attributes="0"/> - <Component id="searchTextField" alignment="3" min="-2" max="-2" attributes="0"/> + <Group type="102" attributes="0"> + <Group type="103" groupAlignment="3" attributes="0"> + <Component id="md5CheckBox" alignment="3" min="-2" max="-2" attributes="0"/> + <Component id="md5TextField" alignment="3" min="-2" max="-2" attributes="0"/> + </Group> + <EmptySpace max="-2" attributes="0"/> + <Group type="103" groupAlignment="3" attributes="0"> + <Component id="sha256CheckBox" alignment="3" min="-2" max="-2" attributes="0"/> + <Component id="sha256TextField" alignment="3" min="-2" max="-2" attributes="0"/> + </Group> </Group> </Group> </DimensionLayout> </Layout> <SubComponents> - <Component class="javax.swing.JCheckBox" name="hashCheckBox"> + <Component class="javax.swing.JCheckBox" name="md5CheckBox"> <Properties> <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> <ResourceString bundle="org/sleuthkit/autopsy/filesearch/Bundle.properties" key="HashSearchPanel.md5CheckBox.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> </Property> </Properties> <Events> - <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="hashCheckBoxActionPerformed"/> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="md5CheckBoxActionPerformed"/> + </Events> + </Component> + <Component class="javax.swing.JTextField" name="md5TextField"> + </Component> + <Component class="javax.swing.JCheckBox" name="sha256CheckBox"> + <Properties> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="org/sleuthkit/autopsy/filesearch/Bundle.properties" key="HashSearchPanel.sha256CheckBox.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> + </Property> + </Properties> + <Events> + <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="sha256CheckBoxActionPerformed"/> </Events> </Component> - <Component class="javax.swing.JTextField" name="searchTextField"> + <Component class="javax.swing.JTextField" name="sha256TextField"> + <Properties> + <Property name="text" type="java.lang.String" editor="org.netbeans.modules.i18n.form.FormI18nStringEditor"> + <ResourceString bundle="org/sleuthkit/autopsy/filesearch/Bundle.properties" key="HashSearchPanel.sha256TextField.text" replaceFormat="org.openide.util.NbBundle.getMessage({sourceFileName}.class, "{key}")"/> + </Property> + </Properties> </Component> </SubComponents> </Form> diff --git a/Core/src/org/sleuthkit/autopsy/filesearch/HashSearchPanel.java b/Core/src/org/sleuthkit/autopsy/filesearch/HashSearchPanel.java index f166d01264de32dcdccb31fbb0090e95af93b47b..66d539642039ab30d87e15954baa326dc9693aca 100644 --- a/Core/src/org/sleuthkit/autopsy/filesearch/HashSearchPanel.java +++ b/Core/src/org/sleuthkit/autopsy/filesearch/HashSearchPanel.java @@ -45,19 +45,19 @@ class HashSearchPanel extends javax.swing.JPanel { private void customizeComponents() { - searchTextField.setComponentPopupMenu(rightClickMenu); + md5TextField.setComponentPopupMenu(rightClickMenu); ActionListener actList = new ActionListener() { @Override public void actionPerformed(ActionEvent e) { JMenuItem jmi = (JMenuItem) e.getSource(); if (jmi.equals(cutMenuItem)) { - searchTextField.cut(); + md5TextField.cut(); } else if (jmi.equals(copyMenuItem)) { - searchTextField.copy(); + md5TextField.copy(); } else if (jmi.equals(pasteMenuItem)) { - searchTextField.paste(); + md5TextField.paste(); } else if (jmi.equals(selectAllMenuItem)) { - searchTextField.selectAll(); + md5TextField.selectAll(); } } }; @@ -65,7 +65,24 @@ public void actionPerformed(ActionEvent e) { copyMenuItem.addActionListener(actList); pasteMenuItem.addActionListener(actList); selectAllMenuItem.addActionListener(actList); - this.searchTextField.getDocument().addDocumentListener(new DocumentListener() { + this.md5TextField.getDocument().addDocumentListener(new DocumentListener() { + @Override + public void insertUpdate(DocumentEvent e) { + firePropertyChange(FileSearchPanel.EVENT.CHECKED.toString(), null, null); + } + + @Override + public void removeUpdate(DocumentEvent e) { + firePropertyChange(FileSearchPanel.EVENT.CHECKED.toString(), null, null); + } + + @Override + public void changedUpdate(DocumentEvent e) { + firePropertyChange(FileSearchPanel.EVENT.CHECKED.toString(), null, null); + } + }); + + this.sha256TextField.getDocument().addDocumentListener(new DocumentListener() { @Override public void insertUpdate(DocumentEvent e) { firePropertyChange(FileSearchPanel.EVENT.CHECKED.toString(), null, null); @@ -84,17 +101,27 @@ public void changedUpdate(DocumentEvent e) { } - JCheckBox getHashCheckBox() { - return hashCheckBox; + JCheckBox getMd5HashCheckBox() { + return md5CheckBox; } - JTextField getSearchTextField() { - return searchTextField; + JTextField getMd5TextField() { + return md5TextField; + } + + JCheckBox getSha256HashCheckBox() { + return sha256CheckBox; + } + + JTextField getSha256TextField() { + return sha256TextField; } void setComponentsEnabled() { - boolean enabled = hashCheckBox.isSelected(); - this.searchTextField.setEnabled(enabled); + boolean md5Enabled = md5CheckBox.isSelected(); + this.md5TextField.setEnabled(md5Enabled); + boolean sha256Enabled = sha256CheckBox.isSelected(); + this.sha256TextField.setEnabled(sha256Enabled); } /** @@ -111,8 +138,10 @@ private void initComponents() { copyMenuItem = new javax.swing.JMenuItem(); pasteMenuItem = new javax.swing.JMenuItem(); selectAllMenuItem = new javax.swing.JMenuItem(); - hashCheckBox = new javax.swing.JCheckBox(); - searchTextField = new javax.swing.JTextField(); + md5CheckBox = new javax.swing.JCheckBox(); + md5TextField = new javax.swing.JTextField(); + sha256CheckBox = new javax.swing.JCheckBox(); + sha256TextField = new javax.swing.JTextField(); cutMenuItem.setText(org.openide.util.NbBundle.getMessage(HashSearchPanel.class, "NameSearchPanel.cutMenuItem.text")); // NOI18N rightClickMenu.add(cutMenuItem); @@ -126,48 +155,75 @@ private void initComponents() { selectAllMenuItem.setText(org.openide.util.NbBundle.getMessage(HashSearchPanel.class, "NameSearchPanel.selectAllMenuItem.text")); // NOI18N rightClickMenu.add(selectAllMenuItem); - hashCheckBox.setText(org.openide.util.NbBundle.getMessage(HashSearchPanel.class, "HashSearchPanel.md5CheckBox.text")); // NOI18N - hashCheckBox.addActionListener(new java.awt.event.ActionListener() { + md5CheckBox.setText(org.openide.util.NbBundle.getMessage(HashSearchPanel.class, "HashSearchPanel.md5CheckBox.text")); // NOI18N + md5CheckBox.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + md5CheckBoxActionPerformed(evt); + } + }); + + sha256CheckBox.setText(org.openide.util.NbBundle.getMessage(HashSearchPanel.class, "HashSearchPanel.sha256CheckBox.text")); // NOI18N + sha256CheckBox.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { - hashCheckBoxActionPerformed(evt); + sha256CheckBoxActionPerformed(evt); } }); + sha256TextField.setText(org.openide.util.NbBundle.getMessage(HashSearchPanel.class, "HashSearchPanel.sha256TextField.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() .addGap(0, 0, 0) - .addComponent(hashCheckBox) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(searchTextField, javax.swing.GroupLayout.PREFERRED_SIZE, 247, javax.swing.GroupLayout.PREFERRED_SIZE) - .addGap(0, 0, 0)) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addComponent(sha256CheckBox) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(sha256TextField, javax.swing.GroupLayout.DEFAULT_SIZE, 254, Short.MAX_VALUE)) + .addGroup(layout.createSequentialGroup() + .addComponent(md5CheckBox) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(md5TextField))) + .addContainerGap()) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(hashCheckBox) - .addComponent(searchTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(md5CheckBox) + .addComponent(md5TextField, 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.BASELINE) + .addComponent(sha256CheckBox) + .addComponent(sha256TextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))) ); }// </editor-fold>//GEN-END:initComponents - private void hashCheckBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_hashCheckBoxActionPerformed + private void md5CheckBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_md5CheckBoxActionPerformed + setComponentsEnabled(); + firePropertyChange(FileSearchPanel.EVENT.CHECKED.toString(), null, null); + }//GEN-LAST:event_md5CheckBoxActionPerformed + + private void sha256CheckBoxActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_sha256CheckBoxActionPerformed setComponentsEnabled(); firePropertyChange(FileSearchPanel.EVENT.CHECKED.toString(), null, null); - }//GEN-LAST:event_hashCheckBoxActionPerformed + }//GEN-LAST:event_sha256CheckBoxActionPerformed // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JMenuItem copyMenuItem; private javax.swing.JMenuItem cutMenuItem; - private javax.swing.JCheckBox hashCheckBox; + private javax.swing.JCheckBox md5CheckBox; + private javax.swing.JTextField md5TextField; private javax.swing.JMenuItem pasteMenuItem; private javax.swing.JPopupMenu rightClickMenu; - private javax.swing.JTextField searchTextField; private javax.swing.JMenuItem selectAllMenuItem; + private javax.swing.JCheckBox sha256CheckBox; + private javax.swing.JTextField sha256TextField; // End of variables declaration//GEN-END:variables void addActionListener(ActionListener l) { - searchTextField.addActionListener(l); + md5TextField.addActionListener(l); } } diff --git a/Core/src/org/sleuthkit/autopsy/ingest/IngestJobPipeline.java b/Core/src/org/sleuthkit/autopsy/ingest/IngestJobPipeline.java index 5921071e6530f9d9aefb4e6f71c63c87d22331cb..8e2f7cf86d1adc1097131c7d813f5d4947361e4d 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/IngestJobPipeline.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/IngestJobPipeline.java @@ -1274,6 +1274,9 @@ void cancel(IngestJob.CancellationReason reason) { } } } + + // If a data source had no tasks in progress it may now be complete. + checkForStageCompleted(); } /** diff --git a/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/DocumentEmbeddedContentExtractor.java b/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/DocumentEmbeddedContentExtractor.java index 35f27ca8f6053a25f930f221a01a8639c4134e44..a25dd9e1d76c1486087cc32e114882f7783baccc 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/DocumentEmbeddedContentExtractor.java +++ b/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/DocumentEmbeddedContentExtractor.java @@ -491,7 +491,7 @@ private List<ExtractedFile> extractEmbeddedContentFromPDF(AbstractFile abstractF try { Path outputDirectory = Paths.get(getOutputFolderPath(parentFileName)); //Get map of attachment name -> location disk. - Map<String, Path> extractedAttachments = pdfExtractor.extract( + Map<String, PDFAttachmentExtractor.NewResourceData> extractedAttachments = pdfExtractor.extract( new ReadContentInputStream(abstractFile), abstractFile.getId(), outputDirectory); @@ -499,10 +499,11 @@ private List<ExtractedFile> extractEmbeddedContentFromPDF(AbstractFile abstractF List<ExtractedFile> extractedFiles = new ArrayList<>(); extractedAttachments.entrySet().forEach((pathEntry) -> { String fileName = pathEntry.getKey(); - Path writeLocation = pathEntry.getValue(); + Path writeLocation = pathEntry.getValue().getPath(); + int fileSize = pathEntry.getValue().getLength(); extractedFiles.add(new ExtractedFile(fileName, getFileRelativePath(writeLocation.getFileName().toString()), - writeLocation.toFile().length())); + fileSize)); }); return extractedFiles; diff --git a/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/EmbeddedFileExtractorIngestModule.java b/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/EmbeddedFileExtractorIngestModule.java index dc9e7c6d1df8f6f82227d50a958671bd08fefa30..b1481ddc015323ea31c0be3d5dbb69cf764410d6 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/EmbeddedFileExtractorIngestModule.java +++ b/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/EmbeddedFileExtractorIngestModule.java @@ -117,6 +117,12 @@ public void startUp(IngestJobContext context) throws IngestModuleException { * while processing archive files. */ mapOfDepthTrees.put(jobId, new ConcurrentHashMap<>()); + /** + * Initialize Java's Image I/O API so that image reading and writing + * (needed for image extraction) happens consistently through the + * same providers. See JIRA-6951 for more details. + */ + initializeImageIO(); } /* * Construct an embedded content extractor for processing Microsoft @@ -127,14 +133,6 @@ public void startUp(IngestJobContext context) throws IngestModuleException { } catch (NoCurrentCaseException ex) { throw new IngestModuleException(Bundle.EmbeddedFileExtractorIngestModule_UnableToGetMSOfficeExtractor_errMsg(), ex); } - - /** - * Initialize Java's Image I/O API so that image reading and writing - * (needed for image extraction) happens consistently through the - * same providers. See JIRA-6951 for more details. - */ - initializeImageIO(); - } /** diff --git a/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/PDFAttachmentExtractor.java b/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/PDFAttachmentExtractor.java index 9cea0f63e7d533fd7a82ebf357d978baba44b278..d898aaa1ca9c5c08a4796022453289724d0751d9 100755 --- a/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/PDFAttachmentExtractor.java +++ b/Core/src/org/sleuthkit/autopsy/modules/embeddedfileextractor/PDFAttachmentExtractor.java @@ -73,7 +73,7 @@ public PDFAttachmentExtractor(AutoDetectParser parser) { * @throws SAXException * @throws TikaException */ - public Map<String, Path> extract(InputStream input, long parentID, Path outputDir) throws IOException, SAXException, TikaException { + public Map<String, NewResourceData> extract(InputStream input, long parentID, Path outputDir) throws IOException, SAXException, TikaException { ExtractionPreconditions.checkArgument(Files.exists(outputDir), String.format("Output directory: %s, does not exist.", outputDir.toString())); //NON-NLS @@ -139,8 +139,8 @@ public void parseEmbedded(InputStream in, ContentHandler ch, Metadata mtdt, bool try (EncodedFileOutputStream outputStream = new EncodedFileOutputStream( new FileOutputStream(outputFile.toFile()), TskData.EncodingType.XOR1)){ - IOUtils.copy(in, outputStream); - watcher.notify(name, outputFile); + int bytesCopied = IOUtils.copy(in, outputStream); + watcher.notify(name, outputFile, bytesCopied); } catch (IOException ex) { logger.log(Level.WARNING, String.format("Could not extract attachment %s into directory %s", //NON-NLS uniqueExtractedName, outputFile), ex); @@ -148,6 +148,29 @@ public void parseEmbedded(InputStream in, ContentHandler ch, Metadata mtdt, bool } } + /** + * Utility class to hold an extracted file's path and length. + * Note that we can not use the length of the file on disk because + * the XOR header has been added to it. + */ + static class NewResourceData { + private final Path path; + private final int length; + + NewResourceData(Path path, int length) { + this.path = path; + this.length = length; + } + + Path getPath() { + return path; + } + + int getLength() { + return length; + } + } + /** * Convenient wrapper for keeping track of new resource paths and the display * name for each of these resources. @@ -157,17 +180,17 @@ public void parseEmbedded(InputStream in, ContentHandler ch, Metadata mtdt, bool */ static class NewResourceWatcher { - private final Map<String, Path> newResourcePaths; + private final Map<String, NewResourceData> newResourcePaths; public NewResourceWatcher() { newResourcePaths = new HashMap<>(); } - public void notify(String name, Path newResource) { - newResourcePaths.put(name, newResource); + public void notify(String name, Path localPath, int length) { + newResourcePaths.put(name, new NewResourceData(localPath, length)); } - public Map<String, Path> getSnapshot() { + public Map<String, NewResourceData> getSnapshot() { return newResourcePaths; } } diff --git a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbIngestModule.java b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbIngestModule.java index 65bf047fa1ca358e7fcf3be16ea92eafc20e34db..19b9ff28b7b0ff9b493a8b227b0ebea6cda912c9 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbIngestModule.java +++ b/Core/src/org/sleuthkit/autopsy/modules/hashdatabase/HashDbIngestModule.java @@ -18,8 +18,8 @@ */ package org.sleuthkit.autopsy.modules.hashdatabase; -import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.List; @@ -208,10 +208,17 @@ public ProcessResult process(AbstractFile file) { // Safely get a reference to the totalsForIngestJobs object IngestJobTotals totals = getTotalsForIngestJobs(jobId); - // calc hash value - String md5Hash = getHash(file, totals); - if (md5Hash == null) { - return ProcessResult.ERROR; + // calc hash values + try { + calculateHashes(file, totals); + } catch (TskCoreException ex) { + logger.log(Level.WARNING, String.format("Error calculating hash of file '%s' (id=%d).", file.getName(), file.getId()), ex); //NON-NLS + services.postMessage(IngestMessage.createErrorMessage( + HashLookupModuleFactory.getModuleName(), + NbBundle.getMessage(this.getClass(), "HashDbIngestModule.fileReadErrorMsg", file.getName()), + NbBundle.getMessage(this.getClass(), "HashDbIngestModule.calcHashValueErr", + file.getParentPath() + file.getName(), + file.isMetaFlagSet(TskData.TSK_FS_META_FLAG_ENUM.ALLOC) ? "Allocated File" : "Deleted File"))); } // the processing result of handling this file @@ -451,50 +458,46 @@ private boolean createArtifactIfNotExists(String hashSetName, AbstractFile file, } /** - * Retrieves the md5 hash for a file or generates one if no one exists on - * the file. + * Generates hashes for the given file if they haven't already been set. + * Hashes are saved to the AbstractFile object. * * @param file The file in order to determine the hash. * @param totals The timing metrics for this process. - * - * @return The found or determined md5 hash or null if none could be - * determined. */ - private String getHash(AbstractFile file, IngestJobTotals totals) { + private void calculateHashes(AbstractFile file, IngestJobTotals totals) throws TskCoreException { + + // First check if we've already calculated the hashes. String md5Hash = file.getMd5Hash(); - if (md5Hash != null && md5Hash.isEmpty()) { - return md5Hash; + String sha256Hash = file.getSha256Hash(); + if ((md5Hash != null && ! md5Hash.isEmpty()) + && (sha256Hash != null && ! sha256Hash.isEmpty())) { + return; } - try { - TimingMetric metric = HealthMonitor.getTimingMetric("Disk Reads: Hash calculation"); - long calcstart = System.currentTimeMillis(); - md5Hash = HashUtility.calculateMd5Hash(file); - if (file.getSize() > 0) { - // Surprisingly, the hash calculation does not seem to be correlated that - // strongly with file size until the files get large. - // Only normalize if the file size is greater than ~1MB. - if (file.getSize() < 1000000) { - HealthMonitor.submitTimingMetric(metric); - } else { - // In testing, this normalization gave reasonable resuls - HealthMonitor.submitNormalizedTimingMetric(metric, file.getSize() / 500000); - } + TimingMetric metric = HealthMonitor.getTimingMetric("Disk Reads: Hash calculation"); + long calcstart = System.currentTimeMillis(); + List<HashUtility.HashResult> newHashResults = + HashUtility.calculateHashes(file, Arrays.asList(HashUtility.HashType.MD5,HashUtility.HashType.SHA256 )); + if (file.getSize() > 0) { + // Surprisingly, the hash calculation does not seem to be correlated that + // strongly with file size until the files get large. + // Only normalize if the file size is greater than ~1MB. + if (file.getSize() < 1000000) { + HealthMonitor.submitTimingMetric(metric); + } else { + // In testing, this normalization gave reasonable resuls + HealthMonitor.submitNormalizedTimingMetric(metric, file.getSize() / 500000); + } + } + for (HashUtility.HashResult hash : newHashResults) { + if (hash.getType().equals(HashUtility.HashType.MD5)) { + file.setMd5Hash(hash.getValue()); + } else if (hash.getType().equals(HashUtility.HashType.SHA256)) { + file.setSha256Hash(hash.getValue()); } - file.setMd5Hash(md5Hash); - long delta = (System.currentTimeMillis() - calcstart); - totals.totalCalctime.addAndGet(delta); - return md5Hash; - } catch (IOException ex) { - logger.log(Level.WARNING, String.format("Error calculating hash of file '%s' (id=%d).", file.getName(), file.getId()), ex); //NON-NLS - services.postMessage(IngestMessage.createErrorMessage( - HashLookupModuleFactory.getModuleName(), - NbBundle.getMessage(this.getClass(), "HashDbIngestModule.fileReadErrorMsg", file.getName()), - NbBundle.getMessage(this.getClass(), "HashDbIngestModule.calcHashValueErr", - file.getParentPath() + file.getName(), - file.isMetaFlagSet(TskData.TSK_FS_META_FLAG_ENUM.ALLOC) ? "Allocated File" : "Deleted File"))); - return null; } + long delta = (System.currentTimeMillis() - calcstart); + totals.totalCalctime.addAndGet(delta); } /** diff --git a/Core/src/org/sleuthkit/autopsy/modules/vmextractor/VMExtractorIngestModule.java b/Core/src/org/sleuthkit/autopsy/modules/vmextractor/VMExtractorIngestModule.java index 843b07a3d68ed62e85f6dbc287e93ae4111e6903..56e6b3fbdb686b17c2b05ac9d398fe6d8764817c 100644 --- a/Core/src/org/sleuthkit/autopsy/modules/vmextractor/VMExtractorIngestModule.java +++ b/Core/src/org/sleuthkit/autopsy/modules/vmextractor/VMExtractorIngestModule.java @@ -198,7 +198,6 @@ public ProcessResult process(Content dataSource, DataSourceIngestModuleProgress // for extracted virtual machines there is no manifest XML file to read data source ID from so use parent data source ID. // ingest the data sources ingestVirtualMachineImage(Paths.get(folder, file)); - logger.log(Level.INFO, "Ingest complete for virtual machine file {0} in folder {1}", new Object[]{file, folder}); //NON-NLS } catch (InterruptedException ex) { logger.log(Level.INFO, "Interrupted while ingesting virtual machine file " + file + " in folder " + folder, ex); //NON-NLS } catch (IOException ex) { @@ -287,8 +286,8 @@ private void ingestVirtualMachineImage(Path vmFile) throws InterruptedException, } /* - * If the image was added, analyze it with the ingest modules for this - * ingest context. + * If the image was added, start analysis on it with the ingest modules for this + * ingest context. Note that this does not wait for ingest to complete. */ if (!dspCallback.vmDataSources.isEmpty()) { Case.getCurrentCaseThrows().notifyDataSourceAdded(dspCallback.vmDataSources.get(0), taskId); @@ -300,7 +299,7 @@ private void ingestVirtualMachineImage(Path vmFile) throws InterruptedException, IngestServices.getInstance().postMessage(IngestMessage.createMessage(IngestMessage.MessageType.INFO, VMExtractorIngestModuleFactory.getModuleName(), NbBundle.getMessage(this.getClass(), "VMExtractorIngestModule.addedVirtualMachineImage.message", vmFile.toString()))); - IngestManager.getInstance().queueIngestJob(dataSourceContent, ingestJobSettings); + IngestManager.getInstance().beginIngestJob(dataSourceContent, ingestJobSettings); } else { Case.getCurrentCaseThrows().notifyFailedAddingDataSource(taskId); } diff --git a/Core/src/org/sleuthkit/autopsy/report/modules/portablecase/PortableCaseReportModule.java b/Core/src/org/sleuthkit/autopsy/report/modules/portablecase/PortableCaseReportModule.java index 83bfb2fdae050478a88e142415ff07aaa9c423cc..67b253a1fa66b6e420ecd4742aaa8af73a582838 100644 --- a/Core/src/org/sleuthkit/autopsy/report/modules/portablecase/PortableCaseReportModule.java +++ b/Core/src/org/sleuthkit/autopsy/report/modules/portablecase/PortableCaseReportModule.java @@ -1117,7 +1117,7 @@ private long copyContent(Content content) throws TskCoreException { newContent = portableSkCase.addLocalFile(abstractFile.getName(), relativePath, abstractFile.getSize(), abstractFile.getCtime(), abstractFile.getCrtime(), abstractFile.getAtime(), abstractFile.getMtime(), - abstractFile.getMd5Hash(), abstractFile.getKnown(), abstractFile.getMIMEType(), + abstractFile.getMd5Hash(), abstractFile.getSha256Hash(), abstractFile.getKnown(), abstractFile.getMIMEType(), true, TskData.EncodingType.NONE, newParent, trans); } catch (IOException ex) { diff --git a/Core/src/org/sleuthkit/autopsy/report/modules/stix/STIXReportModule.java b/Core/src/org/sleuthkit/autopsy/report/modules/stix/STIXReportModule.java index e300171cf11024df1f7d03fafed2101b6f8fbe94..5935dbf89e8a8d074fca646f8193adf9f5252e14 100644 --- a/Core/src/org/sleuthkit/autopsy/report/modules/stix/STIXReportModule.java +++ b/Core/src/org/sleuthkit/autopsy/report/modules/stix/STIXReportModule.java @@ -228,12 +228,19 @@ private void processFile(String stixFile, ReportProgressPanel progressPanel, Buf */ private STIXPackage loadSTIXFile(String stixFileName) throws JAXBException { // Create STIXPackage object from xml. - File file = new File(stixFileName); - JAXBContext jaxbContext = JAXBContext.newInstance("org.mitre.stix.stix_1:org.mitre.stix.common_1:org.mitre.stix.indicator_2:" //NON-NLS - + "org.mitre.cybox.objects:org.mitre.cybox.cybox_2:org.mitre.cybox.common_2"); //NON-NLS - Unmarshaller jaxbUnmarshaller = jaxbContext.createUnmarshaller(); - STIXPackage stix = (STIXPackage) jaxbUnmarshaller.unmarshal(file); - return stix; + // See JIRA-6958 for details about class loading and jaxb. + ClassLoader original = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(STIXReportModule.class.getClassLoader()); + File file = new File(stixFileName); + JAXBContext jaxbContext = JAXBContext.newInstance("org.mitre.stix.stix_1:org.mitre.stix.common_1:org.mitre.stix.indicator_2:" //NON-NLS + + "org.mitre.cybox.objects:org.mitre.cybox.cybox_2:org.mitre.cybox.common_2"); //NON-NLS + Unmarshaller jaxbUnmarshaller = jaxbContext.createUnmarshaller(); + STIXPackage stix = (STIXPackage) jaxbUnmarshaller.unmarshal(file); + return stix; + } finally { + Thread.currentThread().setContextClassLoader(original); + } } /** diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/README.txt b/Core/test/qa-functional/src/org/sleuthkit/autopsy/README.txt new file mode 100644 index 0000000000000000000000000000000000000000..592bb50d728c19776d28c7fd70503def2480a82f --- /dev/null +++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/README.txt @@ -0,0 +1,3 @@ +Netbeans platform does not properly scope classloaders while running qa-functional test code. The result is that NoClassDefError's occur in instances where an external jar (i.e. importing a class from common-io) is referenced in test code and the same external jar is referenced in multiple NBM's. Importing from external jars in qa-functional should be avoided. See jira issue 6954 for more information. + +Many of the functional tests require external data sources. The ant target 'getTestDataFiles' must be run successfully to download the files. This should occur as a part of the 'test-init' ant target in build.xml. \ No newline at end of file diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoAccountsTest.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoAccountsTest.java index ff8bd34250a18244b06e5020360aefef0e6a8c40..a87c2959badf359285f1e25271cc84660c47f092 100755 --- a/Core/test/qa-functional/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoAccountsTest.java +++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoAccountsTest.java @@ -25,11 +25,10 @@ import junit.framework.Assert; import junit.framework.TestCase; import junit.framework.Test; -import org.apache.commons.io.FileUtils; import org.netbeans.junit.NbModuleSuite; -import org.openide.util.Exceptions; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepoAccount.CentralRepoAccountType; +import org.sleuthkit.autopsy.coreutils.FileUtil; import org.sleuthkit.datamodel.Account; import org.sleuthkit.datamodel.InvalidAccountIDException; @@ -95,7 +94,8 @@ public void tearDown() throws CentralRepoException, IOException { if (CentralRepository.isEnabled()) { CentralRepository.getInstance().shutdownConnections(); } - FileUtils.deleteDirectory(testDirectory.toFile()); + + FileUtil.deleteDir(testDirectory.toFile()); } public void testPredefinedAccountTypes() { diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDatamodelTest.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDatamodelTest.java index bf71e5f6a7be0a1dae04083894f2a475bd583177..65907e8e879d6e828497d343b673310d7ee2303c 100755 --- a/Core/test/qa-functional/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDatamodelTest.java +++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoDatamodelTest.java @@ -35,7 +35,6 @@ import java.util.stream.IntStream; import junit.framework.Test; import junit.framework.TestCase; -import org.apache.commons.io.FileUtils; import org.netbeans.junit.NbModuleSuite; import org.openide.util.Exceptions; import junit.framework.Assert; @@ -47,6 +46,7 @@ import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException; +import org.sleuthkit.autopsy.coreutils.FileUtil; /** * Functional tests for the Central Repository data model. @@ -100,8 +100,8 @@ public void setUp() { if (CentralRepository.isEnabled()) { CentralRepository.getInstance().shutdownConnections(); } - FileUtils.deleteDirectory(testDirectory.toFile()); - } catch (IOException | CentralRepoException ex) { + FileUtil.deleteDir(testDirectory.toFile()); + } catch (CentralRepoException ex) { Assert.fail(ex.getMessage()); } } @@ -194,8 +194,8 @@ public void tearDown() { if (CentralRepository.isEnabled()) { CentralRepository.getInstance().shutdownConnections(); } - FileUtils.deleteDirectory(testDirectory.toFile()); - } catch (CentralRepoException | IOException ex) { + FileUtil.deleteDir(testDirectory.toFile()); + } catch (CentralRepoException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } @@ -1256,7 +1256,8 @@ public void testCorrelationTypes() { List<CorrelationAttributeInstance.Type> types = CentralRepository.getInstance().getDefinedCorrelationTypes(); // We expect 11 total - 10 default and the custom one made earlier - assertTrue("getDefinedCorrelationTypes returned " + types.size() + " entries - expected 11", types.size() == 11); + // Note: this test will need to be updated based on the current default items defined in the correlation_types table + assertTrue("getDefinedCorrelationTypes returned " + types.size() + " entries - expected 28", types.size() == 28); } catch (CentralRepoException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); @@ -1267,7 +1268,8 @@ public void testCorrelationTypes() { List<CorrelationAttributeInstance.Type> types = CentralRepository.getInstance().getEnabledCorrelationTypes(); // We expect 10 - the custom type is disabled - assertTrue("getDefinedCorrelationTypes returned " + types.size() + " enabled entries - expected 10", types.size() == 10); + // Note: this test will need to be updated based on the current default items defined in the correlation_types table + assertTrue("getDefinedCorrelationTypes returned " + types.size() + " enabled entries - expected 27", types.size() == 27); } catch (CentralRepoException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); @@ -1278,7 +1280,8 @@ public void testCorrelationTypes() { List<CorrelationAttributeInstance.Type> types = CentralRepository.getInstance().getSupportedCorrelationTypes(); // We expect 10 - the custom type is not supported - assertTrue("getDefinedCorrelationTypes returned " + types.size() + " supported entries - expected 10", types.size() == 10); + // Note: this test will need to be updated based on the current default items defined in the correlation_types table + assertTrue("getDefinedCorrelationTypes returned " + types.size() + " supported entries - expected 27", types.size() == 27); } catch (CentralRepoException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoPersonasTest.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoPersonasTest.java index bbe5ae58f5ad797866212191920eb34b8bed19bb..60636e5f54b5f11793cf5c9fe1d225db88e7f2b9 100644 --- a/Core/test/qa-functional/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoPersonasTest.java +++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/centralrepository/datamodel/CentralRepoPersonasTest.java @@ -28,10 +28,10 @@ import static junit.framework.Assert.assertTrue; import junit.framework.TestCase; import junit.framework.Test; -import org.apache.commons.io.FileUtils; import org.netbeans.junit.NbModuleSuite; import org.openide.util.Exceptions; +import org.sleuthkit.autopsy.coreutils.FileUtil; import org.sleuthkit.datamodel.Account; import org.sleuthkit.datamodel.InvalidAccountIDException; import org.sleuthkit.datamodel.TskData; @@ -257,12 +257,12 @@ public void setUp() throws CentralRepoException, IOException { // This function is run after every test, NOT after the entire collection of // tests defined in the class are run. @Override - public void tearDown() throws CentralRepoException, IOException { + public void tearDown() throws CentralRepoException { // Close and delete the test case and central repo db if (CentralRepository.isEnabled()) { CentralRepository.getInstance().shutdownConnections(); } - FileUtils.deleteDirectory(testDirectory.toFile()); + FileUtil.deleteDir(testDirectory.toFile()); } diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonpropertiessearch/InterCaseTestUtils.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonpropertiessearch/InterCaseTestUtils.java index 85fe4d91c7e01650bf160572b4316d739fb177bf..f76676cd2593994d0c967b9a92acf95dfd719fc9 100644 --- a/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonpropertiessearch/InterCaseTestUtils.java +++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/commonpropertiessearch/InterCaseTestUtils.java @@ -28,7 +28,6 @@ import java.util.Collection; import java.util.HashMap; import java.util.Map; -import org.apache.commons.io.FileUtils; import org.netbeans.junit.NbTestCase; import org.openide.util.Exceptions; import org.sleuthkit.autopsy.casemodule.Case; @@ -63,6 +62,7 @@ import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.autopsy.centralrepository.datamodel.CentralRepository; import org.sleuthkit.autopsy.centralrepository.datamodel.RdbmsCentralRepoFactory; +import org.sleuthkit.autopsy.coreutils.FileUtil; import org.sleuthkit.autopsy.modules.pictureanalyzer.PictureAnalyzerIngestModuleFactory; /** @@ -218,7 +218,9 @@ class InterCaseTestUtils { // kitchenSink.add(keywordSearchTemplate); this.kitchenShink = new IngestJobSettings(InterCaseTestUtils.class.getCanonicalName(), IngestType.ALL_MODULES, kitchenSink); + } + void setupCorrelationTypes() { try { Collection<CorrelationAttributeInstance.Type> types = CentralRepository.getInstance().getDefinedCorrelationTypes(); @@ -247,23 +249,23 @@ void clearTestDir() { if (CentralRepository.isEnabled()) { CentralRepository.getInstance().shutdownConnections(); } - FileUtils.deleteDirectory(CENTRAL_REPO_DIRECTORY_PATH.toFile()); - } catch (IOException | CentralRepoException ex) { + FileUtil.deleteDir(CENTRAL_REPO_DIRECTORY_PATH.toFile()); + } catch (CentralRepoException ex) { Exceptions.printStackTrace(ex); Assert.fail(ex.getMessage()); } } } - + Map<Long, String> getDataSourceMap() throws NoCurrentCaseException, TskCoreException, SQLException { return DataSourceLoader.getAllDataSources(); - } + } Map<String, Integer> getCaseMap() throws CentralRepoException { if (CentralRepository.isEnabled()) { Map<String, Integer> mapOfCaseIdsToCase = new HashMap<>(); - + for (CorrelationCase correlationCase : CentralRepository.getInstance().getCases()) { mapOfCaseIdsToCase.put(correlationCase.getDisplayName(), correlationCase.getID()); } @@ -300,7 +302,7 @@ void enableCentralRepo() throws CentralRepoException { RdbmsCentralRepoFactory centralRepoSchemaFactory = new RdbmsCentralRepoFactory(CentralRepoPlatforms.SQLITE, crSettings); centralRepoSchemaFactory.initializeDatabaseSchema(); centralRepoSchemaFactory.insertDefaultDatabaseContent(); - + crSettings.saveSettings(); CentralRepoDbManager.saveDbChoice(CentralRepoDbChoice.SQLITE); } @@ -313,10 +315,10 @@ void enableCentralRepo() throws CentralRepoException { * The length of caseNames and caseDataSourcePaths should be the same, and * cases should appear in the same order. * - * @param caseNames list case names - * @param caseDataSourcePaths two dimensional array listing the datasources - * in each case - * @param ingestJobSettings HashLookup FileType etc... + * @param caseNames list case names + * @param caseDataSourcePaths two dimensional array listing the datasources + * in each case + * @param ingestJobSettings HashLookup FileType etc... * @param caseReferenceToStore */ Case createCases(String[] caseNames, Path[][] caseDataSourcePaths, IngestJobSettings ingestJobSettings, String caseReferenceToStore) throws TskCoreException { diff --git a/Core/test/qa-functional/src/org/sleuthkit/autopsy/ingest/IngestFileFiltersTest.java b/Core/test/qa-functional/src/org/sleuthkit/autopsy/ingest/IngestFileFiltersTest.java index 262e54774ffde28af61957f5ffefa8db22b93e13..ce0ffb15262c7496a011c3b6dcec5f9ac86b955f 100755 --- a/Core/test/qa-functional/src/org/sleuthkit/autopsy/ingest/IngestFileFiltersTest.java +++ b/Core/test/qa-functional/src/org/sleuthkit/autopsy/ingest/IngestFileFiltersTest.java @@ -253,7 +253,7 @@ public void testCarvingWithExtRuleAndUnallocSpace() { IngestUtils.runIngestJob(currentCase.getDataSources(), ingestJobSettings); FileManager fileManager = currentCase.getServices().getFileManager(); List<AbstractFile> results = fileManager.findFiles("%%"); - assertEquals(70, results.size()); + assertEquals(71, results.size()); int carvedJpgGifFiles = 0; for (AbstractFile file : results) { if (file.getNameExtension().equalsIgnoreCase("jpg") || file.getNameExtension().equalsIgnoreCase("gif")) { //Unalloc file and .jpg files in dir1, dir2, $CarvedFiles, root directory should have MIME type diff --git a/Core/test/unit/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/GetArtifactsTest.java b/Core/test/unit/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceInfoUtilitiesTest.java similarity index 73% rename from Core/test/unit/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/GetArtifactsTest.java rename to Core/test/unit/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceInfoUtilitiesTest.java index ebb7db601a4d80793ce8f2a341e5fb4f4b92bd36..28b55155c15b8816a6124bf494f06fd08e1d3312 100644 --- a/Core/test/unit/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/GetArtifactsTest.java +++ b/Core/test/unit/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceInfoUtilitiesTest.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Date; import java.util.List; import org.junit.Assert; import org.junit.Rule; @@ -35,6 +36,7 @@ import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.autopsy.testutils.TskMockUtils; import static org.mockito.Mockito.*; +import org.sleuthkit.autopsy.testutils.RandomizationUtils; import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE; import org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE; import org.sleuthkit.datamodel.BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE; @@ -42,7 +44,7 @@ /** * Unit tests for DataSourceInfoUtilities.getArtifacts */ -public class GetArtifactsTest { +public class DataSourceInfoUtilitiesTest { @Rule public ExpectedException thrown = ExpectedException.none(); @@ -145,38 +147,13 @@ private <T> List<BlackboardArtifact> getArtifacts(ARTIFACT_TYPE artifactType, Bl List<BlackboardArtifact> toRet = new ArrayList<>(); for (int i = 0; i < values.size(); i++) { - toRet.add(TskMockUtils.mockArtifact(new BlackboardArtifact.Type(artifactType), 1000 + i, dataSource, + toRet.add(TskMockUtils.getArtifact(new BlackboardArtifact.Type(artifactType), 1000 + i, dataSource, attrMaker.make(attrType, "TEST SOURCE", values.get(i)))); } return toRet; } - /** - * Returns list in 0, n-1, 1, n-2 ... order. Deterministic so same results - * each time, but not in original order. - * - * @return Mixed up list. - */ - private <T> List<T> getMixedUp(List<T> list) { - int forward = 0; - int backward = list.size() - 1; - - List<T> newList = new ArrayList<>(); - while (forward <= backward) { - newList.add(list.get(forward)); - - if (forward < backward) { - newList.add(list.get(backward)); - } - - forward++; - backward--; - } - - return newList; - } - /** * Does a basic test passing a list of generated artifacts in mixed up order * to DataSourceInfoUtilities.getArtifacts and expecting a sorted list to be @@ -194,11 +171,11 @@ private <T> List<T> getMixedUp(List<T> list) { private <T> void testSorted(ARTIFACT_TYPE artifactType, ATTRIBUTE_TYPE attrType, List<T> values, AttrMaker<T> attrMaker, SortOrder sortOrder, int count) throws TskCoreException { - DataSource dataSource = TskMockUtils.mockDataSource(1); + DataSource dataSource = TskMockUtils.getDataSource(1); List<BlackboardArtifact> sortedArtifacts = getArtifacts(artifactType, new BlackboardAttribute.Type(attrType), dataSource, values, attrMaker); - List<BlackboardArtifact> mixedUpArtifacts = getMixedUp(sortedArtifacts); + List<BlackboardArtifact> mixedUpArtifacts = RandomizationUtils.getMixedUp(sortedArtifacts); List<BlackboardArtifact> expectedArtifacts = count == 0 ? sortedArtifacts @@ -250,17 +227,17 @@ private void testAscDesc(SortOrder sortOrder) throws TskCoreException { } @Test - public void testSortAscending() throws TskCoreException { + public void getArtifacts_sortAscending() throws TskCoreException { testAscDesc(SortOrder.ASCENDING); } @Test - public void testSortDescending() throws TskCoreException { + public void getArtifacts_sortDescending() throws TskCoreException { testAscDesc(SortOrder.DESCENDING); } @Test - public void testLimits() throws TskCoreException { + public void getArtifacts_limits() throws TskCoreException { List<Integer> integers = Arrays.asList(22, 31, 42, 50, 60); testSorted(ARTIFACT_TYPE.TSK_PROG_RUN, ATTRIBUTE_TYPE.TSK_COUNT, integers, BlackboardAttribute::new, SortOrder.ASCENDING, 3); testSorted(ARTIFACT_TYPE.TSK_PROG_RUN, ATTRIBUTE_TYPE.TSK_COUNT, integers, BlackboardAttribute::new, SortOrder.ASCENDING, 5); @@ -281,11 +258,11 @@ public void testLimits() throws TskCoreException { private <T> void testFailOnBadAttrType(BlackboardArtifact.Type artifactType, BlackboardAttribute.Type attributeType, T val, AttrMaker<T> attrMaker) throws TskCoreException { - DataSource dataSource = TskMockUtils.mockDataSource(1); + DataSource dataSource = TskMockUtils.getDataSource(1); List<BlackboardArtifact> artifacts = Arrays.asList( - TskMockUtils.mockArtifact(artifactType, 2, dataSource, attrMaker.make(attributeType, "TEST SOURCE", val)), - TskMockUtils.mockArtifact(artifactType, 3, dataSource, attrMaker.make(attributeType, "TEST SOURCE", val)) + TskMockUtils.getArtifact(artifactType, 2, dataSource, attrMaker.make(attributeType, "TEST SOURCE", val)), + TskMockUtils.getArtifact(artifactType, 3, dataSource, attrMaker.make(attributeType, "TEST SOURCE", val)) ); test(artifactType, dataSource, @@ -299,7 +276,7 @@ private <T> void testFailOnBadAttrType(BlackboardArtifact.Type artifactType, Bla } @Test - public void testFailOnJson() throws TskCoreException { + public void getArtifacts_failOnJson() throws TskCoreException { testFailOnBadAttrType( new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_GPS_ROUTE), new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_GEO_WAYPOINTS), @@ -308,7 +285,7 @@ public void testFailOnJson() throws TskCoreException { } @Test - public void testFailOnBytes() throws TskCoreException { + public void getArtifacts_failOnBytes() throws TskCoreException { testFailOnBadAttrType( new BlackboardArtifact.Type(999, "BYTE_ARRAY_TYPE", "Byte Array Type"), new BlackboardAttribute.Type(999, "BYTE_ARR_ATTR_TYPE", "Byte Array Attribute Type", TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.BYTE), @@ -317,20 +294,20 @@ public void testFailOnBytes() throws TskCoreException { } @Test - public void testPurgeAttrNotPresent() throws TskCoreException { + public void getArtifacts_purgeAttrNotPresent() throws TskCoreException { long day = 24 * 60 * 60; - DataSource dataSource = TskMockUtils.mockDataSource(1); + DataSource dataSource = TskMockUtils.getDataSource(1); BlackboardArtifact.Type ART_TYPE = new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_PROG_RUN); - BlackboardArtifact mock1 = TskMockUtils.mockArtifact(ART_TYPE, 10, dataSource, + BlackboardArtifact mock1 = TskMockUtils.getArtifact(ART_TYPE, 10, dataSource, new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_COUNT, "TEST SOURCE", 5), new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_DATETIME, "TEST SOURCE", day)); - BlackboardArtifact mock2 = TskMockUtils.mockArtifact(ART_TYPE, 20, dataSource, + BlackboardArtifact mock2 = TskMockUtils.getArtifact(ART_TYPE, 20, dataSource, new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_COUNT, "TEST SOURCE", 6)); - BlackboardArtifact mock3 = TskMockUtils.mockArtifact(ART_TYPE, 30, dataSource, + BlackboardArtifact mock3 = TskMockUtils.getArtifact(ART_TYPE, 30, dataSource, new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_COUNT, "TEST SOURCE", 7), new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_DATETIME, "TEST SOURCE", 3 * day)); @@ -346,20 +323,20 @@ public void testPurgeAttrNotPresent() throws TskCoreException { } @Test - public void testMultAttrsPresent() throws TskCoreException { + public void getArtifacts_multipleAttrsPresent() throws TskCoreException { long day = 24 * 60 * 60; - DataSource dataSource = TskMockUtils.mockDataSource(1); + DataSource dataSource = TskMockUtils.getDataSource(1); BlackboardArtifact.Type ART_TYPE = new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_PROG_RUN); - BlackboardArtifact mock1 = TskMockUtils.mockArtifact(ART_TYPE, 10, dataSource, + BlackboardArtifact mock1 = TskMockUtils.getArtifact(ART_TYPE, 10, dataSource, new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_COUNT, "TEST SOURCE", 7), new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_DATETIME, "TEST SOURCE", day)); - BlackboardArtifact mock2 = TskMockUtils.mockArtifact(ART_TYPE, 20, dataSource, + BlackboardArtifact mock2 = TskMockUtils.getArtifact(ART_TYPE, 20, dataSource, new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_COUNT, "TEST SOURCE", 6)); - BlackboardArtifact mock3 = TskMockUtils.mockArtifact(ART_TYPE, 30, dataSource, + BlackboardArtifact mock3 = TskMockUtils.getArtifact(ART_TYPE, 30, dataSource, new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_COUNT, "TEST SOURCE", 5), new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_DATETIME, "TEST SOURCE", 3 * day)); @@ -375,9 +352,9 @@ public void testMultAttrsPresent() throws TskCoreException { } @Test - public void testTskCoreExceptionThrown() throws TskCoreException { + public void getArtifacts_tskCoreExceptionThrown() throws TskCoreException { test(new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_ACCOUNT), - TskMockUtils.mockDataSource(1), + TskMockUtils.getDataSource(1), new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_ACCOUNT_TYPE), SortOrder.ASCENDING, 0, @@ -388,9 +365,9 @@ public void testTskCoreExceptionThrown() throws TskCoreException { } @Test - public void testThrowOnLessThan0() throws TskCoreException { + public void getArtifacts_throwOnLessThan0() throws TskCoreException { test(new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_ACCOUNT), - TskMockUtils.mockDataSource(1), + TskMockUtils.getDataSource(1), new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_ACCOUNT_TYPE), SortOrder.ASCENDING, -1, @@ -401,9 +378,9 @@ public void testThrowOnLessThan0() throws TskCoreException { } @Test - public void testEmptyListReturned() throws TskCoreException { + public void getArtifacts_emptyListReturned() throws TskCoreException { test(new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_ACCOUNT), - TskMockUtils.mockDataSource(1), + TskMockUtils.getDataSource(1), new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_ACCOUNT_TYPE), SortOrder.ASCENDING, 0, @@ -412,4 +389,76 @@ public void testEmptyListReturned() throws TskCoreException { Collections.emptyList(), null); } + + /** + * Retrieves the value of an artifact. + */ + private interface GetAttrVal<T> { + /** + * A method for retrieving the value of an artifact. + * @param artifact The artifact. + * @param type The type of attribute. + * @return The value. + */ + T getOrNull(BlackboardArtifact artifact, BlackboardAttribute.Type type); + } + + private <T> void testNullAttrValue(String id, GetAttrVal<T> getter, ARTIFACT_TYPE artifactType, + ATTRIBUTE_TYPE attributeType, T nonNullVal) + throws TskCoreException { + + BlackboardAttribute.Type attrType = new BlackboardAttribute.Type(attributeType); + BlackboardArtifact.Type artType = new BlackboardArtifact.Type(artifactType); + + BlackboardArtifact noAttribute = TskMockUtils.getArtifact(artType, 1000, + TskMockUtils.getDataSource(1), new ArrayList<>()); + + T nullValue = getter.getOrNull(noAttribute, attrType); + Assert.assertNull(String.format("Expected function %s to return null when no attribute present", id), nullValue); + + BlackboardArtifact hasAttribute = TskMockUtils.getArtifact(artType, 1000, + TskMockUtils.getDataSource(1), TskMockUtils.getAttribute(attributeType, nonNullVal)); + + T valueReceived = getter.getOrNull(hasAttribute, attrType); + + Assert.assertEquals(String.format("%s did not return the same value present in the attribute", id), nonNullVal, valueReceived); + } + + @Test + public void getStringOrNull_handlesNull() throws TskCoreException { + testNullAttrValue("getStringOrNull", DataSourceInfoUtilities::getStringOrNull, + ARTIFACT_TYPE.TSK_ACCOUNT, ATTRIBUTE_TYPE.TSK_ACCOUNT_TYPE, "Skype"); + } + + @Test + public void getIntOrNull_handlesNull() throws TskCoreException { + testNullAttrValue("getIntOrNull", DataSourceInfoUtilities::getIntOrNull, + ARTIFACT_TYPE.TSK_PROG_RUN, ATTRIBUTE_TYPE.TSK_COUNT, 16); + } + + @Test + public void getLongOrNull_handlesNull() throws TskCoreException { + testNullAttrValue("getLongOrNull", DataSourceInfoUtilities::getLongOrNull, + ARTIFACT_TYPE.TSK_ASSOCIATED_OBJECT, ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT, 1001L); + } + + @Test + public void getDateOrNull_handlesNull() throws TskCoreException { + BlackboardAttribute.Type attrType = new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_DATETIME); + BlackboardArtifact.Type artType = new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_BLUETOOTH_PAIRING); + + long dateTime = 24 * 60 * 60 * 42; + + BlackboardArtifact noAttribute = TskMockUtils.getArtifact(artType, 1000, + TskMockUtils.getDataSource(1), new ArrayList<>()); + + Date nullValue = DataSourceInfoUtilities.getDateOrNull(noAttribute, attrType); + Assert.assertNull(nullValue); + + BlackboardArtifact hasAttribute = TskMockUtils.getArtifact(artType, 1000, + TskMockUtils.getDataSource(1), TskMockUtils.getAttribute(ATTRIBUTE_TYPE.TSK_DATETIME, dateTime)); + + Date curVal = DataSourceInfoUtilities.getDateOrNull(hasAttribute, attrType); + Assert.assertEquals(dateTime, curVal.getTime() / 1000); + } } diff --git a/Core/test/unit/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceSummaryMockUtils.java b/Core/test/unit/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceSummaryMockUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..d73c440e96849423a165272754ffff9dcdae1e70 --- /dev/null +++ b/Core/test/unit/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/DataSourceSummaryMockUtils.java @@ -0,0 +1,57 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> 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.datasourcesummary.datamodel; + +import java.util.List; +import org.apache.commons.lang3.tuple.Pair; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import org.sleuthkit.datamodel.Blackboard; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Common tools for mocking in data source summary. + */ +public final class DataSourceSummaryMockUtils { + + /** + * Creates a pair of a mock SleuthkitCase and mock Blackboard. + * + * @param returnArr The return result when calling getArtifacts on the + * blackboard. + * + * @return The pair of a mock SleuthkitCase and mock Blackboard. + * + * @throws TskCoreException + */ + static Pair<SleuthkitCase, Blackboard> getArtifactsTSKMock(List<BlackboardArtifact> returnArr) throws TskCoreException { + SleuthkitCase mockCase = mock(SleuthkitCase.class); + Blackboard mockBlackboard = mock(Blackboard.class); + when(mockCase.getBlackboard()).thenReturn(mockBlackboard); + when(mockBlackboard.getArtifacts(anyInt(), anyLong())).thenReturn(returnArr); + return Pair.of(mockCase, mockBlackboard); + } + + private DataSourceSummaryMockUtils() { + } +} diff --git a/Core/test/unit/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/RecentFilesSummaryTest.java b/Core/test/unit/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/RecentFilesSummaryTest.java new file mode 100644 index 0000000000000000000000000000000000000000..068f0c3904f8e517842d60500d4f12de018cb4ce --- /dev/null +++ b/Core/test/unit/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/RecentFilesSummaryTest.java @@ -0,0 +1,681 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. Contact: carrier <at> sleuthkit <dot> + * 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.datasourcesummary.datamodel; + +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.Assert; +import static org.junit.Assert.fail; +import org.junit.Test; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.RecentFilesSummary.RecentAttachmentDetails; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.RecentFilesSummary.RecentDownloadDetails; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.RecentFilesSummary.RecentFileDetails; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.SleuthkitCaseProvider.SleuthkitCaseProviderException; +import org.sleuthkit.autopsy.testutils.RandomizationUtils; +import org.sleuthkit.autopsy.testutils.TskMockUtils; +import org.sleuthkit.datamodel.Blackboard; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE; +import org.sleuthkit.datamodel.Content; +import org.sleuthkit.datamodel.DataSource; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Tests for RecentFilesSummaryTest + */ +public class RecentFilesSummaryTest { + + /** + * An interface for calling methods in RecentFilesSummary in a uniform + * manner. + */ + private interface RecentFilesMethod<T> { + + /** + * Means of acquiring data from a method in RecentFilesSummary. + * + * @param recentFilesSummary The RecentFilesSummary object. + * @param dataSource The datasource. + * @param count The number of items to retrieve. + * + * @return The method's return data. + * + * @throws SleuthkitCaseProviderException + * @throws TskCoreException + */ + List<T> fetch(RecentFilesSummary recentFilesSummary, DataSource dataSource, int count) + throws SleuthkitCaseProviderException, TskCoreException; + } + + private static final RecentFilesMethod<RecentFileDetails> RECENT_DOCS_FUNCT + = (summary, dataSource, count) -> summary.getRecentlyOpenedDocuments(dataSource, count); + + private static final RecentFilesMethod<RecentDownloadDetails> RECENT_DOWNLOAD_FUNCT + = (summary, dataSource, count) -> summary.getRecentDownloads(dataSource, count); + + private static final RecentFilesMethod<RecentAttachmentDetails> RECENT_ATTACHMENT_FUNCT + = (summary, dataSource, count) -> summary.getRecentAttachments(dataSource, count); + + /** + * If -1 count passed to method, should throw IllegalArgumentException. + * + * @param method The method to call. + * @param methodName The name of the metho + * + * @throws TskCoreException + * @throws SleuthkitCaseProviderException + */ + private <T> void testNonPositiveCount_ThrowsError(RecentFilesMethod<T> method, String methodName) + throws TskCoreException, SleuthkitCaseProviderException { + Pair<SleuthkitCase, Blackboard> casePair = DataSourceSummaryMockUtils.getArtifactsTSKMock(null); + DataSource dataSource = TskMockUtils.getDataSource(1); + RecentFilesSummary summary = new RecentFilesSummary(() -> casePair.getLeft()); + + try { + method.fetch(summary, dataSource, -1); + fail("Expected method " + methodName + " to fail on negative count."); + } catch (IllegalArgumentException ignored) { + verify(casePair.getRight(), + never().description("Expected negative count for " + methodName + " to not call any methods in SleuthkitCase.")) + .getArtifacts(anyInt(), anyLong()); + } + } + + @Test + public void getRecentlyOpenedDocuments_nonPositiveCount_ThrowsError() throws TskCoreException, SleuthkitCaseProviderException { + testNonPositiveCount_ThrowsError(RECENT_DOCS_FUNCT, "getRecentlyOpenedDocuments"); + } + + @Test + public void getRecentDownloads_nonPositiveCount_ThrowsError() throws TskCoreException, SleuthkitCaseProviderException { + testNonPositiveCount_ThrowsError(RECENT_DOWNLOAD_FUNCT, "getRecentDownloads"); + } + + @Test + public void getRecentAttachments_nonPositiveCount_ThrowsError() throws TskCoreException, SleuthkitCaseProviderException { + testNonPositiveCount_ThrowsError(RECENT_ATTACHMENT_FUNCT, "getRecentAttachments"); + } + + /** + * Tests that if no data source provided, an empty list is returned and + * SleuthkitCase isn't called. + * + * @param recentFilesMethod The method to call. + * @param methodName The name of the method + * + * @throws SleuthkitCaseProviderException + * @throws TskCoreException + */ + private <T> void testNoDataSource_ReturnsEmptyList(RecentFilesMethod<T> recentFilesMethod, String methodName) + throws SleuthkitCaseProviderException, TskCoreException { + + Pair<SleuthkitCase, Blackboard> casePair = DataSourceSummaryMockUtils.getArtifactsTSKMock(null); + RecentFilesSummary summary = new RecentFilesSummary(() -> casePair.getLeft()); + + List<? extends T> items = recentFilesMethod.fetch(summary, null, 10); + Assert.assertNotNull("Expected method " + methodName + " to return an empty list.", items); + Assert.assertEquals("Expected method " + methodName + " to return an empty list.", 0, items.size()); + verify(casePair.getRight(), + never().description("Expected null datasource for " + methodName + " to not call any methods in SleuthkitCase.")) + .getArtifacts(anyInt(), anyLong()); + } + + @Test + public void getRecentlyOpenedDocuments_noDataSource_ReturnsEmptyList() throws TskCoreException, SleuthkitCaseProviderException { + testNoDataSource_ReturnsEmptyList(RECENT_DOCS_FUNCT, "getRecentlyOpenedDocuments"); + } + + @Test + public void getRecentDownloads_noDataSource_ReturnsEmptyList() throws TskCoreException, SleuthkitCaseProviderException { + testNoDataSource_ReturnsEmptyList(RECENT_DOWNLOAD_FUNCT, "getRecentDownloads"); + } + + @Test + public void getRecentAttachments_noDataSource_ReturnsEmptyList() throws TskCoreException, SleuthkitCaseProviderException { + testNonPositiveCount_ThrowsError(RECENT_ATTACHMENT_FUNCT, "getRecentAttachments"); + } + + /** + * If SleuthkitCase returns no results, an empty list is returned. + * + * @param recentFilesMethod The method to call. + * @param methodName The name of the method. + * + * @throws SleuthkitCaseProviderException + * @throws TskCoreException + */ + private <T> void testNoReturnedResults_ReturnsEmptyList(RecentFilesMethod<T> recentFilesMethod, String methodName) + throws SleuthkitCaseProviderException, TskCoreException { + + Pair<SleuthkitCase, Blackboard> casePair = DataSourceSummaryMockUtils.getArtifactsTSKMock(Collections.emptyList()); + RecentFilesSummary summary = new RecentFilesSummary(() -> casePair.getLeft()); + DataSource dataSource = TskMockUtils.getDataSource(1); + List<? extends T> items = recentFilesMethod.fetch(summary, dataSource, 10); + Assert.assertNotNull("Expected method " + methodName + " to return an empty list.", items); + Assert.assertEquals("Expected method " + methodName + " to return an empty list.", 0, items.size()); + verify(casePair.getRight(), + times(1).description("Expected " + methodName + " to call Blackboard once.")) + .getArtifacts(anyInt(), anyLong()); + } + + @Test + public void getRecentlyOpenedDocuments_noReturnedResults_ReturnsEmptyList() throws TskCoreException, SleuthkitCaseProviderException { + testNoReturnedResults_ReturnsEmptyList(RECENT_DOCS_FUNCT, "getRecentlyOpenedDocuments"); + } + + @Test + public void getRecentDownloads_noReturnedResults_ReturnsEmptyList() throws TskCoreException, SleuthkitCaseProviderException { + testNoReturnedResults_ReturnsEmptyList(RECENT_DOWNLOAD_FUNCT, "getRecentDownloads"); + } + + @Test + public void getRecentAttachments_testNoDataSource_ReturnsEmptyList() throws TskCoreException, SleuthkitCaseProviderException { + testNoReturnedResults_ReturnsEmptyList(RECENT_ATTACHMENT_FUNCT, "getRecentAttachments"); + } + + private static final long DAY_SECONDS = 24 * 60 * 60; + + /** + * A means of creating a number representing seconds from epoch where the + * lower the idx, the more recent the time. + */ + private static final Function<Integer, Long> dateTimeRetriever = (idx) -> (365 - idx) * DAY_SECONDS + 1; + + /** + * Gets a mock BlackboardArtifact. + * + * @param ds The data source to which the artifact belongs. + * @param artifactId The artifact id. + * @param artType The artifact type. + * @param attributeArgs The mapping of attribute type to value for each + * attribute in the artifact. + * + * @return The mock artifact. + */ + private BlackboardArtifact getArtifact(DataSource ds, long artifactId, ARTIFACT_TYPE artType, List<Pair<ATTRIBUTE_TYPE, Object>> attributeArgs) { + try { + List<BlackboardAttribute> attributes = attributeArgs.stream() + .filter((arg) -> arg != null && arg.getLeft() != null && arg.getRight() != null) + .map((arg) -> { + return TskMockUtils.getAttribute(arg.getLeft(), arg.getRight()); + }) + .collect(Collectors.toList()); + + return TskMockUtils.getArtifact(new BlackboardArtifact.Type(artType), artifactId, ds, attributes); + } catch (TskCoreException ex) { + fail("There was an error mocking an artifact."); + return null; + } + } + + /** + * Returns a mock artifact for getRecentlyOpenedDocuments. + * + * @param ds The datasource for the artifact. + * @param artifactId The artifact id. + * @param dateTime The time in seconds from epoch. + * @param path The path for the document. + * + * @return The mock artifact with pertinent attributes. + */ + private BlackboardArtifact getRecentDocumentArtifact(DataSource ds, long artifactId, Long dateTime, String path) { + return getArtifact(ds, artifactId, ARTIFACT_TYPE.TSK_RECENT_OBJECT, Arrays.asList( + Pair.of(ATTRIBUTE_TYPE.TSK_DATETIME, dateTime), + Pair.of(ATTRIBUTE_TYPE.TSK_PATH, path) + )); + } + + @Test + public void getRecentlyOpenedDocuments_sortedByDateTimeAndLimited() throws SleuthkitCaseProviderException, TskCoreException { + Function<Integer, String> pathRetriever = (idx) -> "/path/to/downloads/" + idx; + DataSource dataSource = TskMockUtils.getDataSource(1); + + int countRequest = 10; + for (int countToGenerate : new int[]{1, 9, 10, 11}) { + // generate artifacts for each artifact + List<BlackboardArtifact> artifacts = new ArrayList<>(); + for (int idx = 0; idx < countToGenerate; idx++) { + BlackboardArtifact artifact = getRecentDocumentArtifact(dataSource, + 1000 + idx, dateTimeRetriever.apply(idx), pathRetriever.apply(idx)); + artifacts.add(artifact); + } + + // run through method + Pair<SleuthkitCase, Blackboard> casePair = DataSourceSummaryMockUtils.getArtifactsTSKMock(RandomizationUtils.getMixedUp(artifacts)); + RecentFilesSummary summary = new RecentFilesSummary(() -> casePair.getLeft()); + List<RecentFileDetails> results = summary.getRecentlyOpenedDocuments(dataSource, countRequest); + + // verify results + int expectedCount = Math.min(countRequest, countToGenerate); + Assert.assertNotNull(results); + Assert.assertEquals(expectedCount, results.size()); + for (int i = 0; i < expectedCount; i++) { + Assert.assertEquals(dateTimeRetriever.apply(i), results.get(i).getDateAsLong()); + Assert.assertEquals(pathRetriever.apply(i), results.get(i).getPath()); + } + } + } + + @Test + public void getRecentlyOpenedDocuments_filtersMissingData() throws SleuthkitCaseProviderException, TskCoreException { + DataSource dataSource = TskMockUtils.getDataSource(1); + + BlackboardArtifact successItem = getRecentDocumentArtifact(dataSource, 1001, DAY_SECONDS, "/a/path"); + BlackboardArtifact nullTime = getRecentDocumentArtifact(dataSource, 1002, null, "/a/path2"); + BlackboardArtifact zeroTime = getRecentDocumentArtifact(dataSource, 10021, 0L, "/a/path2a"); + List<BlackboardArtifact> artifacts = Arrays.asList(nullTime, zeroTime, successItem); + + Pair<SleuthkitCase, Blackboard> casePair = DataSourceSummaryMockUtils.getArtifactsTSKMock(RandomizationUtils.getMixedUp(artifacts)); + RecentFilesSummary summary = new RecentFilesSummary(() -> casePair.getLeft()); + List<RecentFileDetails> results = summary.getRecentlyOpenedDocuments(dataSource, 10); + + // verify results (only successItem) + Assert.assertNotNull(results); + Assert.assertEquals(1, results.size()); + Assert.assertEquals((Long) DAY_SECONDS, results.get(0).getDateAsLong()); + Assert.assertTrue("/a/path".equalsIgnoreCase(results.get(0).getPath())); + } + + /** + * Creates a mock blackboard artifact for getRecentDownloads. + * + * @param ds The datasource. + * @param artifactId The artifact id. + * @param dateTime The time in seconds from epoch. + * @param domain The domain. + * @param path The path for the download. + * + * @return The mock artifact. + */ + private BlackboardArtifact getRecentDownloadArtifact(DataSource ds, long artifactId, Long dateTime, String domain, String path) { + return getArtifact(ds, artifactId, ARTIFACT_TYPE.TSK_WEB_DOWNLOAD, Arrays.asList( + Pair.of(ATTRIBUTE_TYPE.TSK_DATETIME_ACCESSED, dateTime), + Pair.of(ATTRIBUTE_TYPE.TSK_DOMAIN, domain), + Pair.of(ATTRIBUTE_TYPE.TSK_PATH, path) + )); + } + + @Test + public void getRecentDownloads_sortedByDateTimeAndLimited() throws SleuthkitCaseProviderException, TskCoreException { + Function<Integer, String> domainRetriever = (idx) -> String.format("www.domain%d.com", idx); + Function<Integer, String> pathRetriever = (idx) -> "/path/to/downloads/doc" + idx + ".pdf"; + + // run through method + DataSource dataSource = TskMockUtils.getDataSource(1); + + int countRequest = 10; + for (int countToGenerate : new int[]{1, 9, 10, 11}) { + // generate artifacts for each artifact + List<BlackboardArtifact> artifacts = new ArrayList<>(); + for (int idx = 0; idx < countToGenerate; idx++) { + BlackboardArtifact artifact = getRecentDownloadArtifact(dataSource, + 1000 + idx, dateTimeRetriever.apply(idx), domainRetriever.apply(idx), + pathRetriever.apply(idx)); + + artifacts.add(artifact); + } + + // call method + Pair<SleuthkitCase, Blackboard> casePair = DataSourceSummaryMockUtils.getArtifactsTSKMock(RandomizationUtils.getMixedUp(artifacts)); + RecentFilesSummary summary = new RecentFilesSummary(() -> casePair.getLeft()); + List<RecentDownloadDetails> results = summary.getRecentDownloads(dataSource, countRequest); + + // verify results + int expectedCount = Math.min(countRequest, countToGenerate); + Assert.assertNotNull(results); + Assert.assertEquals(expectedCount, results.size()); + for (int i = 0; i < expectedCount; i++) { + Assert.assertEquals(dateTimeRetriever.apply(i), results.get(i).getDateAsLong()); + Assert.assertEquals(pathRetriever.apply(i), results.get(i).getPath()); + Assert.assertEquals(domainRetriever.apply(i), results.get(i).getWebDomain()); + } + } + } + + @Test + public void getRecentDownloads_filtersMissingData() throws SleuthkitCaseProviderException, TskCoreException { + DataSource dataSource = TskMockUtils.getDataSource(1); + + BlackboardArtifact successItem = getRecentDownloadArtifact(dataSource, 1001, DAY_SECONDS, "domain1.com", "/a/path1"); + BlackboardArtifact nullTime = getRecentDownloadArtifact(dataSource, 1002, null, "domain2.com", "/a/path2"); + BlackboardArtifact zeroTime = getRecentDownloadArtifact(dataSource, 10021, 0L, "domain2a.com", "/a/path2a"); + List<BlackboardArtifact> artifacts = Arrays.asList(nullTime, zeroTime, successItem); + + Pair<SleuthkitCase, Blackboard> casePair = DataSourceSummaryMockUtils.getArtifactsTSKMock(RandomizationUtils.getMixedUp(artifacts)); + RecentFilesSummary summary = new RecentFilesSummary(() -> casePair.getLeft()); + + // call method + List<RecentDownloadDetails> results = summary.getRecentDownloads(dataSource, 10); + + // verify results + Assert.assertNotNull(results); + Assert.assertEquals(1, results.size()); + Assert.assertEquals((Long) DAY_SECONDS, results.get(0).getDateAsLong()); + Assert.assertTrue("/a/path1".equalsIgnoreCase(results.get(0).getPath())); + } + + /** + * getRecentAttachments method has special setup conditions. This class + * encapsulates all the SleuthkitCase/BlackboardArtifact setup for on + * possible return item. + */ + private class AttachmentArtifactItem { + + private final Integer messageArtifactTypeId; + private final boolean associatedAttrFormed; + private final String emailFrom; + private final Long messageTime; + private final boolean isParent; + private final String fileParentPath; + private final String fileName; + + /** + * Constructor with all parameters. + * + * @param messageArtifactTypeId The type id for the artifact or null if + * no message artifact to be created. + * @param emailFrom Who the message is from or null not to + * include attribute. + * @param messageTime Time in seconds from epoch or null not + * to include attribute. + * @param fileParentPath The parent AbstractFile's path value. + * @param fileName The parent AbstractFile's filename + * value. + * @param associatedAttrFormed If false, the TSK_ASSOCIATED_OBJECT + * artifact has no attribute (even though + * it is required). + * @param hasParent Whether or not the artifact has a parent + * AbstractFile. + */ + AttachmentArtifactItem(Integer messageArtifactTypeId, String emailFrom, Long messageTime, + String fileParentPath, String fileName, + boolean associatedAttrFormed, boolean hasParent) { + + this.messageArtifactTypeId = messageArtifactTypeId; + this.associatedAttrFormed = associatedAttrFormed; + this.emailFrom = emailFrom; + this.messageTime = messageTime; + this.isParent = hasParent; + this.fileParentPath = fileParentPath; + this.fileName = fileName; + } + + /** + * Convenience constructor where defaults of required attributes and + * SleuthkitCase assumed. + * + * @param messageArtifactTypeId The type id for the artifact or null if + * no message artifact to be created. + * @param emailFrom Who the message is from or null not to + * include attribute. + * @param messageTime Time in seconds from epoch or null not + * to include attribute. + * @param fileParentPath The parent AbstractFile's path value. + * @param fileName The parent AbstractFile's filename + * value. + */ + AttachmentArtifactItem(Integer messageArtifactTypeId, String emailFrom, Long messageTime, String fileParentPath, String fileName) { + this(messageArtifactTypeId, emailFrom, messageTime, fileParentPath, fileName, true, true); + } + + boolean isAssociatedAttrFormed() { + return associatedAttrFormed; + } + + String getEmailFrom() { + return emailFrom; + } + + Long getMessageTime() { + return messageTime; + } + + boolean hasParent() { + return isParent; + } + + String getFileParentPath() { + return fileParentPath; + } + + String getFileName() { + return fileName; + } + + Integer getMessageArtifactTypeId() { + return messageArtifactTypeId; + } + } + + /** + * Sets up the associated artifact message for the TSK_ASSOCIATED_OBJECT. + * + * @param artifacts The mapping of artifact id to artifact. + * @param item The record to setup. + * @param dataSource The datasource. + * @param associatedId The associated attribute id. + * @param artifactId The artifact id. + * + * @return The associated Artifact blackboard attribute. + * + * @throws TskCoreException + */ + private BlackboardAttribute setupAssociatedMessage(Map<Long, BlackboardArtifact> artifacts, AttachmentArtifactItem item, + DataSource dataSource, Long associatedId, Long artifactId) throws TskCoreException { + + BlackboardAttribute associatedAttr = TskMockUtils.getAttribute(ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT, associatedId); + + if (item.getMessageArtifactTypeId() == null) { + return associatedAttr; + } + + // find the artifact type or null if not found + ARTIFACT_TYPE messageType = Stream.of(ARTIFACT_TYPE.values()) + .filter((artType) -> artType.getTypeID() == item.getMessageArtifactTypeId()) + .findFirst() + .orElse(null); + + // if there is a message type, create the artifact + if (messageType != null) { + List<BlackboardAttribute> attributes = new ArrayList<>(); + if (item.getEmailFrom() != null) { + attributes.add(TskMockUtils.getAttribute(ATTRIBUTE_TYPE.TSK_EMAIL_FROM, item.getEmailFrom())); + } + + if (item.getMessageTime() != null) { + attributes.add(TskMockUtils.getAttribute(ATTRIBUTE_TYPE.TSK_DATETIME_SENT, item.getMessageTime())); + } + + artifacts.put(associatedId, TskMockUtils.getArtifact( + new BlackboardArtifact.Type(messageType), artifactId, dataSource, attributes)); + } + return associatedAttr; + } + + /** + * Since getRecentAttachments does not simply query one type of artifact and + * return results, this method sets up a mock SleuthkitCase and Blackboard + * to return pertinent data. + * + * @param items Each attachment item where each item could represent a + * return result if fully formed. + * + * @return The mock SleuthkitCase and Blackboard. + */ + private Pair<SleuthkitCase, Blackboard> getRecentAttachmentArtifactCase(List<AttachmentArtifactItem> items) { + SleuthkitCase skCase = mock(SleuthkitCase.class); + Blackboard blackboard = mock(Blackboard.class); + when(skCase.getBlackboard()).thenReturn(blackboard); + + DataSource dataSource = TskMockUtils.getDataSource(1); + + long objIdCounter = 100; + Map<Long, BlackboardArtifact> artifacts = new HashMap<>(); + try { + for (AttachmentArtifactItem item : items) { + BlackboardAttribute associatedAttr = null; + // if the associated attribute is fully formed, + // create the associated attribute and related artifact + if (item.isAssociatedAttrFormed()) { + associatedAttr = setupAssociatedMessage(artifacts, item, dataSource, ++objIdCounter, ++objIdCounter); + } + + // create the content parent for the associated object if one should be present + Content parent = (item.hasParent()) + ? TskMockUtils.getAbstractFile(++objIdCounter, item.getFileParentPath(), item.getFileName()) + : null; + + Long associatedId = ++objIdCounter; + artifacts.put(associatedId, TskMockUtils.getArtifact( + new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_ASSOCIATED_OBJECT), + parent, associatedId, dataSource, associatedAttr)); + } + + // set up the blackboard to return artifacts that match the type id. + when(blackboard.getArtifacts(anyInt(), anyLong())).thenAnswer((inv) -> { + Object[] args = inv.getArguments(); + int artifactType = (Integer) args[0]; + return artifacts.values().stream() + .filter(art -> art.getArtifactTypeID() == artifactType) + .collect(Collectors.toList()); + }); + + // also set up the sleuthkitcase to return the artifact with the matching id or null. + when(skCase.getBlackboardArtifact(anyLong())).thenAnswer((inv2) -> { + Object[] args2 = inv2.getArguments(); + long id = (Long) args2[0]; + return artifacts.get(id); + }); + + return Pair.of(skCase, blackboard); + } catch (TskCoreException ex) { + fail("There was an error while creating SleuthkitCase for getRecentAttachments"); + return null; + } + } + + @Test + public void getRecentAttachments_sortedByDateTimeAndLimited() throws SleuthkitCaseProviderException, TskCoreException { + DataSource dataSource = TskMockUtils.getDataSource(1); + // a deterministic means of transforming an index into a particular attribute type so that they can be created + // and compared on return + Function<Integer, String> emailFromRetriever = (idx) -> String.format("person%d@basistech.com", idx); + Function<Integer, String> pathRetriever = (idx) -> "/path/to/attachment/" + idx; + Function<Integer, String> fileNameRetriever = (idx) -> String.format("%d-filename.png", idx); + + int countRequest = 10; + for (int countToGenerate : new int[]{1, 9, 10, 11}) { + // set up the items in the sleuthkit case + List<AttachmentArtifactItem> items = IntStream.range(0, countToGenerate) + .mapToObj((idx) -> new AttachmentArtifactItem(ARTIFACT_TYPE.TSK_MESSAGE.getTypeID(), + emailFromRetriever.apply(idx), dateTimeRetriever.apply(idx), + pathRetriever.apply(idx), fileNameRetriever.apply(idx))) + .collect(Collectors.toList()); + + List<AttachmentArtifactItem> mixedUpItems = RandomizationUtils.getMixedUp(items); + Pair<SleuthkitCase, Blackboard> casePair = getRecentAttachmentArtifactCase(mixedUpItems); + RecentFilesSummary summary = new RecentFilesSummary(() -> casePair.getLeft()); + + // retrieve results + List<RecentAttachmentDetails> results = summary.getRecentAttachments(dataSource, countRequest); + + // verify results + int expectedCount = Math.min(countRequest, countToGenerate); + Assert.assertNotNull(results); + Assert.assertEquals(expectedCount, results.size()); + + for (int i = 0; i < expectedCount; i++) { + RecentAttachmentDetails result = results.get(i); + Assert.assertEquals(dateTimeRetriever.apply(i), result.getDateAsLong()); + Assert.assertTrue(emailFromRetriever.apply(i).equalsIgnoreCase(result.getSender())); + Assert.assertTrue(Paths.get(pathRetriever.apply(i), fileNameRetriever.apply(i)).toString() + .equalsIgnoreCase(result.getPath())); + } + } + } + + @Test + public void getRecentAttachments_filterData() throws SleuthkitCaseProviderException, TskCoreException { + // setup data + DataSource dataSource = TskMockUtils.getDataSource(1); + + AttachmentArtifactItem successItem = new AttachmentArtifactItem(ARTIFACT_TYPE.TSK_EMAIL_MSG.getTypeID(), + "person@sleuthkit.com", DAY_SECONDS, "/parent/path", "msg.pdf"); + AttachmentArtifactItem successItem2 = new AttachmentArtifactItem(ARTIFACT_TYPE.TSK_MESSAGE.getTypeID(), + "person_on_skype", DAY_SECONDS + 1, "/parent/path/to/skype", "skype.png"); + AttachmentArtifactItem wrongArtType = new AttachmentArtifactItem(ARTIFACT_TYPE.TSK_CALLLOG.getTypeID(), + "5555675309", DAY_SECONDS + 2, "/path/to/callog/info", "callog.dat"); + AttachmentArtifactItem missingTimeStamp = new AttachmentArtifactItem(ARTIFACT_TYPE.TSK_EMAIL_MSG.getTypeID(), + "person2@sleuthkit.com", null, "/parent/path", "msg2.pdf"); + AttachmentArtifactItem zeroTimeStamp = new AttachmentArtifactItem(ARTIFACT_TYPE.TSK_EMAIL_MSG.getTypeID(), + "person2a@sleuthkit.com", 0L, "/parent/path", "msg2a.png"); + AttachmentArtifactItem noParentFile = new AttachmentArtifactItem(ARTIFACT_TYPE.TSK_EMAIL_MSG.getTypeID(), + "person4@sleuthkit.com", DAY_SECONDS + 4, "/parent/path", "msg4.jpg", true, false); + AttachmentArtifactItem noAssocAttr = new AttachmentArtifactItem(ARTIFACT_TYPE.TSK_EMAIL_MSG.getTypeID(), + "person3@sleuthkit.com", DAY_SECONDS + 5, "/parent/path", "msg5.gif", false, true); + AttachmentArtifactItem missingAssocArt = new AttachmentArtifactItem(null, + "person3@sleuthkit.com", DAY_SECONDS + 6, "/parent/path", "msg6.pdf"); + + List<AttachmentArtifactItem> items = Arrays.asList(successItem, successItem2, + wrongArtType, missingTimeStamp, zeroTimeStamp, + noParentFile, noAssocAttr, missingAssocArt); + + Pair<SleuthkitCase, Blackboard> casePair = getRecentAttachmentArtifactCase(items); + RecentFilesSummary summary = new RecentFilesSummary(() -> casePair.getLeft()); + + // get data + List<RecentAttachmentDetails> results = summary.getRecentAttachments(dataSource, 10); + + // verify results + Assert.assertNotNull(results); + Assert.assertEquals(2, results.size()); + RecentAttachmentDetails successItem2Details = results.get(0); + RecentAttachmentDetails successItemDetails = results.get(1); + + Assert.assertEquals(successItemDetails.getDateAsLong(), (Long) DAY_SECONDS); + Assert.assertTrue(Paths.get(successItem.getFileParentPath(), successItem.getFileName()) + .toString().equalsIgnoreCase(successItemDetails.getPath())); + Assert.assertTrue(successItem.getEmailFrom().equalsIgnoreCase(successItemDetails.getSender())); + + Assert.assertEquals(successItem2Details.getDateAsLong(), (Long) (DAY_SECONDS + 1)); + Assert.assertTrue(Paths.get(successItem2.getFileParentPath(), successItem2.getFileName()) + .toString().equalsIgnoreCase(successItem2Details.getPath())); + Assert.assertTrue(successItem2.getEmailFrom().equalsIgnoreCase(successItem2Details.getSender())); + } +} diff --git a/Core/test/unit/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/UserActivitySummaryTest.java b/Core/test/unit/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/UserActivitySummaryTest.java new file mode 100644 index 0000000000000000000000000000000000000000..0aff8a56b5b2c8c3d41ec828ab757c8e97933166 --- /dev/null +++ b/Core/test/unit/src/org/sleuthkit/autopsy/datasourcesummary/datamodel/UserActivitySummaryTest.java @@ -0,0 +1,1243 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> 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.datasourcesummary.datamodel; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.Assert; +import static org.junit.Assert.fail; +import org.junit.Test; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.sleuthkit.autopsy.datasourcesummary.datamodel.DataSourceSummaryMockUtils.getArtifactsTSKMock; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.SleuthkitCaseProvider.SleuthkitCaseProviderException; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.UserActivitySummary.TopAccountResult; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.UserActivitySummary.TopDeviceAttachedResult; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.UserActivitySummary.TopDomainsResult; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.UserActivitySummary.TopProgramsResult; +import org.sleuthkit.autopsy.datasourcesummary.datamodel.UserActivitySummary.TopWebSearchResult; +import org.sleuthkit.autopsy.testutils.TskMockUtils; +import org.sleuthkit.autopsy.texttranslation.NoServiceProviderException; +import org.sleuthkit.autopsy.texttranslation.TextTranslationService; +import org.sleuthkit.autopsy.texttranslation.TranslationException; +import org.sleuthkit.datamodel.Blackboard; +import org.sleuthkit.datamodel.BlackboardArtifact; +import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE; +import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE; +import org.sleuthkit.datamodel.DataSource; +import org.sleuthkit.datamodel.SleuthkitCase; +import org.sleuthkit.datamodel.TskCoreException; + +/** + * Tests for UserActivitySummary. + */ +public class UserActivitySummaryTest { + /** + * Function to retrieve data from UserActivitySummary with the provided arguments. + */ + private interface DataFunction<T> { + /** + * A UserActivitySummary method encapsulated in a uniform manner. + * @param userActivitySummary The UserActivitySummary class to use. + * @param datasource The data source. + * @param count The count. + * @return The list of objects to return. + * @throws SleuthkitCaseProviderException + * @throws TskCoreException + */ + List<T> retrieve(UserActivitySummary userActivitySummary, DataSource datasource, int count) throws + SleuthkitCaseProviderException, TskCoreException; + } + + private static final DataFunction<TopWebSearchResult> WEB_SEARCH_QUERY + = (userActivity, dataSource, count) -> userActivity.getMostRecentWebSearches(dataSource, count); + + private static final DataFunction<TopAccountResult> ACCOUNT_QUERY + = (userActivity, dataSource, count) -> userActivity.getRecentAccounts(dataSource, count); + + private static final DataFunction<TopDomainsResult> DOMAINS_QUERY + = (userActivity, dataSource, count) -> userActivity.getRecentDomains(dataSource, count); + + private static final DataFunction<TopDeviceAttachedResult> DEVICE_QUERY + = (userActivity, dataSource, count) -> userActivity.getRecentDevices(dataSource, count); + + private static final DataFunction<TopProgramsResult> PROGRAMS_QUERY + = (userActivity, dataSource, count) -> userActivity.getTopPrograms(dataSource, count); + + private static final Map<String, DataFunction<?>> USER_ACTIVITY_METHODS = new HashMap<String, DataFunction<?>>() { + { + put("getMostRecentWebSearches", WEB_SEARCH_QUERY); + put("getRecentAccounts", ACCOUNT_QUERY); + put("getRecentDomains", DOMAINS_QUERY); + put("getRecentDevices", DEVICE_QUERY); + put("getTopPrograms", PROGRAMS_QUERY); + } + }; + + private static final long DAY_SECONDS = 24 * 60 * 60; + + private static void verifyCalled(Blackboard mockBlackboard, int artifactType, long datasourceId, String failureMessage) throws TskCoreException { + verify(mockBlackboard, times(1).description(failureMessage)).getArtifacts(artifactType, datasourceId); + } + + /** + * Gets a UserActivitySummary class to test. + * + * @param tskCase The SleuthkitCase. + * @param hasTranslation Whether the translation service is functional. + * @param translateFunction Function for translation. + * + * @return The UserActivitySummary class to use for testing. + * + * @throws NoServiceProviderException + * @throws TranslationException + */ + private static UserActivitySummary getTestClass(SleuthkitCase tskCase, boolean hasTranslation, Function<String, String> translateFunction) + throws NoServiceProviderException, TranslationException { + + return new UserActivitySummary( + () -> tskCase, + TskMockUtils.getTextTranslationService(translateFunction, hasTranslation), + TskMockUtils.getJavaLogger("UNIT TEST LOGGER") + ); + } + + private <T> void testMinCount(DataFunction<T> funct, String id) + throws TskCoreException, NoServiceProviderException, TranslationException, SleuthkitCaseProviderException { + + for (int count : new int[]{0, -1}) { + Pair<SleuthkitCase, Blackboard> tskPair = getArtifactsTSKMock(null); + UserActivitySummary summary = getTestClass(tskPair.getLeft(), false, null); + + try { + funct.retrieve(summary, TskMockUtils.getDataSource(1), -1); + } catch (IllegalArgumentException ignored) { + // this exception is expected so continue if getArtifacts never called + verify(tskPair.getRight(), never().description( + String.format("Expected %s would not call getArtifacts for count %d", id, count))) + .getArtifacts(anyInt(), anyLong()); + + continue; + } + fail(String.format("Expected an Illegal argument exception to be thrown in method %s with count of %d", id, count)); + } + } + + /** + * Ensures that passing a non-positive count causes + * IllegalArgumentException. + * + * @throws TskCoreException + * @throws NoServiceProviderException + * @throws TranslationException + * @throws SleuthkitCaseProviderException + */ + @Test + public void testMinCountInvariant() + throws TskCoreException, NoServiceProviderException, TranslationException, SleuthkitCaseProviderException { + + for (Entry<String, DataFunction<?>> query : USER_ACTIVITY_METHODS.entrySet()) { + testMinCount(query.getValue(), query.getKey()); + } + } + + private <T> void testNullDataSource(DataFunction<T> funct, String id) + throws TskCoreException, NoServiceProviderException, TranslationException, SleuthkitCaseProviderException { + + Pair<SleuthkitCase, Blackboard> tskPair = getArtifactsTSKMock(null); + UserActivitySummary summary = getTestClass(tskPair.getLeft(), false, null); + List<T> retArr = funct.retrieve(summary, null, 10); + verify(tskPair.getRight(), never() + .description(String.format("Expected method %s to return empty list for null data source and not call SleuthkitCase", id))) + .getArtifacts(anyInt(), anyLong()); + + String errorMessage = String.format("Expected %s would return empty list for null data source", id); + Assert.assertTrue(errorMessage, retArr != null); + Assert.assertTrue(errorMessage, retArr.isEmpty()); + } + + /** + * If datasource is null, all methods return an empty list. + * + * @throws TskCoreException + * @throws NoServiceProviderException + * @throws TranslationException + * @throws SleuthkitCaseProviderException + */ + @Test + public void testNullDataSource() + throws TskCoreException, NoServiceProviderException, TranslationException, SleuthkitCaseProviderException { + + for (Entry<String, DataFunction<?>> query : USER_ACTIVITY_METHODS.entrySet()) { + testNullDataSource(query.getValue(), query.getKey()); + } + } + + private <T> void testNoResultsReturned(DataFunction<T> funct, String id) + throws TskCoreException, NoServiceProviderException, TranslationException, SleuthkitCaseProviderException { + long dataSourceId = 1; + int count = 10; + Pair<SleuthkitCase, Blackboard> tskPair = getArtifactsTSKMock(new ArrayList<>()); + UserActivitySummary summary = getTestClass(tskPair.getLeft(), false, null); + List<T> retArr = funct.retrieve(summary, TskMockUtils.getDataSource(dataSourceId), count); + + Assert.assertTrue(String.format("Expected non null empty list returned from %s", id), retArr != null); + Assert.assertTrue(String.format("Expected non null empty list returned from %s", id), retArr.isEmpty()); + } + + /** + * If no artifacts in SleuthkitCase, all data returning methods return an + * empty list. + * + * @throws TskCoreException + * @throws NoServiceProviderException + * @throws TranslationException + * @throws SleuthkitCaseProviderException + */ + @Test + public void testNoResultsReturned() + throws TskCoreException, NoServiceProviderException, TranslationException, SleuthkitCaseProviderException { + + for (Entry<String, DataFunction<?>> query : USER_ACTIVITY_METHODS.entrySet()) { + testNoResultsReturned(query.getValue(), query.getKey()); + } + } + + private static List<String> EXCLUDED_DEVICES = Arrays.asList("ROOT_HUB", "ROOT_HUB20"); + + private static BlackboardArtifact getRecentDeviceArtifact(long artifactId, DataSource dataSource, + String deviceId, String deviceMake, String deviceModel, Long date) { + + try { + return TskMockUtils.getArtifact(new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_DEVICE_ATTACHED), artifactId, dataSource, + TskMockUtils.getAttribute(ATTRIBUTE_TYPE.TSK_DEVICE_ID, deviceId), + TskMockUtils.getAttribute(ATTRIBUTE_TYPE.TSK_DATETIME, date), + TskMockUtils.getAttribute(ATTRIBUTE_TYPE.TSK_DEVICE_MAKE, deviceMake), + TskMockUtils.getAttribute(ATTRIBUTE_TYPE.TSK_DEVICE_MODEL, deviceModel) + ); + } catch (TskCoreException e) { + fail("Something went wrong while mocking"); + return null; + } + } + + /** + * Tests that UserActivitySummary.getRecentDevices removes things like + * ROOT_HUB. See EXCLUDED_DEVICES for excluded items. + * + * @throws TskCoreException + * @throws NoServiceProviderException + * @throws SleuthkitCaseProviderException + * @throws TranslationException + */ + @Test + public void getRecentDevices_appropriateFiltering() throws TskCoreException, NoServiceProviderException, + SleuthkitCaseProviderException, TranslationException { + + long dataSourceId = 1; + int count = 10; + long time = DAY_SECONDS * 42; + String acceptedDevice = "ACCEPTED DEVICE"; + + DataSource ds = TskMockUtils.getDataSource(dataSourceId); + + List<String> allKeys = new ArrayList<>(EXCLUDED_DEVICES); + allKeys.add(acceptedDevice); + + List<BlackboardArtifact> artifacts = IntStream.range(0, allKeys.size()) + .mapToObj((idx) -> { + String key = allKeys.get(idx); + return getRecentDeviceArtifact(1000L + idx, ds, "ID " + key, "MAKE " + key, key, time); + }) + .collect(Collectors.toList()); + + Pair<SleuthkitCase, Blackboard> tskPair = getArtifactsTSKMock(artifacts); + UserActivitySummary summary = getTestClass(tskPair.getLeft(), false, null); + + List<TopDeviceAttachedResult> results = summary.getRecentDevices(ds, count); + + verifyCalled(tskPair.getRight(), ARTIFACT_TYPE.TSK_DEVICE_ATTACHED.getTypeID(), dataSourceId, + "Expected getRecentDevices to call getArtifacts with correct arguments."); + Assert.assertEquals(1, results.size()); + Assert.assertEquals(acceptedDevice, results.get(0).getDeviceModel()); + Assert.assertEquals("MAKE " + acceptedDevice, results.get(0).getDeviceMake()); + Assert.assertEquals("ID " + acceptedDevice, results.get(0).getDeviceId()); + Assert.assertEquals(time, results.get(0).getDateAccessed().getTime() / 1000); + } + + /** + * Ensures that UserActivitySummary.getRecentDevices limits returned entries + * to count provided. + * + * @throws TskCoreException + * @throws NoServiceProviderException + * @throws SleuthkitCaseProviderException + * @throws TskCoreException + * @throws TranslationException + */ + @Test + public void getRecentDevices_limitedToCount() + throws TskCoreException, NoServiceProviderException, SleuthkitCaseProviderException, TskCoreException, TranslationException { + + int countRequested = 10; + for (int returnedCount : new int[]{1, 9, 10, 11}) { + long dataSourceId = 1L; + DataSource dataSource = TskMockUtils.getDataSource(dataSourceId); + + List<BlackboardArtifact> returnedArtifacts = IntStream.range(0, returnedCount) + .mapToObj((idx) -> getRecentDeviceArtifact(1000 + idx, dataSource, "ID" + idx, "MAKE" + idx, "MODEL" + idx, DAY_SECONDS * idx)) + .collect(Collectors.toList()); + + Pair<SleuthkitCase, Blackboard> tskPair = getArtifactsTSKMock(returnedArtifacts); + UserActivitySummary summary = getTestClass(tskPair.getLeft(), false, null); + + List<TopDeviceAttachedResult> results = summary.getRecentDevices(dataSource, countRequested); + verifyCalled(tskPair.getRight(), ARTIFACT_TYPE.TSK_DEVICE_ATTACHED.getTypeID(), dataSourceId, + "Expected getRecentDevices to call getArtifacts with correct arguments."); + + Assert.assertEquals(Math.min(countRequested, returnedCount), results.size()); + } + } + + private static BlackboardArtifact getWebSearchArtifact(long artifactId, DataSource dataSource, String query, Long date) { + try { + return TskMockUtils.getArtifact(new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_WEB_SEARCH_QUERY), artifactId, dataSource, + TskMockUtils.getAttribute(ATTRIBUTE_TYPE.TSK_TEXT, query), + TskMockUtils.getAttribute(ATTRIBUTE_TYPE.TSK_DATETIME_ACCESSED, date) + ); + } catch (TskCoreException e) { + fail("Something went wrong while mocking"); + return null; + } + } + + /** + * Ensures that UserActivitySummary.getMostRecentWebSearches groups + * artifacts appropriately (i.e. queries with the same name). + * + * @throws TskCoreException + * @throws NoServiceProviderException + * @throws TranslationException + * @throws SleuthkitCaseProviderException + */ + @Test + public void getMostRecentWebSearches_grouping() throws TskCoreException, NoServiceProviderException, TranslationException, SleuthkitCaseProviderException { + long dataSourceId = 1; + DataSource ds = TskMockUtils.getDataSource(dataSourceId); + + String query1 = "This is Query 1"; + String query2 = "This is Query 2"; + BlackboardArtifact art1a = getWebSearchArtifact(1001, ds, query1, DAY_SECONDS * 1); + BlackboardArtifact art2a = getWebSearchArtifact(1002, ds, query2, DAY_SECONDS * 2); + BlackboardArtifact art2b = getWebSearchArtifact(1003, ds, query2.toUpperCase(), DAY_SECONDS * 3); + BlackboardArtifact art1b = getWebSearchArtifact(1004, ds, query1.toUpperCase(), DAY_SECONDS * 4); + BlackboardArtifact art1c = getWebSearchArtifact(1005, ds, query1.toLowerCase(), DAY_SECONDS * 5); + + List<BlackboardArtifact> artList = Arrays.asList(art1a, art2a, art2b, art1b, art1c); + + Pair<SleuthkitCase, Blackboard> tskPair = getArtifactsTSKMock(artList); + UserActivitySummary summary = getTestClass(tskPair.getLeft(), false, null); + List<TopWebSearchResult> results = summary.getMostRecentWebSearches(ds, 10); + verifyCalled(tskPair.getRight(), ARTIFACT_TYPE.TSK_WEB_SEARCH_QUERY.getTypeID(), dataSourceId, + "Expected getRecentDevices to call getArtifacts with correct arguments."); + + Assert.assertEquals("Expected two different search queries", 2, results.size()); + Assert.assertTrue(query1.equalsIgnoreCase(results.get(0).getSearchString())); + Assert.assertEquals(DAY_SECONDS * 5, results.get(0).getDateAccessed().getTime() / 1000); + Assert.assertTrue(query2.equalsIgnoreCase(results.get(1).getSearchString())); + Assert.assertEquals(DAY_SECONDS * 3, results.get(1).getDateAccessed().getTime() / 1000); + } + + private void webSearchTranslationTest(List<String> queries, boolean hasProvider, String translationSuffix) + throws SleuthkitCaseProviderException, TskCoreException, NoServiceProviderException, TranslationException { + + long dataSourceId = 1; + DataSource ds = TskMockUtils.getDataSource(dataSourceId); + + // create artifacts for each query where first query in the list will have most recent time. + List<BlackboardArtifact> artList = IntStream.range(0, queries.size()) + .mapToObj((idx) -> getWebSearchArtifact(1000 + idx, ds, queries.get(idx), DAY_SECONDS * (queries.size() - idx))) + .collect(Collectors.toList()); + + Pair<SleuthkitCase, Blackboard> tskPair = getArtifactsTSKMock(artList); + + // return name with suffix if original exists and suffix is not null. + Function<String, String> translator = (orig) -> { + if (orig == null || translationSuffix == null) { + return null; + } else { + return orig + translationSuffix; + } + }; + + // set up a mock TextTranslationService returning a translation + TextTranslationService translationService = TskMockUtils.getTextTranslationService(translator, hasProvider); + + UserActivitySummary summary = new UserActivitySummary( + () -> tskPair.getLeft(), + translationService, + TskMockUtils.getJavaLogger("UNIT TEST LOGGER") + ); + + List<TopWebSearchResult> results = summary.getMostRecentWebSearches(ds, queries.size()); + + // verify translation service only called if hasProvider + if (hasProvider) { + verify(translationService, + times(queries.size()).description("Expected translation to be called for each query")) + .translate(anyString()); + } else { + verify(translationService, + never().description("Expected translation not to be called because no provider")) + .translate(anyString()); + } + + Assert.assertEquals(queries.size(), results.size()); + + // verify the translation if there should be one + for (int i = 0; i < queries.size(); i++) { + String query = queries.get(i); + TopWebSearchResult result = results.get(i); + + Assert.assertTrue(query.equalsIgnoreCase(result.getSearchString())); + if (hasProvider) { + if (StringUtils.isBlank(translationSuffix)) { + Assert.assertNull(result.getTranslatedResult()); + } else { + Assert.assertTrue((query + translationSuffix).equalsIgnoreCase(result.getTranslatedResult())); + } + } else { + Assert.assertNull(result.getTranslatedResult()); + } + } + } + + /** + * Verify that UserActivitySummary.getMostRecentWebSearches handles + * translation appropriately. + * + * @throws SleuthkitCaseProviderException + * @throws TskCoreException + * @throws NoServiceProviderException + * @throws TranslationException + */ + @Test + public void getMostRecentWebSearches_handlesTranslation() + throws SleuthkitCaseProviderException, TskCoreException, NoServiceProviderException, TranslationException { + + List<String> queryList = Arrays.asList("query1", "query2", "query3"); + String translationSuffix = " [TRANSLATED]"; + // if no provider. + webSearchTranslationTest(queryList, false, translationSuffix); + + // if no translation. + webSearchTranslationTest(queryList, true, null); + + // if translation is the same (translation suffix doesn't change the trimmed string value) + webSearchTranslationTest(queryList, true, ""); + webSearchTranslationTest(queryList, true, " "); + + // if there is an actual translation + webSearchTranslationTest(queryList, true, translationSuffix); + } + + /** + * Ensure that UserActivitySummary.getMostRecentWebSearches results limited + * to count. + * + * @throws TskCoreException + * @throws NoServiceProviderException + * @throws SleuthkitCaseProviderException + * @throws TskCoreException + * @throws TranslationException + */ + @Test + public void getMostRecentWebSearches_limitedToCount() + throws TskCoreException, NoServiceProviderException, SleuthkitCaseProviderException, TskCoreException, TranslationException { + + int countRequested = 10; + for (int returnedCount : new int[]{1, 9, 10, 11}) { + long dataSourceId = 1L; + DataSource dataSource = TskMockUtils.getDataSource(dataSourceId); + + List<BlackboardArtifact> returnedArtifacts = IntStream.range(0, returnedCount) + .mapToObj((idx) -> getWebSearchArtifact(1000 + idx, dataSource, "Query" + idx, DAY_SECONDS * idx + 1)) + .collect(Collectors.toList()); + + Pair<SleuthkitCase, Blackboard> tskPair = getArtifactsTSKMock(returnedArtifacts); + UserActivitySummary summary = getTestClass(tskPair.getLeft(), false, null); + + List<TopWebSearchResult> results = summary.getMostRecentWebSearches(dataSource, countRequested); + verifyCalled(tskPair.getRight(), ARTIFACT_TYPE.TSK_WEB_SEARCH_QUERY.getTypeID(), dataSourceId, + "Expected getRecentDevices to call getArtifacts with correct arguments."); + + Assert.assertEquals(Math.min(countRequested, returnedCount), results.size()); + } + } + + private BlackboardArtifact getDomainsArtifact(DataSource dataSource, long id, String domain, Long time) { + List<BlackboardAttribute> attributes = new ArrayList<>(); + if (domain != null) { + attributes.add(TskMockUtils.getAttribute(ATTRIBUTE_TYPE.TSK_DOMAIN, domain)); + } + + if (time != null) { + attributes.add(TskMockUtils.getAttribute(ATTRIBUTE_TYPE.TSK_DATETIME_ACCESSED, time)); + } + + try { + return TskMockUtils.getArtifact( + new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_WEB_HISTORY), id, dataSource, + attributes); + } catch (TskCoreException e) { + fail("TskCoreException occurred while trying to mock a blackboard artifact"); + return null; + } + } + + private static final long DOMAIN_WINDOW_DAYS = 30; + + /** + * UserActivitySummary.getRecentDomains should return results within 30 days + * of the most recent access. + * + * @throws TskCoreException + * @throws SleuthkitCaseProviderException + * @throws NoServiceProviderException + * @throws TranslationException + */ + @Test + public void getRecentDomains_withinTimeWIndow() throws TskCoreException, SleuthkitCaseProviderException, NoServiceProviderException, TranslationException { + long dataSourceId = 1; + DataSource dataSource = TskMockUtils.getDataSource(dataSourceId); + String domain1 = "www.google.com"; + String domain2 = "www.basistech.com"; + String domain3 = "www.github.com"; + String domain4 = "www.stackoverflow.com"; + + BlackboardArtifact artifact1 = getDomainsArtifact(dataSource, 1000, domain1, DAY_SECONDS * DOMAIN_WINDOW_DAYS * 2); + BlackboardArtifact artifact1a = getDomainsArtifact(dataSource, 10001, domain1, DAY_SECONDS * DOMAIN_WINDOW_DAYS); + + BlackboardArtifact artifact2 = getDomainsArtifact(dataSource, 1001, domain2, DAY_SECONDS * DOMAIN_WINDOW_DAYS - 1); + + BlackboardArtifact artifact3 = getDomainsArtifact(dataSource, 1002, domain3, DAY_SECONDS * DOMAIN_WINDOW_DAYS); + BlackboardArtifact artifact3a = getDomainsArtifact(dataSource, 10021, domain3, 1L); + + BlackboardArtifact artifact4 = getDomainsArtifact(dataSource, 1003, domain4, 1L); + + List<BlackboardArtifact> retArr = Arrays.asList(artifact1, artifact1a, artifact2, artifact3, artifact3a, artifact4); + + Pair<SleuthkitCase, Blackboard> tskPair = getArtifactsTSKMock(retArr); + + UserActivitySummary summary = getTestClass(tskPair.getLeft(), false, null); + + List<TopDomainsResult> domains = summary.getRecentDomains(dataSource, 10); + + verifyCalled(tskPair.getRight(), ARTIFACT_TYPE.TSK_WEB_HISTORY.getTypeID(), dataSourceId, + "Expected getRecentDomains to call getArtifacts with correct arguments."); + + Assert.assertEquals(2, domains.size()); + + Assert.assertTrue("Expected " + domain1 + " to be first domain", domain1.equalsIgnoreCase(domains.get(0).getDomain())); + Assert.assertEquals(DAY_SECONDS * DOMAIN_WINDOW_DAYS * 2, domains.get(0).getLastVisit().getTime() / 1000); + Assert.assertEquals((Long) 2L, domains.get(0).getVisitTimes()); + + Assert.assertTrue("Expected " + domain3 + " to be second domain", domain3.equalsIgnoreCase(domains.get(1).getDomain())); + Assert.assertEquals(DAY_SECONDS * DOMAIN_WINDOW_DAYS, domains.get(1).getLastVisit().getTime() / 1000); + Assert.assertEquals((Long) 1L, domains.get(1).getVisitTimes()); + } + + /** + * Ensure that items like localhost and 127.0.0.1 are removed from results. + * + * @throws TskCoreException + * @throws NoServiceProviderException + * @throws TranslationException + * @throws SleuthkitCaseProviderException + */ + @Test + public void getRecentDomains_appropriatelyFiltered() throws TskCoreException, NoServiceProviderException, TranslationException, SleuthkitCaseProviderException { + long dataSourceId = 1; + DataSource dataSource = TskMockUtils.getDataSource(dataSourceId); + String domain1 = "www.google.com"; + + // excluded + String domain2 = "localhost"; + String domain3 = "127.0.0.1"; + + BlackboardArtifact artifact1 = getDomainsArtifact(dataSource, 1000, domain1, DAY_SECONDS); + BlackboardArtifact artifact2 = getDomainsArtifact(dataSource, 1001, domain2, DAY_SECONDS * 2); + BlackboardArtifact artifact3 = getDomainsArtifact(dataSource, 1002, domain3, DAY_SECONDS * 3); + + List<BlackboardArtifact> retArr = Arrays.asList(artifact1, artifact2, artifact3); + + Pair<SleuthkitCase, Blackboard> tskPair = getArtifactsTSKMock(retArr); + + UserActivitySummary summary = getTestClass(tskPair.getLeft(), false, null); + + List<TopDomainsResult> domains = summary.getRecentDomains(dataSource, 10); + + verifyCalled(tskPair.getRight(), ARTIFACT_TYPE.TSK_WEB_HISTORY.getTypeID(), dataSourceId, + "Expected getRecentDomains to call getArtifacts with correct arguments."); + + Assert.assertEquals(1, domains.size()); + + Assert.assertTrue("Expected " + domain1 + " to be most recent domain", domain1.equalsIgnoreCase(domains.get(0).getDomain())); + Assert.assertEquals(DAY_SECONDS, domains.get(0).getLastVisit().getTime() / 1000); + } + + /** + * Ensure domains are grouped by name. + * + * @throws TskCoreException + * @throws NoServiceProviderException + * @throws TranslationException + * @throws SleuthkitCaseProviderException + */ + @Test + public void getRecentDomains_groupedAppropriately() throws TskCoreException, NoServiceProviderException, TranslationException, SleuthkitCaseProviderException { + long dataSourceId = 1; + DataSource dataSource = TskMockUtils.getDataSource(dataSourceId); + String domain1 = "www.google.com"; + String domain2 = "www.basistech.com"; + + BlackboardArtifact artifact1 = getDomainsArtifact(dataSource, 1000, domain1, 1L); + BlackboardArtifact artifact1a = getDomainsArtifact(dataSource, 1001, domain1, 6L); + BlackboardArtifact artifact2 = getDomainsArtifact(dataSource, 1002, domain2, 2L); + BlackboardArtifact artifact2a = getDomainsArtifact(dataSource, 1003, domain2, 3L); + BlackboardArtifact artifact2b = getDomainsArtifact(dataSource, 1004, domain2, 4L); + + List<BlackboardArtifact> retArr = Arrays.asList(artifact1, artifact1a, artifact2, artifact2a, artifact2b); + + Pair<SleuthkitCase, Blackboard> tskPair = getArtifactsTSKMock(retArr); + UserActivitySummary summary = getTestClass(tskPair.getLeft(), false, null); + + List<TopDomainsResult> domains = summary.getRecentDomains(dataSource, 10); + + verifyCalled(tskPair.getRight(), ARTIFACT_TYPE.TSK_WEB_HISTORY.getTypeID(), dataSourceId, + "Expected getRecentDomains to call getArtifacts with correct arguments."); + + Assert.assertEquals(2, domains.size()); + + Assert.assertTrue(domain1.equalsIgnoreCase(domains.get(1).getDomain())); + Assert.assertEquals(6L, domains.get(1).getLastVisit().getTime() / 1000); + Assert.assertEquals((Long) 2L, domains.get(1).getVisitTimes()); + + Assert.assertTrue(domain2.equalsIgnoreCase(domains.get(0).getDomain())); + Assert.assertEquals(4L, domains.get(0).getLastVisit().getTime() / 1000); + Assert.assertEquals((Long) 3L, domains.get(0).getVisitTimes()); + } + + /** + * Ensure that UserActivitySummary.getRecentDomains limits to count + * appropriately. + * + * @throws TskCoreException + * @throws NoServiceProviderException + * @throws TranslationException + * @throws SleuthkitCaseProviderException + */ + @Test + public void getRecentDomains_limitedAppropriately() + throws TskCoreException, NoServiceProviderException, TranslationException, SleuthkitCaseProviderException { + + int countRequested = 10; + for (int returnedCount : new int[]{1, 9, 10, 11}) { + long dataSourceId = 1L; + DataSource dataSource = TskMockUtils.getDataSource(dataSourceId); + + // create a list where there are 1 accesses for first, 2 for second, etc. + List<BlackboardArtifact> returnedArtifacts = IntStream.range(0, returnedCount) + .mapToObj((idx) -> { + return IntStream.range(0, idx + 1) + .mapToObj((numIdx) -> { + int hash = 100 * idx + numIdx; + return getDomainsArtifact(dataSource, 1000 + hash, "Domain " + idx, 10L); + }); + }) + .flatMap((s) -> s) + .collect(Collectors.toList()); + + Pair<SleuthkitCase, Blackboard> tskPair = getArtifactsTSKMock(returnedArtifacts); + UserActivitySummary summary = getTestClass(tskPair.getLeft(), false, null); + + List<TopDomainsResult> results = summary.getRecentDomains(dataSource, countRequested); + verifyCalled(tskPair.getRight(), ARTIFACT_TYPE.TSK_WEB_HISTORY.getTypeID(), dataSourceId, + "Expected getRecentDevices to call getArtifacts with correct arguments."); + + Assert.assertEquals(Math.min(countRequested, returnedCount), results.size()); + } + } + + /** + * Get email artifact to be used with getRecentAccounts + * + * @param artifactId The artifact id. + * @param dataSource The datasource. + * @param dateRcvd The date received in seconds or null to exclude. + * @param dateSent The date sent in seconds or null to exclude. + * + * @return The mock artifact. + */ + private static BlackboardArtifact getEmailArtifact(long artifactId, DataSource dataSource, Long dateRcvd, Long dateSent) { + List<BlackboardAttribute> attributes = new ArrayList<>(); + + if (dateRcvd != null) { + attributes.add(TskMockUtils.getAttribute(ATTRIBUTE_TYPE.TSK_DATETIME_RCVD, dateRcvd)); + } + + if (dateSent != null) { + attributes.add(TskMockUtils.getAttribute(ATTRIBUTE_TYPE.TSK_DATETIME_SENT, dateSent)); + } + + try { + return TskMockUtils.getArtifact(new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_EMAIL_MSG), + artifactId, dataSource, attributes); + } catch (TskCoreException ignored) { + fail("Something went wrong while mocking"); + return null; + } + } + + /** + * Get calllog artifact to be used with getRecentAccounts + * + * @param artifactId The artifact id. + * @param dataSource The datasource. + * @param dateStart The date start in seconds or null to exclude. + * @param dateEnd The date end in seconds or null to exclude. + * + * @return The mock artifact. + */ + private static BlackboardArtifact getCallogArtifact(long artifactId, DataSource dataSource, Long dateStart, Long dateEnd) { + List<BlackboardAttribute> attributes = new ArrayList<>(); + + if (dateStart != null) { + attributes.add(TskMockUtils.getAttribute(ATTRIBUTE_TYPE.TSK_DATETIME_START, dateStart)); + } + + if (dateEnd != null) { + attributes.add(TskMockUtils.getAttribute(ATTRIBUTE_TYPE.TSK_DATETIME_END, dateEnd)); + } + + try { + return TskMockUtils.getArtifact(new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_CALLLOG), + artifactId, dataSource, attributes); + } catch (TskCoreException ignored) { + fail("Something went wrong while mocking"); + return null; + } + } + + /** + * Get message artifact to be used with getRecentAccounts + * + * @param artifactId The artifact id. + * @param dataSource The datasource. + * @param type The account type. + * @param dateSent The date of the message in seconds. + */ + private static BlackboardArtifact getMessageArtifact(long artifactId, DataSource dataSource, String type, Long dateTime) { + List<BlackboardAttribute> attributes = new ArrayList<>(); + + if (type != null) { + attributes.add(TskMockUtils.getAttribute(ATTRIBUTE_TYPE.TSK_MESSAGE_TYPE, type)); + } + + if (dateTime != null) { + attributes.add(TskMockUtils.getAttribute(ATTRIBUTE_TYPE.TSK_DATETIME, dateTime)); + } + + try { + return TskMockUtils.getArtifact(new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_MESSAGE), + artifactId, dataSource, attributes); + } catch (TskCoreException ignored) { + fail("Something went wrong while mocking"); + return null; + } + } + + /** + * Performs a test on UserActivitySummary.getRecentAccounts. + * + * @param dataSource The datasource to use as parameter. + * @param count The count to use as a parameter. + * @param retArtifacts The artifacts to return from + * SleuthkitCase.getArtifacts. This method filters + * based on artifact type from the call. + * @param expectedResults The expected results. + * + * @throws TskCoreException + * @throws NoServiceProviderException + * @throws TranslationException + * @throws + * org.sleuthkit.autopsy.datasourcesummary.datamodel.SleuthkitCaseProvider.SleuthkitCaseProviderException + */ + private void getRecentAccountsTest(DataSource dataSource, int count, + List<BlackboardArtifact> retArtifacts, List<TopAccountResult> expectedResults) + throws TskCoreException, NoServiceProviderException, TranslationException, SleuthkitCaseProviderException { + + SleuthkitCase mockCase = mock(SleuthkitCase.class); + Blackboard mockBlackboard = mock(Blackboard.class); + when(mockCase.getBlackboard()).thenReturn(mockBlackboard); + + when(mockBlackboard.getArtifacts(anyInt(), anyLong())).thenAnswer((invocation) -> { + Object[] args = invocation.getArguments(); + int artifactType = (Integer) args[0]; + return retArtifacts.stream() + .filter((art) -> art.getArtifactTypeID() == artifactType) + .collect(Collectors.toList()); + }); + + UserActivitySummary summary = getTestClass(mockCase, false, null); + + List<TopAccountResult> receivedResults = summary.getRecentAccounts(dataSource, count); + + verifyCalled(mockBlackboard, ARTIFACT_TYPE.TSK_MESSAGE.getTypeID(), dataSource.getId(), + "Expected getRecentAccounts to call getArtifacts requesting TSK_MESSAGE."); + + verifyCalled(mockBlackboard, ARTIFACT_TYPE.TSK_EMAIL_MSG.getTypeID(), dataSource.getId(), + "Expected getRecentAccounts to call getArtifacts requesting TSK_EMAIL_MSG."); + + verifyCalled(mockBlackboard, ARTIFACT_TYPE.TSK_CALLLOG.getTypeID(), dataSource.getId(), + "Expected getRecentAccounts to call getArtifacts requesting TSK_CALLLOG."); + + Assert.assertEquals(expectedResults.size(), receivedResults.size()); + for (int i = 0; i < expectedResults.size(); i++) { + TopAccountResult expectedItem = expectedResults.get(i); + TopAccountResult receivedItem = receivedResults.get(i); + + // since this may be somewhat variable + Assert.assertTrue(expectedItem.getAccountType().equalsIgnoreCase(receivedItem.getAccountType())); + Assert.assertEquals(expectedItem.getLastAccess().getTime(), receivedItem.getLastAccess().getTime()); + } + } + + private void getRecentAccountsOneArtTest(DataSource dataSource, BlackboardArtifact retArtifact, TopAccountResult expectedResult) + throws TskCoreException, NoServiceProviderException, TranslationException, SleuthkitCaseProviderException { + getRecentAccountsTest(dataSource, 10, Arrays.asList(retArtifact), expectedResult != null ? Arrays.asList(expectedResult) : Collections.emptyList()); + } + + /** + * Verify that UserActivitySummary.getRecentAccounts attempts to find a date + * but if none present, the artifact is excluded. + * + * @throws TskCoreException + * @throws NoServiceProviderException + * @throws TranslationException + * @throws SleuthkitCaseProviderException + */ + @Test + public void getRecentAccounts_filtersNoDate() + throws TskCoreException, NoServiceProviderException, TranslationException, SleuthkitCaseProviderException { + + DataSource ds1 = TskMockUtils.getDataSource(1); + BlackboardArtifact email1 = getEmailArtifact(31, ds1, DAY_SECONDS, null); + getRecentAccountsOneArtTest(ds1, email1, + new TopAccountResult( + Bundle.DataSourceUserActivitySummary_getRecentAccounts_emailMessage(), + new Date(DAY_SECONDS * 1000))); + + BlackboardArtifact email2 = getEmailArtifact(2, ds1, null, DAY_SECONDS); + getRecentAccountsOneArtTest(ds1, email2, + new TopAccountResult( + Bundle.DataSourceUserActivitySummary_getRecentAccounts_emailMessage(), + new Date(DAY_SECONDS * 1000))); + + BlackboardArtifact email3 = getEmailArtifact(3, ds1, null, null); + getRecentAccountsOneArtTest(ds1, email3, null); + + BlackboardArtifact email4 = getEmailArtifact(4, ds1, DAY_SECONDS, DAY_SECONDS * 2); + getRecentAccountsOneArtTest(ds1, email4, + new TopAccountResult( + Bundle.DataSourceUserActivitySummary_getRecentAccounts_emailMessage(), + new Date(DAY_SECONDS * 2 * 1000))); + + BlackboardArtifact callog1 = getCallogArtifact(11, ds1, DAY_SECONDS, null); + getRecentAccountsOneArtTest(ds1, callog1, + new TopAccountResult( + Bundle.DataSourceUserActivitySummary_getRecentAccounts_calllogMessage(), + new Date(DAY_SECONDS * 1000))); + + BlackboardArtifact callog2 = getCallogArtifact(12, ds1, null, DAY_SECONDS); + getRecentAccountsOneArtTest(ds1, callog2, + new TopAccountResult( + Bundle.DataSourceUserActivitySummary_getRecentAccounts_calllogMessage(), + new Date(DAY_SECONDS * 1000))); + + BlackboardArtifact callog3 = getCallogArtifact(13, ds1, null, null); + getRecentAccountsOneArtTest(ds1, callog3, null); + + BlackboardArtifact callog4 = getCallogArtifact(14, ds1, DAY_SECONDS, DAY_SECONDS * 2); + getRecentAccountsOneArtTest(ds1, callog4, + new TopAccountResult( + Bundle.DataSourceUserActivitySummary_getRecentAccounts_calllogMessage(), + new Date(DAY_SECONDS * 2 * 1000))); + + BlackboardArtifact message1 = getMessageArtifact(21, ds1, "Skype", null); + getRecentAccountsOneArtTest(ds1, message1, null); + + BlackboardArtifact message2 = getMessageArtifact(22, ds1, null, DAY_SECONDS); + getRecentAccountsOneArtTest(ds1, message2, null); + + BlackboardArtifact message3 = getMessageArtifact(23, ds1, null, null); + getRecentAccountsOneArtTest(ds1, message3, null); + + BlackboardArtifact message4 = getMessageArtifact(24, ds1, "Skype", DAY_SECONDS); + getRecentAccountsOneArtTest(ds1, message4, new TopAccountResult("Skype", new Date(DAY_SECONDS * 1000))); + + } + + /** + * Verifies that UserActivitySummary.getRecentAccounts groups appropriately + * by account type. + * + * @throws TskCoreException + * @throws NoServiceProviderException + * @throws TranslationException + * @throws SleuthkitCaseProviderException + */ + @Test + public void getRecentAccounts_rightGrouping() + throws TskCoreException, NoServiceProviderException, TranslationException, SleuthkitCaseProviderException { + DataSource ds1 = TskMockUtils.getDataSource(1); + BlackboardArtifact email1 = getEmailArtifact(11, ds1, DAY_SECONDS - 11, null); + BlackboardArtifact email2 = getEmailArtifact(12, ds1, DAY_SECONDS - 12, null); + BlackboardArtifact email3 = getEmailArtifact(13, ds1, DAY_SECONDS + 13, null); + + BlackboardArtifact callog1 = getCallogArtifact(21, ds1, DAY_SECONDS - 21, null); + BlackboardArtifact callog2 = getCallogArtifact(22, ds1, DAY_SECONDS + 22, null); + + BlackboardArtifact message1a = getMessageArtifact(31, ds1, "Skype", DAY_SECONDS - 31); + BlackboardArtifact message1b = getMessageArtifact(32, ds1, "Skype", DAY_SECONDS + 32); + + BlackboardArtifact message2a = getMessageArtifact(41, ds1, "Facebook", DAY_SECONDS - 41); + BlackboardArtifact message2b = getMessageArtifact(41, ds1, "Facebook", DAY_SECONDS + 42); + + getRecentAccountsTest(ds1, 10, + Arrays.asList(email1, email2, email3, callog1, callog2, message1a, message1b, message2a, message2b), + Arrays.asList( + new TopAccountResult("Facebook", new Date((DAY_SECONDS + 42) * 1000)), + new TopAccountResult("Skype", new Date((DAY_SECONDS + 32) * 1000)), + new TopAccountResult(Bundle.DataSourceUserActivitySummary_getRecentAccounts_calllogMessage(), new Date((DAY_SECONDS + 22) * 1000)), + new TopAccountResult(Bundle.DataSourceUserActivitySummary_getRecentAccounts_emailMessage(), new Date((DAY_SECONDS + 13) * 1000)) + )); + } + + /** + * Verifies that UserActivitySummary.getRecentAccounts properly limits + * results returned. + * + * @throws TskCoreException + * @throws NoServiceProviderException + * @throws TranslationException + * @throws SleuthkitCaseProviderException + */ + @Test + public void getRecentAccounts_rightLimit() + throws TskCoreException, NoServiceProviderException, TranslationException, SleuthkitCaseProviderException { + int countRequested = 10; + for (int returnedCount : new int[]{1, 9, 10, 11}) { + long dataSourceId = 1L; + DataSource dataSource = TskMockUtils.getDataSource(dataSourceId); + + List<BlackboardArtifact> returnedArtifacts = IntStream.range(0, returnedCount) + .mapToObj((idx) -> getMessageArtifact(1000 + idx, dataSource, "Message Type " + idx, DAY_SECONDS * idx + 1)) + .collect(Collectors.toList()); + + Pair<SleuthkitCase, Blackboard> tskPair = getArtifactsTSKMock(returnedArtifacts); + UserActivitySummary summary = getTestClass(tskPair.getLeft(), false, null); + + List<TopAccountResult> results = summary.getRecentAccounts(dataSource, countRequested); + verifyCalled(tskPair.getRight(), ARTIFACT_TYPE.TSK_MESSAGE.getTypeID(), dataSource.getId(), + "Expected getRecentAccounts to call getArtifacts requesting TSK_MESSAGE."); + + verifyCalled(tskPair.getRight(), ARTIFACT_TYPE.TSK_EMAIL_MSG.getTypeID(), dataSource.getId(), + "Expected getRecentAccounts to call getArtifacts requesting TSK_EMAIL_MSG."); + + verifyCalled(tskPair.getRight(), ARTIFACT_TYPE.TSK_CALLLOG.getTypeID(), dataSource.getId(), + "Expected getRecentAccounts to call getArtifacts requesting TSK_CALLLOG."); + + Assert.assertEquals(Math.min(countRequested, returnedCount), results.size()); + } + } + + /** + * Ensures that UserActivity.getShortFolderName handles paths appropriately + * including Program Files and AppData folders. + * + * @throws NoServiceProviderException + * @throws TskCoreException + * @throws TranslationException + */ + @Test + public void getShortFolderName_rightConversions() throws NoServiceProviderException, TskCoreException, TranslationException { + Map<String, String> expected = new HashMap<>(); + expected.put("/Program Files/Item/Item.exe", "Item"); + expected.put("/Program Files (x86)/Item/Item.exe", "Item"); + expected.put("/Program_Files/Item/Item.exe", ""); + + expected.put("/User/test_user/item/AppData/Item/Item.exe", "AppData"); + expected.put("/User/test_user/item/Application Data/Item/Item.exe", "AppData"); + + expected.put("/Other Path/Item/Item.exe", ""); + + Pair<SleuthkitCase, Blackboard> tskPair = getArtifactsTSKMock(null); + UserActivitySummary summary = getTestClass(tskPair.getLeft(), false, null); + + for (Entry<String, String> path : expected.entrySet()) { + Assert.assertTrue(path.getValue().equalsIgnoreCase(summary.getShortFolderName(path.getKey(), "Item.exe"))); + Assert.assertTrue(path.getValue().equalsIgnoreCase(summary.getShortFolderName(path.getKey().toUpperCase(), "Item.exe".toUpperCase()))); + Assert.assertTrue(path.getValue().equalsIgnoreCase(summary.getShortFolderName(path.getKey().toLowerCase(), "Item.exe".toLowerCase()))); + } + } + + private static BlackboardArtifact getProgramArtifact(long artifactId, DataSource dataSource, String programName, String path, Integer count, Long dateTime) { + List<BlackboardAttribute> attributes = new ArrayList<>(); + + if (programName != null) { + attributes.add(TskMockUtils.getAttribute(ATTRIBUTE_TYPE.TSK_PROG_NAME, programName)); + } + + if (path != null) { + attributes.add(TskMockUtils.getAttribute(ATTRIBUTE_TYPE.TSK_PATH, path)); + } + + if (dateTime != null) { + attributes.add(TskMockUtils.getAttribute(ATTRIBUTE_TYPE.TSK_DATETIME, dateTime)); + } + + if (count != null) { + attributes.add(TskMockUtils.getAttribute(ATTRIBUTE_TYPE.TSK_COUNT, count)); + } + + try { + return TskMockUtils.getArtifact(new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_PROG_RUN), + artifactId, dataSource, attributes); + } catch (TskCoreException ignored) { + fail("Something went wrong while mocking"); + return null; + } + } + + /** + * Ensures that getTopPrograms filters results like ntosboot programs or + * /Windows folders. + * + * @throws TskCoreException + * @throws NoServiceProviderException + * @throws TranslationException + * @throws SleuthkitCaseProviderException + */ + @Test + public void getTopPrograms_filtered() + throws TskCoreException, NoServiceProviderException, TranslationException, SleuthkitCaseProviderException { + + DataSource ds1 = TskMockUtils.getDataSource(1); + BlackboardArtifact ntosToRemove = getProgramArtifact(1, ds1, "ntosboot", "/Program Files/etc/", 21, 21L); + BlackboardArtifact windowsToRemove = getProgramArtifact(2, ds1, "Program.exe", "/Windows/", 21, 21L); + BlackboardArtifact windowsToRemove2 = getProgramArtifact(3, ds1, "Program.exe", "/Windows/Nested/", 21, 21L); + BlackboardArtifact noProgramNameToRemove = getProgramArtifact(4, ds1, null, "/Program Files/", 21, 21L); + BlackboardArtifact noProgramNameToRemove2 = getProgramArtifact(5, ds1, " ", "/Program Files/", 21, 21L); + BlackboardArtifact successful = getProgramArtifact(6, ds1, "ProgramSuccess.exe", "/AppData/Success/", null, null); + BlackboardArtifact successful2 = getProgramArtifact(7, ds1, "ProgramSuccess2.exe", "/AppData/Success/", 22, 22L); + + Pair<SleuthkitCase, Blackboard> tskPair = getArtifactsTSKMock(Arrays.asList( + ntosToRemove, + windowsToRemove, + windowsToRemove2, + noProgramNameToRemove, + noProgramNameToRemove2, + successful, + successful2 + )); + UserActivitySummary summary = getTestClass(tskPair.getLeft(), false, null); + List<TopProgramsResult> results = summary.getTopPrograms(ds1, 10); + + Assert.assertEquals(2, results.size()); + Assert.assertTrue("ProgramSuccess2.exe".equalsIgnoreCase(results.get(0).getProgramName())); + Assert.assertTrue("ProgramSuccess.exe".equalsIgnoreCase(results.get(1).getProgramName())); + } + + /** + * Ensures proper grouping of programs with index of program name and path. + * + * @throws TskCoreException + * @throws NoServiceProviderException + * @throws TranslationException + * @throws SleuthkitCaseProviderException + */ + @Test + public void getTopPrograms_correctGrouping() + throws TskCoreException, NoServiceProviderException, TranslationException, SleuthkitCaseProviderException { + + DataSource ds1 = TskMockUtils.getDataSource(1); + BlackboardArtifact prog1 = getProgramArtifact(1, ds1, "program1.exe", "/Program Files/etc/", 21, 21L); + BlackboardArtifact prog1a = getProgramArtifact(1, ds1, "program1.exe", "/Program Files/etc/", 1, 31L); + BlackboardArtifact prog1b = getProgramArtifact(1, ds1, "program1.exe", "/Program Files/etc/", 2, 11L); + + BlackboardArtifact prog2 = getProgramArtifact(1, ds1, "program1.exe", "/Program Files/another/", 31, 21L); + BlackboardArtifact prog2a = getProgramArtifact(1, ds1, "program1.exe", "/Program Files/another/", 1, 31L); + BlackboardArtifact prog2b = getProgramArtifact(1, ds1, "program1.exe", "/Program Files/another/", 2, 11L); + + BlackboardArtifact prog3 = getProgramArtifact(1, ds1, "program2.exe", "/Program Files/another/", 10, 21L); + BlackboardArtifact prog3a = getProgramArtifact(1, ds1, "program2.exe", "/Program Files/another/", 1, 22L); + BlackboardArtifact prog3b = getProgramArtifact(1, ds1, "program2.exe", "/Program Files/another/", 2, 11L); + + Pair<SleuthkitCase, Blackboard> tskPair = getArtifactsTSKMock(Arrays.asList( + prog1, prog1a, prog1b, + prog2, prog2a, prog2b, + prog3, prog3a, prog3b + )); + UserActivitySummary summary = getTestClass(tskPair.getLeft(), false, null); + List<TopProgramsResult> results = summary.getTopPrograms(ds1, 10); + + Assert.assertEquals(3, results.size()); + Assert.assertTrue("program1.exe".equalsIgnoreCase(results.get(0).getProgramName())); + Assert.assertTrue("/Program Files/another/".equalsIgnoreCase(results.get(0).getProgramPath())); + Assert.assertEquals((Long) 31L, results.get(0).getRunTimes()); + Assert.assertEquals((Long) 31L, (Long) (results.get(0).getLastRun().getTime() / 1000)); + + Assert.assertTrue("program1.exe".equalsIgnoreCase(results.get(1).getProgramName())); + Assert.assertTrue("/Program Files/etc/".equalsIgnoreCase(results.get(1).getProgramPath())); + Assert.assertEquals((Long) 21L, results.get(1).getRunTimes()); + Assert.assertEquals((Long) 31L, (Long) (results.get(1).getLastRun().getTime() / 1000)); + + Assert.assertTrue("program2.exe".equalsIgnoreCase(results.get(2).getProgramName())); + Assert.assertTrue("/Program Files/another/".equalsIgnoreCase(results.get(2).getProgramPath())); + Assert.assertEquals((Long) 10L, results.get(2).getRunTimes()); + Assert.assertEquals((Long) 22L, (Long) (results.get(2).getLastRun().getTime() / 1000)); + } + + private void assertProgramOrder(DataSource ds1, List<BlackboardArtifact> artifacts, List<String> programNamesReturned) + throws TskCoreException, NoServiceProviderException, TranslationException, SleuthkitCaseProviderException { + + Pair<SleuthkitCase, Blackboard> tskPair = getArtifactsTSKMock(artifacts); + UserActivitySummary summary = getTestClass(tskPair.getLeft(), false, null); + List<TopProgramsResult> results = summary.getTopPrograms(ds1, 10); + + Assert.assertEquals(programNamesReturned.size(), results.size()); + for (int i = 0; i < programNamesReturned.size(); i++) { + Assert.assertTrue(programNamesReturned.get(i).equalsIgnoreCase(results.get(i).getProgramName())); + } + } + + /** + * Ensure that UserActivitySummary.getTopPrograms properly orders results + * (first by run count, then date, then program name). + * + * @throws TskCoreException + * @throws NoServiceProviderException + * @throws TranslationException + * @throws SleuthkitCaseProviderException + */ + @Test + public void getTopPrograms_correctOrdering() + throws TskCoreException, NoServiceProviderException, TranslationException, SleuthkitCaseProviderException { + + DataSource ds1 = TskMockUtils.getDataSource(1); + BlackboardArtifact sortByRunsCount1 = getProgramArtifact(1001, ds1, "Program1.exe", "/Program Files/Folder/", 8, 1L); + BlackboardArtifact sortByRunsCount2 = getProgramArtifact(1002, ds1, "Program2.exe", "/Program Files/Folder/", 9, 2L); + BlackboardArtifact sortByRunsCount3 = getProgramArtifact(1003, ds1, "Program3.exe", "/Program Files/Folder/", 10, 3L); + assertProgramOrder(ds1, Arrays.asList(sortByRunsCount1, sortByRunsCount2, sortByRunsCount3), Arrays.asList("Program3.exe", "Program2.exe", "Program1.exe")); + + BlackboardArtifact sortByRunDate1 = getProgramArtifact(1011, ds1, "Program1.exe", "/Program Files/Folder/", null, 1L); + BlackboardArtifact sortByRunDate2 = getProgramArtifact(1012, ds1, "Program2.exe", "/Program Files/Folder/", null, 3L); + BlackboardArtifact sortByRunDate3 = getProgramArtifact(1013, ds1, "Program3.exe", "/Program Files/Folder/", null, 2L); + assertProgramOrder(ds1, Arrays.asList(sortByRunDate1, sortByRunDate2, sortByRunDate3), Arrays.asList("Program2.exe", "Program3.exe", "Program1.exe")); + + BlackboardArtifact sortByProgName1 = getProgramArtifact(1021, ds1, "cProgram.exe", "/Program Files/Folder/", null, null); + BlackboardArtifact sortByProgName2 = getProgramArtifact(1022, ds1, "BProgram.exe", "/Program Files/Folder/", null, null); + BlackboardArtifact sortByProgName3 = getProgramArtifact(1023, ds1, "aProgram.exe", "/Program Files/Folder/", null, null); + assertProgramOrder(ds1, Arrays.asList(sortByProgName1, sortByProgName2, sortByProgName3), Arrays.asList("aProgram.exe", "BProgram.exe", "cProgram.exe")); + } + + /** + * Ensure that UserActivitySummary.getTopPrograms properly limits results + * (if no run count and no run date, then no limit). + * + * @throws TskCoreException + * @throws NoServiceProviderException + * @throws TranslationException + * @throws SleuthkitCaseProviderException + */ + @Test + public void getTopPrograms_limited() + throws TskCoreException, NoServiceProviderException, + TranslationException, SleuthkitCaseProviderException { + + int countRequested = 10; + for (int returnedCount : new int[]{1, 9, 10, 11}) { + long dataSourceId = 1L; + DataSource dataSource = TskMockUtils.getDataSource(dataSourceId); + + // if data is present for counts and dates, the results are limited + List<BlackboardArtifact> returnedArtifacts = IntStream.range(0, returnedCount) + .mapToObj((idx) -> getProgramArtifact(1000 + idx, dataSource, "Program" + idx, + "/Program Files/Folder/", idx + 1, DAY_SECONDS * idx + 1)) + .collect(Collectors.toList()); + + Pair<SleuthkitCase, Blackboard> tskPair = getArtifactsTSKMock(returnedArtifacts); + UserActivitySummary summary = getTestClass(tskPair.getLeft(), false, null); + + List<TopProgramsResult> results = summary.getTopPrograms(dataSource, countRequested); + verifyCalled(tskPair.getRight(), ARTIFACT_TYPE.TSK_PROG_RUN.getTypeID(), dataSourceId, + "Expected getRecentDevices to call getArtifacts with correct arguments."); + + Assert.assertEquals(Math.min(countRequested, returnedCount), results.size()); + + // if that data is not present, it is not limited + List<BlackboardArtifact> returnedArtifactsAlphabetical = IntStream.range(0, returnedCount) + .mapToObj((idx) -> getProgramArtifact(1000 + idx, dataSource, "Program" + idx, null, null, null)) + .collect(Collectors.toList()); + + Pair<SleuthkitCase, Blackboard> tskPairAlphabetical = getArtifactsTSKMock(returnedArtifactsAlphabetical); + UserActivitySummary summaryAlphabetical = getTestClass(tskPairAlphabetical.getLeft(), false, null); + + List<TopProgramsResult> resultsAlphabetical = summaryAlphabetical.getTopPrograms(dataSource, countRequested); + verifyCalled(tskPairAlphabetical.getRight(), ARTIFACT_TYPE.TSK_PROG_RUN.getTypeID(), dataSourceId, + "Expected getRecentDevices to call getArtifacts with correct arguments."); + + // ensure alphabetical by name + for (int i = 0; i < resultsAlphabetical.size() - 1; i++) { + Assert.assertTrue(resultsAlphabetical.get(i).getProgramName().compareToIgnoreCase(resultsAlphabetical.get(i + 1).getProgramName()) < 0); + } + + Assert.assertEquals(returnedArtifacts.size(), resultsAlphabetical.size()); + } + } +} diff --git a/Core/test/unit/src/org/sleuthkit/autopsy/discovery/search/DomainSearchTestUtils.java b/Core/test/unit/src/org/sleuthkit/autopsy/discovery/search/DomainSearchTestUtils.java index ffadb3a16b554d2eeb4e1fc11d9bd9aba03353e3..9596ae432f3d9eee61ef89ac398dfcb6739a7f8a 100755 --- a/Core/test/unit/src/org/sleuthkit/autopsy/discovery/search/DomainSearchTestUtils.java +++ b/Core/test/unit/src/org/sleuthkit/autopsy/discovery/search/DomainSearchTestUtils.java @@ -32,7 +32,7 @@ private DomainSearchTestUtils() { public static ResultDomain mockDomainResult(String domain, long start, long end, long totalVisits, long visits, long filesDownloaded, long dataSourceId) { - Content dataSource = TskMockUtils.mockDataSource(dataSourceId); + Content dataSource = TskMockUtils.getDataSource(dataSourceId); return new ResultDomain(domain, start, end, totalVisits, visits, filesDownloaded, dataSource); } diff --git a/Core/test/unit/src/org/sleuthkit/autopsy/testutils/RandomizationUtils.java b/Core/test/unit/src/org/sleuthkit/autopsy/testutils/RandomizationUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..848830284b6a8e56e202ad47dd3afb1f722f7c00 --- /dev/null +++ b/Core/test/unit/src/org/sleuthkit/autopsy/testutils/RandomizationUtils.java @@ -0,0 +1,56 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2020 Basis Technology Corp. + * Contact: carrier <at> sleuthkit <dot> 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.testutils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tools for pseudo-randomization. + */ +public final class RandomizationUtils { + + /** + * Returns list in 0, n-1, 1, n-2 ... order. Deterministic so same results + * each time, but not in original order. + * + * @return Mixed up list. + */ + public static <T> List<T> getMixedUp(List<T> list) { + int forward = 0; + int backward = list.size() - 1; + + List<T> newList = new ArrayList<>(); + while (forward <= backward) { + newList.add(list.get(forward)); + + if (forward < backward) { + newList.add(list.get(backward)); + } + + forward++; + backward--; + } + + return newList; + } + + private RandomizationUtils() { + } +} diff --git a/Core/test/unit/src/org/sleuthkit/autopsy/testutils/TskMockUtils.java b/Core/test/unit/src/org/sleuthkit/autopsy/testutils/TskMockUtils.java index b47ecdece354878c4e481764dc24c0968fb82350..b6a832acb79343ac9ba48a2570e292df130935af 100644 --- a/Core/test/unit/src/org/sleuthkit/autopsy/testutils/TskMockUtils.java +++ b/Core/test/unit/src/org/sleuthkit/autopsy/testutils/TskMockUtils.java @@ -19,15 +19,26 @@ package org.sleuthkit.autopsy.testutils; import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.function.Function; +import java.util.logging.ConsoleHandler; +import java.util.logging.Handler; +import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import org.sleuthkit.autopsy.texttranslation.NoServiceProviderException; +import org.sleuthkit.autopsy.texttranslation.TextTranslationService; +import org.sleuthkit.autopsy.texttranslation.TranslationException; +import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardAttribute; +import org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE; +import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.DataSource; import org.sleuthkit.datamodel.TskCoreException; @@ -44,7 +55,7 @@ public class TskMockUtils { * * @return The mocked datasource. */ - public static DataSource mockDataSource(long dataSourceId) { + public static DataSource getDataSource(long dataSourceId) { DataSource dataSource = mock(DataSource.class); when(dataSource.getName()).thenReturn(""); when(dataSource.getId()).thenReturn(dataSourceId); @@ -65,14 +76,35 @@ public static DataSource mockDataSource(long dataSourceId) { * * @throws TskCoreException */ - public static BlackboardArtifact mockArtifact(BlackboardArtifact.Type artifactType, long artifactId, + public static BlackboardArtifact getArtifact(BlackboardArtifact.Type artifactType, long artifactId, + DataSource dataSource, BlackboardAttribute... attributes) throws TskCoreException { + return getArtifact(artifactType, null, artifactId, dataSource, attributes); + } + + /** + * Gets a mock Blackboard artifact. + * + * @param artifactType The artifact type for the artifact. + * @param parent The parent file of the artifact. + * @param artifactId The artifact id. + * @param dataSource The datasource. + * @param attributes The attributes for the artifact. + * + * @return The mocked artifact. + * + * @throws TskCoreException + */ + public static BlackboardArtifact getArtifact(BlackboardArtifact.Type artifactType, Content parent, long artifactId, DataSource dataSource, BlackboardAttribute... attributes) throws TskCoreException { BlackboardArtifact artifact = mock(BlackboardArtifact.class); final Map<BlackboardAttribute.Type, BlackboardAttribute> attributeTypes = Stream.of(attributes) + .filter(attr -> attr != null) .collect(Collectors.toMap((attr) -> attr.getAttributeType(), Function.identity())); + when(artifact.getParent()).thenReturn(parent); + when(artifact.getArtifactID()).thenReturn(artifactId); when(artifact.getArtifactTypeID()).thenReturn(artifactType.getTypeID()); @@ -89,6 +121,146 @@ public static BlackboardArtifact mockArtifact(BlackboardArtifact.Type artifactTy return artifact; } + public static BlackboardArtifact getArtifact(BlackboardArtifact.Type artifactType, long artifactId, + DataSource dataSource, List<BlackboardAttribute> attributes) throws TskCoreException { + + return getArtifact(artifactType, artifactId, dataSource, attributes.toArray(new BlackboardAttribute[0])); + } + + private static final String DEFAULT_ATTR_SOURCE = "TEST SOURCE"; + + public static BlackboardAttribute getAttribute(ATTRIBUTE_TYPE attrType, Object value) { + + return getAttribute(new BlackboardAttribute.Type(attrType), DEFAULT_ATTR_SOURCE, value); + } + + public static BlackboardAttribute getAttribute(BlackboardAttribute.Type attrType, String source, Object value) { + switch (attrType.getValueType()) { + case STRING: + case JSON: + if (value instanceof String) { + return new BlackboardAttribute(attrType, source, (String) value); + } + break; + case DATETIME: + case LONG: + if (value instanceof Long) { + return new BlackboardAttribute(attrType, source, (Long) value); + } + break; + case INTEGER: + if (value instanceof Integer) { + return new BlackboardAttribute(attrType, source, (Integer) value); + } + break; + case DOUBLE: + if (value instanceof Double) { + return new BlackboardAttribute(attrType, source, (Double) value); + } + break; + case BYTE: + if (value instanceof byte[]) { + return new BlackboardAttribute(attrType, source, (byte[]) value); + } + break; + default: + throw new IllegalArgumentException(String.format("Unknown attribute value type: %s", attrType.getValueType())); + } + + throw new IllegalArgumentException(String.format("Attribute type expected type of %s but received argument of %s", attrType.getValueType(), value)); + } + + /** + * Returns a mock TextTranslationService. + * + * @param onTranslate A function that performs the translation. If null, a + * null result is always returned for .translate method. + * @param hasProvider What to return for the hasProvider method. + * + * @return The mocked text translation service. + * + * @throws NoServiceProviderException + * @throws TranslationException + */ + public static TextTranslationService getTextTranslationService(Function<String, String> onTranslate, boolean hasProvider) + throws NoServiceProviderException, TranslationException { + TextTranslationService translationService = mock(TextTranslationService.class); + when(translationService.hasProvider()).thenReturn(hasProvider); + + when(translationService.translate(anyString())).thenAnswer((invocation) -> { + if (onTranslate == null) { + throw new NoServiceProviderException("No onTranslate function provided"); + } + + Object[] args = invocation.getArguments(); + String input = (String) args[0]; + return (input == null) ? null : onTranslate.apply(input); + }); + + return translationService; + } + + /** + * Returns an AbstractFile mocking getPath and getName. + * + * @param objId The object id. + * @param path The path for the file. + * @param name The name + * + * @return + */ + public static AbstractFile getAbstractFile(long objId, String path, String name) { + AbstractFile mocked = mock(AbstractFile.class); + when(mocked.getId()).thenReturn(objId); + when(mocked.getName()).thenReturn(name); + when(mocked.getParentPath()).thenReturn(path); + return mocked; + } + + private static void setConsoleHandler(Logger logger) { + // taken from https://stackoverflow.com/a/981230 + // Handler for console (reuse it if it already exists) + Handler consoleHandler = null; + + //see if there is already a console handler + for (Handler handler : logger.getHandlers()) { + if (handler instanceof ConsoleHandler) { + //found the console handler + consoleHandler = handler; + break; + } + } + + if (consoleHandler == null) { + //there was no console handler found, create a new one + consoleHandler = new ConsoleHandler(); + logger.addHandler(consoleHandler); + } + + //set the console handler to fine: + consoleHandler.setLevel(java.util.logging.Level.FINEST); + } + + /** + * Retrieves an autopsy logger that does not write to disk. + * + * @param loggerName The name of the logger. + * + * @return The autopsy logger for the console + * + * @throws InstantiationException + * @throws IllegalStateException + */ + public static Logger getJavaLogger(String loggerName) { + // The logger doesn't appear to respond well to mocking with mockito. + // It appears that the issue may have to do with mocking methods in the java.* packages + // since the autopsy logger extends the java.util.logging.Logger class: + // https://javadoc.io/static/org.mockito/mockito-core/3.5.13/org/mockito/Mockito.html#39 + Logger logger = Logger.getLogger(loggerName); + setConsoleHandler(logger); + return logger; + } + private TskMockUtils() { } } diff --git a/Experimental/nbproject/project.xml b/Experimental/nbproject/project.xml index e6cf918c7538dd066d103ff90d2bc57720ec95fd..953292b3c5f6c5b7a2bb3cfbe52dad747c1dc563 100644 --- a/Experimental/nbproject/project.xml +++ b/Experimental/nbproject/project.xml @@ -134,7 +134,8 @@ <build-prerequisite/> <compile-dependency/> <run-dependency> - <specification-version>1.0</specification-version> + <release-version>1</release-version> + <specification-version>23</specification-version> </run-dependency> </dependency> <dependency> diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java index a6ece45667317b25ed88bd0c2310099cafd0dd0c..b3a25be6774606aceb76f92b62d974ab07acea46 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/AutoIngestManager.java @@ -2930,7 +2930,9 @@ public void propertyChange(PropertyChangeEvent event) { String eventType = event.getPropertyName(); if (eventType.equals(IngestManager.IngestJobEvent.COMPLETED.toString()) || eventType.equals(IngestManager.IngestJobEvent.CANCELLED.toString())) { synchronized (ingestLock) { - ingestLock.notify(); + if (! IngestManager.getInstance().isIngestRunning()) { + ingestLock.notify(); + } } } } 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 56a675e256e41fb7f89b9aa01eb7059a07ec8096..823399e0d018c827352747704d53deb468de25ce 100755 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties-MERGED +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/Bundle.properties-MERGED @@ -205,7 +205,9 @@ DeleteCaseTask.progress.parsingManifest=Parsing manifest file {0}... DeleteCaseTask.progress.releasingManifestLock=Releasing lock on the manifest file {0}... DeleteCaseTask.progress.startMessage=Starting deletion... DeleteOrphanCaseNodesAction.progressDisplayName=Cleanup Case Znodes +# {0} - item count DeleteOrphanCaseNodesDialog.additionalInit.lblNodeCount.text=Znodes found: {0} +# {0} - item count DeleteOrphanCaseNodesDialog.additionalInit.znodesTextArea.countMessage=ZNODES FOUND: {0} DeleteOrphanCaseNodesTask.progress.connectingToCoordSvc=Connecting to the coordination service # {0} - node path diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/SharedConfiguration.java b/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/SharedConfiguration.java index fbbbd1999b9f73a11b4ed07e411adc4459a74f05..f7c9c6d8ff5ac7f38622b9bd8fa4e624a12beb68 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/SharedConfiguration.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/configuration/SharedConfiguration.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2015 Basis Technology Corp. + * Copyright 2015 - 2020 Basis Technology Corp. * Contact: carrier <at> sleuthkit <dot> org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -151,6 +151,8 @@ private void publishTask(String task) { /** * Upload the current multi-user ingest settings to a shared folder. * + * @return + * * @throws SharedConfigurationException * @throws CoordinationServiceException * @throws InterruptedException @@ -208,6 +210,7 @@ public SharedConfigResult uploadConfiguration() throws SharedConfigurationExcept uploadCentralRepositorySettings(remoteFolder); uploadObjectDetectionClassifiers(remoteFolder); uploadPythonModules(remoteFolder); + uploadYARASetting(remoteFolder); try { Files.deleteIfExists(uploadInProgress.toPath()); @@ -222,6 +225,8 @@ public SharedConfigResult uploadConfiguration() throws SharedConfigurationExcept /** * Download the multi-user settings from a shared folder. * + * @return + * * @throws SharedConfigurationException * @throws InterruptedException */ @@ -252,13 +257,16 @@ public synchronized SharedConfigResult downloadConfiguration() throws SharedConf } try { - /* Make sure all recent changes are saved to the preference file. - This also releases open file handles to the preference files. If this - is not done, then occasionally downloading of shared configuration - fails silently, likely because Java/OS is still holding the file handle. - The problem manifests itself by some of the old/original configuration files - sticking around after shared configuration has seemingly been successfully - updated. */ + /* + * Make sure all recent changes are saved to the preference + * file. This also releases open file handles to the preference + * files. If this is not done, then occasionally downloading of + * shared configuration fails silently, likely because Java/OS + * is still holding the file handle. The problem manifests + * itself by some of the old/original configuration files + * sticking around after shared configuration has seemingly been + * successfully updated. + */ UserPreferences.saveToStorage(); } catch (BackingStoreException ex) { throw new SharedConfigurationException("Failed to save shared configuration settings", ex); @@ -275,6 +283,7 @@ public synchronized SharedConfigResult downloadConfiguration() throws SharedConf downloadCentralRepositorySettings(remoteFolder); downloadObjectDetectionClassifiers(remoteFolder); downloadPythonModules(remoteFolder); + downloadYARASettings(remoteFolder); // Download general settings, then restore the current // values for the unshared fields @@ -344,7 +353,7 @@ private boolean isServiceUp(String serviceName) { private void saveNonSharedSettings() { sharedConfigMaster = AutoIngestUserPreferences.getSharedConfigMaster(); sharedConfigFolder = AutoIngestUserPreferences.getSharedConfigFolder(); - showToolsWarning = AutoIngestUserPreferences.getShowToolsWarning(); + showToolsWarning = AutoIngestUserPreferences.getShowToolsWarning(); displayLocalTime = UserPreferences.displayTimesInLocalTime(); hideKnownFilesInDataSource = UserPreferences.hideKnownFilesInDataSourcesTree(); hideKnownFilesInViews = UserPreferences.hideKnownFilesInViewsTree(); @@ -360,7 +369,7 @@ private void saveNonSharedSettings() { private void restoreNonSharedSettings() { AutoIngestUserPreferences.setSharedConfigFolder(sharedConfigFolder); AutoIngestUserPreferences.setSharedConfigMaster(sharedConfigMaster); - AutoIngestUserPreferences.setShowToolsWarning(showToolsWarning); + AutoIngestUserPreferences.setShowToolsWarning(showToolsWarning); UserPreferences.setDisplayTimesInLocalTime(displayLocalTime); UserPreferences.setHideKnownFilesInDataSourcesTree(hideKnownFilesInDataSource); UserPreferences.setHideKnownFilesInViewsTree(hideKnownFilesInViews); @@ -515,21 +524,23 @@ private static void copyToLocalFolder(String fileName, String localFolder, File throw new SharedConfigurationException(String.format("Failed to copy %s to %s", remoteFile.getAbsolutePath(), localSettingsFolder.getAbsolutePath()), ex); } } - + /** - * Copy an entire local settings folder to the remote folder, deleting any existing files. - * + * Copy an entire local settings folder to the remote folder, deleting any + * existing files. + * * @param localFolder The local folder to copy - * @param remoteBaseFolder The remote folder that will hold a copy of the original folder - * - * @throws SharedConfigurationException + * @param remoteBaseFolder The remote folder that will hold a copy of the + * original folder + * + * @throws SharedConfigurationException */ private void copyLocalFolderToRemoteFolder(File localFolder, File remoteBaseFolder) throws SharedConfigurationException { logger.log(Level.INFO, "Uploading {0} to {1}", new Object[]{localFolder.getAbsolutePath(), remoteBaseFolder.getAbsolutePath()}); - + File newRemoteFolder = new File(remoteBaseFolder, localFolder.getName()); - - if(newRemoteFolder.exists()) { + + if (newRemoteFolder.exists()) { try { FileUtils.deleteDirectory(newRemoteFolder); } catch (IOException ex) { @@ -537,29 +548,30 @@ private void copyLocalFolderToRemoteFolder(File localFolder, File remoteBaseFold throw new SharedConfigurationException(String.format("Failed to delete remote folder {0}", newRemoteFolder.getAbsolutePath()), ex); } } - + try { FileUtils.copyDirectoryToDirectory(localFolder, remoteBaseFolder); } catch (IOException ex) { throw new SharedConfigurationException(String.format("Failed to copy %s to %s", localFolder, remoteBaseFolder.getAbsolutePath()), ex); - } + } } - + /** - * Copy an entire remote settings folder to the local folder, deleting any existing files. - * No error if the remote folder does not exist. - * + * Copy an entire remote settings folder to the local folder, deleting any + * existing files. No error if the remote folder does not exist. + * * @param localFolder The local folder that will be overwritten. - * @param remoteBaseFolder The remote folder holding the folder that will be copied - * - * @throws SharedConfigurationException + * @param remoteBaseFolder The remote folder holding the folder that will be + * copied + * + * @throws SharedConfigurationException */ private void copyRemoteFolderToLocalFolder(File localFolder, File remoteBaseFolder) throws SharedConfigurationException { logger.log(Level.INFO, "Downloading {0} from {1}", new Object[]{localFolder.getAbsolutePath(), remoteBaseFolder.getAbsolutePath()}); - + // Clean out the local folder regardless of whether the remote version exists. leave the // folder in place since Autopsy expects it to exist. - if(localFolder.exists()) { + if (localFolder.exists()) { try { FileUtils.cleanDirectory(localFolder); } catch (IOException ex) { @@ -567,19 +579,19 @@ private void copyRemoteFolderToLocalFolder(File localFolder, File remoteBaseFold throw new SharedConfigurationException(String.format("Failed to delete files from local folder {0}", localFolder.getAbsolutePath()), ex); } } - + File remoteSubFolder = new File(remoteBaseFolder, localFolder.getName()); - if(! remoteSubFolder.exists()) { + if (!remoteSubFolder.exists()) { logger.log(Level.INFO, "{0} does not exist", remoteSubFolder.getAbsolutePath()); return; } - + try { FileUtils.copyDirectory(remoteSubFolder, localFolder); } catch (IOException ex) { throw new SharedConfigurationException(String.format("Failed to copy %s from %s", localFolder, remoteBaseFolder.getAbsolutePath()), ex); - } - } + } + } /** * Upload the basic set of auto-ingest settings to the shared folder. @@ -899,56 +911,56 @@ private void downloadMultiUserAndGeneralSettings(File remoteFolder) throws Share /** * Upload the object detection classifiers. - * + * * @param remoteFolder Shared settings folder - * - * @throws SharedConfigurationException + * + * @throws SharedConfigurationException */ private void uploadObjectDetectionClassifiers(File remoteFolder) throws SharedConfigurationException { publishTask("Uploading object detection classfiers"); File classifiersFolder = new File(PlatformUtil.getObjectDetectionClassifierPath()); copyLocalFolderToRemoteFolder(classifiersFolder, remoteFolder); } - + /** * Download the object detection classifiers. - * + * * @param remoteFolder Shared settings folder - * - * @throws SharedConfigurationException + * + * @throws SharedConfigurationException */ private void downloadObjectDetectionClassifiers(File remoteFolder) throws SharedConfigurationException { publishTask("Downloading object detection classfiers"); File classifiersFolder = new File(PlatformUtil.getObjectDetectionClassifierPath()); copyRemoteFolderToLocalFolder(classifiersFolder, remoteFolder); } - - /** + + /** * Upload the Python modules. - * + * * @param remoteFolder Shared settings folder - * - * @throws SharedConfigurationException + * + * @throws SharedConfigurationException */ private void uploadPythonModules(File remoteFolder) throws SharedConfigurationException { publishTask("Uploading python modules"); File classifiersFolder = new File(PlatformUtil.getUserPythonModulesPath()); copyLocalFolderToRemoteFolder(classifiersFolder, remoteFolder); } - + /** * Download the Python modules. - * + * * @param remoteFolder Shared settings folder - * - * @throws SharedConfigurationException + * + * @throws SharedConfigurationException */ private void downloadPythonModules(File remoteFolder) throws SharedConfigurationException { publishTask("Downloading python modules"); File classifiersFolder = new File(PlatformUtil.getUserPythonModulesPath()); copyRemoteFolderToLocalFolder(classifiersFolder, remoteFolder); } - + /** * Upload settings and hash databases to the shared folder. The general * algorithm is: - Copy the general settings in hashsets.xml - For each hash @@ -1093,12 +1105,10 @@ private void downloadHashDbSettings(File remoteFolder) throws SharedConfiguratio Map<String, String> remoteVersions = readVersionsFromFile(remoteVersionFile); /* - Iterate through remote list - If local needs it, download - - Download remote settings files to local - Download remote versions file to local - HashDbManager reload + * Iterate through remote list If local needs it, download + * + * Download remote settings files to local Download remote versions file + * to local HashDbManager reload */ File localDb = new File(""); File sharedDb = new File(""); @@ -1247,7 +1257,7 @@ private static List<String> getHashFileNamesFromSettingsFile() throws SharedConf if (hashDb.getIndexPath().isEmpty() && hashDb.getDatabasePath().isEmpty()) { continue; } - + if (hashDb.hasIndexOnly()) { results.add(hashDb.getIndexPath()); } else { @@ -1356,4 +1366,41 @@ private static String calculateCRC(String filePath) throws SharedConfigurationEx throw new SharedConfigurationException(String.format("Failed to calculate CRC for %s", file.getAbsolutePath()), ex); } } + + /** + * Copy the YARA settings directory from the local directory to the remote + * directory. + * + * @param remoteFolder Shared settings folder + * + * @throws + * org.sleuthkit.autopsy.experimental.configuration.SharedConfiguration.SharedConfigurationException + */ + private void uploadYARASetting(File remoteFolder) throws SharedConfigurationException { + publishTask("Uploading YARA module configuration"); + + File localYara = Paths.get(PlatformUtil.getUserDirectory().getAbsolutePath(), "yara").toFile(); + + if (!localYara.exists()) { + return; + } + + copyLocalFolderToRemoteFolder(localYara, remoteFolder); + } + + /** + * Downloads the YARA settings folder from the remote directory to the local + * one. + * + * @param remoteFolder Shared settings folder + * + * @throws + * org.sleuthkit.autopsy.experimental.configuration.SharedConfiguration.SharedConfigurationException + */ + private void downloadYARASettings(File remoteFolder) throws SharedConfigurationException { + publishTask("Downloading YARA module configuration"); + File localYara = Paths.get(PlatformUtil.getUserDirectory().getAbsolutePath(), "yara").toFile(); + + copyRemoteFolderToLocalFolder(localYara, remoteFolder); + } } diff --git a/KeywordSearch/nbproject/project.xml b/KeywordSearch/nbproject/project.xml index aa8cb0cf8efaab9e157feef06cfde644ed9710c7..c6f056f5a4dee72f4f5d4ea76fd53fe93edf76c4 100644 --- a/KeywordSearch/nbproject/project.xml +++ b/KeywordSearch/nbproject/project.xml @@ -118,7 +118,8 @@ <build-prerequisite/> <compile-dependency/> <run-dependency> - <specification-version>1.0</specification-version> + <release-version>1</release-version> + <specification-version>23</specification-version> </run-dependency> </dependency> <dependency> diff --git a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Chunker.java b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Chunker.java index 08ca0ab5110d975598e6f216ab3f08e514b15919..fd0f11ab74a6e4393d88424b1d62769d3af9f85a 100644 --- a/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Chunker.java +++ b/KeywordSearch/src/org/sleuthkit/autopsy/keywordsearch/Chunker.java @@ -187,7 +187,6 @@ private static StringBuilder replaceInvalidUTF16(String s) { private static StringBuilder sanitize(String s) { String normStr = Normalizer.normalize(s, Normalizer.Form.NFKC); return sanitizeToUTF8(replaceInvalidUTF16(normStr)); - } @Override @@ -336,8 +335,9 @@ private void readToWhiteSpaceHelper(int maxBytes, StringBuilder currentChunk, St String chunkSegment; if (Character.isHighSurrogate(ch)) { //read another char into the buffer. - charsRead = reader.read(tempChunkBuf, 1, 1); - if (charsRead == -1) { + int surrogateCharsRead = reader.read(tempChunkBuf, 1, 1); + charsRead += surrogateCharsRead; + if (surrogateCharsRead == -1) { //this is the last chunk, so just drop the unpaired surrogate endOfReaderReached = true; return; @@ -352,17 +352,32 @@ private void readToWhiteSpaceHelper(int maxBytes, StringBuilder currentChunk, St //cleanup any invalid utf-16 sequences StringBuilder sanitizedChunkSegment = sanitize(chunkSegment); - //check for whitespace. - whitespaceFound = Character.isWhitespace(sanitizedChunkSegment.codePointAt(0)); - //add read chars to the chunk and update the length. - currentChunk.append(sanitizedChunkSegment); - chunkSizeBytes += sanitizedChunkSegment.toString().getBytes(UTF_8).length; - + //get the length in utf8 bytes of the read chars + int segmentSize = chunkSegment.getBytes(UTF_8).length; + // lower case the string and get it's size. NOTE: lower casing can // change the size of the string. String lowerCasedSegment = sanitizedChunkSegment.toString().toLowerCase(); - lowerCasedChunk.append(lowerCasedSegment); - lowerCasedChunkSizeBytes += lowerCasedSegment.getBytes(UTF_8).length; + int lowerCasedSegmentSize = lowerCasedSegment.getBytes(UTF_8).length; + + //if it will not put us past maxBytes + if ((chunkSizeBytes + segmentSize < maxBytes - MAX_CHAR_SIZE_INCREASE_IN_BYTES) + && (lowerCasedChunkSizeBytes + lowerCasedSegmentSize < maxBytes - MAX_CHAR_SIZE_INCREASE_IN_BYTES)) { + + //add read chars to the chunk and update the length. + currentChunk.append(sanitizedChunkSegment); + chunkSizeBytes += segmentSize; + + lowerCasedChunk.append(lowerCasedSegment); + lowerCasedChunkSizeBytes += lowerCasedSegmentSize; + + //check for whitespace. + whitespaceFound = Character.isWhitespace(sanitizedChunkSegment.codePointAt(0)); + } else { + //unread it, and break out of read loop. + reader.unread(tempChunkBuf, 0, charsRead); + return; + } } } } diff --git a/Tika/manifest.mf b/Tika/manifest.mf index 770d374a5dae80ad25a8523b23518be57616a903..d5e404d1e321247c95869053adb97024d560007c 100755 --- a/Tika/manifest.mf +++ b/Tika/manifest.mf @@ -1,6 +1,7 @@ Manifest-Version: 1.0 AutoUpdate-Show-In-Client: true -OpenIDE-Module: org.sleuthkit.autopsy.Tika +OpenIDE-Module: org.sleuthkit.autopsy.Tika/1 +OpenIDE-Module-Implementation-Version: 1 OpenIDE-Module-Localizing-Bundle: org/sleuthkit/autopsy/Tika/Bundle.properties -OpenIDE-Module-Specification-Version: 1.0 +OpenIDE-Module-Specification-Version: 23 diff --git a/ManifestTool/ManifestGenerationAlgorithms.au3 b/Tools/ManifestTool/ManifestGenerationAlgorithms.au3 similarity index 100% rename from ManifestTool/ManifestGenerationAlgorithms.au3 rename to Tools/ManifestTool/ManifestGenerationAlgorithms.au3 diff --git a/ManifestTool/ManifestTool.au3 b/Tools/ManifestTool/ManifestTool.au3 similarity index 100% rename from ManifestTool/ManifestTool.au3 rename to Tools/ManifestTool/ManifestTool.au3 diff --git a/ManifestTool/ManifestTool.exe b/Tools/ManifestTool/ManifestTool.exe similarity index 100% rename from ManifestTool/ManifestTool.exe rename to Tools/ManifestTool/ManifestTool.exe diff --git a/ZookeeperNodeMigration/.gitignore b/Tools/ZookeeperNodeMigration/.gitignore similarity index 62% rename from ZookeeperNodeMigration/.gitignore rename to Tools/ZookeeperNodeMigration/.gitignore index 0e56a449c757c3333320e858ada0ebab0df8aff1..46cba4a7dd698d3709a826334085e3365667fbad 100755 --- a/ZookeeperNodeMigration/.gitignore +++ b/Tools/ZookeeperNodeMigration/.gitignore @@ -1,3 +1,2 @@ /nbproject/private/ -/build/ - +/build/ \ No newline at end of file diff --git a/ZookeeperNodeMigration/build.xml b/Tools/ZookeeperNodeMigration/build.xml similarity index 100% rename from ZookeeperNodeMigration/build.xml rename to Tools/ZookeeperNodeMigration/build.xml diff --git a/ZookeeperNodeMigration/dist/README.TXT b/Tools/ZookeeperNodeMigration/dist/README.TXT similarity index 100% rename from ZookeeperNodeMigration/dist/README.TXT rename to Tools/ZookeeperNodeMigration/dist/README.TXT diff --git a/ZookeeperNodeMigration/dist/ZookeeperNodeMigration.jar b/Tools/ZookeeperNodeMigration/dist/ZookeeperNodeMigration.jar similarity index 95% rename from ZookeeperNodeMigration/dist/ZookeeperNodeMigration.jar rename to Tools/ZookeeperNodeMigration/dist/ZookeeperNodeMigration.jar index 92b11bc86ac07058ee7d31ad5a4a2028869611ca..b2bb4f8e60e391a085f784853532cb63e49d26f4 100755 Binary files a/ZookeeperNodeMigration/dist/ZookeeperNodeMigration.jar and b/Tools/ZookeeperNodeMigration/dist/ZookeeperNodeMigration.jar differ diff --git a/ZookeeperNodeMigration/dist/lib/curator-client-2.8.0.jar b/Tools/ZookeeperNodeMigration/dist/lib/curator-client-2.8.0.jar similarity index 100% rename from ZookeeperNodeMigration/dist/lib/curator-client-2.8.0.jar rename to Tools/ZookeeperNodeMigration/dist/lib/curator-client-2.8.0.jar diff --git a/ZookeeperNodeMigration/dist/lib/curator-framework-2.8.0.jar b/Tools/ZookeeperNodeMigration/dist/lib/curator-framework-2.8.0.jar similarity index 100% rename from ZookeeperNodeMigration/dist/lib/curator-framework-2.8.0.jar rename to Tools/ZookeeperNodeMigration/dist/lib/curator-framework-2.8.0.jar diff --git a/ZookeeperNodeMigration/dist/lib/curator-recipes-2.8.0.jar b/Tools/ZookeeperNodeMigration/dist/lib/curator-recipes-2.8.0.jar similarity index 100% rename from ZookeeperNodeMigration/dist/lib/curator-recipes-2.8.0.jar rename to Tools/ZookeeperNodeMigration/dist/lib/curator-recipes-2.8.0.jar diff --git a/ZookeeperNodeMigration/dist/lib/guava-17.0.jar b/Tools/ZookeeperNodeMigration/dist/lib/guava-17.0.jar similarity index 100% rename from ZookeeperNodeMigration/dist/lib/guava-17.0.jar rename to Tools/ZookeeperNodeMigration/dist/lib/guava-17.0.jar diff --git a/ZookeeperNodeMigration/dist/lib/log4j-1.2.17.jar b/Tools/ZookeeperNodeMigration/dist/lib/log4j-1.2.17.jar similarity index 100% rename from ZookeeperNodeMigration/dist/lib/log4j-1.2.17.jar rename to Tools/ZookeeperNodeMigration/dist/lib/log4j-1.2.17.jar diff --git a/ZookeeperNodeMigration/dist/lib/slf4j-api-1.7.24.jar b/Tools/ZookeeperNodeMigration/dist/lib/slf4j-api-1.7.24.jar similarity index 100% rename from ZookeeperNodeMigration/dist/lib/slf4j-api-1.7.24.jar rename to Tools/ZookeeperNodeMigration/dist/lib/slf4j-api-1.7.24.jar diff --git a/ZookeeperNodeMigration/dist/lib/slf4j-log4j12-1.7.6.jar b/Tools/ZookeeperNodeMigration/dist/lib/slf4j-log4j12-1.7.6.jar similarity index 100% rename from ZookeeperNodeMigration/dist/lib/slf4j-log4j12-1.7.6.jar rename to Tools/ZookeeperNodeMigration/dist/lib/slf4j-log4j12-1.7.6.jar diff --git a/ZookeeperNodeMigration/dist/lib/zookeeper-3.4.6.jar b/Tools/ZookeeperNodeMigration/dist/lib/zookeeper-3.4.6.jar similarity index 100% rename from ZookeeperNodeMigration/dist/lib/zookeeper-3.4.6.jar rename to Tools/ZookeeperNodeMigration/dist/lib/zookeeper-3.4.6.jar diff --git a/ZookeeperNodeMigration/docs/README.TXT b/Tools/ZookeeperNodeMigration/docs/README.TXT similarity index 100% rename from ZookeeperNodeMigration/docs/README.TXT rename to Tools/ZookeeperNodeMigration/docs/README.TXT diff --git a/ZookeeperNodeMigration/manifest.mf b/Tools/ZookeeperNodeMigration/manifest.mf similarity index 100% rename from ZookeeperNodeMigration/manifest.mf rename to Tools/ZookeeperNodeMigration/manifest.mf diff --git a/ZookeeperNodeMigration/nbproject/build-impl.xml b/Tools/ZookeeperNodeMigration/nbproject/build-impl.xml similarity index 100% rename from ZookeeperNodeMigration/nbproject/build-impl.xml rename to Tools/ZookeeperNodeMigration/nbproject/build-impl.xml diff --git a/ZookeeperNodeMigration/nbproject/project.properties b/Tools/ZookeeperNodeMigration/nbproject/project.properties similarity index 100% rename from ZookeeperNodeMigration/nbproject/project.properties rename to Tools/ZookeeperNodeMigration/nbproject/project.properties diff --git a/ZookeeperNodeMigration/nbproject/project.xml b/Tools/ZookeeperNodeMigration/nbproject/project.xml similarity index 100% rename from ZookeeperNodeMigration/nbproject/project.xml rename to Tools/ZookeeperNodeMigration/nbproject/project.xml diff --git a/ZookeeperNodeMigration/release/curator-client-2.8.0.jar b/Tools/ZookeeperNodeMigration/release/curator-client-2.8.0.jar similarity index 100% rename from ZookeeperNodeMigration/release/curator-client-2.8.0.jar rename to Tools/ZookeeperNodeMigration/release/curator-client-2.8.0.jar diff --git a/ZookeeperNodeMigration/release/curator-framework-2.8.0.jar b/Tools/ZookeeperNodeMigration/release/curator-framework-2.8.0.jar similarity index 100% rename from ZookeeperNodeMigration/release/curator-framework-2.8.0.jar rename to Tools/ZookeeperNodeMigration/release/curator-framework-2.8.0.jar diff --git a/ZookeeperNodeMigration/release/curator-recipes-2.8.0.jar b/Tools/ZookeeperNodeMigration/release/curator-recipes-2.8.0.jar similarity index 100% rename from ZookeeperNodeMigration/release/curator-recipes-2.8.0.jar rename to Tools/ZookeeperNodeMigration/release/curator-recipes-2.8.0.jar diff --git a/ZookeeperNodeMigration/release/guava-17.0.jar b/Tools/ZookeeperNodeMigration/release/guava-17.0.jar similarity index 100% rename from ZookeeperNodeMigration/release/guava-17.0.jar rename to Tools/ZookeeperNodeMigration/release/guava-17.0.jar diff --git a/ZookeeperNodeMigration/release/log4j-1.2.17.jar b/Tools/ZookeeperNodeMigration/release/log4j-1.2.17.jar similarity index 100% rename from ZookeeperNodeMigration/release/log4j-1.2.17.jar rename to Tools/ZookeeperNodeMigration/release/log4j-1.2.17.jar diff --git a/ZookeeperNodeMigration/release/slf4j-api-1.7.24.jar b/Tools/ZookeeperNodeMigration/release/slf4j-api-1.7.24.jar similarity index 100% rename from ZookeeperNodeMigration/release/slf4j-api-1.7.24.jar rename to Tools/ZookeeperNodeMigration/release/slf4j-api-1.7.24.jar diff --git a/ZookeeperNodeMigration/release/slf4j-log4j12-1.7.6.jar b/Tools/ZookeeperNodeMigration/release/slf4j-log4j12-1.7.6.jar similarity index 100% rename from ZookeeperNodeMigration/release/slf4j-log4j12-1.7.6.jar rename to Tools/ZookeeperNodeMigration/release/slf4j-log4j12-1.7.6.jar diff --git a/ZookeeperNodeMigration/release/zookeeper-3.4.6.jar b/Tools/ZookeeperNodeMigration/release/zookeeper-3.4.6.jar similarity index 100% rename from ZookeeperNodeMigration/release/zookeeper-3.4.6.jar rename to Tools/ZookeeperNodeMigration/release/zookeeper-3.4.6.jar diff --git a/ZookeeperNodeMigration/src/zookeepernodemigration/AutoIngestJobNodeData.java b/Tools/ZookeeperNodeMigration/src/zookeepernodemigration/AutoIngestJobNodeData.java similarity index 100% rename from ZookeeperNodeMigration/src/zookeepernodemigration/AutoIngestJobNodeData.java rename to Tools/ZookeeperNodeMigration/src/zookeepernodemigration/AutoIngestJobNodeData.java diff --git a/ZookeeperNodeMigration/src/zookeepernodemigration/TimeStampUtils.java b/Tools/ZookeeperNodeMigration/src/zookeepernodemigration/TimeStampUtils.java similarity index 100% rename from ZookeeperNodeMigration/src/zookeepernodemigration/TimeStampUtils.java rename to Tools/ZookeeperNodeMigration/src/zookeepernodemigration/TimeStampUtils.java diff --git a/ZookeeperNodeMigration/src/zookeepernodemigration/ZookeeperNodeMigration.java b/Tools/ZookeeperNodeMigration/src/zookeepernodemigration/ZookeeperNodeMigration.java similarity index 100% rename from ZookeeperNodeMigration/src/zookeepernodemigration/ZookeeperNodeMigration.java rename to Tools/ZookeeperNodeMigration/src/zookeepernodemigration/ZookeeperNodeMigration.java diff --git a/appveyor.yml b/appveyor.yml index bc3c7fa82dedea596719db6878779aad4a2e674a..fe70c368501d751568ab14a070bd69117d3a52a8 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,14 +6,13 @@ cache: - C:\ProgramData\chocolatey\lib - '%APPVEYOR_BUILD_FOLDER%\Core\test\qa-functional\data' - image: Visual Studio 2015 platform: x64 environment: - global: - TSK_HOME: "C:\\sleuthkit" - JDK_HOME: C:\Program Files\Java\jdk1.8.0 - PYTHON: "C:\\Python36-x64" + global: + TSK_HOME: "C:\\sleuthkit" + JDK_HOME: C:\Program Files\Java\jdk1.8.0 + PYTHON: "C:\\Python36-x64" install: - ps: choco install nuget.commandline @@ -42,4 +41,6 @@ build_script: - cd %APPVEYOR_BUILD_FOLDER% - cmd: ant -q build -test: off +test_script: + - cd %APPVEYOR_BUILD_FOLDER% + - cmd: ant -q test-no-regression diff --git a/build.xml b/build.xml index 7377d59c04f238240221b2ae1bb1b1fdf88f299c..fc25cba14d208609f58c6359ab677885079827b3 100644 --- a/build.xml +++ b/build.xml @@ -24,7 +24,7 @@ <!-- Verify that the java version running is . --> <fail message="Unsupported Java version: ${ant.java.version}. Make sure that the Java version is 1.8.0_66 or higher." - unless="supported-java-versions"/> + unless="supported-java-versions"/> <!-- Determine platform and include specific file --> <condition property="os.family" value="unix"> @@ -68,22 +68,54 @@ </target> <target name="clean" depends="suite.clean"> - <delete includeEmptyDirs="true" failonerror="false"> + <delete includeEmptyDirs="true" failonerror="false"> <fileset dir="docs\doxygen-user\user-docs" includes="**/*"/> - </delete> + </delete> <delete includeEmptyDirs="true" failonerror="false"> <fileset dir="docs\doxygen\doxygen_docs\api-docs" includes="**/*"/> - </delete> + </delete> <delete includeemptydirs="true" failonerror="false"> <fileset dir="${basedir}/docs/doxygen-dev/build-docs" includes="**/*"/> </delete> </target> + + <!-- This target is similar to the regular test target that calls test on all nbm's, + but this target excludes the Testing nbm which runs the regression tests --> + <target name="test-no-regression" depends="build" description="Runs tests for all modules in the suite excluding the regression tests of the Testing NBM."> + <!--taken from https://stackoverflow.com/a/10859103; remove "Testing" from the modules and provide 'modulesNoTesting' as result. --> + <property name="modulesBeforeChange" value="${modules}"/> + <script language="javascript"> + <![CDATA[ + var before = project.getProperty("modulesBeforeChange"); + var separator = ":"; + var testingNbm = "Testing"; + var beforeSplit = before.split(separator); + var items = []; + for (var i = 0; i < beforeSplit.length; i++) { + if (beforeSplit[i].toUpperCase() !== testingNbm.toUpperCase()) { + items.push(beforeSplit[i]); + } + } + var itemsJoined = items.join(separator); + project.setNewProperty("modulesNoTesting", itemsJoined); + ]]> + </script> + + <sortsuitemodules unsortedmodules="${modulesNoTesting}" sortedmodulesproperty="modules.test.sorted" sorttests="true"/> + <!-- continue on fail --> + <property name="continue.after.failing.tests" value="true"/> + <subant target="test" buildpath="${modules.test.sorted}" inheritrefs="false" inheritall="false"> + <property name="cluster.path.evaluated" value="${cluster.path.evaluated}"/> <!-- Just for speed of pre-7.0 projects --> + <property name="harness.taskdefs.done" value="${harness.taskdefs.done}"/> <!-- optimization --> + <property name="continue.after.failing.tests" value="${continue.after.failing.tests}"/> + </subant> + </target> <!-- This target will create a custom ZIP file for us. It first uses the general - ZIP target and then opens it up and adds in any files that we want. This is where we customize the - version number. --> + ZIP target and then opens it up and adds in any files that we want. This is where we customize the + version number. --> <target name="build-zip" depends="doxygen, suite.build-zip"> <!--,findJRE" --> <property name="release.dir" value="${nbdist.dir}/${app.name}"/> @@ -106,7 +138,7 @@ <copy file="${basedir}/NEWS.txt" tofile="${zip-tmp}/${app.name}/NEWS.txt"/> <copy file="${basedir}/Running_Linux_OSX.txt" tofile="${zip-tmp}/${app.name}/Running_Linux_OSX.txt"/> <copy file="${basedir}/unix_setup.sh" tofile="${zip-tmp}/${app.name}/unix_setup.sh"/> - <copy file="${basedir}/ManifestTool/ManifestTool.exe" todir="${zip-tmp}/${app.name}/bin"/> + <copy file="${basedir}/Tools/ManifestTool/ManifestTool.exe" todir="${zip-tmp}/${app.name}/bin"/> <copy file="${basedir}/icons/icon.ico" tofile="${zip-tmp}/${app.name}/icon.ico" overwrite="true"/> @@ -117,10 +149,10 @@ <!-- Copy the ZooKeeper migration tool, it's JAR files, and documentation --> <copy flatten="false" todir="${zip-tmp}/${app.name}/autopsy/ZookeeperNodeMigration"> - <fileset dir="${basedir}/ZookeeperNodeMigration/dist"/> + <fileset dir="${basedir}/Tools/ZookeeperNodeMigration/dist"/> </copy> <copy flatten="false" todir="${zip-tmp}/${app.name}/autopsy/ZookeeperNodeMigration" overwrite="true"> - <fileset dir="${basedir}/ZookeeperNodeMigration/docs"/> + <fileset dir="${basedir}/Tools/ZookeeperNodeMigration/docs"/> </copy> <property name="app.property.file" value="${zip-tmp}/${app.name}/etc/${app.name}.conf" /> @@ -128,15 +160,15 @@ <!-- for Japanese localized version add option: -Duser.language=ja --> - <if> - <equals arg1="${os.family}" arg2="mac"/> - <then> - <property name="jvm.options" value=""${jvm-value} -J-Xdock:name=${app.title}""/> - </then> - <else> - <property name="jvm.options" value=""${jvm-value}""/> - </else> - </if> + <if> + <equals arg1="${os.family}" arg2="mac"/> + <then> + <property name="jvm.options" value=""${jvm-value} -J-Xdock:name=${app.title}""/> + </then> + <else> + <property name="jvm.options" value=""${jvm-value}""/> + </else> + </if> <propertyfile file="${app.property.file}"> <!-- Note: can be higher on 64 bit systems, should be in sync with project.properties --> @@ -146,9 +178,9 @@ <replace file="${app.property.file}" token="@JVM_OPTIONS" value="${jvm.options}" /> <!-- We want to remove the dlls in autopsy/modules/lib because they will - shadow the files in the autopsy/modules/lib/ARCHITECTURE folder in the JAR. - These files are legacy from when we used to copy the dlls to this location. - This check should do away in the future. Added Sept '13--> + shadow the files in the autopsy/modules/lib/ARCHITECTURE folder in the JAR. + These files are legacy from when we used to copy the dlls to this location. + This check should do away in the future. Added Sept '13--> <delete failonerror="false"> <fileset dir="${zip-tmp}/${app.name}/autopsy/modules/lib"> <include name="libtsk_jni.dll" /> @@ -188,9 +220,9 @@ <target name="input-build-type" unless="build.type"> <input addProperty="build.type" - message="Enter the desired build type:" - validargs="DEVELOPMENT,RELEASE" - defaultvalue="DEVELOPMENT"/> + message="Enter the desired build type:" + validargs="DEVELOPMENT,RELEASE" + defaultvalue="DEVELOPMENT"/> </target> <target name="input-version" unless="app.version"> @@ -228,21 +260,21 @@ <echo>${app.name} branding</echo> <propertyfile - file="${branding.dir}/core/core.jar/org/netbeans/core/startup/Bundle.properties" - comment="Updated by build script"> + file="${branding.dir}/core/core.jar/org/netbeans/core/startup/Bundle.properties" + comment="Updated by build script"> <entry key="currentVersion" value="${app.title} ${app.version}" /> </propertyfile> <propertyfile - file="${branding.dir}/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties" - comment="Updated by build script"> + file="${branding.dir}/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties" + comment="Updated by build script"> <entry key="CTL_MainWindow_Title" value="${app.title} ${app.version}" /> <entry key="CTL_MainWindow_Title_No_Project" value="${app.title} ${app.version}" /> </propertyfile> <propertyfile - file="${basedir}/Core/src/org/sleuthkit/autopsy/coreutils/Version.properties" - comment="Updated by build script"> + file="${basedir}/Core/src/org/sleuthkit/autopsy/coreutils/Version.properties" + comment="Updated by build script"> <entry key="app.name" value="${app.title}" /> <entry key="app.version" value="${app.version}" /> <entry key="build.type" value="${build.type}" /> @@ -329,7 +361,7 @@ <antcall target="build-installer-${os.family}" /> </target> - <target name="chmod_executables" > + <target name="chmod_executables" > <chmod perm="a+x"> <fileset dir="${cluster}/markmckinnon" casesensitive="no" id="mm"> <include name="*_linux"/> diff --git a/test/script/tskdbdiff.py b/test/script/tskdbdiff.py index f152cd923a5d08675172042edc3deba19d6e0675..e202c3e111a490cb7bb3006a062d8f95fd1b4a10 100644 --- a/test/script/tskdbdiff.py +++ b/test/script/tskdbdiff.py @@ -464,6 +464,16 @@ def normalize_db_entry(line, files_table, vs_parts_table, vs_info_table, fs_info # remove object ID if files_index: + + # Ignore TIFF size and hash if extracted from PDFs. + # See JIRA-6951 for more details. + # index -1 = last element in the list, which is extension + # index -3 = 3rd from the end, which is the parent path. + if fields_list[-1] == "'tif'" and fields_list[-3].endswith(".pdf/'"): + fields_list[15] = "'SIZE_IGNORED'" + fields_list[23] = "'MD5_IGNORED'" + fields_list[24] = "'SHA256_IGNORED'" + newLine = ('INSERT INTO "tsk_files" VALUES(' + ', '.join(fields_list[1:]) + ');') # Remove object ID from Unalloc file name newLine = re.sub('Unalloc_[0-9]+_', 'Unalloc_', newLine) diff --git a/thirdparty/rr-full/plugins/shellactivities.pl b/thirdparty/rr-full/plugins/shellactivities.pl index 8b1691753625c29e8b7c50a94984baa1bbcf4fee..5df9f5615f6c41b2fe722dd32407d14b6327b533 100644 --- a/thirdparty/rr-full/plugins/shellactivities.pl +++ b/thirdparty/rr-full/plugins/shellactivities.pl @@ -70,6 +70,7 @@ sub processShellActivities { ::rptMsg(""); while ($offset < ($sz - 10)) { + # Code to locate the appropriate identifier $tag = 1; while ($tag) { @@ -78,9 +79,15 @@ sub processShellActivities { } else { $offset++; + # Check if at end of file and exit loop if it is + last if ($offset >= $sz ); } } - + + # Check if at end of file and exit loop if it is + last if ($offset >= $sz ); + + $offset += 2; $l = unpack("C",substr($data,$offset,1)); # ::rptMsg("String Length: ".sprintf "0x%x",$l); diff --git a/thunderbirdparser/nbproject/project.xml b/thunderbirdparser/nbproject/project.xml index 6643e72e0a6806340b57caf42e754fdc545cbdd9..b4d9ca8cbfeed2614ead6c8667fc28d308f890a6 100644 --- a/thunderbirdparser/nbproject/project.xml +++ b/thunderbirdparser/nbproject/project.xml @@ -44,7 +44,8 @@ <build-prerequisite/> <compile-dependency/> <run-dependency> - <specification-version>1.0</specification-version> + <release-version>1</release-version> + <specification-version>23</specification-version> </run-dependency> </dependency> <dependency> diff --git a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/MimeJ4MessageParser.java b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/MimeJ4MessageParser.java index 6a95e8729af7189a7701e160e72bb638d77f7fa4..828968d750be880ace3de728974ff955c2876d08 100755 --- a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/MimeJ4MessageParser.java +++ b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/MimeJ4MessageParser.java @@ -318,8 +318,10 @@ private static void handleAttachment(EmailMessage email, Entity e, long fileID, Body body = e.getBody(); if (body instanceof SingleBody) { + long fileLength; try (EncodedFileOutputStream fos = new EncodedFileOutputStream(new FileOutputStream(outPath), TskData.EncodingType.XOR1)) { ((SingleBody) body).writeTo(fos); + fileLength = fos.getBytesWritten(); } catch (IOException ex) { logger.log(Level.WARNING, "Failed to create file output stream for: " + outPath, ex); //NON-NLS return; @@ -328,7 +330,7 @@ private static void handleAttachment(EmailMessage email, Entity e, long fileID, EmailMessage.Attachment attach = new EmailMessage.Attachment(); attach.setName(filename); attach.setLocalPath(relModuleOutputPath + uniqueFilename); - attach.setSize(new File(outPath).length()); + attach.setSize(fileLength); attach.setEncodingType(TskData.EncodingType.XOR1); email.addAttachment(attach); } diff --git a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java index 0f9759819f9adc50616ed9f0c7a93ab8b838dd92..12f3342daca83eb74205b7dfa23c9cc204d10a5a 100644 --- a/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java +++ b/thunderbirdparser/src/org/sleuthkit/autopsy/thunderbirdparser/ThunderbirdMboxFileIngestModule.java @@ -23,9 +23,11 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.regex.Matcher; @@ -139,6 +141,10 @@ public ProcessResult process(AbstractFile abstractFile) { boolean isPstFile = PstParser.isPstFile(abstractFile); boolean isVcardFile = VcardParser.isVcardFile(abstractFile); + if (context.fileIngestIsCancelled()) { + return ProcessResult.OK; + } + if (isMbox || isEMLFile || isPstFile || isVcardFile ) { try { communicationArtifactsHelper = new CommunicationArtifactsHelper(currentCase.getSleuthkitCase(), @@ -148,7 +154,7 @@ public ProcessResult process(AbstractFile abstractFile) { return ProcessResult.ERROR; } } - + if (isMbox) { return processMBox(abstractFile); } @@ -164,7 +170,7 @@ public ProcessResult process(AbstractFile abstractFile) { if (isVcardFile) { return processVcard(abstractFile); } - + return ProcessResult.OK; } @@ -207,12 +213,16 @@ private ProcessResult processPst(AbstractFile abstractFile) { PstParser parser = new PstParser(services); PstParser.ParseResult result = parser.open(file, abstractFile.getId()); - + + switch( result) { case OK: Iterator<EmailMessage> pstMsgIterator = parser.getEmailMessageIterator(); if (pstMsgIterator != null) { - processEmails(parser.getPartialEmailMessages(), pstMsgIterator , abstractFile); + processEmails(parser.getPartialEmailMessages(), pstMsgIterator, abstractFile); + if (context.fileIngestIsCancelled()) { + return ProcessResult.OK; + } } else { // sometimes parser returns ParseResult=OK but there are no messages postErrorMessage( @@ -265,7 +275,7 @@ private ProcessResult processPst(AbstractFile abstractFile) { if (file.delete() == false) { logger.log(Level.INFO, "Failed to delete temp file: {0}", file.getName()); //NON-NLS } - + return ProcessResult.OK; } @@ -321,6 +331,9 @@ private ProcessResult processMBox(AbstractFile abstractFile) { } processMboxFile(file, abstractFile, emailFolder); + if (context.fileIngestIsCancelled()) { + return ProcessResult.OK; + } if (file.delete() == false) { logger.log(Level.INFO, "Failed to delete temp file: {0}", file.getName()); //NON-NLS @@ -349,7 +362,9 @@ private ProcessResult processMBox(AbstractFile abstractFile) { if (splitFile.delete() == false) { logger.log(Level.INFO, "Failed to delete temp file: {0}", splitFile); //NON-NLS } - + if (context.fileIngestIsCancelled()) { + return ProcessResult.OK; + } } } @@ -385,6 +400,9 @@ private void processMboxFile(File file, AbstractFile abstractFile, String emailF List<EmailMessage> emails = new ArrayList<>(); if(emailIterator != null) { while(emailIterator.hasNext()) { + if (context.fileIngestIsCancelled()) { + return; + } EmailMessage emailMessage = emailIterator.next(); if(emailMessage != null) { emails.add(emailMessage); @@ -436,7 +454,9 @@ private ProcessResult processEMLFile(AbstractFile abstractFile) { List<AbstractFile> derivedFiles = new ArrayList<>(); - BlackboardArtifact msgArtifact = addEmailArtifact(message, abstractFile); + AccountFileInstanceCache accountFileInstanceCache = new AccountFileInstanceCache(abstractFile, currentCase); + BlackboardArtifact msgArtifact = addEmailArtifact(message, abstractFile, accountFileInstanceCache); + accountFileInstanceCache.clear(); if ((msgArtifact != null) && (message.hasAttachment())) { derivedFiles.addAll(handleAttachments(message.getAttachments(), abstractFile, msgArtifact)); @@ -512,7 +532,11 @@ static String getRelModuleOutputPath() throws NoCurrentCaseException { * @param fullMessageIterator * @param abstractFile */ - private void processEmails(List<EmailMessage> partialEmailsForThreading, Iterator<EmailMessage> fullMessageIterator, AbstractFile abstractFile) { + private void processEmails(List<EmailMessage> partialEmailsForThreading, Iterator<EmailMessage> fullMessageIterator, + AbstractFile abstractFile) { + + // Create cache for accounts + AccountFileInstanceCache accountFileInstanceCache = new AccountFileInstanceCache(abstractFile, currentCase); // Putting try/catch around this to catch any exception and still allow // the creation of the artifacts to continue. @@ -526,6 +550,10 @@ private void processEmails(List<EmailMessage> partialEmailsForThreading, Iterato int msgCnt = 0; while(fullMessageIterator.hasNext()) { + if (context.fileIngestIsCancelled()) { + return; + } + EmailMessage current = fullMessageIterator.next(); if(current == null) { @@ -541,7 +569,7 @@ private void processEmails(List<EmailMessage> partialEmailsForThreading, Iterato } } - BlackboardArtifact msgArtifact = addEmailArtifact(current, abstractFile); + BlackboardArtifact msgArtifact = addEmailArtifact(current, abstractFile, accountFileInstanceCache); if ((msgArtifact != null) && (current.hasAttachment())) { derivedFiles.addAll(handleAttachments(current.getAttachments(), abstractFile, msgArtifact )); @@ -550,6 +578,9 @@ private void processEmails(List<EmailMessage> partialEmailsForThreading, Iterato if (derivedFiles.isEmpty() == false) { for (AbstractFile derived : derivedFiles) { + if (context.fileIngestIsCancelled()) { + return; + } services.fireModuleContentEvent(new ModuleContentEvent(derived)); } } @@ -654,7 +685,7 @@ private Set<String> findEmailAddresess(String input) { * @return The generated e-mail message artifact. */ @Messages({"ThunderbirdMboxFileIngestModule.addArtifact.indexError.message=Failed to index email message detected artifact for keyword search."}) - private BlackboardArtifact addEmailArtifact(EmailMessage email, AbstractFile abstractFile) { + private BlackboardArtifact addEmailArtifact(EmailMessage email, AbstractFile abstractFile, AccountFileInstanceCache accountFileInstanceCache) { BlackboardArtifact bbart = null; List<BlackboardAttribute> bbattributes = new ArrayList<>(); String to = email.getRecipients(); @@ -675,12 +706,16 @@ private BlackboardArtifact addEmailArtifact(EmailMessage email, AbstractFile abs String senderAddress; senderAddressList.addAll(findEmailAddresess(from)); + if (context.fileIngestIsCancelled()) { + return null; + } + AccountFileInstance senderAccountInstance = null; if (senderAddressList.size() == 1) { senderAddress = senderAddressList.get(0); try { - senderAccountInstance = currentCase.getSleuthkitCase().getCommunicationsManager().createAccountFileInstance(Account.Type.EMAIL, senderAddress, EmailParserModuleFactory.getModuleName(), abstractFile); + senderAccountInstance = accountFileInstanceCache.getAccountInstance(senderAddress); } catch(TskCoreException ex) { logger.log(Level.WARNING, "Failed to create account for email address " + senderAddress, ex); //NON-NLS @@ -690,23 +725,28 @@ private BlackboardArtifact addEmailArtifact(EmailMessage email, AbstractFile abs logger.log(Level.WARNING, "Failed to find sender address, from = {0}", from); //NON-NLS } + if (context.fileIngestIsCancelled()) { + return null; + } + List<String> recipientAddresses = new ArrayList<>(); recipientAddresses.addAll(findEmailAddresess(to)); recipientAddresses.addAll(findEmailAddresess(cc)); recipientAddresses.addAll(findEmailAddresess(bcc)); List<AccountFileInstance> recipientAccountInstances = new ArrayList<>(); - recipientAddresses.forEach((addr) -> { + for (String addr : recipientAddresses) { + if (context.fileIngestIsCancelled()) { + return null; + } try { - AccountFileInstance recipientAccountInstance = - currentCase.getSleuthkitCase().getCommunicationsManager().createAccountFileInstance(Account.Type.EMAIL, addr, - EmailParserModuleFactory.getModuleName(), abstractFile); + AccountFileInstance recipientAccountInstance = accountFileInstanceCache.getAccountInstance(addr); recipientAccountInstances.add(recipientAccountInstance); } catch(TskCoreException ex) { logger.log(Level.WARNING, "Failed to create account for email address " + addr, ex); //NON-NLS } - }); + } addArtifactAttribute(headers, ATTRIBUTE_TYPE.TSK_HEADERS, bbattributes); addArtifactAttribute(from, ATTRIBUTE_TYPE.TSK_EMAIL_FROM, bbattributes); @@ -731,12 +771,23 @@ private BlackboardArtifact addEmailArtifact(EmailMessage email, AbstractFile abs try { + if (context.fileIngestIsCancelled()) { + return null; + } bbart = abstractFile.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_EMAIL_MSG); bbart.addAttributes(bbattributes); + if (context.fileIngestIsCancelled()) { + return null; + } + // Add account relationships currentCase.getSleuthkitCase().getCommunicationsManager().addRelationships(senderAccountInstance, recipientAccountInstances, bbart,Relationship.Type.MESSAGE, dateL); + + if (context.fileIngestIsCancelled()) { + return null; + } try { // index the artifact for keyword search @@ -791,6 +842,56 @@ static void addArtifactAttribute(long longVal, ATTRIBUTE_TYPE attrType, Collecti } } + /** + * Cache for storing AccountFileInstance. + * The idea is that emails will be used multiple times in a file and + * we shouldn't do a database lookup each time. + */ + static private class AccountFileInstanceCache { + private final Map<String, AccountFileInstance> cacheMap; + private final AbstractFile file; + private final Case currentCase; + + /** + * Create a new cache. Caches are linked to a specific file. + * @param file + * @param currentCase + */ + AccountFileInstanceCache(AbstractFile file, Case currentCase) { + cacheMap= new HashMap<>(); + this.file = file; + this.currentCase = currentCase; + } + + /** + * Get the account file instance from the cache or the database. + * + * @param email The email for this account. + * + * @return The corresponding AccountFileInstance + * + * @throws TskCoreException + */ + AccountFileInstance getAccountInstance(String email) throws TskCoreException { + if (cacheMap.containsKey(email)) { + return cacheMap.get(email); + } + + AccountFileInstance accountInstance = + currentCase.getSleuthkitCase().getCommunicationsManager().createAccountFileInstance(Account.Type.EMAIL, email, + EmailParserModuleFactory.getModuleName(), file); + cacheMap.put(email, accountInstance); + return accountInstance; + } + + /** + * Clears the cache. + */ + void clear() { + cacheMap.clear(); + } + } + /** * Post an error message for the user. * diff --git a/travis_build.sh b/travis_build.sh index 5f574d57263e0a0570ba40f412f644278fff6b9d..d3753fd934fde4121d06960d7ccdd7035bb66aaf 100755 --- a/travis_build.sh +++ b/travis_build.sh @@ -8,5 +8,19 @@ pushd bindings/java && ant -q dist && popd echo "Building Autopsy..." && echo -en 'travis_fold:start:script.build\\r' cd $TRAVIS_BUILD_DIR/ -ant build +ant -q build echo -en 'travis_fold:end:script.build\\r' + +echo "Testing Autopsy..." && echo -en 'travis_fold:start:script.tests\\r' +echo "Free Space:" +echo `df -h .` + +if [ "${TRAVIS_OS_NAME}" = "osx" ]; then + # if os x, just run it + ant -q test-no-regression +elif [ "${TRAVIS_OS_NAME}" = "linux" ]; then + # if linux use xvfb + xvfb-run ant -q test-no-regression +fi + +echo -en 'travis_fold:end:script.tests\\r'